smithtek-mako-rf 2.8.0 → 2.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithtek-mako-rf",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "Smithtek dedicated node for communicating with the Mako PLC over RS485 or RF",
5
5
  "keywords": [
6
6
  "node-red",
@@ -249,7 +249,14 @@
249
249
  $("#st-row-parity").toggle(!isRF);
250
250
  $("#st-row-stopBits").toggle(!isRF);
251
251
 
252
- // Optional: force defaults for RF (still stored, just not user-adjustable)
252
+ // show RSSI options ONLY for RF
253
+ $("#st-rssi-wrap").toggle(isRF);
254
+
255
+ // Optional: if not RF, force RSSI off so it can't be accidentally enabled
256
+ if (!isRF) {
257
+ $("#node-config-input-appendRssi").prop("checked", false);
258
+ }
259
+
253
260
  if (isRF) {
254
261
  $("#node-config-input-baudRate").val(9600);
255
262
  $("#node-config-input-parity").val("none");
@@ -264,6 +271,9 @@
264
271
  category: "config",
265
272
  defaults: {
266
273
  busName: { value: "smithtek mako rf", required: true },
274
+ appendRssi: { value: false },
275
+ rssiGuardMs: { value: 10, validate: RED.validators.number() },
276
+ rssiTimeoutMs: { value: 100, validate: RED.validators.number() },
267
277
 
268
278
  // Dropdown only: RS485-1 (/dev/ttyAMA0), RS485-2 (/dev/ttyAMA1), RF (/dev/ttyAMA2)
269
279
  serialPort: { value: "/dev/ttyAMA2", required: true },
@@ -398,19 +408,57 @@
398
408
 
399
409
  <hr />
400
410
 
411
+ <div id="st-rssi-wrap">
412
+
413
+ <div class="form-row">
414
+ <label for="node-config-input-appendRssi"><i class="fa fa-signal"></i> RSSI</label>
415
+ <input type="checkbox" id="node-config-input-appendRssi" style="width:auto;">
416
+ <span style="margin-left:8px; font-size:12px; color:#888;">
417
+ (Tick on RSSI diagnostic in dBi, using this feeature slows down RF polling by 30%.)
418
+ </span>
419
+ </div>
420
+
421
+ <div class="form-row">
422
+ <label for="node-config-input-rssiGuardMs"><i class="fa fa-clock-o"></i> RSSI guard (ms)</label>
423
+ <input type="number" id="node-config-input-rssiGuardMs" min="0" style="width:140px;">
424
+ <span style="margin-left:8px; font-size:12px; color:#888;">
425
+ (The RSSI guard is the time between the Modbus poll and the RSSI read from the LoRa module 5 to 10ms is ideal)
426
+ </span>
427
+ </div>
428
+
429
+ <div class="form-row">
430
+ <label for="node-config-input-rssiTimeoutMs"><i class="fa fa-hourglass-end"></i> RSSI timeout (ms)</label>
431
+ <input type="number" id="node-config-input-rssiTimeoutMs" min="10" style="width:140px;">
432
+ <span style="margin-left:8px; font-size:12px; color:#888;">
433
+ (The RSSI timeout is how long the module will be givien to respond with the RSSI)
434
+ </span>
435
+ </div>
436
+
437
+ </div>
438
+
439
+
401
440
  <div class="form-row">
402
441
  <label for="node-config-input-timeout_s"><i class="fa fa-clock-o"></i> Timeout (seconds)</label>
403
442
  <input type="number" id="node-config-input-timeout_s" style="width:140px;">
443
+ <span style="margin-left:8px; font-size:12px; color:#888;">
444
+ (This is your modbus response timeout, set high to ensure you capture slow Mako responses)
445
+ </span>
404
446
  </div>
405
447
 
406
448
  <div class="form-row">
407
449
  <label for="node-config-input-retries"><i class="fa fa-repeat"></i> Retries</label>
408
450
  <input type="number" id="node-config-input-retries" style="width:140px;">
451
+ <span style="margin-left:8px; font-size:12px; color:#888;">
452
+ (Retry will attempt to poll the same device multiple times before it timesout. Ideal for modbus writes)
453
+ </span>
409
454
  </div>
410
455
 
411
456
  <div class="form-row">
412
457
  <label for="node-config-input-gap_s"><i class="fa fa-arrows-h"></i> Gap (seconds)</label>
413
458
  <input type="number" id="node-config-input-gap_s" step="0.1" min="0" style="width:140px;">
459
+ <span style="margin-left:8px; font-size:12px; color:#888;">
460
+ (The gap is the time between each poll, When a device has finished responded the gap will wait then start the next poll)
461
+ </span>
414
462
  </div>
415
463
  <div class="form-tips">
416
464
  Select a channel. RF is for the LoRa long range radio RTU coms.
@@ -28,6 +28,124 @@ module.exports = function (RED) {
28
28
  function sleep(ms) {
29
29
  return new Promise((resolve) => setTimeout(resolve, ms));
30
30
  }
31
+ function sanitizeKey(s) {
32
+ return String(s || "")
33
+ .trim()
34
+ .replace(/\s+/g, "_")
35
+ .replace(/[^\w]/g, "_")
36
+ .replace(/_+/g, "_");
37
+ }
38
+
39
+ function buildRssiCommandBuffer() {
40
+ // AF AF 00 00 AF 80 06 02 00 00 CS 0D 0A
41
+ const data = new Uint8Array(13);
42
+ data[0] = 0xAF; data[1] = 0xAF; data[2] = 0x00; data[3] = 0x00; data[4] = 0xAF;
43
+ data[5] = 0x80;
44
+ data[6] = 0x06; data[7] = 0x02; data[8] = 0x00; data[9] = 0x00;
45
+
46
+ let sum = 0;
47
+ for (let i = 0; i <= 9; i++) sum += data[i];
48
+ data[10] = sum & 0xFF;
49
+
50
+ data[11] = 0x0D; data[12] = 0x0A;
51
+ return Buffer.from(data);
52
+ }
53
+
54
+ function tryParseRssiAck(buf) {
55
+ // ACK is 12 bytes:
56
+ // AF AF 00 00 AF 00 06 01 XX CS 0D 0A
57
+ if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
58
+
59
+ for (let i = 0; i <= buf.length - 12; i++) {
60
+ if (buf[i] === 0xAF && buf[i + 1] === 0xAF) {
61
+ const f = buf.slice(i, i + 12);
62
+
63
+ const ok =
64
+ f[0] === 0xAF && f[1] === 0xAF &&
65
+ f[2] === 0x00 && f[3] === 0x00 &&
66
+ f[4] === 0xAF &&
67
+ f[5] === 0x00 &&
68
+ f[6] === 0x06 &&
69
+ f[7] === 0x01 &&
70
+ f[10] === 0x0D && f[11] === 0x0A;
71
+
72
+ if (!ok) continue;
73
+
74
+ const xx = f[8];
75
+ const rssiDbm = -xx; // Manufacturer rule
76
+ return { rssiDbm, frame: f };
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ async function queryRssiOnSamePort(state, guardMs, timeoutMs) {
83
+ // modbus-serial keeps the underlying serialport here in most builds:
84
+ // state.client._port or state.client._client (varies by version)
85
+ const p0 = state?.client?._port;
86
+ const port =
87
+ (p0 && (p0._client || p0.port || p0._port)) || // unwrap common wrappers
88
+ state?.client?._client ||
89
+ p0;
90
+
91
+ if (!port || typeof port.write !== "function" || typeof port.on !== "function") {
92
+ throw new Error("No underlying serial port handle found (modbus-serial port not accessible)");
93
+ }
94
+
95
+ if (guardMs > 0) await sleep(guardMs);
96
+
97
+ const cmd = buildRssiCommandBuffer();
98
+
99
+ // Optional: flush any buffered junk (some serialport builds support this)
100
+ // try { if (typeof port.flush === "function") port.flush(() => {}); } catch (_e) {}
101
+
102
+ let rx = Buffer.alloc(0);
103
+
104
+ return await new Promise((resolve, reject) => {
105
+ let finished = false;
106
+
107
+ const timer = setTimeout(() => {
108
+ cleanup();
109
+ reject(new Error("RSSI timeout"));
110
+ }, Math.max(10, timeoutMs | 0));
111
+
112
+ function cleanup() {
113
+ if (finished) return;
114
+ finished = true;
115
+ clearTimeout(timer);
116
+ try { port.off("data", onData); } catch (_e) {
117
+ // older serialport uses removeListener
118
+ try { port.removeListener("data", onData); } catch (_e2) {}
119
+ }
120
+ }
121
+
122
+ function onData(chunk) {
123
+ rx = Buffer.concat([rx, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
124
+ const parsed = tryParseRssiAck(rx);
125
+ if (parsed) {
126
+ cleanup();
127
+ resolve(parsed.rssiDbm);
128
+ } else {
129
+ // keep rx from growing forever
130
+ if (rx.length > 512) rx = rx.slice(-128);
131
+ }
132
+ }
133
+
134
+ port.on("data", onData);
135
+
136
+ port.write(cmd, (err) => {
137
+ if (err) {
138
+ cleanup();
139
+ reject(err);
140
+ return;
141
+ }
142
+ // ensure bytes actually leave the building
143
+ try {
144
+ if (typeof port.drain === "function") port.drain(() => {});
145
+ } catch (_e) {}
146
+ });
147
+ });
148
+ }
31
149
 
32
150
  // Nested property setter using delimiter "=>"
33
151
  function setObjectProperty(obj, path, value, delimiter) {
@@ -470,12 +588,43 @@ module.exports = function (RED) {
470
588
  msg.payload = res;
471
589
  }
472
590
 
591
+ // ---- OPTIONAL RSSI APPEND (after successful Modbus) ----
592
+
593
+ try {
594
+ const appendRssi = !!cfg.appendRssi;
595
+ if (appendRssi && req.mode === "read" && cfg.serialPort === "/dev/ttyAMA2") {
596
+ const guardMs = toNum(cfg.rssiGuardMs, 10);
597
+ const timeoutMs = toNum(cfg.rssiTimeoutMs, 100);
598
+
599
+
600
+ const rssiDbm = await queryRssiOnSamePort(state, guardMs, timeoutMs);
601
+
602
+ const nodeKeyBase = sanitizeKey(node.name || busName || "mako_rf");
603
+ const key = `${nodeKeyBase}_rssi_dbm`;
604
+
605
+ if (msg.payload && typeof msg.payload === "object" && !Array.isArray(msg.payload)) {
606
+ msg.payload[key] = rssiDbm;
607
+ } else {
608
+ msg.rssi_dbm = rssiDbm;
609
+ }
610
+ }
611
+ } catch (e) {
612
+ msg.rssi_error = (e && e.message) ? e.message : String(e);
613
+ }
614
+ // -------------------------------------------------------
615
+
473
616
  send(msg);
474
617
  done();
618
+
475
619
  }
476
620
  })().finally(() => {
477
621
  state.busy = false;
622
+ // If something got queued right as we finished, kick the processor again
623
+ if (state.queue.length > 0) {
624
+ scheduleProcess(state, busId);
625
+ }
478
626
  });
627
+
479
628
  }
480
629
 
481
630
  // =========================
@@ -486,6 +635,11 @@ module.exports = function (RED) {
486
635
 
487
636
  this.busName = (n.busName || "mako rf").trim();
488
637
  this.serialPort = n.serialPort || "";
638
+ // ADD THESE HERE:
639
+ this.appendRssi = !!n.appendRssi;
640
+ this.rssiGuardMs = toNum(n.rssiGuardMs, 10);
641
+ this.rssiTimeoutMs = toNum(n.rssiTimeoutMs, 100);
642
+
489
643
  this.baudRate = toNum(n.baudRate, 9600);
490
644
  this.stopBits = toNum(n.stopBits, 1);
491
645
  this.parity = n.parity || "none";
@@ -524,6 +678,10 @@ module.exports = function (RED) {
524
678
  function SmithtekMakoRfNode(config) {
525
679
  RED.nodes.createNode(this, config);
526
680
  const node = this;
681
+ // TEMP: until we add editor UI in the .html
682
+ // node._config_appendRssi = !!config.appendRssi; // set true in flow JSON to enable
683
+ // node._config_rssiGuardMs = toNum(config.rssiGuardMs, 3);
684
+ // node._config_rssiTimeoutMs = toNum(config.rssiTimeoutMs, 50);
527
685
 
528
686
  node.on("input", function (msg, send, done) {
529
687
  send = send || function () { node.send.apply(node, arguments); };
@@ -578,12 +736,30 @@ module.exports = function (RED) {
578
736
  const maxQ = clampInt(busCfg.maxQueue, 1, 1000, 50);
579
737
 
580
738
  // If queue is full, drop NEW requests (keeps fairness for the ones already queued)
739
+ const qItem = { node, msg, send, done, req, items };
740
+ const isWrite = (req.mode === "write");
741
+
742
+ // If queue is full:
743
+ // - NEVER drop writes
744
+ // - Drop one pending READ to make room
581
745
  if (state.queue.length >= maxQ) {
582
- try { done(); } catch (_e) {}
583
- return;
746
+ if (isWrite) {
747
+ const idx = state.queue.findIndex(it => it && it.req && it.req.mode === "read");
748
+ if (idx >= 0) state.queue.splice(idx, 1);
749
+ else {
750
+ try { done(); } catch (_e) {}
751
+ return;
752
+ }
753
+ } else {
754
+ try { done(); } catch (_e) {}
755
+ return;
756
+ }
584
757
  }
585
758
 
586
- state.queue.push({ node, msg, send, done, req, items });
759
+ // PRIORITY: writes go to the FRONT
760
+ if (isWrite) state.queue.unshift(qItem);
761
+ else state.queue.push(qItem);
762
+
587
763
  scheduleProcess(state, busCfg.id);
588
764
 
589
765