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.
- package/package.json +25 -25
- package/smithtek-mako-rf.js +60 -44
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.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
|
+
}
|
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,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
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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(
|
|
349
|
-
if (scaler) {
|
|
350
|
-
|
|
351
|
-
|
|
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, "=>");
|