smithtek-mako-rf 2.9.8 → 3.0.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.
Files changed (2) hide show
  1. package/package.json +25 -25
  2. package/smithtek-mako-rf.js +60 -44
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
- {
2
- "name": "smithtek-mako-rf",
3
- "version": "2.9.8",
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.1",
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
+ }
@@ -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,12 +230,21 @@ 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
 
229
237
  const w = (i) => (regs[i] & 0xffff);
238
+ const isBoolArray = (Array.isArray(regs) && typeof regs[0] === "boolean");
230
239
 
240
+ function coilWord(wordIndex) {
241
+ let out = 0;
242
+ const base = wordIndex * 16;
243
+ for (let b = 0; b < 16; b++) {
244
+ if (regs[base + b]) out |= (1 << b);
245
+ }
246
+ return out >>> 0;
247
+ }
231
248
  // 4 bytes from two 16-bit words in common Modbus orders
232
249
  function bytesABCD(a, b) {
233
250
  return Buffer.from([(a >> 8) & 0xff, a & 0xff, (b >> 8) & 0xff, b & 0xff]);
@@ -242,34 +259,11 @@ state._rssiCleanup = cleanup;
242
259
  return Buffer.from([b & 0xff, (b >> 8) & 0xff, a & 0xff, (a >> 8) & 0xff]);
243
260
  }
244
261
 
245
- function pick32OrderIfNeeded(a, b) {
246
- if (state32.order) return state32.order;
247
-
248
- const candidates = [
249
- { o: "ABCD", v: bytesABCD(a, b).readFloatBE(0) },
250
- { o: "CDAB", v: bytesCDAB(a, b).readFloatBE(0) },
251
- { o: "BADC", v: bytesBADC(a, b).readFloatBE(0) },
252
- { o: "DCBA", v: bytesDCBA(a, b).readFloatBE(0) },
253
- ];
254
-
255
- // Sane heuristic: finite, not denormal-tiny, not insane huge
256
- for (const c of candidates) {
257
- if (Number.isFinite(c.v) && Math.abs(c.v) > 1e-6 && Math.abs(c.v) < 1e9) {
258
- state32.order = c.o;
259
- return c.o;
260
- }
261
- }
262
262
 
263
- state32.order = "ABCD";
264
- return state32.order;
265
- }
266
263
 
267
264
  function get32Bytes(a, b) {
268
- const order = pick32OrderIfNeeded(a, b);
269
- if (order === "ABCD") return bytesABCD(a, b);
270
- if (order === "CDAB") return bytesCDAB(a, b);
271
- if (order === "BADC") return bytesBADC(a, b);
272
- return bytesDCBA(a, b);
265
+ // LOCKED CDAB (matches your confirmed raw->decoded results)
266
+ return bytesCDAB(a, b);
273
267
  }
274
268
 
275
269
  switch (t) {
@@ -302,12 +296,25 @@ state._rssiCleanup = cleanup;
302
296
  return buf.readFloatBE(0);
303
297
  }
304
298
 
305
- case "digital to unsigned binary encoder":
306
- return w(regIndex) >>> 0;
299
+ case "digital to unsigned binary encoder": {
300
+ const word = w(regIndex) >>> 0;
301
+
302
+ // If the UI provided a bit (0–15), return that bit as TRUE/FALSE
303
+ const bit = clampInt(offsetBit, 0, 15, 0);
304
+ return ((word >> bit) & 1) === 1;
305
+ }
306
+
307
307
 
308
- // optional internal bool support
309
308
  case "bool": {
310
309
  const bit = clampInt(offsetBit, 0, 15, 0);
310
+
311
+ // If this is FC1/FC2 data (boolean array), read bit directly
312
+ if (isBoolArray) {
313
+ const idx = (regIndex * 16) + bit;
314
+ return !!regs[idx];
315
+ }
316
+
317
+ // Otherwise treat as 16-bit register
311
318
  return ((w(regIndex) >> bit) & 1) === 1;
312
319
  }
313
320
 
@@ -321,7 +328,7 @@ state._rssiCleanup = cleanup;
321
328
  if (!Array.isArray(regArray) || !Array.isArray(items) || !items.length) return decoded;
322
329
 
323
330
  // Auto-detect 32-bit order on first 32-bit value, then reuse for all
324
- const state32 = { order: null };
331
+ // const state32 = { order: null };
325
332
 
326
333
  for (let i = 0; i < items.length; i++) {
327
334
  const it = items[i] || {};
@@ -337,18 +344,27 @@ state._rssiCleanup = cleanup;
337
344
 
338
345
  let val;
339
346
  try {
340
- val = readTyped(regArray, type, byteOffset, offsetBit, state32);
347
+ val = readTyped(regArray, type, byteOffset, offsetBit);
341
348
  } catch (_e) {
342
349
  continue;
343
350
  }
344
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)
345
359
  const mask = parseMask(it.mask);
346
- 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
+ }
347
363
 
348
- const scaler = parseScaler(it.scale);
349
- if (scaler) {
350
- if (scaler.op === "*" && typeof val === "number") val = val * scaler.val;
351
- 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);
352
368
  }
353
369
 
354
370
  setObjectProperty(decoded, name, val, "=>");