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.
@@ -0,0 +1,552 @@
1
+ <script type="text/javascript">
2
+ (function () {
3
+
4
+ const TYPE_OPTIONS = [
5
+ "16 bit integer",
6
+ "16 bit unsigned",
7
+ "32 bit integer",
8
+ "32 bit unsigned",
9
+ "32 bit float",
10
+ "digital to unsigned binary encoder"
11
+ ];
12
+
13
+ function is32BitType(t) {
14
+ t = String(t || "").toLowerCase();
15
+ return (t === "32 bit integer" || t === "32 bit unsigned" || t === "32 bit float");
16
+ }
17
+
18
+ function isDigitalUnsigned(t) {
19
+ return String(t || "").toLowerCase() === "digital to unsigned binary encoder";
20
+ }
21
+
22
+ function defaultItemRow() {
23
+ return {
24
+ name: "",
25
+ type: "16 bit integer",
26
+ offset: 0, // BYTES stored in config (legacy decoder compatibility)
27
+ length: 1, // stored but hidden (1=16-bit, 2=32-bit)
28
+ scale: "",
29
+ offsetbit: 0
30
+ };
31
+ }
32
+
33
+ function buildTypeSelect(value) {
34
+ const $sel = $("<select></select>")
35
+ .addClass("st-item-type")
36
+ .css("width", "100%");
37
+ TYPE_OPTIONS.forEach(t => {
38
+ $sel.append($("<option></option>").attr("value", t).text(t));
39
+ });
40
+ $sel.val(String(value || "16 bit integer"));
41
+ return $sel;
42
+ }
43
+
44
+ function applyTypeRules($tr) {
45
+ const t = String($tr.find(".st-item-type").val() || "");
46
+ const $lenHidden = $tr.find(".st-item-length"); // hidden
47
+ const $bit = $tr.find(".st-item-offsetbit");
48
+ const $bitCol = $tr.find(".st-bit-col");
49
+
50
+ // auto length (hidden but saved)
51
+ if (is32BitType(t)) $lenHidden.val(2);
52
+ else $lenHidden.val(1);
53
+
54
+ // bit only for digital unsigned
55
+ if (isDigitalUnsigned(t)) {
56
+ $bit.prop("disabled", false).show();
57
+ $bitCol.show();
58
+ } else {
59
+ $bit.val(0);
60
+ $bit.prop("disabled", true).hide();
61
+ $bitCol.hide();
62
+ }
63
+ }
64
+
65
+ // Determine next row behaviour based on the last row (REGS visible)
66
+ function nextRowFromLast($tbody) {
67
+ const $last = $tbody.find("tr").last();
68
+ if (!$last.length) {
69
+ return { offsetRegs: 0, type: "16 bit integer", offsetbit: 0 };
70
+ }
71
+
72
+ const lastRegs = Number($last.find(".st-item-offset-regs").val());
73
+ const lastType = String($last.find(".st-item-type").val() || "");
74
+ const lastBit = Number($last.find(".st-item-offsetbit").val());
75
+
76
+ const safeRegs = Number.isFinite(lastRegs) ? lastRegs : 0;
77
+ const safeBit = Number.isFinite(lastBit) ? lastBit : 0;
78
+
79
+ // DIGITAL BIT-LADDER (same register, bit++, then next register)
80
+ if (isDigitalUnsigned(lastType)) {
81
+ if (safeBit < 15) {
82
+ return { offsetRegs: safeRegs, type: lastType, offsetbit: safeBit + 1 };
83
+ }
84
+ return { offsetRegs: safeRegs + 1, type: lastType, offsetbit: 0 };
85
+ }
86
+
87
+ // Normal stepping (REGS)
88
+ const stepRegs = is32BitType(lastType) ? 2 : 1;
89
+ return { offsetRegs: safeRegs + stepRegs, type: lastType, offsetbit: 0 };
90
+ }
91
+
92
+ function addItemRow($tbody, item, isNew) {
93
+ const it = item || defaultItemRow();
94
+ const $tr = $("<tr></tr>");
95
+ const rowIndex = $tbody.find("tr").length + 1;
96
+
97
+ const $name = $("<input/>")
98
+ .attr("type", "text")
99
+ .addClass("st-item-name")
100
+ .css("width", "100%")
101
+ .val(it.name ?? (isNew ? `item${rowIndex}` : ""));
102
+
103
+ const $type = buildTypeSelect(it.type);
104
+
105
+ // Visible: regs
106
+ const existingBytes = Number(it.offset ?? 0);
107
+ const existingRegs = Number.isFinite(existingBytes) ? (existingBytes / 2) : 0;
108
+
109
+ const $offsetRegs = $("<input/>")
110
+ .attr("type", "number")
111
+ .addClass("st-item-offset-regs")
112
+ .css("width", "100%")
113
+ .val(existingRegs);
114
+
115
+ // Hidden: bytes (this is what the JS runtime expects)
116
+ const $offsetBytes = $("<input/>")
117
+ .attr("type", "hidden")
118
+ .addClass("st-item-offset")
119
+ .val(Number.isFinite(existingBytes) ? existingBytes : 0);
120
+
121
+ // keep hidden bytes synced when user edits regs
122
+ $offsetRegs.on("input", function () {
123
+ const r = Number($offsetRegs.val());
124
+ const regs = Number.isFinite(r) ? r : 0;
125
+ $offsetBytes.val(regs * 2);
126
+ });
127
+
128
+ // hidden length field (kept in config, not shown)
129
+ const $lengthHidden = $("<input/>")
130
+ .attr("type", "hidden")
131
+ .addClass("st-item-length")
132
+ .val(it.length ?? 1);
133
+
134
+ const $scale = $("<input/>")
135
+ .attr("type", "text")
136
+ .addClass("st-item-scale")
137
+ .css("width", "100%")
138
+ .val(it.scale ?? "");
139
+
140
+ const $offsetbit = $("<input/>")
141
+ .attr("type", "number")
142
+ .addClass("st-item-offsetbit")
143
+ .css("width", "100%")
144
+ .val(it.offsetbit ?? 0);
145
+
146
+ if (isNew) {
147
+ const next = nextRowFromLast($tbody);
148
+
149
+ $type.val(next.type);
150
+
151
+ // set regs, hidden bytes follows
152
+ $offsetRegs.val(next.offsetRegs).trigger("input");
153
+
154
+ // set bit for digital ladder
155
+ $offsetbit.val(next.offsetbit);
156
+ } else {
157
+ // ensure hidden offset is correct for existing rows
158
+ $offsetRegs.trigger("input");
159
+ }
160
+
161
+ const $del = $("<a href='#' style='color:#c00;'>Remove</a>").on("click", function (e) {
162
+ e.preventDefault();
163
+ $tr.remove();
164
+ });
165
+
166
+ $tr.append($("<td></td>").append($name));
167
+ $tr.append($("<td></td>").append($type));
168
+ $tr.append($("<td></td>").append($offsetRegs).append($offsetBytes));
169
+ $tr.append($("<td></td>").append($scale));
170
+
171
+ const $bitTd = $("<td></td>").addClass("st-bit-col").append($offsetbit);
172
+ $tr.append($bitTd);
173
+
174
+ // attach hidden length to the row so it saves
175
+ $tr.append($("<td style='display:none;'></td>").append($lengthHidden));
176
+
177
+ $tr.append($("<td></td>").append($del));
178
+
179
+ $tbody.append($tr);
180
+
181
+ applyTypeRules($tr);
182
+ $type.on("change", function () {
183
+ applyTypeRules($tr);
184
+ });
185
+ }
186
+
187
+ function readItemsFromTable($tbody) {
188
+ const items = [];
189
+ $tbody.find("tr").each(function () {
190
+ const $tr = $(this);
191
+ items.push({
192
+ name: String($tr.find(".st-item-name").val() ?? "").trim(),
193
+ type: String($tr.find(".st-item-type").val() ?? "16 bit integer").trim(),
194
+ offset: Number($tr.find(".st-item-offset").val()) || 0, // BYTES saved
195
+ length: Number($tr.find(".st-item-length").val()) || 1, // hidden but saved
196
+ scale: String($tr.find(".st-item-scale").val() ?? "").trim(),
197
+ offsetbit: Number($tr.find(".st-item-offsetbit").val()) || 0
198
+ });
199
+ });
200
+ return items;
201
+ }
202
+
203
+ function buildFcList(mode, $fc, preferred) {
204
+ const isRead = mode === "read";
205
+ const keep = (preferred !== undefined && preferred !== null && preferred !== "")
206
+ ? String(preferred)
207
+ : String($fc.val() || "");
208
+
209
+ $fc.empty();
210
+
211
+ const readFcs = [
212
+ { v: 1, t: "1 - Read Coils" },
213
+ { v: 2, t: "2 - Read Digital Inputs" },
214
+ { v: 3, t: "3 - Read Holding Registers" },
215
+ { v: 4, t: "4 - Read Input Registers" }
216
+ ];
217
+
218
+ const writeFcs = [
219
+ { v: 5, t: "5 - Write Single Coil" },
220
+ { v: 6, t: "6 - Write Single Register" },
221
+ { v: 15, t: "15 - Write Multiple Coils" },
222
+ { v: 16, t: "16 - Write Multiple Registers" }
223
+ ];
224
+
225
+ const list = isRead ? readFcs : writeFcs;
226
+ list.forEach(o => $fc.append($("<option></option>").attr("value", o.v).text(o.t)));
227
+
228
+ if (list.some(o => String(o.v) === keep)) $fc.val(keep);
229
+ else $fc.val(String(list[0].v));
230
+ }
231
+
232
+ function toggleRuntimeUi(mode) {
233
+ const isRead = mode === "read";
234
+ $("#st-rf-write").toggle(!isRead);
235
+ $("#st-rf-decode-wrap").toggle(isRead);
236
+ }
237
+
238
+ function toggleValueUi(valueSource) {
239
+ const fixed = (valueSource || "msg.payload") === "fixed";
240
+ $("#st-rf-fixedwrap").toggle(fixed);
241
+ }
242
+
243
+ // Bus UI: hide serial settings when RF channel selected
244
+ function updateSerialUiForChannel() {
245
+ const ch = $("#node-config-input-serialPort").val();
246
+ const isRF = (ch === "/dev/ttyAMA2");
247
+
248
+ $("#st-row-baudRate").toggle(!isRF);
249
+ $("#st-row-parity").toggle(!isRF);
250
+ $("#st-row-stopBits").toggle(!isRF);
251
+
252
+ // Optional: force defaults for RF (still stored, just not user-adjustable)
253
+ if (isRF) {
254
+ $("#node-config-input-baudRate").val(9600);
255
+ $("#node-config-input-parity").val("none");
256
+ $("#node-config-input-stopBits").val(1);
257
+ }
258
+ }
259
+
260
+ // =========================
261
+ // Config node: smithtek-mako-rf-bus
262
+ // =========================
263
+ RED.nodes.registerType("smithtek-mako-rf-bus", {
264
+ category: "config",
265
+ defaults: {
266
+ busName: { value: "smithtek mako rf", required: true },
267
+
268
+ // Dropdown only: RS485-1 (/dev/ttyAMA0), RS485-2 (/dev/ttyAMA1), RF (/dev/ttyAMA2)
269
+ serialPort: { value: "/dev/ttyAMA2", required: true },
270
+
271
+ baudRate: { value: 9600, required: true, validate: RED.validators.number() },
272
+ stopBits: { value: 1, required: true, validate: RED.validators.number() },
273
+ parity: { value: "none", required: true },
274
+
275
+ timeout_s: { value: 8, required: true, validate: RED.validators.number() },
276
+ retries: { value: 2, required: true, validate: RED.validators.number() },
277
+ gap_s: { value: 0, required: true, validate: RED.validators.number() }
278
+ },
279
+ label: function () {
280
+ return this.busName || "smithtek mako rf";
281
+ },
282
+ oneditprepare: function () {
283
+ updateSerialUiForChannel();
284
+ $("#node-config-input-serialPort").on("change", updateSerialUiForChannel);
285
+ }
286
+ });
287
+
288
+ // =========================
289
+ // Main node: smithtek-mako-rf
290
+ // =========================
291
+ RED.nodes.registerType("smithtek-mako-rf", {
292
+ category: "Smithtek",
293
+ color: "#B9B6B8",
294
+ defaults: {
295
+ name: { value: "" },
296
+ bus: { type: "smithtek-mako-rf-bus", required: true },
297
+
298
+ mode: { value: "read" },
299
+ fc: { value: 3, required: true, validate: RED.validators.number() },
300
+ unitid: { value: 1, required: true, validate: RED.validators.number() },
301
+ address: { value: 0, required: true, validate: RED.validators.number() },
302
+ quantity: { value: 1, required: true, validate: RED.validators.number() },
303
+
304
+ valueSource: { value: "msg.payload" },
305
+ valueType: { value: "bool" },
306
+ valueText: { value: "" },
307
+ includeWriteQuantity: { value: false },
308
+
309
+ items: { value: [ defaultItemRow() ] }
310
+ },
311
+ inputs: 1,
312
+ outputs: 1,
313
+ icon: "bridge.svg",
314
+ label: function () { return this.name || "Smithtek Mako RF"; },
315
+
316
+ oneditprepare: function () {
317
+ buildFcList(this.mode, $("#node-input-fc"), this.fc);
318
+ toggleRuntimeUi(this.mode);
319
+ toggleValueUi(this.valueSource);
320
+
321
+ $("#node-input-mode").on("change", () => {
322
+ const mode = $("#node-input-mode").val();
323
+ buildFcList(mode, $("#node-input-fc"), $("#node-input-fc").val());
324
+ toggleRuntimeUi(mode);
325
+ });
326
+
327
+ $("#node-input-valueSource").on("change", () => {
328
+ toggleValueUi($("#node-input-valueSource").val());
329
+ });
330
+
331
+ const $tbody = $("#st-items-tbody");
332
+ $tbody.empty();
333
+
334
+ let items = Array.isArray(this.items) ? this.items : [];
335
+ if (!items.length) items = [defaultItemRow()];
336
+ items.forEach(it => addItemRow($tbody, it, false));
337
+
338
+ $("#st-add-item").on("click", function (e) {
339
+ e.preventDefault();
340
+ addItemRow($tbody, defaultItemRow(), true);
341
+ });
342
+
343
+ $("#st-clear-items").on("click", function (e) {
344
+ e.preventDefault();
345
+ $tbody.empty();
346
+ addItemRow($tbody, defaultItemRow(), false);
347
+ });
348
+ },
349
+
350
+ oneditsave: function () {
351
+ this.items = readItemsFromTable($("#st-items-tbody"));
352
+ }
353
+ });
354
+
355
+ })();
356
+ </script>
357
+
358
+ <!-- =========================
359
+ BUS CONFIG TEMPLATE
360
+ ========================= -->
361
+ <script type="text/html" data-template-name="smithtek-mako-rf-bus">
362
+ <div class="form-row">
363
+ <label for="node-config-input-busName"><i class="fa fa-tag"></i> Bus name</label>
364
+ <input type="text" id="node-config-input-busName" placeholder="smithtek mako rf">
365
+ </div>
366
+
367
+ <div class="form-row">
368
+ <label for="node-config-input-serialPort"><i class="fa fa-plug"></i> Channel</label>
369
+ <select id="node-config-input-serialPort" style="width:260px;">
370
+ <option value="/dev/ttyAMA0">RS485-1</option>
371
+ <option value="/dev/ttyAMA1">RS485-2</option>
372
+ <option value="/dev/ttyAMA2">RF</option>
373
+ </select>
374
+ </div>
375
+
376
+ <div class="form-row" id="st-row-baudRate">
377
+ <label for="node-config-input-baudRate"><i class="fa fa-cog"></i> Baud</label>
378
+ <input type="number" id="node-config-input-baudRate" style="width:140px;">
379
+ </div>
380
+
381
+ <div class="form-row" id="st-row-parity">
382
+ <label for="node-config-input-parity"><i class="fa fa-adjust"></i> Parity</label>
383
+ <select id="node-config-input-parity" style="width:160px;">
384
+ <option value="none">none</option>
385
+ <option value="even">even</option>
386
+ <option value="odd">odd</option>
387
+ <option value="mark">mark</option>
388
+ <option value="space">space</option>
389
+ </select>
390
+ </div>
391
+
392
+ <div class="form-row" id="st-row-stopBits">
393
+ <label for="node-config-input-stopBits"><i class="fa fa-pause"></i> Stop bits</label>
394
+ <input type="number" id="node-config-input-stopBits" style="width:140px;">
395
+ </div>
396
+
397
+ <hr />
398
+
399
+ <div class="form-row">
400
+ <label for="node-config-input-timeout_s"><i class="fa fa-clock-o"></i> Timeout (seconds)</label>
401
+ <input type="number" id="node-config-input-timeout_s" style="width:140px;">
402
+ </div>
403
+
404
+ <div class="form-row">
405
+ <label for="node-config-input-retries"><i class="fa fa-repeat"></i> Retries</label>
406
+ <input type="number" id="node-config-input-retries" style="width:140px;">
407
+ </div>
408
+
409
+ <div class="form-row">
410
+ <label for="node-config-input-gap_s"><i class="fa fa-arrows-h"></i> Gap (seconds)</label>
411
+ <input type="number" id="node-config-input-gap_s" style="width:140px;">
412
+ </div>
413
+
414
+ <div class="form-tips">
415
+ Select a channel. RF is for the LoRa long range radio RTU coms.
416
+ </div>
417
+ </script>
418
+
419
+ <!-- =========================
420
+ MAIN NODE TEMPLATE
421
+ ========================= -->
422
+ <script type="text/html" data-template-name="smithtek-mako-rf">
423
+ <div class="form-row">
424
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
425
+ <input type="text" id="node-input-name" placeholder="">
426
+ </div>
427
+
428
+ <div class="form-row">
429
+ <label for="node-input-bus"><i class="fa fa-sitemap"></i> Bus</label>
430
+ <input type="text" id="node-input-bus" style="width: 25%;">
431
+ <span style="margin-left:8px; font-size:12px; color:#888;">
432
+ (Add or edit your Modbus channel here, RF is for the long range radio Coms (LoRa) )
433
+ </span>
434
+ </div>
435
+
436
+ <div class="form-row">
437
+ <label for="node-input-mode"><i class="fa fa-random"></i> Mode</label>
438
+ <select id="node-input-mode" style="width: 20%;">
439
+ <option value="read">Read</option>
440
+ <option value="write">Write</option>
441
+ </select>
442
+ <span style="margin-left:8px; font-size:12px; color:#888;">
443
+ (Read will poll the Mako's modbus, Write will send commands to it, digital BOOLS, values etc. )
444
+ </span>
445
+ </div>
446
+
447
+ <div class="form-row">
448
+ <label for="node-input-unitid"><i class="fa fa-hashtag"></i> Device ID</label>
449
+ <input type="number" id="node-input-unitid" min="1" max="247" style="width: 140px;">
450
+ <span style="margin-left:8px; font-size:12px; color:#888;">
451
+ (The ID of your Mako Device, via RF or RS485)
452
+ </span>
453
+ </div>
454
+
455
+ <div class="form-row">
456
+ <label for="node-input-fc"><i class="fa fa-code"></i> Function</label>
457
+ <select id="node-input-fc" style="width: 25%;"></select>
458
+ <span style="margin-left:8px; font-size:12px; color:#888;">
459
+ (Match the function code with your V-NET program, Input register, holding register and so on.)
460
+ </span>
461
+ </div>
462
+
463
+ <div class="form-row">
464
+ <label for="node-input-address"><i class="fa fa-crosshairs"></i> Address</label>
465
+ <input type="number" id="node-input-address" min="0" style="width: 140px;">
466
+ <span style="margin-left:8px; font-size:12px; color:#888;">
467
+ (The address of the first register you want to read,)
468
+ </span>
469
+ </div>
470
+
471
+ <div class="form-row">
472
+ <label for="node-input-quantity"><i class="fa fa-list-ol"></i> Quantity</label>
473
+ <input type="number" id="node-input-quantity" min="1" style="width: 100px;">
474
+ <span style="margin-left:8px; font-size:12px; color:#888;">
475
+ (Qty of registers to read. 16bit = 1 register, 32bit = 2 registers. example, 3* floats would = 6 registers)
476
+ </span>
477
+ </div>
478
+
479
+ <div id="st-rf-write" style="margin-top:10px; display:none;">
480
+ <div class="form-row">
481
+ <label for="node-input-valueSource"><i class="fa fa-pencil"></i> Value source</label>
482
+ <select id="node-input-valueSource" style="width: 70%;">
483
+ <option value="msg.payload">From msg.payload</option>
484
+ <option value="fixed">Fixed</option>
485
+ </select>
486
+ </div>
487
+
488
+ <div id="st-rf-fixedwrap" style="margin-top:6px;">
489
+ <div class="form-row">
490
+ <label for="node-input-valueType"><i class="fa fa-sliders"></i> Fixed type</label>
491
+ <select id="node-input-valueType" style="width: 70%;">
492
+ <option value="bool">Boolean (true/false)</option>
493
+ <option value="number">Number</option>
494
+ <option value="json">JSON (array/object)</option>
495
+ <option value="string">String</option>
496
+ </select>
497
+ </div>
498
+
499
+ <div class="form-row">
500
+ <label for="node-input-valueText"><i class="fa fa-edit"></i> Fixed value</label>
501
+ <input type="text" id="node-input-valueText" placeholder='e.g. true | 123 | [1,0,1] | [100,200]'>
502
+ </div>
503
+ </div>
504
+
505
+ <div class="form-row">
506
+ <label for="node-input-includeWriteQuantity"><i class="fa fa-check-square-o"></i> Include quantity</label>
507
+ <input type="checkbox" id="node-input-includeWriteQuantity" style="width:auto;">
508
+ </div>
509
+ </div>
510
+
511
+ <hr />
512
+
513
+ <div id="st-rf-decode-wrap">
514
+ <div class="form-row" style="margin-bottom:6px;">
515
+ <b>Decoder map</b>
516
+ <span style="margin-left:8px; font-size:12px; color:#888;">
517
+ (Build your register table in the exact same order as your Mako V-NET program, Add new rows and change the register data type to match)
518
+ </span>
519
+ <div style="margin-top:6px;">
520
+ <a href="#" class="editor-button" id="st-add-item"><i class="fa fa-plus"></i> Add row</a>
521
+ <a href="#" class="editor-button" id="st-clear-items" style="margin-left:8px;"><i class="fa fa-trash"></i> Reset</a>
522
+ </div>
523
+ </div>
524
+
525
+ <div class="form-row">
526
+ <table class="table table-condensed" style="width:100%;">
527
+ <thead>
528
+ <tr>
529
+ <th style="width:28%;">Name </th>
530
+ <th style="width:26%;">Register data Type</th>
531
+ <th style="width:14%;">Offset (regs)</th>
532
+ <th style="width:22%;">Scale</th>
533
+ <th style="width:6%;">Bit</th>
534
+ <th style="width:4%;">Action</th>
535
+ </tr>
536
+ </thead>
537
+ <tbody id="st-items-tbody"></tbody>
538
+ </table>
539
+ </div>
540
+
541
+ <div style="font-size:12px; color:#666; margin-top:6px;">
542
+ (Register Offsets numbers should match the modbus table you have built in V-NET!, if you add a digital it will automatically increase the bit number. Bit 1 = digital 1 and so on.
543
+ </div>
544
+ </div>
545
+ </script>
546
+
547
+ <script type="text/markdown" data-help-name="smithtek-mako-rf">
548
+ Smithtek Mako RF (RS-485 only).
549
+
550
+ Read mode outputs decoded JSON in msg.payload using the Decoder map rows.
551
+ Raw modbus response is kept in msg.modbus.raw.
552
+ </script>