smithtek-mako-rf 0.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/README.md +190 -0
- package/common-functions.js +190 -0
- package/icon.png +0 -0
- package/icons/icon.png +0 -0
- package/package.json +24 -0
- package/smithtek-mako-rf.html +552 -0
- package/smithtek-mako-rf.js +581 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
// smithtek-mako-rf.js
|
|
2
|
+
module.exports = function (RED) {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const ModbusRTU = require("modbus-serial");
|
|
6
|
+
|
|
7
|
+
// Shared bus state across all node instances (keyed by config node id)
|
|
8
|
+
const BUS = new Map();
|
|
9
|
+
|
|
10
|
+
function toNum(v, fallback) {
|
|
11
|
+
const n = Number(v);
|
|
12
|
+
return Number.isFinite(n) ? n : fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toInt(v, fallback) {
|
|
16
|
+
const n = parseInt(v, 10);
|
|
17
|
+
return Number.isFinite(n) ? n : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clampInt(v, min, max, fallback) {
|
|
21
|
+
let n = toInt(v, fallback);
|
|
22
|
+
if (!Number.isFinite(n)) n = fallback;
|
|
23
|
+
if (n < min) n = min;
|
|
24
|
+
if (n > max) n = max;
|
|
25
|
+
return n;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Nested property setter using delimiter "=>"
|
|
33
|
+
function setObjectProperty(obj, path, value, delimiter) {
|
|
34
|
+
if (!path || typeof path !== "string") {
|
|
35
|
+
obj[path] = value;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const parts = path
|
|
39
|
+
.split(delimiter || "=>")
|
|
40
|
+
.map((s) => s.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
|
|
43
|
+
if (!parts.length) return;
|
|
44
|
+
|
|
45
|
+
let ref = obj;
|
|
46
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
47
|
+
const k = parts[i];
|
|
48
|
+
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
|
49
|
+
ref = ref[k];
|
|
50
|
+
}
|
|
51
|
+
ref[parts[parts.length - 1]] = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mask supports hex "0x0F", decimal "15", or blank
|
|
55
|
+
function parseMask(mask) {
|
|
56
|
+
if (mask == null) return 0;
|
|
57
|
+
const s = String(mask).trim();
|
|
58
|
+
if (!s) return 0;
|
|
59
|
+
if (s.startsWith("0x") || s.startsWith("0X")) {
|
|
60
|
+
const n = parseInt(s, 16);
|
|
61
|
+
return Number.isFinite(n) ? n : 0;
|
|
62
|
+
}
|
|
63
|
+
const n = parseInt(s, 10);
|
|
64
|
+
return Number.isFinite(n) ? n : 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Scale supports:
|
|
68
|
+
// - numeric => multiply
|
|
69
|
+
// - operators like ">100", "<<2", "==1", etc.
|
|
70
|
+
const scalingOps = {
|
|
71
|
+
">": (v, o) => v > o,
|
|
72
|
+
"<": (v, o) => v < o,
|
|
73
|
+
"==": (v, o) => v == o,
|
|
74
|
+
"!=": (v, o) => v != o,
|
|
75
|
+
"%": (v, o) => v % o,
|
|
76
|
+
"<<": (v, o) => v << o,
|
|
77
|
+
">>": (v, o) => v >> o,
|
|
78
|
+
">>>": (v, o) => v >>> o,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const scalerRegex = /^\s*([<>!=%]{1,3}|<<|>>>|>>)\s*(-?\d+(\.\d+)?)\s*$/;
|
|
82
|
+
|
|
83
|
+
function parseScaler(scale) {
|
|
84
|
+
if (scale == null) return null;
|
|
85
|
+
const s = String(scale).trim();
|
|
86
|
+
if (!s || s === "0" || s === "1") return null;
|
|
87
|
+
|
|
88
|
+
if (!Number.isNaN(Number(s))) {
|
|
89
|
+
return { op: "*", val: Number(s) };
|
|
90
|
+
}
|
|
91
|
+
const m = s.match(scalerRegex);
|
|
92
|
+
if (m && m[1] && !Number.isNaN(Number(m[2])) && scalingOps[m[1]]) {
|
|
93
|
+
return { op: m[1], val: Number(m[2]) };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ==========================
|
|
99
|
+
// Decoder (auto 32-bit order)
|
|
100
|
+
// ==========================
|
|
101
|
+
|
|
102
|
+
// Read a typed value using your Mako dropdown names.
|
|
103
|
+
// Offset is BYTES (0,2,4...) like your original decoder.
|
|
104
|
+
// For 32-bit values, auto-detect word/byte order once per decode pass.
|
|
105
|
+
function readTyped(regs, type, byteOffset, offsetBit, state32) {
|
|
106
|
+
const t = String(type || "").toLowerCase();
|
|
107
|
+
const regIndex = Math.floor(byteOffset / 2);
|
|
108
|
+
|
|
109
|
+
const w = (i) => (regs[i] & 0xffff);
|
|
110
|
+
|
|
111
|
+
// 4 bytes from two 16-bit words in common Modbus orders
|
|
112
|
+
function bytesABCD(a, b) {
|
|
113
|
+
return Buffer.from([(a >> 8) & 0xff, a & 0xff, (b >> 8) & 0xff, b & 0xff]);
|
|
114
|
+
}
|
|
115
|
+
function bytesCDAB(a, b) {
|
|
116
|
+
return Buffer.from([(b >> 8) & 0xff, b & 0xff, (a >> 8) & 0xff, a & 0xff]);
|
|
117
|
+
}
|
|
118
|
+
function bytesBADC(a, b) {
|
|
119
|
+
return Buffer.from([a & 0xff, (a >> 8) & 0xff, b & 0xff, (b >> 8) & 0xff]);
|
|
120
|
+
}
|
|
121
|
+
function bytesDCBA(a, b) {
|
|
122
|
+
return Buffer.from([b & 0xff, (b >> 8) & 0xff, a & 0xff, (a >> 8) & 0xff]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pick32OrderIfNeeded(a, b) {
|
|
126
|
+
if (state32.order) return state32.order;
|
|
127
|
+
|
|
128
|
+
const candidates = [
|
|
129
|
+
{ o: "ABCD", v: bytesABCD(a, b).readFloatBE(0) },
|
|
130
|
+
{ o: "CDAB", v: bytesCDAB(a, b).readFloatBE(0) },
|
|
131
|
+
{ o: "BADC", v: bytesBADC(a, b).readFloatBE(0) },
|
|
132
|
+
{ o: "DCBA", v: bytesDCBA(a, b).readFloatBE(0) },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Sane heuristic: finite, not denormal-tiny, not insane huge
|
|
136
|
+
for (const c of candidates) {
|
|
137
|
+
if (Number.isFinite(c.v) && Math.abs(c.v) > 1e-6 && Math.abs(c.v) < 1e9) {
|
|
138
|
+
state32.order = c.o;
|
|
139
|
+
return c.o;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
state32.order = "ABCD";
|
|
144
|
+
return state32.order;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function get32Bytes(a, b) {
|
|
148
|
+
const order = pick32OrderIfNeeded(a, b);
|
|
149
|
+
if (order === "ABCD") return bytesABCD(a, b);
|
|
150
|
+
if (order === "CDAB") return bytesCDAB(a, b);
|
|
151
|
+
if (order === "BADC") return bytesBADC(a, b);
|
|
152
|
+
return bytesDCBA(a, b);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
switch (t) {
|
|
156
|
+
case "16 bit integer": {
|
|
157
|
+
const v = w(regIndex);
|
|
158
|
+
return v & 0x8000 ? v - 0x10000 : v;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "16 bit unsigned":
|
|
162
|
+
return w(regIndex);
|
|
163
|
+
|
|
164
|
+
case "32 bit integer": {
|
|
165
|
+
const a = w(regIndex),
|
|
166
|
+
b = w(regIndex + 1);
|
|
167
|
+
const buf = get32Bytes(a, b);
|
|
168
|
+
return buf.readInt32BE(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "32 bit unsigned": {
|
|
172
|
+
const a = w(regIndex),
|
|
173
|
+
b = w(regIndex + 1);
|
|
174
|
+
const buf = get32Bytes(a, b);
|
|
175
|
+
return buf.readUInt32BE(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case "32 bit float": {
|
|
179
|
+
const a = w(regIndex),
|
|
180
|
+
b = w(regIndex + 1);
|
|
181
|
+
const buf = get32Bytes(a, b);
|
|
182
|
+
return buf.readFloatBE(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case "digital to unsigned binary encoder":
|
|
186
|
+
return w(regIndex) >>> 0;
|
|
187
|
+
|
|
188
|
+
// optional internal bool support
|
|
189
|
+
case "bool": {
|
|
190
|
+
const bit = clampInt(offsetBit, 0, 15, 0);
|
|
191
|
+
return ((w(regIndex) >> bit) & 1) === 1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
default:
|
|
195
|
+
return w(regIndex);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function decodeWithItems(regArray, items) {
|
|
200
|
+
const decoded = {};
|
|
201
|
+
if (!Array.isArray(regArray) || !Array.isArray(items) || !items.length) return decoded;
|
|
202
|
+
|
|
203
|
+
// Auto-detect 32-bit order on first 32-bit value, then reuse for all
|
|
204
|
+
const state32 = { order: null };
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < items.length; i++) {
|
|
207
|
+
const it = items[i] || {};
|
|
208
|
+
const name =
|
|
209
|
+
it.name && String(it.name).trim() ? String(it.name).trim() : `item${i + 1}`;
|
|
210
|
+
|
|
211
|
+
const type = it.type || "16 bit integer";
|
|
212
|
+
const byteOffset = clampInt(it.offset, 0, 10000000, 0); // BYTES
|
|
213
|
+
const offsetBit = clampInt(it.offsetbit, 0, 15, 0);
|
|
214
|
+
|
|
215
|
+
const regIndex = Math.floor(byteOffset / 2);
|
|
216
|
+
if (regIndex < 0 || regIndex >= regArray.length) continue;
|
|
217
|
+
|
|
218
|
+
let val;
|
|
219
|
+
try {
|
|
220
|
+
val = readTyped(regArray, type, byteOffset, offsetBit, state32);
|
|
221
|
+
} catch (_e) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const mask = parseMask(it.mask);
|
|
226
|
+
if (mask && typeof val === "number") val = val & mask;
|
|
227
|
+
|
|
228
|
+
const scaler = parseScaler(it.scale);
|
|
229
|
+
if (scaler) {
|
|
230
|
+
if (scaler.op === "*" && typeof val === "number") val = val * scaler.val;
|
|
231
|
+
else if (scaler.op && scalingOps[scaler.op]) val = scalingOps[scaler.op](val, scaler.val);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setObjectProperty(decoded, name, val, "=>");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return decoded;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ==========================
|
|
241
|
+
// Modbus bus + queue engine
|
|
242
|
+
// ==========================
|
|
243
|
+
|
|
244
|
+
function ensureBusState(busConfig) {
|
|
245
|
+
const key = busConfig.id;
|
|
246
|
+
let s = BUS.get(key);
|
|
247
|
+
if (!s) {
|
|
248
|
+
s = {
|
|
249
|
+
id: key,
|
|
250
|
+
client: new ModbusRTU(),
|
|
251
|
+
connecting: false,
|
|
252
|
+
connected: false,
|
|
253
|
+
queue: [],
|
|
254
|
+
busy: false,
|
|
255
|
+
lastPortKey: "",
|
|
256
|
+
};
|
|
257
|
+
BUS.set(key, s);
|
|
258
|
+
}
|
|
259
|
+
return s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getFreshBusConfig(busId) {
|
|
263
|
+
return RED.nodes.getNode(busId) || null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function makePortKey(cfg) {
|
|
267
|
+
return [cfg.serialPort || "", String(cfg.baudRate ?? ""), String(cfg.parity ?? ""), String(cfg.stopBits ?? "")].join(
|
|
268
|
+
"|"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function enforceFcForMode(mode, fc) {
|
|
273
|
+
if (mode === "read") return [1, 2, 3, 4].includes(fc) ? fc : 3;
|
|
274
|
+
return [5, 6, 15, 16].includes(fc) ? fc : 5;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function connectIfNeeded(cfg, state) {
|
|
278
|
+
if (!cfg) throw new Error("Bus config missing");
|
|
279
|
+
|
|
280
|
+
// NOTE: we will hard-lock /dev/ttyAMA2 later (PassPort mode)
|
|
281
|
+
// if (process.env.SMITHTEK_PASSPORT === "1") cfg.serialPort = "/dev/ttyAMA2";
|
|
282
|
+
|
|
283
|
+
const portKey = makePortKey(cfg);
|
|
284
|
+
|
|
285
|
+
// If port settings changed, force reconnect
|
|
286
|
+
if (state.connected && state.lastPortKey !== portKey) {
|
|
287
|
+
state.connected = false;
|
|
288
|
+
try {
|
|
289
|
+
state.client.close(() => {});
|
|
290
|
+
} catch (_e) {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (state.connected) return;
|
|
294
|
+
|
|
295
|
+
// If another request is already connecting, wait for it
|
|
296
|
+
if (state.connecting) {
|
|
297
|
+
for (let i = 0; i < 200; i++) {
|
|
298
|
+
if (state.connected) return;
|
|
299
|
+
if (!state.connecting) break;
|
|
300
|
+
await sleep(50);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
state.connecting = true;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const port = cfg.serialPort;
|
|
308
|
+
if (!port) throw new Error("No serial port configured");
|
|
309
|
+
|
|
310
|
+
const options = {
|
|
311
|
+
baudRate: clampInt(cfg.baudRate, 1200, 921600, 9600),
|
|
312
|
+
dataBits: 8,
|
|
313
|
+
stopBits: clampInt(cfg.stopBits, 1, 2, 1),
|
|
314
|
+
parity: cfg.parity || "none",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
state.client.close(() => {});
|
|
319
|
+
} catch (_e) {}
|
|
320
|
+
|
|
321
|
+
await state.client.connectRTUBuffered(port, options);
|
|
322
|
+
|
|
323
|
+
// UI uses seconds; library uses ms
|
|
324
|
+
const timeout_s = Math.max(1, toNum(cfg.timeout_s, 8));
|
|
325
|
+
state.client.setTimeout(Math.max(1000, Math.floor(timeout_s * 1000)));
|
|
326
|
+
|
|
327
|
+
state.connected = true;
|
|
328
|
+
state.lastPortKey = portKey;
|
|
329
|
+
} finally {
|
|
330
|
+
state.connecting = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function doRequest(cfg, state, req) {
|
|
335
|
+
await connectIfNeeded(cfg, state);
|
|
336
|
+
|
|
337
|
+
state.client.setID(req.unitid);
|
|
338
|
+
|
|
339
|
+
const fc = req.fc;
|
|
340
|
+
const addr = req.address;
|
|
341
|
+
|
|
342
|
+
switch (fc) {
|
|
343
|
+
// READS
|
|
344
|
+
case 1:
|
|
345
|
+
return state.client.readCoils(addr, req.quantity);
|
|
346
|
+
case 2:
|
|
347
|
+
return state.client.readDiscreteInputs(addr, req.quantity);
|
|
348
|
+
case 3:
|
|
349
|
+
return state.client.readHoldingRegisters(addr, req.quantity);
|
|
350
|
+
case 4:
|
|
351
|
+
return state.client.readInputRegisters(addr, req.quantity);
|
|
352
|
+
|
|
353
|
+
// WRITES
|
|
354
|
+
case 5:
|
|
355
|
+
return state.client.writeCoil(addr, !!req.value);
|
|
356
|
+
|
|
357
|
+
case 6:
|
|
358
|
+
return state.client.writeRegister(addr, clampInt(req.value, 0, 65535, 0));
|
|
359
|
+
|
|
360
|
+
case 15: {
|
|
361
|
+
if (!Array.isArray(req.value)) throw new Error("FC15 requires an array value");
|
|
362
|
+
const arr = req.value.map((v) => !!v);
|
|
363
|
+
return state.client.writeCoils(addr, arr);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case 16: {
|
|
367
|
+
if (!Array.isArray(req.value)) throw new Error("FC16 requires an array value");
|
|
368
|
+
const arr = req.value.map((v) => clampInt(v, 0, 65535, 0));
|
|
369
|
+
return state.client.writeRegisters(addr, arr);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
default:
|
|
373
|
+
throw new Error(`Unsupported function code: ${fc}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function scheduleProcess(state, busId) {
|
|
378
|
+
if (state.busy) return;
|
|
379
|
+
state.busy = true;
|
|
380
|
+
|
|
381
|
+
(async () => {
|
|
382
|
+
while (state.queue.length > 0) {
|
|
383
|
+
const item = state.queue.shift();
|
|
384
|
+
const { node, msg, send, done, req, items } = item;
|
|
385
|
+
|
|
386
|
+
const cfg = getFreshBusConfig(busId);
|
|
387
|
+
if (!cfg) {
|
|
388
|
+
node.status({ fill: "red", shape: "ring", text: "bus config missing" });
|
|
389
|
+
msg.payload = { ok: false, error: "Bus config missing", req };
|
|
390
|
+
msg.modbus = { ok: false, error: "Bus config missing", req };
|
|
391
|
+
send(msg);
|
|
392
|
+
done();
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const busName = (cfg.busName || "smithtek mako rf").trim();
|
|
397
|
+
|
|
398
|
+
// retries=0 means exactly 1 attempt
|
|
399
|
+
const retries = clampInt(cfg.retries, 0, 10, 2);
|
|
400
|
+
const totalTries = retries + 1;
|
|
401
|
+
|
|
402
|
+
const timeout_s = Math.max(1, toNum(cfg.timeout_s, 8));
|
|
403
|
+
const gap_s = Math.max(0, toNum(cfg.gap_s, 0));
|
|
404
|
+
|
|
405
|
+
let attempt = 0;
|
|
406
|
+
let lastErr = null;
|
|
407
|
+
let res = null;
|
|
408
|
+
|
|
409
|
+
while (attempt < totalTries) {
|
|
410
|
+
attempt += 1;
|
|
411
|
+
try {
|
|
412
|
+
node.status({
|
|
413
|
+
fill: "blue",
|
|
414
|
+
shape: "dot",
|
|
415
|
+
text: `${busName}: fc${req.fc} uid${req.unitid} @${req.address} (try ${attempt}/${totalTries})`,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// apply timeout every time
|
|
419
|
+
try {
|
|
420
|
+
state.client.setTimeout(Math.max(1000, Math.floor(timeout_s * 1000)));
|
|
421
|
+
} catch (_e) {}
|
|
422
|
+
|
|
423
|
+
res = await doRequest(cfg, state, req);
|
|
424
|
+
lastErr = null;
|
|
425
|
+
break;
|
|
426
|
+
} catch (err) {
|
|
427
|
+
lastErr = err;
|
|
428
|
+
|
|
429
|
+
// Force reconnect next attempt
|
|
430
|
+
state.connected = false;
|
|
431
|
+
try {
|
|
432
|
+
state.client.close(() => {});
|
|
433
|
+
} catch (_e) {}
|
|
434
|
+
|
|
435
|
+
if (attempt < totalTries && gap_s > 0) {
|
|
436
|
+
await sleep(Math.floor(gap_s * 1000));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (gap_s > 0) await sleep(Math.floor(gap_s * 1000));
|
|
442
|
+
|
|
443
|
+
if (lastErr) {
|
|
444
|
+
node.status({ fill: "red", shape: "ring", text: `${busName}: fail fc${req.fc} uid${req.unitid}` });
|
|
445
|
+
|
|
446
|
+
msg.modbus = msg.modbus || {};
|
|
447
|
+
msg.modbus.ok = false;
|
|
448
|
+
msg.modbus.bus = busName;
|
|
449
|
+
msg.modbus.req = req;
|
|
450
|
+
msg.modbus.error = lastErr && lastErr.message ? lastErr.message : String(lastErr);
|
|
451
|
+
|
|
452
|
+
msg.payload = { ok: false, error: msg.modbus.error, req };
|
|
453
|
+
send(msg);
|
|
454
|
+
done();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
node.status({ fill: "green", shape: "dot", text: `${busName}: ok fc${req.fc} uid${req.unitid}` });
|
|
459
|
+
|
|
460
|
+
msg.modbus = msg.modbus || {};
|
|
461
|
+
msg.modbus.ok = true;
|
|
462
|
+
msg.modbus.bus = busName;
|
|
463
|
+
msg.modbus.req = req;
|
|
464
|
+
msg.modbus.raw = res;
|
|
465
|
+
|
|
466
|
+
if (req.mode === "read" && res && Array.isArray(res.data)) {
|
|
467
|
+
msg.payload = decodeWithItems(res.data, items);
|
|
468
|
+
} else {
|
|
469
|
+
msg.payload = res;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
send(msg);
|
|
473
|
+
done();
|
|
474
|
+
}
|
|
475
|
+
})().finally(() => {
|
|
476
|
+
state.busy = false;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// =========================
|
|
481
|
+
// Config Node: smithtek-mako-rf-bus
|
|
482
|
+
// =========================
|
|
483
|
+
function SmithtekMakoRfBusConfig(n) {
|
|
484
|
+
RED.nodes.createNode(this, n);
|
|
485
|
+
|
|
486
|
+
this.busName = (n.busName || "smithtek mako rf").trim();
|
|
487
|
+
this.serialPort = n.serialPort || "";
|
|
488
|
+
this.baudRate = toNum(n.baudRate, 9600);
|
|
489
|
+
this.stopBits = toNum(n.stopBits, 1);
|
|
490
|
+
this.parity = n.parity || "none";
|
|
491
|
+
|
|
492
|
+
this.timeout_s = toNum(n.timeout_s, 8);
|
|
493
|
+
this.retries = toNum(n.retries, 2);
|
|
494
|
+
this.gap_s = toNum(n.gap_s, 0);
|
|
495
|
+
|
|
496
|
+
ensureBusState(this);
|
|
497
|
+
|
|
498
|
+
this.on("close", (removed, done) => {
|
|
499
|
+
if (removed) {
|
|
500
|
+
const s = BUS.get(this.id);
|
|
501
|
+
if (s) {
|
|
502
|
+
try {
|
|
503
|
+
s.client.close(() => {});
|
|
504
|
+
} catch (_e) {}
|
|
505
|
+
BUS.delete(this.id);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
done();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
RED.nodes.registerType("smithtek-mako-rf-bus", SmithtekMakoRfBusConfig);
|
|
512
|
+
|
|
513
|
+
// =========================
|
|
514
|
+
// Runtime Node: smithtek-mako-rf
|
|
515
|
+
// =========================
|
|
516
|
+
function SmithtekMakoRfNode(config) {
|
|
517
|
+
RED.nodes.createNode(this, config);
|
|
518
|
+
const node = this;
|
|
519
|
+
|
|
520
|
+
node.on("input", function (msg, send, done) {
|
|
521
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
522
|
+
|
|
523
|
+
const busCfg = RED.nodes.getNode(config.bus);
|
|
524
|
+
if (!busCfg) {
|
|
525
|
+
node.status({ fill: "red", shape: "ring", text: "no bus config" });
|
|
526
|
+
done(new Error("No Smithtek Mako RF bus configured"));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const state = ensureBusState(busCfg);
|
|
531
|
+
|
|
532
|
+
const mode = (config.mode || "read").toLowerCase();
|
|
533
|
+
|
|
534
|
+
let fc = clampInt(config.fc, 1, 16, 3);
|
|
535
|
+
fc = enforceFcForMode(mode, fc);
|
|
536
|
+
|
|
537
|
+
const unitid = clampInt(config.unitid, 1, 247, 1);
|
|
538
|
+
const address = clampInt(config.address, 0, 100000, 0);
|
|
539
|
+
const quantity = clampInt(config.quantity, 1, 2000, 1);
|
|
540
|
+
|
|
541
|
+
// Decoder items from UI
|
|
542
|
+
const items = Array.isArray(config.items) ? config.items : [];
|
|
543
|
+
|
|
544
|
+
const req = { mode, fc, unitid, address };
|
|
545
|
+
|
|
546
|
+
if (mode === "read") {
|
|
547
|
+
req.quantity = quantity;
|
|
548
|
+
} else {
|
|
549
|
+
const valueSource = (config.valueSource || "msg.payload").toLowerCase();
|
|
550
|
+
let value;
|
|
551
|
+
|
|
552
|
+
if (valueSource === "fixed") {
|
|
553
|
+
const vt = (config.valueType || "bool").toLowerCase();
|
|
554
|
+
const raw = config.valueText ?? "";
|
|
555
|
+
|
|
556
|
+
if (vt === "bool") value = String(raw).trim().toLowerCase() === "true";
|
|
557
|
+
else if (vt === "number") value = toNum(raw, 0);
|
|
558
|
+
else if (vt === "json") {
|
|
559
|
+
try { value = JSON.parse(String(raw)); } catch (_e) { value = String(raw); }
|
|
560
|
+
} else value = String(raw);
|
|
561
|
+
} else {
|
|
562
|
+
value = msg.payload;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
req.value = value;
|
|
566
|
+
|
|
567
|
+
if (config.includeWriteQuantity) req.quantity = quantity;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
state.queue.push({ node, msg, send, done, req, items });
|
|
571
|
+
scheduleProcess(state, busCfg.id);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
node.on("close", function (_removed, done) {
|
|
575
|
+
node.status({});
|
|
576
|
+
done();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
RED.nodes.registerType("smithtek-mako-rf", SmithtekMakoRfNode);
|
|
581
|
+
};
|