node-red-dashboard-2-hmi 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -140
- package/LICENSE +21 -21
- package/README.md +273 -273
- package/docs/mensagens.md +266 -266
- package/examples/clp-schneider.json +431 -431
- package/examples/envase.json +545 -545
- package/examples/esteira.json +639 -639
- package/examples/pintura.json +610 -610
- package/examples/tela-basica.json +572 -572
- package/nodes/access.js +69 -69
- package/nodes/admin.js +170 -166
- package/nodes/alarms.js +177 -177
- package/nodes/clients.js +31 -31
- package/nodes/plugin.js +24 -24
- package/nodes/trend.js +54 -54
- package/nodes/ui-hmi-canvas.html +383 -383
- package/nodes/ui-hmi-canvas.js +335 -335
- package/nodes/values.js +176 -176
- package/package.json +63 -63
- package/resources/hmi-editor.js +10893 -10887
- package/resources/ui-hmi-canvas.umd.js +667 -667
package/nodes/values.js
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Merges an incoming msg into the canvas values.
|
|
4
|
-
// - object payload: every key is merged (e.g. the output of schneider read)
|
|
5
|
-
// - any other payload with msg.topic: stored under that topic
|
|
6
|
-
// Returns true when something changed.
|
|
7
|
-
function mergeValues (values, msg) {
|
|
8
|
-
const p = msg && msg.payload
|
|
9
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p) && !Buffer.isBuffer(p)) {
|
|
10
|
-
Object.assign(values, p)
|
|
11
|
-
return Object.keys(p).length > 0
|
|
12
|
-
}
|
|
13
|
-
if (msg && typeof msg.topic === 'string' && msg.topic !== '' && p !== undefined) {
|
|
14
|
-
values[msg.topic] = p
|
|
15
|
-
return true
|
|
16
|
-
}
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Text typed in the editor -> number, boolean or string
|
|
21
|
-
function parseValue (text) {
|
|
22
|
-
if (text === undefined || text === null || text === '') return undefined
|
|
23
|
-
if (text === 'true') return true
|
|
24
|
-
if (text === 'false') return false
|
|
25
|
-
const n = Number(String(text).replace(',', '.'))
|
|
26
|
-
return isFinite(n) ? n : String(text)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const truthy = v => v === true || (v !== false && v !== null && v !== undefined && v !== '' && v !== 'false' && Number(v) !== 0)
|
|
30
|
-
|
|
31
|
-
const num = v => (v === '' || v === undefined || v === null || typeof v === 'boolean' || !isFinite(Number(v))) ? null : Number(v)
|
|
32
|
-
|
|
33
|
-
// raw PLC value -> value on screen (raw × factor ÷ divisor + offset), and back
|
|
34
|
-
function scaleOf (sym) {
|
|
35
|
-
const sc = sym.scale || {}
|
|
36
|
-
const d = num(sc.divisor)
|
|
37
|
-
const k = (num(sc.factor) === null ? 1 : num(sc.factor)) / (d === null || d === 0 ? 1 : d)
|
|
38
|
-
const o = num(sc.offset) === null ? 0 : num(sc.offset)
|
|
39
|
-
return { k, o }
|
|
40
|
-
}
|
|
41
|
-
const toScreen = (sym, raw) => { const n = num(raw); if (n === null) return null; const { k, o } = scaleOf(sym); return Math.round((n * k + o) * 1e9) / 1e9 }
|
|
42
|
-
const toRaw = (sym, shown) => { const { k, o } = scaleOf(sym); return k !== 0 && (k !== 1 || o !== 0) ? Math.round(((shown - o) / k) * 1e9) / 1e9 : shown }
|
|
43
|
-
|
|
44
|
-
// Interlocks of an action: "permissivo, !emergencia" -> every key must be on
|
|
45
|
-
// (or off with "!"). Returns the reason when the command is blocked, else ''.
|
|
46
|
-
function interlockOf (act, values) {
|
|
47
|
-
const list = String((act && act.interlock) || '').split(',').map(t => t.trim()).filter(Boolean)
|
|
48
|
-
for (const item of list) {
|
|
49
|
-
const not = item.startsWith('!')
|
|
50
|
-
const key = not ? item.slice(1).trim() : item
|
|
51
|
-
if (!key) continue
|
|
52
|
-
const on = truthy(values[key])
|
|
53
|
-
if (not ? on : !on) return String(act.interlockText || '').trim() || ((not ? key + ' ligado' : key + ' desligado'))
|
|
54
|
-
}
|
|
55
|
-
return ''
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function inRange (n, min, max) {
|
|
59
|
-
const lo = num(min)
|
|
60
|
-
const hi = num(max)
|
|
61
|
-
return !((lo !== null && n < lo) || (hi !== null && n > hi))
|
|
62
|
-
}
|
|
63
|
-
const rangeText = (min, max) => num(min) !== null && num(max) !== null ? ' (' + num(min) + ' a ' + num(max) + ')' : ''
|
|
64
|
-
|
|
65
|
-
// Builds the output msg for a click on a symbol. The dashboard only says which
|
|
66
|
-
// symbol and which phase (click, down, up, input...); what is sent comes from the
|
|
67
|
-
// node's own config, so a page cannot make the node send arbitrary values.
|
|
68
|
-
// Returns null when nothing should be sent, or { denied | blocked | rejected | armed }.
|
|
69
|
-
// opts.pin: the canvas password (node credential); actions marked "pedir senha" need it.
|
|
70
|
-
// opts.armed: a pulse whose password was already given (phase "arm").
|
|
71
|
-
function actionMessage (symbols, values, msg, pinOrOpts) {
|
|
72
|
-
const opts = pinOrOpts && typeof pinOrOpts === 'object' ? pinOrOpts : { pin: pinOrOpts }
|
|
73
|
-
const pin = opts.pin
|
|
74
|
-
const a = (msg && msg._hmiAction) || {}
|
|
75
|
-
const sym = (Array.isArray(symbols) ? symbols : []).find(s => s && s.id === a.id)
|
|
76
|
-
const act = sym && sym.action
|
|
77
|
-
if (!act) return null
|
|
78
|
-
const needPin = act.pin === 'yes'
|
|
79
|
-
const pinOk = () => !!pin && String(a.pin || '') === String(pin)
|
|
80
|
-
if (a.phase === 'arm') {
|
|
81
|
-
if (act.mode !== 'pulse' || !needPin) return null
|
|
82
|
-
return pinOk() ? { armed: true } : { denied: true }
|
|
83
|
-
}
|
|
84
|
-
// releasing a pulse is always allowed: it only sends the 0
|
|
85
|
-
const release = act.mode === 'pulse' && a.phase === 'up'
|
|
86
|
-
if (needPin && !release && !(act.mode === 'pulse' && opts.armed) && !pinOk()) return { denied: true }
|
|
87
|
-
if (!release) {
|
|
88
|
-
const why = interlockOf(act, values)
|
|
89
|
-
if (why) return { blocked: why }
|
|
90
|
-
}
|
|
91
|
-
const cur = sym.key ? values[sym.key] : undefined
|
|
92
|
-
const asBool = typeof cur === 'boolean'
|
|
93
|
-
let payload
|
|
94
|
-
let topic = act.topic || sym.key || ''
|
|
95
|
-
const fp = act.mode === 'faceplate' ? String(a.phase || '') : ''
|
|
96
|
-
if (fp === 'fp-on' || fp === 'fp-off' || fp === 'fp-reset' || fp === 'fp-auto' || fp === 'fp-man') {
|
|
97
|
-
// faceplate buttons: topic and value come from the symbol's config
|
|
98
|
-
const k = fp.slice(3)
|
|
99
|
-
const tk = k === 'auto' || k === 'man' ? 'modeTopic' : k + 'Topic'
|
|
100
|
-
if (!act[tk]) return null
|
|
101
|
-
topic = act[tk]
|
|
102
|
-
payload = parseValue(act[k + 'Value'])
|
|
103
|
-
if (payload === undefined) payload = k === 'man' ? 0 : (k === 'auto' ? 1 : true)
|
|
104
|
-
} else if (fp === 'fp-sp' || fp === 'fp-op') {
|
|
105
|
-
// faceplate setpoint / manual output typed on screen
|
|
106
|
-
const k = fp.slice(3)
|
|
107
|
-
if (!act[k + 'Topic']) return null
|
|
108
|
-
topic = act[k + 'Topic']
|
|
109
|
-
payload = Number(String(a.value).replace(',', '.'))
|
|
110
|
-
if (String(a.value ?? '').trim() === '' || !isFinite(payload)) return null
|
|
111
|
-
const min = act[k + 'Min'] ?? (k === 'op' ? '0' : '')
|
|
112
|
-
const max = act[k + 'Max'] ?? (k === 'op' ? '100' : '')
|
|
113
|
-
if (!inRange(payload, min, max)) return { rejected: 'Valor fora da faixa' + rangeText(min, max) }
|
|
114
|
-
} else if (act.mode === 'value' && a.phase === 'click') {
|
|
115
|
-
payload = parseValue(act.value)
|
|
116
|
-
if (payload === undefined) payload = true
|
|
117
|
-
} else if (act.mode === 'toggle' && a.phase === 'click') {
|
|
118
|
-
const on = truthy(cur)
|
|
119
|
-
payload = asBool ? !on : (on ? 0 : 1)
|
|
120
|
-
} else if (act.mode === 'pulse' && (a.phase === 'down' || a.phase === 'up')) {
|
|
121
|
-
const down = a.phase === 'down'
|
|
122
|
-
payload = asBool ? down : (down ? 1 : 0)
|
|
123
|
-
} else if (act.mode === 'input' && a.phase === 'input') {
|
|
124
|
-
payload = Number(String(a.value).replace(',', '.'))
|
|
125
|
-
if (String(a.value ?? '').trim() === '' || !isFinite(payload)) return null
|
|
126
|
-
if (!inRange(payload, act.min, act.max)) return { rejected: 'Valor fora da faixa' + rangeText(act.min, act.max) }
|
|
127
|
-
// biggest change in one step, in the units shown on screen
|
|
128
|
-
const step = num(act.maxStep)
|
|
129
|
-
const shown = sym.key ? toScreen(sym, cur) : null
|
|
130
|
-
if (step !== null && step > 0 && shown !== null && Math.abs(payload - shown) > step) return { rejected: 'Mudança maior que ' + step + ' de uma vez' }
|
|
131
|
-
// limits are in the units shown on screen; the PLC gets the raw value
|
|
132
|
-
payload = toRaw(sym, payload)
|
|
133
|
-
} else {
|
|
134
|
-
return null
|
|
135
|
-
}
|
|
136
|
-
const p = sym.props || {}
|
|
137
|
-
const name = String(p.label || p.title || p.text || sym.key || sym.type)
|
|
138
|
-
const user = userOf(msg)
|
|
139
|
-
// who sent what and when, for a command log (database, file...)
|
|
140
|
-
const audit = { ts: new Date().toISOString(), symbol: name, action: a.phase === 'click' || a.phase === 'input' ? act.mode : a.phase, topic, value: payload }
|
|
141
|
-
if (user) audit.user = user
|
|
142
|
-
if (msg._client && msg._client.socketId) audit.socketId = msg._client.socketId
|
|
143
|
-
const out = { topic, payload, symbol: { id: sym.id, type: sym.type, key: sym.key || '' }, audit }
|
|
144
|
-
if (msg._client) out._client = msg._client
|
|
145
|
-
return out
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// What the PLC should echo back on the symbol's key after a command (for the
|
|
149
|
-
// "waiting for the PLC" mark), or undefined when it cannot be told.
|
|
150
|
-
function expectedEcho (sym, out, phase) {
|
|
151
|
-
if (!sym || !sym.key || !out || !sym.action) return undefined
|
|
152
|
-
const m = sym.action.mode
|
|
153
|
-
if (m === 'toggle' || m === 'input') return out.payload
|
|
154
|
-
if (m === 'value' && (!sym.action.topic || sym.action.topic === sym.key)) return out.payload
|
|
155
|
-
if (m === 'faceplate' && (phase === 'fp-on' || phase === 'fp-off')) return phase === 'fp-on'
|
|
156
|
-
return undefined
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// does the value read back match what was written? (1 == true, "5" == 5)
|
|
160
|
-
function sameValue (a, b) {
|
|
161
|
-
if (typeof b === 'boolean' || typeof a === 'boolean') return truthy(a) === truthy(b)
|
|
162
|
-
const x = num(a)
|
|
163
|
-
const y = num(b)
|
|
164
|
-
if (x !== null && y !== null) return Math.abs(x - y) < 1e-9 * Math.max(1, Math.abs(y))
|
|
165
|
-
return String(a) === String(b)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// user name from the dashboard connection (FlowFuse / multi-user plugins), if any
|
|
169
|
-
function userOf (msg) {
|
|
170
|
-
const u = msg && msg._client && msg._client.user
|
|
171
|
-
if (!u) return ''
|
|
172
|
-
if (typeof u === 'string') return u
|
|
173
|
-
return String(u.name || u.username || u.email || u.userId || u.id || '')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
module.exports = { mergeValues, parseValue, actionMessage, userOf, interlockOf, expectedEcho, sameValue, truthy }
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Merges an incoming msg into the canvas values.
|
|
4
|
+
// - object payload: every key is merged (e.g. the output of schneider read)
|
|
5
|
+
// - any other payload with msg.topic: stored under that topic
|
|
6
|
+
// Returns true when something changed.
|
|
7
|
+
function mergeValues (values, msg) {
|
|
8
|
+
const p = msg && msg.payload
|
|
9
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p) && !Buffer.isBuffer(p)) {
|
|
10
|
+
Object.assign(values, p)
|
|
11
|
+
return Object.keys(p).length > 0
|
|
12
|
+
}
|
|
13
|
+
if (msg && typeof msg.topic === 'string' && msg.topic !== '' && p !== undefined) {
|
|
14
|
+
values[msg.topic] = p
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Text typed in the editor -> number, boolean or string
|
|
21
|
+
function parseValue (text) {
|
|
22
|
+
if (text === undefined || text === null || text === '') return undefined
|
|
23
|
+
if (text === 'true') return true
|
|
24
|
+
if (text === 'false') return false
|
|
25
|
+
const n = Number(String(text).replace(',', '.'))
|
|
26
|
+
return isFinite(n) ? n : String(text)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const truthy = v => v === true || (v !== false && v !== null && v !== undefined && v !== '' && v !== 'false' && Number(v) !== 0)
|
|
30
|
+
|
|
31
|
+
const num = v => (v === '' || v === undefined || v === null || typeof v === 'boolean' || !isFinite(Number(v))) ? null : Number(v)
|
|
32
|
+
|
|
33
|
+
// raw PLC value -> value on screen (raw × factor ÷ divisor + offset), and back
|
|
34
|
+
function scaleOf (sym) {
|
|
35
|
+
const sc = sym.scale || {}
|
|
36
|
+
const d = num(sc.divisor)
|
|
37
|
+
const k = (num(sc.factor) === null ? 1 : num(sc.factor)) / (d === null || d === 0 ? 1 : d)
|
|
38
|
+
const o = num(sc.offset) === null ? 0 : num(sc.offset)
|
|
39
|
+
return { k, o }
|
|
40
|
+
}
|
|
41
|
+
const toScreen = (sym, raw) => { const n = num(raw); if (n === null) return null; const { k, o } = scaleOf(sym); return Math.round((n * k + o) * 1e9) / 1e9 }
|
|
42
|
+
const toRaw = (sym, shown) => { const { k, o } = scaleOf(sym); return k !== 0 && (k !== 1 || o !== 0) ? Math.round(((shown - o) / k) * 1e9) / 1e9 : shown }
|
|
43
|
+
|
|
44
|
+
// Interlocks of an action: "permissivo, !emergencia" -> every key must be on
|
|
45
|
+
// (or off with "!"). Returns the reason when the command is blocked, else ''.
|
|
46
|
+
function interlockOf (act, values) {
|
|
47
|
+
const list = String((act && act.interlock) || '').split(',').map(t => t.trim()).filter(Boolean)
|
|
48
|
+
for (const item of list) {
|
|
49
|
+
const not = item.startsWith('!')
|
|
50
|
+
const key = not ? item.slice(1).trim() : item
|
|
51
|
+
if (!key) continue
|
|
52
|
+
const on = truthy(values[key])
|
|
53
|
+
if (not ? on : !on) return String(act.interlockText || '').trim() || ((not ? key + ' ligado' : key + ' desligado'))
|
|
54
|
+
}
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function inRange (n, min, max) {
|
|
59
|
+
const lo = num(min)
|
|
60
|
+
const hi = num(max)
|
|
61
|
+
return !((lo !== null && n < lo) || (hi !== null && n > hi))
|
|
62
|
+
}
|
|
63
|
+
const rangeText = (min, max) => num(min) !== null && num(max) !== null ? ' (' + num(min) + ' a ' + num(max) + ')' : ''
|
|
64
|
+
|
|
65
|
+
// Builds the output msg for a click on a symbol. The dashboard only says which
|
|
66
|
+
// symbol and which phase (click, down, up, input...); what is sent comes from the
|
|
67
|
+
// node's own config, so a page cannot make the node send arbitrary values.
|
|
68
|
+
// Returns null when nothing should be sent, or { denied | blocked | rejected | armed }.
|
|
69
|
+
// opts.pin: the canvas password (node credential); actions marked "pedir senha" need it.
|
|
70
|
+
// opts.armed: a pulse whose password was already given (phase "arm").
|
|
71
|
+
function actionMessage (symbols, values, msg, pinOrOpts) {
|
|
72
|
+
const opts = pinOrOpts && typeof pinOrOpts === 'object' ? pinOrOpts : { pin: pinOrOpts }
|
|
73
|
+
const pin = opts.pin
|
|
74
|
+
const a = (msg && msg._hmiAction) || {}
|
|
75
|
+
const sym = (Array.isArray(symbols) ? symbols : []).find(s => s && s.id === a.id)
|
|
76
|
+
const act = sym && sym.action
|
|
77
|
+
if (!act) return null
|
|
78
|
+
const needPin = act.pin === 'yes'
|
|
79
|
+
const pinOk = () => !!pin && String(a.pin || '') === String(pin)
|
|
80
|
+
if (a.phase === 'arm') {
|
|
81
|
+
if (act.mode !== 'pulse' || !needPin) return null
|
|
82
|
+
return pinOk() ? { armed: true } : { denied: true }
|
|
83
|
+
}
|
|
84
|
+
// releasing a pulse is always allowed: it only sends the 0
|
|
85
|
+
const release = act.mode === 'pulse' && a.phase === 'up'
|
|
86
|
+
if (needPin && !release && !(act.mode === 'pulse' && opts.armed) && !pinOk()) return { denied: true }
|
|
87
|
+
if (!release) {
|
|
88
|
+
const why = interlockOf(act, values)
|
|
89
|
+
if (why) return { blocked: why }
|
|
90
|
+
}
|
|
91
|
+
const cur = sym.key ? values[sym.key] : undefined
|
|
92
|
+
const asBool = typeof cur === 'boolean'
|
|
93
|
+
let payload
|
|
94
|
+
let topic = act.topic || sym.key || ''
|
|
95
|
+
const fp = act.mode === 'faceplate' ? String(a.phase || '') : ''
|
|
96
|
+
if (fp === 'fp-on' || fp === 'fp-off' || fp === 'fp-reset' || fp === 'fp-auto' || fp === 'fp-man') {
|
|
97
|
+
// faceplate buttons: topic and value come from the symbol's config
|
|
98
|
+
const k = fp.slice(3)
|
|
99
|
+
const tk = k === 'auto' || k === 'man' ? 'modeTopic' : k + 'Topic'
|
|
100
|
+
if (!act[tk]) return null
|
|
101
|
+
topic = act[tk]
|
|
102
|
+
payload = parseValue(act[k + 'Value'])
|
|
103
|
+
if (payload === undefined) payload = k === 'man' ? 0 : (k === 'auto' ? 1 : true)
|
|
104
|
+
} else if (fp === 'fp-sp' || fp === 'fp-op') {
|
|
105
|
+
// faceplate setpoint / manual output typed on screen
|
|
106
|
+
const k = fp.slice(3)
|
|
107
|
+
if (!act[k + 'Topic']) return null
|
|
108
|
+
topic = act[k + 'Topic']
|
|
109
|
+
payload = Number(String(a.value).replace(',', '.'))
|
|
110
|
+
if (String(a.value ?? '').trim() === '' || !isFinite(payload)) return null
|
|
111
|
+
const min = act[k + 'Min'] ?? (k === 'op' ? '0' : '')
|
|
112
|
+
const max = act[k + 'Max'] ?? (k === 'op' ? '100' : '')
|
|
113
|
+
if (!inRange(payload, min, max)) return { rejected: 'Valor fora da faixa' + rangeText(min, max) }
|
|
114
|
+
} else if (act.mode === 'value' && a.phase === 'click') {
|
|
115
|
+
payload = parseValue(act.value)
|
|
116
|
+
if (payload === undefined) payload = true
|
|
117
|
+
} else if (act.mode === 'toggle' && a.phase === 'click') {
|
|
118
|
+
const on = truthy(cur)
|
|
119
|
+
payload = asBool ? !on : (on ? 0 : 1)
|
|
120
|
+
} else if (act.mode === 'pulse' && (a.phase === 'down' || a.phase === 'up')) {
|
|
121
|
+
const down = a.phase === 'down'
|
|
122
|
+
payload = asBool ? down : (down ? 1 : 0)
|
|
123
|
+
} else if (act.mode === 'input' && a.phase === 'input') {
|
|
124
|
+
payload = Number(String(a.value).replace(',', '.'))
|
|
125
|
+
if (String(a.value ?? '').trim() === '' || !isFinite(payload)) return null
|
|
126
|
+
if (!inRange(payload, act.min, act.max)) return { rejected: 'Valor fora da faixa' + rangeText(act.min, act.max) }
|
|
127
|
+
// biggest change in one step, in the units shown on screen
|
|
128
|
+
const step = num(act.maxStep)
|
|
129
|
+
const shown = sym.key ? toScreen(sym, cur) : null
|
|
130
|
+
if (step !== null && step > 0 && shown !== null && Math.abs(payload - shown) > step) return { rejected: 'Mudança maior que ' + step + ' de uma vez' }
|
|
131
|
+
// limits are in the units shown on screen; the PLC gets the raw value
|
|
132
|
+
payload = toRaw(sym, payload)
|
|
133
|
+
} else {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
const p = sym.props || {}
|
|
137
|
+
const name = String(p.label || p.title || p.text || sym.key || sym.type)
|
|
138
|
+
const user = userOf(msg)
|
|
139
|
+
// who sent what and when, for a command log (database, file...)
|
|
140
|
+
const audit = { ts: new Date().toISOString(), symbol: name, action: a.phase === 'click' || a.phase === 'input' ? act.mode : a.phase, topic, value: payload }
|
|
141
|
+
if (user) audit.user = user
|
|
142
|
+
if (msg._client && msg._client.socketId) audit.socketId = msg._client.socketId
|
|
143
|
+
const out = { topic, payload, symbol: { id: sym.id, type: sym.type, key: sym.key || '' }, audit }
|
|
144
|
+
if (msg._client) out._client = msg._client
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// What the PLC should echo back on the symbol's key after a command (for the
|
|
149
|
+
// "waiting for the PLC" mark), or undefined when it cannot be told.
|
|
150
|
+
function expectedEcho (sym, out, phase) {
|
|
151
|
+
if (!sym || !sym.key || !out || !sym.action) return undefined
|
|
152
|
+
const m = sym.action.mode
|
|
153
|
+
if (m === 'toggle' || m === 'input') return out.payload
|
|
154
|
+
if (m === 'value' && (!sym.action.topic || sym.action.topic === sym.key)) return out.payload
|
|
155
|
+
if (m === 'faceplate' && (phase === 'fp-on' || phase === 'fp-off')) return phase === 'fp-on'
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// does the value read back match what was written? (1 == true, "5" == 5)
|
|
160
|
+
function sameValue (a, b) {
|
|
161
|
+
if (typeof b === 'boolean' || typeof a === 'boolean') return truthy(a) === truthy(b)
|
|
162
|
+
const x = num(a)
|
|
163
|
+
const y = num(b)
|
|
164
|
+
if (x !== null && y !== null) return Math.abs(x - y) < 1e-9 * Math.max(1, Math.abs(y))
|
|
165
|
+
return String(a) === String(b)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// user name from the dashboard connection (FlowFuse / multi-user plugins), if any
|
|
169
|
+
function userOf (msg) {
|
|
170
|
+
const u = msg && msg._client && msg._client.user
|
|
171
|
+
if (!u) return ''
|
|
172
|
+
if (typeof u === 'string') return u
|
|
173
|
+
return String(u.name || u.username || u.email || u.userId || u.id || '')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { mergeValues, parseValue, actionMessage, userOf, interlockOf, expectedEcho, sameValue, truthy }
|
package/package.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-red-dashboard-2-hmi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Build machine HMI screens for Node-RED Dashboard 2.0 by dragging symbols onto a canvas (ISA-101 and classic themes)",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"node-red",
|
|
7
|
-
"node-red-dashboard-2",
|
|
8
|
-
"hmi",
|
|
9
|
-
"scada",
|
|
10
|
-
"isa-101",
|
|
11
|
-
"synoptic",
|
|
12
|
-
"svg"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/Eduardo-556/node-red-dashboard-2-hmi#readme",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/Eduardo-556/node-red-dashboard-2-hmi/issues"
|
|
17
|
-
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/Eduardo-556/node-red-dashboard-2-hmi.git"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "Eduardo",
|
|
24
|
-
"files": [
|
|
25
|
-
"nodes",
|
|
26
|
-
"resources",
|
|
27
|
-
"examples",
|
|
28
|
-
"README.md",
|
|
29
|
-
"LICENSE",
|
|
30
|
-
"CHANGELOG.md",
|
|
31
|
-
"docs/mensagens.md"
|
|
32
|
-
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "vite build && vite build --config vite.editor.config.mjs",
|
|
35
|
-
"test": "node --test test/symbols.test.mjs test/canvas.test.js test/server.test.js"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@vitejs/plugin-vue": "^6.0.0",
|
|
39
|
-
"vite": "^8.0.0",
|
|
40
|
-
"vue": "^3.5.0"
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">=18"
|
|
44
|
-
},
|
|
45
|
-
"node-red": {
|
|
46
|
-
"version": ">=3.0.0",
|
|
47
|
-
"nodes": {
|
|
48
|
-
"ui-hmi-canvas": "nodes/ui-hmi-canvas.js"
|
|
49
|
-
},
|
|
50
|
-
"plugins": {
|
|
51
|
-
"hmi-canvas-actions": "nodes/plugin.js"
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"node-red-dashboard-2": {
|
|
55
|
-
"version": "1.0.0",
|
|
56
|
-
"widgets": {
|
|
57
|
-
"ui-hmi-canvas": {
|
|
58
|
-
"output": "ui-hmi-canvas.umd.js",
|
|
59
|
-
"component": "UIHmiCanvas"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-dashboard-2-hmi",
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"description": "Build machine HMI screens for Node-RED Dashboard 2.0 by dragging symbols onto a canvas (ISA-101 and classic themes)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"node-red-dashboard-2",
|
|
8
|
+
"hmi",
|
|
9
|
+
"scada",
|
|
10
|
+
"isa-101",
|
|
11
|
+
"synoptic",
|
|
12
|
+
"svg"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Eduardo-556/node-red-dashboard-2-hmi#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Eduardo-556/node-red-dashboard-2-hmi/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Eduardo-556/node-red-dashboard-2-hmi.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Eduardo",
|
|
24
|
+
"files": [
|
|
25
|
+
"nodes",
|
|
26
|
+
"resources",
|
|
27
|
+
"examples",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"docs/mensagens.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vite build && vite build --config vite.editor.config.mjs",
|
|
35
|
+
"test": "node --test test/symbols.test.mjs test/canvas.test.js test/server.test.js"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
39
|
+
"vite": "^8.0.0",
|
|
40
|
+
"vue": "^3.5.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"node-red": {
|
|
46
|
+
"version": ">=3.0.0",
|
|
47
|
+
"nodes": {
|
|
48
|
+
"ui-hmi-canvas": "nodes/ui-hmi-canvas.js"
|
|
49
|
+
},
|
|
50
|
+
"plugins": {
|
|
51
|
+
"hmi-canvas-actions": "nodes/plugin.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"node-red-dashboard-2": {
|
|
55
|
+
"version": "1.0.0",
|
|
56
|
+
"widgets": {
|
|
57
|
+
"ui-hmi-canvas": {
|
|
58
|
+
"output": "ui-hmi-canvas.umd.js",
|
|
59
|
+
"component": "UIHmiCanvas"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|