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 +1 -1
- package/smithtek-mako-rf.html +49 -1
- package/smithtek-mako-rf.js +179 -3
package/package.json
CHANGED
package/smithtek-mako-rf.html
CHANGED
|
@@ -249,7 +249,14 @@
|
|
|
249
249
|
$("#st-row-parity").toggle(!isRF);
|
|
250
250
|
$("#st-row-stopBits").toggle(!isRF);
|
|
251
251
|
|
|
252
|
-
//
|
|
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.
|
package/smithtek-mako-rf.js
CHANGED
|
@@ -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
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
|