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": "2.9.10",
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
@@ -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 == o,
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
- const scalerRegex = /^\s*([<>!=%]{1,3}|<<|>>>|>>)\s*(-?\d+(\.\d+)?)\s*$/;
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 || s === "0" || s === "1") return null;
212
+ if (!s) return null;
207
213
 
208
- if (!Number.isNaN(Number(s))) {
209
- return { op: "*", val: Number(s) };
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, state32) {
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
- const order = pick32OrderIfNeeded(a, b);
278
- if (order === "ABCD") return bytesABCD(a, b);
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
- const state32 = { order: null };
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, state32);
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") val = val & mask;
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(it.scale);
371
- if (scaler) {
372
- if (scaler.op === "*" && typeof val === "number") val = val * scaler.val;
373
- else if (scaler.op && scalingOps[scaler.op]) val = scalingOps[scaler.op](val, scaler.val);
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 port settings changed, force reconnect
435
+ // If settings changed, force reconnect
429
436
  if (state.connected && state.lastPortKey !== portKey) {
430
437
  state.connected = false;
431
- try {
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 another request is already connecting, wait for it
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
- try {
461
- state.client.close(() => {});
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
- if (s.client) {
816
- try { s.client.close(() => {}); } catch (_e) {}
817
- s.connected = false;
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