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 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
- - Writes use FIFO order. Queue overflow, expiry and superseded polls return explicit errors and complete once.
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 still emit msg.modbus.ok=false and a failure payload. 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithtek-mako-rf",
3
- "version": "3.2.4",
3
+ "version": "3.3.2",
4
4
  "description": "Smithtek dedicated node for communicating with the Mako PLC over RS485 or RF",
5
5
  "keywords": [
6
6
  "node-red",
@@ -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: 1,
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>
@@ -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 || (err && err.code === 'ECANCELLED')) return;
388
- msg.modbus = {ok:!err,bus:cfg && cfg.busName,req};
389
- if(err) {
390
- msg.modbus.error = err.message || String(err);
391
- msg.modbus.code = err.code || (err.modbusCode ? 'MODBUS_EXCEPTION_'+err.modbusCode : 'ECOMM');
392
- msg.payload = {ok:false,error:msg.modbus.error,req};
393
- node.status({fill:err.code === 'EOFFLINE' ? 'yellow' : 'red',shape:'ring',text:msg.modbus.error});
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.modbus.raw = res;
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
- send(msg);
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(err || undefined);}
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;