smithtek-mako-rf 3.2.4 → 3.3.2
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/README.md +37 -2
- package/package.json +1 -1
- package/smithtek-mako-rf.html +15 -1
- package/smithtek-mako-rf.js +75 -15
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
# Cloud data/alarms and local debug — 3.3.2
|
|
2
|
+
|
|
3
|
+
- **Top pin, output 1 — Data / alarm:** successful decoded readings and write responses, or the friendly named fault JSON when a request fails. Connect this to the cloud flow.
|
|
4
|
+
- **Bottom pin, output 2 — Debug:** technical error payloads containing `ok:false`, `error`, `code` and `req`. Connect this to a local Debug node, or leave it disconnected. Successful requests send nothing here.
|
|
5
|
+
|
|
6
|
+
A timeout alarm is emitted after **one poll exhausts its configured retries**. Retries 0 means one failed attempt; retries 2 means three failed attempts. There is no separate bad-poll counter. If a retry succeeds, only normal data is emitted. Permanent Modbus exceptions (such as an unsupported register), invalid configuration and queue rejection are reported immediately. Each failed poll emits one friendly alarm on output 1 and one technical message on output 2. Intentional shutdown/redeploy cancellation emits neither.
|
|
7
|
+
|
|
8
|
+
Existing first-output wires keep both data and friendly alarms. Node-RED logs, status and Catch handling remain enabled. Refresh the editor after updating to see the pin labels. This corrects the output routing introduced in 3.3.1.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Prefixes and operator fault messages — 3.3.0
|
|
13
|
+
|
|
14
|
+
## Prefix
|
|
15
|
+
|
|
16
|
+
Set the node's **Prefix** once, for example `b1`. A decoder row named `Pressure` then outputs `{"b1 Pressure":12}`. Copy the node and change its prefix to `b2` to reuse every table entry without renaming them. Leading/trailing prefix whitespace is trimmed, and exactly one space separates it from the row name. A blank prefix preserves existing output names. Existing nested names retain their structure: `tank=>level` becomes `{"b1 tank":{"level":12}}`.
|
|
17
|
+
|
|
18
|
+
## Operator fault JSON
|
|
19
|
+
|
|
20
|
+
Failed requests emit a flat numeric alarm on output 1 in `msg.payload`, suitable for forwarding as a named cloud value. For example:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{"b1 RF ID2 Not communicating — check power/RF; disable if out of service":1}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The key uses the prefix (or node Name when no prefix is set), actual channel (RF, RS485-1 or RS485-2), device ID and a short operator message. RS485 timeouts advise checking power and wiring. Disabling an out-of-service asset is operator advice; the node never disables a device automatically.
|
|
27
|
+
|
|
28
|
+
Only a final failed request emits this alarm: a timeout that recovers within its configured retries produces normal data, with no fault payload. Permanent device exceptions and configuration errors are reported immediately when retrying cannot help. Each failed poll emits one fault message. Queue overload, gateway port problems, invalid configuration and decoding failures have distinct messages; they are not described as RF failures. Intentional shutdown/redeploy cancellation emits no fault alarm. An optional RSSI diagnostic failure remains in `msg.rssi_error` and does not turn a successful register read into an asset communication fault.
|
|
29
|
+
|
|
30
|
+
Full technical details remain in `msg.modbus`: `ok:false`, `code`, `error`, `req`, and `fault` (key, reason, operator summary, channel, device ID, prefix, node name and timestamp). Node-RED Catch handling remains supported. Successful requests output normal prefixed readings. No synthetic zero/clear alarm is sent on recovery; cloud alarm reset/expiry must be handled by the receiving flow or service.
|
|
31
|
+
|
|
32
|
+
**Output change:** failure payloads previously contained `{ok:false,error,req}`; they now contain the flat named alarm above. Use `msg.modbus.ok`, `msg.modbus.error` and `msg.modbus.req` in flows that inspect errors. This package does not connect to or configure 3dm.space; compatibility with a particular cloud ingestion schema must be verified there.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
1
36
|
Digital read outputs in msg.payload use numeric 1 (on) and 0 (off), including coils (FC1), discrete inputs (FC2), and individual register bits. The raw Modbus response retains its original representation. Coil writes still accept true/false and 1/0.
|
|
2
37
|
|
|
3
38
|
# Simple bus settings and range scaling — 3.2.4
|
|
@@ -28,7 +63,7 @@ This release retains the pinned serial dependencies and supports Node.js 14 synt
|
|
|
28
63
|
- One worker owns each bus. Closing a runtime node cancels only its own work.
|
|
29
64
|
- Open, request, diagnostic and close operations have deadlines. A port whose close cannot be confirmed stays quarantined rather than acquiring a second owner.
|
|
30
65
|
- Serial errors are handled. Dead connections are retired before reopening.
|
|
31
|
-
-
|
|
66
|
+
- Reads and writes use FIFO order. Queue overflow returns an explicit error and completes once.
|
|
32
67
|
- Every accepted request is retained in FIFO order, including repeated reads from the same node. Requests are not coalesced or expired while waiting.
|
|
33
68
|
- Each queued request gets the configured timeout retry count before moving to the next. There is no offline cooldown or skipped polling.
|
|
34
69
|
- After a communication error, the bus waits for a quiet period and flushes stale buffered data. Automatic quiet time is the response timeout on RF and 100ms on RS485. Late RF frames outside that interval remain a protocol limitation: Modbus RTU read replies carry no register address or transaction ID.
|
|
@@ -37,7 +72,7 @@ This release retains the pinned serial dependencies and supports Node.js 14 synt
|
|
|
37
72
|
|
|
38
73
|
No additional bus settings are required. Recovery timing is automatic: the response timeout on RF and 100ms on RS485.
|
|
39
74
|
|
|
40
|
-
Failures
|
|
75
|
+
Failures emit msg.modbus.ok=false and the fault payload documented above. msg.modbus.code identifies queue, offline and transport conditions; done(error) also enables Node-RED Catch handling. Intentional node/bus shutdown settles callbacks without emitting from closed nodes. No retry can guarantee a timed-out write was not applied at the device: retain an appropriate retry setting for the PLC command semantics.
|
|
41
76
|
|
|
42
77
|
Validated on a Passport with Raspbian Buster, Node.js 14.21.3, native serial dependencies and two physical RF Makos. Node.js 16 has not been separately executed. No dependency upgrade is required.
|
|
43
78
|
|
package/package.json
CHANGED
package/smithtek-mako-rf.html
CHANGED
|
@@ -369,6 +369,7 @@
|
|
|
369
369
|
color: "#B9B6B8",
|
|
370
370
|
defaults: {
|
|
371
371
|
name: { value: "" },
|
|
372
|
+
prefix: { value: "" },
|
|
372
373
|
bus: { type: "smithtek-mako-rf-bus", required: true },
|
|
373
374
|
|
|
374
375
|
mode: { value: "read" },
|
|
@@ -385,7 +386,8 @@
|
|
|
385
386
|
items: { value: [ defaultItemRow() ], validate: validDecoderItems }
|
|
386
387
|
},
|
|
387
388
|
inputs: 1,
|
|
388
|
-
outputs:
|
|
389
|
+
outputs: 2,
|
|
390
|
+
outputLabels: ["Data / alarm", "Debug"],
|
|
389
391
|
icon: "bridge.svg",
|
|
390
392
|
label: function () { return this.name || "Smithtek Mako RF"; },
|
|
391
393
|
|
|
@@ -547,6 +549,12 @@
|
|
|
547
549
|
<input type="text" id="node-input-name" placeholder="">
|
|
548
550
|
</div>
|
|
549
551
|
|
|
552
|
+
<div class="form-row">
|
|
553
|
+
<label for="node-input-prefix"><i class="fa fa-tag"></i> Prefix</label>
|
|
554
|
+
<input type="text" id="node-input-prefix" placeholder="e.g. b1">
|
|
555
|
+
<div class="form-tips">Added before each decoder name with a space: b1 + Pressure becomes b1 Pressure. Leave blank to keep existing names.</div>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
550
558
|
<div class="form-row">
|
|
551
559
|
<label for="node-input-bus"><i class="fa fa-sitemap"></i> Bus</label>
|
|
552
560
|
<input type="text" id="node-input-bus" style="width: 25%;">
|
|
@@ -673,5 +681,11 @@ Smithtek Mako RF / RS485.
|
|
|
673
681
|
Read mode outputs decoded JSON in msg.payload using the Decoder map rows. Digital points (coils, discrete inputs and register bits) output numeric 1 for on and 0 for off.
|
|
674
682
|
Raw modbus response is kept in msg.modbus.raw.
|
|
675
683
|
|
|
684
|
+
Prefix adds text and one space before each decoder name. Leave blank to preserve names.
|
|
685
|
+
|
|
686
|
+
Output 1 (Data / alarm) carries successful readings and write responses, or the friendly named fault JSON on failure. Output 2 (Debug) carries technical error details: ok, error, code and req. Connect it to a local Debug node or leave it disconnected. Node-RED error logs and Catch handling remain enabled. A timeout alarm is sent after one poll exhausts its retries (retries 2 means 3 attempts), not after multiple bad polls. Permanent Modbus exceptions report immediately. Each failed poll emits one alarm and one debug message.
|
|
687
|
+
|
|
688
|
+
Failed requests output a named numeric alarm on output 1, for example {"b1 RF ID2 Not communicating — check power/RF; disable if out of service":1}. Details remain in msg.modbus, including ok, code, error and fault. Only final failures are reported; a successful retry or intentional shutdown does not emit an alarm. Successful reads resume normal data; no automatic alarm-clear value is sent.
|
|
689
|
+
|
|
676
690
|
Each numeric decoder row starts compact with None (unchanged). Choose Math or Range to show the inline settings and enable scaling. Selecting None hides both controls and disables both calculations, while retaining the entered settings. Range maps input low/high to output low/high using a straight line; for example 4–20 maps to 0–100, with 12 mapping to 50. Use raw register endpoints, not assumed electrical units. Input endpoints must differ. Reversed ranges and negative/decimal endpoints are supported. Clamp optionally limits output to the configured range. Existing rows without a scaling mode retain their Math expression.
|
|
677
691
|
</script>
|
package/smithtek-mako-rf.js
CHANGED
|
@@ -78,7 +78,7 @@ function toNum(v, fallback) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Nested property setter using delimiter "=>"
|
|
81
|
-
function setObjectProperty(obj, path, value, delimiter) {
|
|
81
|
+
function setObjectProperty(obj, path, value, delimiter, prefix) {
|
|
82
82
|
if (!path || typeof path !== "string") {
|
|
83
83
|
obj[path] = value;
|
|
84
84
|
return;
|
|
@@ -91,6 +91,9 @@ function toNum(v, fallback) {
|
|
|
91
91
|
if (!parts.length) return;
|
|
92
92
|
|
|
93
93
|
if (parts.some(k => ["__proto__","prototype","constructor"].includes(k))) throw new Error("Invalid decoder property path");
|
|
94
|
+
// Prefix is literal text, even if it contains the nesting delimiter.
|
|
95
|
+
const labelPrefix = String(prefix == null ? '' : prefix).trim();
|
|
96
|
+
if (labelPrefix) parts[0] = labelPrefix + ' ' + parts[0];
|
|
94
97
|
let ref = obj;
|
|
95
98
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
96
99
|
const k = parts[i];
|
|
@@ -252,7 +255,7 @@ function toNum(v, fallback) {
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
function decodeWithItems(regArray, items) {
|
|
258
|
+
function decodeWithItems(regArray, items, prefix) {
|
|
256
259
|
const decoded = {};
|
|
257
260
|
if (!Array.isArray(regArray) || !Array.isArray(items) || !items.length) return decoded;
|
|
258
261
|
|
|
@@ -305,7 +308,7 @@ function toNum(v, fallback) {
|
|
|
305
308
|
// Digital points are numeric at the output boundary. Keep the internal
|
|
306
309
|
// boolean until here so saved math/masks cannot change a bit into another value.
|
|
307
310
|
if (typeof val === 'boolean') val = val ? 1 : 0;
|
|
308
|
-
setObjectProperty(decoded, name, val, "=>");
|
|
311
|
+
setObjectProperty(decoded, name, val, "=>", prefix);
|
|
309
312
|
}
|
|
310
313
|
|
|
311
314
|
return decoded;
|
|
@@ -370,6 +373,45 @@ function toNum(v, fallback) {
|
|
|
370
373
|
return req;
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
function faultInfo(err, config, cfg, req) {
|
|
377
|
+
const code = err.code || (err.modbusCode ? 'MODBUS_EXCEPTION_'+err.modbusCode : 'ECOMM');
|
|
378
|
+
const message = String(err.message || err);
|
|
379
|
+
const labels = {ECONFIG:'Configuration error',EDECODE:'Decode error',EQUEUEFULL:'Queue full',
|
|
380
|
+
EQUEUEEXPIRED:'Queue expired',EOFFLINE:'Offline',EPORTBUSY:'Port busy',ECLOSE:'Serial close failure',
|
|
381
|
+
EDISCONNECT:'Serial disconnected',EIO:'Serial error',ENOENT:'Serial port unavailable',EACCES:'Serial permission denied'};
|
|
382
|
+
let reason = labels[code];
|
|
383
|
+
if (!reason && err.modbusCode) reason = 'Modbus exception '+err.modbusCode;
|
|
384
|
+
if (!reason && (code === 'ETIMEDOUT' || /timed?\s*out|timeout/i.test(message))) reason = 'Timeout';
|
|
385
|
+
if (!reason && code === 'EDEADLINE') reason = /request/i.test(message) ? 'Timeout' : 'Operation deadline';
|
|
386
|
+
if (!reason && /crc/i.test(message)) reason = 'CRC error';
|
|
387
|
+
if (!reason) reason = 'Communication error';
|
|
388
|
+
const port = cfg && cfg.serialPort;
|
|
389
|
+
const channel = {'/dev/ttyAMA2':'RF','/dev/ttyAMA0':'RS485-1','/dev/ttyAMA1':'RS485-2'}[port] || (cfg && cfg.busName) || 'Bus';
|
|
390
|
+
const prefix = String(config.prefix == null ? '' : config.prefix).trim();
|
|
391
|
+
const label = prefix || String(config.name || '').trim();
|
|
392
|
+
const unitid = req ? req.unitid : (config.unitid == null ? 1 : config.unitid);
|
|
393
|
+
const link = channel === 'RF' ? 'RF' : /^RS485/.test(channel) ? 'RS485 wiring' : 'connection';
|
|
394
|
+
const advice = {
|
|
395
|
+
'Timeout':'Not communicating — check power/'+link+'; disable if out of service',
|
|
396
|
+
'Configuration error':'Setup error — check node settings',
|
|
397
|
+
'Decode error':'Data format error — check register map/scaling',
|
|
398
|
+
'Queue full':'Polling overloaded — reduce polling rate',
|
|
399
|
+
'Queue expired':'Polling delayed — reduce polling rate',
|
|
400
|
+
'Offline':'Polling paused — check device availability',
|
|
401
|
+
'Port busy':'Bus in use — check duplicate bus settings',
|
|
402
|
+
'Serial close failure':'Gateway port fault — check bus connection',
|
|
403
|
+
'Serial disconnected':'Gateway disconnected — check bus connection',
|
|
404
|
+
'Serial error':'Gateway port fault — check bus connection',
|
|
405
|
+
'Serial port unavailable':'Gateway port missing — check bus settings',
|
|
406
|
+
'Serial permission denied':'Gateway port blocked — check access permissions',
|
|
407
|
+
'Operation deadline':'Gateway operation stalled — check bus connection',
|
|
408
|
+
'CRC error':'Invalid reply — check '+link+' link'
|
|
409
|
+
};
|
|
410
|
+
const summary = advice[reason] || (err.modbusCode ? 'Request rejected — check function/register settings' : 'Communication fault — check power/'+link);
|
|
411
|
+
const key = [label,channel,'ID'+unitid,summary].filter(Boolean).join(' ');
|
|
412
|
+
return {key,code,message,reason,summary,channel,unitid,prefix,node:config.name || '',timestamp:new Date().toISOString()};
|
|
413
|
+
}
|
|
414
|
+
|
|
373
415
|
function SmithtekMakoRfNode(config) {
|
|
374
416
|
RED.nodes.createNode(this,config);
|
|
375
417
|
const node = this;
|
|
@@ -383,23 +425,41 @@ function toNum(v, fallback) {
|
|
|
383
425
|
const cfg = RED.nodes.getNode(config.bus);
|
|
384
426
|
let req;
|
|
385
427
|
const complete = (err,res) => {
|
|
428
|
+
let failure = err;
|
|
386
429
|
try {
|
|
387
|
-
if(node._makoClosed || (
|
|
388
|
-
|
|
389
|
-
if(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
430
|
+
if(node._makoClosed || (failure && failure.code === 'ECANCELLED')) return;
|
|
431
|
+
let payload;
|
|
432
|
+
if (!failure) {
|
|
433
|
+
try {
|
|
434
|
+
payload = req.mode === 'read' && res && Array.isArray(res.data) ? decodeWithItems(res.data,config.items || [],config.prefix) : res;
|
|
435
|
+
} catch (decodeError) {
|
|
436
|
+
failure = decodeError;
|
|
437
|
+
failure.code = 'EDECODE';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
msg.modbus = {ok:!failure,bus:cfg && cfg.busName,req};
|
|
441
|
+
if(res) msg.modbus.raw = res;
|
|
442
|
+
if(failure) {
|
|
443
|
+
const fault = faultInfo(failure,config,cfg,req);
|
|
444
|
+
msg.modbus.error = fault.message;
|
|
445
|
+
msg.modbus.code = fault.code;
|
|
446
|
+
msg.modbus.fault = fault;
|
|
447
|
+
msg.payload = {[fault.key]:1};
|
|
448
|
+
node.status({fill:failure.code === 'EOFFLINE' ? 'yellow' : 'red',shape:'ring',text:fault.message});
|
|
394
449
|
} else {
|
|
395
|
-
msg.
|
|
396
|
-
msg.payload = req.mode === 'read' && res && Array.isArray(res.data) ? decodeWithItems(res.data,config.items || []) : res;
|
|
450
|
+
msg.payload = payload;
|
|
397
451
|
if(msg._makoRssi != null) {msg.payload[sanitizeKey(node.name || config.name || cfg.busName)+'_rssi_dbm']=msg._makoRssi;delete msg._makoRssi;}
|
|
398
452
|
node.status({fill:'green',shape:'dot',text:'ok uid'+req.unitid});
|
|
399
453
|
}
|
|
400
|
-
|
|
454
|
+
// Cloud output always carries either decoded data or the friendly alarm.
|
|
455
|
+
// The second output contains a separate technical error payload.
|
|
456
|
+
const debugMsg = failure ? Object.assign({}, msg, {
|
|
457
|
+
payload: {ok:false,error:msg.modbus.error,code:msg.modbus.code,req},
|
|
458
|
+
modbus: Object.assign({}, msg.modbus)
|
|
459
|
+
}) : null;
|
|
460
|
+
send([msg, debugMsg]);
|
|
401
461
|
} catch(e) {completeOnce(e);return;}
|
|
402
|
-
finally {completeOnce(
|
|
462
|
+
finally {completeOnce(failure || undefined);}
|
|
403
463
|
};
|
|
404
464
|
try {
|
|
405
465
|
req=requestFor(config,msg);
|
|
@@ -417,7 +477,7 @@ function toNum(v, fallback) {
|
|
|
417
477
|
catch(e){if(e.code === 'ECANCELLED')throw e;msg.rssi_error=e.message;}
|
|
418
478
|
}
|
|
419
479
|
}});
|
|
420
|
-
}catch(err){complete(err);}
|
|
480
|
+
}catch(err){if(!err.code) err.code='ECONFIG';complete(err);}
|
|
421
481
|
});
|
|
422
482
|
node.on('close',(_removed,done)=>{
|
|
423
483
|
node._makoClosed=true;
|