smithtek-mako-rf 2.9.10 → 3.0.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/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "smithtek-mako-rf",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Smithtek dedicated node for communicating with the Mako PLC over RS485 or RF",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"node-red",
|
|
7
|
-
"smithtek",
|
|
8
|
-
"modbus",
|
|
9
|
-
"rtu",
|
|
10
|
-
"rs485",
|
|
11
|
-
"lora"
|
|
12
|
-
],
|
|
13
|
-
"license": "GPL-3.0-only",
|
|
14
|
-
"author": "Smithtek",
|
|
15
|
-
"homepage": "https://www.smithtek.com.au",
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"modbus-serial": "8.0.23-no-serial-port",
|
|
18
|
-
"serialport": "10.4.0"
|
|
19
|
-
},
|
|
20
|
-
"node-red": {
|
|
21
|
-
"nodes": {
|
|
22
|
-
"smithtek-mako-rf": "smithtek-mako-rf.js"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "smithtek-mako-rf",
|
|
3
|
+
"version": "3.0.2",
|
|
4
|
+
"description": "Smithtek dedicated node for communicating with the Mako PLC over RS485 or RF",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"smithtek",
|
|
8
|
+
"modbus",
|
|
9
|
+
"rtu",
|
|
10
|
+
"rs485",
|
|
11
|
+
"lora"
|
|
12
|
+
],
|
|
13
|
+
"license": "GPL-3.0-only",
|
|
14
|
+
"author": "Smithtek",
|
|
15
|
+
"homepage": "https://www.smithtek.com.au",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"modbus-serial": "8.0.23-no-serial-port",
|
|
18
|
+
"serialport": "10.4.0"
|
|
19
|
+
},
|
|
20
|
+
"node-red": {
|
|
21
|
+
"nodes": {
|
|
22
|
+
"smithtek-mako-rf": "smithtek-mako-rf.js"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
package/smithtek-mako-rf.js
CHANGED
|
@@ -185,12 +185,17 @@ state._rssiCleanup = cleanup;
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// Scale supports:
|
|
188
|
-
// - numeric => multiply
|
|
189
|
-
// - operators like ">100", "<<2", "==1", etc.
|
|
188
|
+
// - numeric => multiply (including 0)
|
|
189
|
+
// - operators like "*2", "/10", "+5", "-7", ">100", "<<2", "==1", etc.
|
|
190
190
|
const scalingOps = {
|
|
191
|
+
"*": (v, o) => v * o,
|
|
192
|
+
"/": (v, o) => v / o,
|
|
193
|
+
"+": (v, o) => v + o,
|
|
194
|
+
"-": (v, o) => v - o,
|
|
195
|
+
|
|
191
196
|
">": (v, o) => v > o,
|
|
192
197
|
"<": (v, o) => v < o,
|
|
193
|
-
"==": (v, o) => v
|
|
198
|
+
"==": (v, o) => v === o,
|
|
194
199
|
"!=": (v, o) => v != o,
|
|
195
200
|
"%": (v, o) => v % o,
|
|
196
201
|
"<<": (v, o) => v << o,
|
|
@@ -198,16 +203,19 @@ state._rssiCleanup = cleanup;
|
|
|
198
203
|
">>>": (v, o) => v >>> o,
|
|
199
204
|
};
|
|
200
205
|
|
|
201
|
-
|
|
206
|
+
// allow optional leading operator, otherwise a plain number
|
|
207
|
+
const scalerRegex = /^\s*([*\/+\-]|[<>!=%]{1,3}|<<|>>>|>>)\s*(-?\d+(\.\d+)?)\s*$/;
|
|
202
208
|
|
|
203
209
|
function parseScaler(scale) {
|
|
204
210
|
if (scale == null) return null;
|
|
205
211
|
const s = String(scale).trim();
|
|
206
|
-
if (!s
|
|
212
|
+
if (!s) return null;
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
214
|
+
// Plain numeric means multiply (0 is valid!)
|
|
215
|
+
const asNum = Number(s);
|
|
216
|
+
if (!Number.isNaN(asNum)) return { op: "*", val: asNum };
|
|
217
|
+
|
|
218
|
+
// Operator form: "*2" "/10" "+5" etc
|
|
211
219
|
const m = s.match(scalerRegex);
|
|
212
220
|
if (m && m[1] && !Number.isNaN(Number(m[2])) && scalingOps[m[1]]) {
|
|
213
221
|
return { op: m[1], val: Number(m[2]) };
|
|
@@ -222,7 +230,7 @@ state._rssiCleanup = cleanup;
|
|
|
222
230
|
// Read a typed value using your Mako dropdown names.
|
|
223
231
|
// Offset is BYTES (0,2,4...) like your original decoder.
|
|
224
232
|
// For 32-bit values, auto-detect word/byte order once per decode pass.
|
|
225
|
-
function readTyped(regs, type, byteOffset, offsetBit
|
|
233
|
+
function readTyped(regs, type, byteOffset, offsetBit) {
|
|
226
234
|
const t = String(type || "").toLowerCase();
|
|
227
235
|
const regIndex = Math.floor(byteOffset / 2);
|
|
228
236
|
|
|
@@ -251,34 +259,11 @@ state._rssiCleanup = cleanup;
|
|
|
251
259
|
return Buffer.from([b & 0xff, (b >> 8) & 0xff, a & 0xff, (a >> 8) & 0xff]);
|
|
252
260
|
}
|
|
253
261
|
|
|
254
|
-
function pick32OrderIfNeeded(a, b) {
|
|
255
|
-
if (state32.order) return state32.order;
|
|
256
|
-
|
|
257
|
-
const candidates = [
|
|
258
|
-
{ o: "ABCD", v: bytesABCD(a, b).readFloatBE(0) },
|
|
259
|
-
{ o: "CDAB", v: bytesCDAB(a, b).readFloatBE(0) },
|
|
260
|
-
{ o: "BADC", v: bytesBADC(a, b).readFloatBE(0) },
|
|
261
|
-
{ o: "DCBA", v: bytesDCBA(a, b).readFloatBE(0) },
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
// Sane heuristic: finite, not denormal-tiny, not insane huge
|
|
265
|
-
for (const c of candidates) {
|
|
266
|
-
if (Number.isFinite(c.v) && Math.abs(c.v) > 1e-6 && Math.abs(c.v) < 1e9) {
|
|
267
|
-
state32.order = c.o;
|
|
268
|
-
return c.o;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
262
|
|
|
272
|
-
state32.order = "ABCD";
|
|
273
|
-
return state32.order;
|
|
274
|
-
}
|
|
275
263
|
|
|
276
264
|
function get32Bytes(a, b) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (order === "CDAB") return bytesCDAB(a, b);
|
|
280
|
-
if (order === "BADC") return bytesBADC(a, b);
|
|
281
|
-
return bytesDCBA(a, b);
|
|
265
|
+
// LOCKED CDAB (matches your confirmed raw->decoded results)
|
|
266
|
+
return bytesCDAB(a, b);
|
|
282
267
|
}
|
|
283
268
|
|
|
284
269
|
switch (t) {
|
|
@@ -343,7 +328,7 @@ state._rssiCleanup = cleanup;
|
|
|
343
328
|
if (!Array.isArray(regArray) || !Array.isArray(items) || !items.length) return decoded;
|
|
344
329
|
|
|
345
330
|
// Auto-detect 32-bit order on first 32-bit value, then reuse for all
|
|
346
|
-
|
|
331
|
+
// const state32 = { order: null };
|
|
347
332
|
|
|
348
333
|
for (let i = 0; i < items.length; i++) {
|
|
349
334
|
const it = items[i] || {};
|
|
@@ -359,18 +344,27 @@ state._rssiCleanup = cleanup;
|
|
|
359
344
|
|
|
360
345
|
let val;
|
|
361
346
|
try {
|
|
362
|
-
val = readTyped(regArray, type, byteOffset, offsetBit
|
|
347
|
+
val = readTyped(regArray, type, byteOffset, offsetBit);
|
|
363
348
|
} catch (_e) {
|
|
364
349
|
continue;
|
|
365
350
|
}
|
|
366
351
|
|
|
352
|
+
// Read scale from multiple possible editor field names (prevents silent "never scales")
|
|
353
|
+
const scaleRaw = (it.scale != null) ? it.scale
|
|
354
|
+
: (it.scaler != null) ? it.scaler
|
|
355
|
+
: (it.scaleFactor != null) ? it.scaleFactor
|
|
356
|
+
: null;
|
|
357
|
+
|
|
358
|
+
// Apply mask ONLY if value is an integer (otherwise it will trash floats)
|
|
367
359
|
const mask = parseMask(it.mask);
|
|
368
|
-
if (mask && typeof val === "number"
|
|
360
|
+
if (mask && typeof val === "number" && Number.isInteger(val)) {
|
|
361
|
+
val = (val & mask) >>> 0; // keep it sane as unsigned
|
|
362
|
+
}
|
|
369
363
|
|
|
370
|
-
const scaler = parseScaler(
|
|
371
|
-
if (scaler) {
|
|
372
|
-
|
|
373
|
-
|
|
364
|
+
const scaler = parseScaler(scaleRaw);
|
|
365
|
+
if (scaler && typeof val === "number") {
|
|
366
|
+
const fn = scalingOps[scaler.op] || scalingOps["*"];
|
|
367
|
+
val = fn(val, scaler.val);
|
|
374
368
|
}
|
|
375
369
|
|
|
376
370
|
setObjectProperty(decoded, name, val, "=>");
|
|
@@ -417,25 +411,37 @@ state._rssiCleanup = cleanup;
|
|
|
417
411
|
return [5, 6, 15, 16].includes(fc) ? fc : 5;
|
|
418
412
|
}
|
|
419
413
|
|
|
414
|
+
async function closeClientSafe(state) {
|
|
415
|
+
// Best-effort: close and wait a moment for the fd to actually drop
|
|
416
|
+
try {
|
|
417
|
+
await new Promise((resolve) => {
|
|
418
|
+
try {
|
|
419
|
+
state.client.close(() => resolve());
|
|
420
|
+
} catch (_e) {
|
|
421
|
+
resolve();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
} catch (_e) {}
|
|
425
|
+
|
|
426
|
+
// tiny settle time helps on Pi serial
|
|
427
|
+
await sleep(50);
|
|
428
|
+
}
|
|
429
|
+
|
|
420
430
|
async function connectIfNeeded(cfg, state) {
|
|
421
431
|
if (!cfg) throw new Error("Bus config missing");
|
|
422
432
|
|
|
423
|
-
// NOTE: we will hard-lock /dev/ttyAMA2 later (PassPort mode)
|
|
424
|
-
// if (process.env.SMITHTEK_PASSPORT === "1") cfg.serialPort = "/dev/ttyAMA2";
|
|
425
|
-
|
|
426
433
|
const portKey = makePortKey(cfg);
|
|
427
434
|
|
|
428
|
-
// If
|
|
435
|
+
// If settings changed, force reconnect
|
|
429
436
|
if (state.connected && state.lastPortKey !== portKey) {
|
|
430
437
|
state.connected = false;
|
|
431
|
-
|
|
432
|
-
state.client.close(() => {});
|
|
433
|
-
} catch (_e) {}
|
|
438
|
+
await closeClientSafe(state);
|
|
434
439
|
}
|
|
435
440
|
|
|
441
|
+
// Already connected
|
|
436
442
|
if (state.connected) return;
|
|
437
443
|
|
|
438
|
-
// If
|
|
444
|
+
// If already connecting, wait for that attempt to finish
|
|
439
445
|
if (state.connecting) {
|
|
440
446
|
for (let i = 0; i < 200; i++) {
|
|
441
447
|
if (state.connected) return;
|
|
@@ -457,10 +463,10 @@ state._rssiCleanup = cleanup;
|
|
|
457
463
|
parity: cfg.parity || "none",
|
|
458
464
|
};
|
|
459
465
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
} catch (_e) {}
|
|
466
|
+
// Always close cleanly before opening (and WAIT for it)
|
|
467
|
+
await closeClientSafe(state);
|
|
463
468
|
|
|
469
|
+
// Open
|
|
464
470
|
await state.client.connectRTUBuffered(port, options);
|
|
465
471
|
|
|
466
472
|
// UI uses seconds; library uses ms
|
|
@@ -805,18 +811,15 @@ try {
|
|
|
805
811
|
if (busCfg) {
|
|
806
812
|
const s = BUS.get(busCfg.id);
|
|
807
813
|
if (s) {
|
|
808
|
-
// NEW: stop any in-flight queue processor from the old deploy
|
|
809
814
|
s.queue = [];
|
|
810
815
|
s.busy = false;
|
|
811
816
|
|
|
812
817
|
if (typeof s._rssiCleanup === "function") {
|
|
813
818
|
try { s._rssiCleanup(); } catch (_e) {}
|
|
814
819
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
s.client = new (require("modbus-serial"))();
|
|
819
|
-
}
|
|
820
|
+
|
|
821
|
+
s.connected = false;
|
|
822
|
+
try { s.client.close(() => {}); } catch (_e) {}
|
|
820
823
|
}
|
|
821
824
|
}
|
|
822
825
|
} catch (_e) {}
|
|
Binary file
|
|
Binary file
|