smithtek-mako-rf 3.4.2 → 3.5.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/LICENSE.txt +80 -0
- package/LICENSING.md +90 -0
- package/MAKO-RF.md +22 -12
- package/PASSPORT-CONNECT.md +24 -2
- package/README.md +32 -4
- package/THIRD-PARTY-NOTICES.md +659 -0
- package/UPSTREAM.md +21 -1
- package/bus-engine.js +266 -142
- package/channel-map.mjs +56 -54
- package/package.json +7 -4
- package/passport-link-node.js +39 -18
- package/smithtek-mako-rf.html +782 -765
- package/smithtek-mako-rf.js +7 -2
package/smithtek-mako-rf.html
CHANGED
|
@@ -1,765 +1,771 @@
|
|
|
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
|
-
scaleMode: "none",
|
|
30
|
-
inputLow: 4,
|
|
31
|
-
inputHigh: 20,
|
|
32
|
-
outputLow: 0,
|
|
33
|
-
outputHigh: 100,
|
|
34
|
-
clampRange: false,
|
|
35
|
-
offsetbit: 0
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function buildTypeSelect(value) {
|
|
40
|
-
const $sel = $("<select></select>")
|
|
41
|
-
.addClass("st-item-type")
|
|
42
|
-
.css("width", "100%");
|
|
43
|
-
TYPE_OPTIONS.forEach(t => {
|
|
44
|
-
$sel.append($("<option></option>").attr("value", t).text(t));
|
|
45
|
-
});
|
|
46
|
-
$sel.val(String(value || "16 bit integer"));
|
|
47
|
-
return $sel;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function applyTypeRules($tr) {
|
|
51
|
-
const t = String($tr.find(".st-item-type").val() || "");
|
|
52
|
-
const $lenHidden = $tr.find(".st-item-length"); // hidden
|
|
53
|
-
const $bit = $tr.find(".st-item-offsetbit");
|
|
54
|
-
const $bitCol = $tr.find(".st-bit-col");
|
|
55
|
-
|
|
56
|
-
// auto length (hidden but saved)
|
|
57
|
-
if (is32BitType(t)) $lenHidden.val(2);
|
|
58
|
-
else $lenHidden.val(1);
|
|
59
|
-
|
|
60
|
-
// bit only for digital unsigned
|
|
61
|
-
if (isDigitalUnsigned(t)) {
|
|
62
|
-
$bit.prop("disabled", false).show();
|
|
63
|
-
$bitCol.show();
|
|
64
|
-
} else {
|
|
65
|
-
$bit.val(0);
|
|
66
|
-
$bit.prop("disabled", true).hide();
|
|
67
|
-
$bitCol.show();
|
|
68
|
-
}
|
|
69
|
-
updateScaleUi($tr);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function updateScaleUi($tr) {
|
|
73
|
-
const mode = $tr.find('.st-scale-mode').val();
|
|
74
|
-
const range = mode === 'range';
|
|
75
|
-
const digital = isDigitalUnsigned($tr.find('.st-item-type').val());
|
|
76
|
-
$tr.find('.st-item-scale').toggle(mode === 'math');
|
|
77
|
-
$tr.find('.st-range-fields').toggle(range);
|
|
78
|
-
$tr.find('.st-range-note').toggle(range && digital);
|
|
79
|
-
const inputs = $tr.find('.st-range-number');
|
|
80
|
-
inputs.each(function () {
|
|
81
|
-
const raw = String($(this).val()).trim();
|
|
82
|
-
const bad = range && (raw === '' || !Number.isFinite(Number(raw)));
|
|
83
|
-
this.setCustomValidity(bad ? 'Enter a finite number.' : '');
|
|
84
|
-
$(this).attr('aria-invalid', bad ? 'true' : 'false');
|
|
85
|
-
});
|
|
86
|
-
const low = $tr.find('.st-inputLow'), high = $tr.find('.st-inputHigh');
|
|
87
|
-
if (range && String(low.val()).trim() !== '' && String(high.val()).trim() !== '' && Number(low.val()) === Number(high.val())) {
|
|
88
|
-
high[0].setCustomValidity('Input low and input high must differ.');
|
|
89
|
-
high.attr('aria-invalid', 'true');
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function validDecoderItems(items) {
|
|
94
|
-
return Array.isArray(items) && items.every(item => {
|
|
95
|
-
if (item.scaleMode !== 'range') return true;
|
|
96
|
-
if (isDigitalUnsigned(item.type) || item.type === 'bool') return false;
|
|
97
|
-
const values = ['inputLow','inputHigh','outputLow','outputHigh'].map(key => item[key]);
|
|
98
|
-
return values.every(value => (typeof value === 'string' || typeof value === 'number') && String(value).trim() !== '' && Number.isFinite(Number(value))) &&
|
|
99
|
-
Number(item.inputLow) !== Number(item.inputHigh) && Number.isFinite(Number(item.inputHigh)-Number(item.inputLow)) && Number.isFinite(Number(item.outputHigh)-Number(item.outputLow));
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Determine next row behaviour based on the last row (REGS visible)
|
|
104
|
-
function nextRowFromLast($tbody) {
|
|
105
|
-
const $last = $tbody.find("tr").last();
|
|
106
|
-
if (!$last.length) {
|
|
107
|
-
return { offsetRegs: 0, type: "16 bit integer", offsetbit: 0 };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const lastRegs = Number($last.find(".st-item-offset-regs").val());
|
|
111
|
-
const lastType = String($last.find(".st-item-type").val() || "");
|
|
112
|
-
const lastBit = Number($last.find(".st-item-offsetbit").val());
|
|
113
|
-
|
|
114
|
-
const safeRegs = Number.isFinite(lastRegs) ? lastRegs : 0;
|
|
115
|
-
const safeBit = Number.isFinite(lastBit) ? lastBit : 0;
|
|
116
|
-
|
|
117
|
-
// DIGITAL BIT-LADDER (same register, bit++, then next register)
|
|
118
|
-
if (isDigitalUnsigned(lastType)) {
|
|
119
|
-
if (safeBit < 15) {
|
|
120
|
-
return { offsetRegs: safeRegs, type: lastType, offsetbit: safeBit + 1 };
|
|
121
|
-
}
|
|
122
|
-
return { offsetRegs: safeRegs + 1, type: lastType, offsetbit: 0 };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Normal stepping (REGS)
|
|
126
|
-
const stepRegs = is32BitType(lastType) ? 2 : 1;
|
|
127
|
-
return { offsetRegs: safeRegs + stepRegs, type: lastType, offsetbit: 0 };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function addItemRow($tbody, item, isNew) {
|
|
131
|
-
const it = item || defaultItemRow();
|
|
132
|
-
const $tr = $("<tr></tr>");
|
|
133
|
-
const rowIndex = $tbody.find("tr").length + 1;
|
|
134
|
-
|
|
135
|
-
const $name = $("<input/>")
|
|
136
|
-
.attr("type", "text")
|
|
137
|
-
.addClass("st-item-name")
|
|
138
|
-
.css("width", "100%")
|
|
139
|
-
.val(it.name ?? (isNew ? `item${rowIndex}` : ""));
|
|
140
|
-
|
|
141
|
-
const $type = buildTypeSelect(it.type);
|
|
142
|
-
|
|
143
|
-
// Visible: regs
|
|
144
|
-
const existingBytes = Number(it.offset ?? 0);
|
|
145
|
-
const existingRegs = Number.isFinite(existingBytes) ? (existingBytes / 2) : 0;
|
|
146
|
-
|
|
147
|
-
const $offsetRegs = $("<input/>")
|
|
148
|
-
.attr("type", "number")
|
|
149
|
-
.addClass("st-item-offset-regs")
|
|
150
|
-
.css("width", "100%")
|
|
151
|
-
.val(existingRegs);
|
|
152
|
-
|
|
153
|
-
// Hidden: bytes (this is what the JS runtime expects)
|
|
154
|
-
const $offsetBytes = $("<input/>")
|
|
155
|
-
.attr("type", "hidden")
|
|
156
|
-
.addClass("st-item-offset")
|
|
157
|
-
.val(Number.isFinite(existingBytes) ? existingBytes : 0);
|
|
158
|
-
|
|
159
|
-
// keep hidden bytes synced when user edits regs
|
|
160
|
-
$offsetRegs.on("input", function () {
|
|
161
|
-
const r = Number($offsetRegs.val());
|
|
162
|
-
const regs = Number.isFinite(r) ? r : 0;
|
|
163
|
-
$offsetBytes.val(regs * 2);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// hidden length field (kept in config, not shown)
|
|
167
|
-
const $lengthHidden = $("<input/>")
|
|
168
|
-
.attr("type", "hidden")
|
|
169
|
-
.addClass("st-item-length")
|
|
170
|
-
.val(it.length ?? 1);
|
|
171
|
-
|
|
172
|
-
const $scale = $("<input/>")
|
|
173
|
-
.attr("type", "text")
|
|
174
|
-
.addClass("st-item-scale")
|
|
175
|
-
.css("width", "100%")
|
|
176
|
-
.val(it.scale ?? it.scaler ?? it.scaleFactor ?? "");
|
|
177
|
-
|
|
178
|
-
const $scaleMode = $('<select></select>').addClass('st-scale-mode').attr('aria-label','Scaling mode').css({width:'82px',flex:'0 0 82px'})
|
|
179
|
-
.attr('title','None: unchanged; Math: expression such as /100; Range: input low/high to output low/high.')
|
|
180
|
-
.append($('<option value="none">None</option>'))
|
|
181
|
-
.append($('<option value="math">Math</option>'))
|
|
182
|
-
.append($('<option value="range">Range</option>'))
|
|
183
|
-
.val(it.scaleMode === 'none' ? 'none' : it.scaleMode === 'range' ? 'range' : String($scale.val()).trim() ? 'math' : 'none');
|
|
184
|
-
$scale.css({width:'100px',flex:'0 0 100px'}).attr('placeholder','/100 or +5').attr('aria-label','Math expression');
|
|
185
|
-
const $rangeFields = $('<div></div>').addClass('st-range-fields').css({display:'flex','align-items':'center',gap:'5px','flex-wrap':'nowrap'});
|
|
186
|
-
const $grid = $('<div></div>').css({display:'flex','align-items':'center',gap:'5px','flex-wrap':'nowrap'});
|
|
187
|
-
[['inputLow','In low',4],['inputHigh','In high',20],['outputLow','Out low',0],['outputHigh','Out high',100]].forEach(([key,label,fallback]) => {
|
|
188
|
-
const $input = $('<input/>').attr({type:'number',step:'any','aria-label':label,title:label.replace('In ', 'Input ').replace('Out ', 'Output ')}).addClass('st-range-number st-'+key).css({width:'62px','min-width':'62px',padding:'4px'}).val(it[key] ?? fallback);
|
|
189
|
-
const $label = $('<label></label>').css({display:'flex','align-items':'center',gap:'3px',width:'auto','white-space':'nowrap','font-size':'10px',margin:0}).append($('<span></span>').text(label)).append($input);
|
|
190
|
-
$input.on('input',() => updateScaleUi($tr));
|
|
191
|
-
$grid.append($label);
|
|
192
|
-
});
|
|
193
|
-
const $clamp = $('<input/>').attr('type','checkbox').addClass('st-clamp-range').prop('checked',it.clampRange === true).css({width:'auto',margin:'0 5px 0 0'});
|
|
194
|
-
$rangeFields.append($grid).append($('<label></label>').css({display:'flex','align-items':'center',width:'auto','white-space':'nowrap','font-size':'11px',margin:0}).append($clamp).append(document.createTextNode('Clamp')));
|
|
195
|
-
$rangeFields.append($('<div></div>').addClass('st-range-note').attr('title','Choose a numeric register type for range scaling.').css({color:'#b33','font-size':'11px'}).text('!'));
|
|
196
|
-
$scaleMode.on('change',() => updateScaleUi($tr));
|
|
197
|
-
|
|
198
|
-
const $offsetbit = $("<input/>")
|
|
199
|
-
.attr("type", "number")
|
|
200
|
-
.addClass("st-item-offsetbit")
|
|
201
|
-
.css("width", "100%")
|
|
202
|
-
.val(it.offsetbit ?? 0);
|
|
203
|
-
|
|
204
|
-
if (isNew) {
|
|
205
|
-
const next = nextRowFromLast($tbody);
|
|
206
|
-
|
|
207
|
-
$type.val(next.type);
|
|
208
|
-
|
|
209
|
-
// set regs, hidden bytes follows
|
|
210
|
-
$offsetRegs.val(next.offsetRegs).trigger("input");
|
|
211
|
-
|
|
212
|
-
// set bit for digital ladder
|
|
213
|
-
$offsetbit.val(next.offsetbit);
|
|
214
|
-
} else {
|
|
215
|
-
// ensure hidden offset is correct for existing rows
|
|
216
|
-
$offsetRegs.trigger("input");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const $del = $("<a href='#' style='color:#c00;'>Remove</a>").on("click", function (e) {
|
|
220
|
-
e.preventDefault();
|
|
221
|
-
$tr.remove();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
$tr.append($("<td></td>").append($name));
|
|
225
|
-
$tr.append($("<td></td>").append($type));
|
|
226
|
-
$tr.append($("<td></td>").append($offsetRegs).append($offsetBytes));
|
|
227
|
-
$tr.append($("<td></td>").append($("<div></div>").addClass("st-scaling-inline").css({display:"flex","align-items":"center",gap:"5px","flex-wrap":"nowrap"}).append($scaleMode).append($scale).append($rangeFields)));
|
|
228
|
-
|
|
229
|
-
const $bitTd = $("<td></td>").addClass("st-bit-col").append($offsetbit);
|
|
230
|
-
$tr.append($bitTd);
|
|
231
|
-
|
|
232
|
-
// attach hidden length to the row so it saves
|
|
233
|
-
$tr.append($("<td style='display:none;'></td>").append($lengthHidden));
|
|
234
|
-
|
|
235
|
-
$tr.append($("<td></td>").append($del));
|
|
236
|
-
|
|
237
|
-
$tbody.append($tr);
|
|
238
|
-
|
|
239
|
-
applyTypeRules($tr);
|
|
240
|
-
$type.on("change", function () {
|
|
241
|
-
applyTypeRules($tr);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function readItemsFromTable($tbody) {
|
|
246
|
-
const items = [];
|
|
247
|
-
$tbody.find("tr").each(function () {
|
|
248
|
-
const $tr = $(this);
|
|
249
|
-
items.push({
|
|
250
|
-
name: String($tr.find(".st-item-name").val() ?? "").trim(),
|
|
251
|
-
type: String($tr.find(".st-item-type").val() ?? "16 bit integer").trim(),
|
|
252
|
-
offset: Number($tr.find(".st-item-offset").val()) || 0, // BYTES saved
|
|
253
|
-
length: Number($tr.find(".st-item-length").val()) || 1, // hidden but saved
|
|
254
|
-
scale: String($tr.find(".st-item-scale").val() ?? "").trim(),
|
|
255
|
-
scaleMode: $tr.find('.st-scale-mode').val() || 'none',
|
|
256
|
-
inputLow: String($tr.find('.st-inputLow').val() ?? '').trim(),
|
|
257
|
-
inputHigh: String($tr.find('.st-inputHigh').val() ?? '').trim(),
|
|
258
|
-
outputLow: String($tr.find('.st-outputLow').val() ?? '').trim(),
|
|
259
|
-
outputHigh: String($tr.find('.st-outputHigh').val() ?? '').trim(),
|
|
260
|
-
clampRange: $tr.find('.st-clamp-range').prop('checked') === true,
|
|
261
|
-
offsetbit: Number($tr.find(".st-item-offsetbit").val()) || 0
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
return items;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function buildFcList(mode, $fc, preferred) {
|
|
268
|
-
const isRead = mode === "read";
|
|
269
|
-
const keep = (preferred !== undefined && preferred !== null && preferred !== "")
|
|
270
|
-
? String(preferred)
|
|
271
|
-
: String($fc.val() || "");
|
|
272
|
-
|
|
273
|
-
$fc.empty();
|
|
274
|
-
|
|
275
|
-
const readFcs = [
|
|
276
|
-
{ v: 1, t: "1 - Read Coils" },
|
|
277
|
-
{ v: 2, t: "2 - Read Digital Inputs" },
|
|
278
|
-
{ v: 3, t: "3 - Read Holding Registers" },
|
|
279
|
-
{ v: 4, t: "4 - Read Input Registers" }
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
const writeFcs = [
|
|
283
|
-
{ v: 5, t: "5 - Write Single Coil" },
|
|
284
|
-
{ v: 6, t: "6 - Write Single Register" },
|
|
285
|
-
{ v: 15, t: "15 - Write Multiple Coils" },
|
|
286
|
-
{ v: 16, t: "16 - Write Multiple Registers" }
|
|
287
|
-
];
|
|
288
|
-
|
|
289
|
-
const list = isRead ? readFcs : writeFcs;
|
|
290
|
-
list.forEach(o => $fc.append($("<option></option>").attr("value", o.v).text(o.t)));
|
|
291
|
-
|
|
292
|
-
if (list.some(o => String(o.v) === keep)) $fc.val(keep);
|
|
293
|
-
else $fc.val(String(list[0].v));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function toggleRuntimeUi(mode) {
|
|
297
|
-
const isRead = mode === "read";
|
|
298
|
-
$("#st-rf-write").toggle(!isRead);
|
|
299
|
-
$("#st-rf-decode-wrap").toggle(isRead);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function toggleValueUi(valueSource) {
|
|
303
|
-
const fixed = (valueSource || "msg.payload") === "fixed";
|
|
304
|
-
$("#st-rf-fixedwrap").toggle(fixed);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Bus UI: hide serial settings when RF channel selected
|
|
308
|
-
function updateSerialUiForChannel() {
|
|
309
|
-
const ch = $("#node-config-input-serialPort").val();
|
|
310
|
-
const isRF = (ch === "/dev/ttyAMA2");
|
|
311
|
-
|
|
312
|
-
$("#st-row-baudRate").toggle(!isRF);
|
|
313
|
-
$("#st-row-parity").toggle(!isRF);
|
|
314
|
-
$("#st-row-stopBits").toggle(!isRF);
|
|
315
|
-
|
|
316
|
-
// show RSSI options ONLY for RF
|
|
317
|
-
$("#st-rssi-wrap").toggle(isRF);
|
|
318
|
-
|
|
319
|
-
// Optional: if not RF, force RSSI off so it can't be accidentally enabled
|
|
320
|
-
if (!isRF) {
|
|
321
|
-
$("#node-config-input-appendRssi").prop("checked", false);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (isRF) {
|
|
325
|
-
$("#node-config-input-baudRate").val(9600);
|
|
326
|
-
$("#node-config-input-parity").val("none");
|
|
327
|
-
$("#node-config-input-stopBits").val(1);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// =========================
|
|
332
|
-
// Config node: smithtek-mako-rf-bus
|
|
333
|
-
// =========================
|
|
334
|
-
RED.nodes.registerType("smithtek-mako-rf-bus", {
|
|
335
|
-
category: "config",
|
|
336
|
-
defaults: {
|
|
337
|
-
busName: { value: "smithtek mako rf", required: true },
|
|
338
|
-
appendRssi: { value: false },
|
|
339
|
-
rssiGuardMs: { value: 10, validate: RED.validators.number() },
|
|
340
|
-
rssiTimeoutMs: { value: 100, validate: RED.validators.number() },
|
|
341
|
-
|
|
342
|
-
// Keep imported USB/by-id paths editable as well as the built-in ports.
|
|
343
|
-
serialPort: { value: "/dev/ttyAMA2", required: true },
|
|
344
|
-
|
|
345
|
-
baudRate: { value: 9600, required: true, validate: RED.validators.number() },
|
|
346
|
-
stopBits: { value: 1, required: true, validate: RED.validators.number() },
|
|
347
|
-
parity: { value: "none", required: true },
|
|
348
|
-
|
|
349
|
-
timeout_s: { value: 8, required: true, validate: RED.validators.number() },
|
|
350
|
-
retries: { value: 2, required: true, validate: RED.validators.number() },
|
|
351
|
-
gap_s: { value: 0, required: true, validate: RED.validators.number() },
|
|
352
|
-
maxQueue: { value: 50, required: true, validate: RED.validators.number() }
|
|
353
|
-
|
|
354
|
-
},
|
|
355
|
-
label: function () {
|
|
356
|
-
return this.busName || "smithtek mako rf";
|
|
357
|
-
},
|
|
358
|
-
oneditprepare: function () {
|
|
359
|
-
const select = $("#node-config-input-serialPort"), saved = this.serialPort || "/dev/ttyAMA2";
|
|
360
|
-
const builtins = ['/dev/ttyAMA2','/dev/ttyAMA0','/dev/ttyAMA1','usb:auto'];
|
|
361
|
-
if (!builtins.includes(saved)) $('<option>').val(saved).text('USB · saved device').appendTo(select);
|
|
362
|
-
select.val(saved);
|
|
363
|
-
const refresh = function () {
|
|
364
|
-
const current = select.val();
|
|
365
|
-
$('#st-usb-status').text('Checking USB connections…');
|
|
366
|
-
$.getJSON('smithtek-passport/usb').done(function(devices) {
|
|
367
|
-
select.find('option[data-detected]').remove();
|
|
368
|
-
devices.forEach(function(device) {
|
|
369
|
-
if (select.find('option').toArray().some(function(o){return o.value===device.value;})) return;
|
|
370
|
-
$('<option>').val(device.value).text(device.label).attr('data-detected','true').appendTo(select);
|
|
371
|
-
});
|
|
372
|
-
select.val(current);
|
|
373
|
-
$('#st-usb-status').text(devices.length ? 'USB devices found. Choose your device if more than one is connected.' : 'No USB devices found. Check power and the data cable.');
|
|
374
|
-
}).fail(function() { $('#st-usb-status').text('Unable to check USB connections. Try Refresh USB again.'); });
|
|
375
|
-
};
|
|
376
|
-
$('#st-usb-refresh').on('click',function(e){e.preventDefault();refresh();});
|
|
377
|
-
refresh();
|
|
378
|
-
updateSerialUiForChannel();
|
|
379
|
-
$("#node-config-input-serialPort").on("change", updateSerialUiForChannel);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// =========================
|
|
384
|
-
// Main node: smithtek-mako-rf
|
|
385
|
-
// =========================
|
|
386
|
-
RED.nodes.registerType("smithtek-mako-rf", {
|
|
387
|
-
category: "Smithtek",
|
|
388
|
-
color: "#B9B6B8",
|
|
389
|
-
defaults: {
|
|
390
|
-
name: { value: "" },
|
|
391
|
-
prefix: { value: "" },
|
|
392
|
-
bus: { type: "smithtek-mako-rf-bus", required: true },
|
|
393
|
-
|
|
394
|
-
mode: { value: "read" },
|
|
395
|
-
fc: { value: 3, required: true, validate: RED.validators.number() },
|
|
396
|
-
unitid: { value: 1, required: true, validate: RED.validators.number() },
|
|
397
|
-
address: { value: 0, required: true, validate: RED.validators.number() },
|
|
398
|
-
quantity: { value: 1, required: true, validate: RED.validators.number() },
|
|
399
|
-
|
|
400
|
-
valueSource: { value: "msg.payload" },
|
|
401
|
-
valueType: { value: "bool" },
|
|
402
|
-
valueText: { value: "" },
|
|
403
|
-
includeWriteQuantity: { value: false },
|
|
404
|
-
|
|
405
|
-
items: { value: [ defaultItemRow() ], validate: validDecoderItems }
|
|
406
|
-
},
|
|
407
|
-
inputs: 1,
|
|
408
|
-
outputs: 2,
|
|
409
|
-
outputLabels: ["Data / alarm", "Debug"],
|
|
410
|
-
icon: "bridge.svg",
|
|
411
|
-
label: function () { return this.name || "Smithtek Mako RF"; },
|
|
412
|
-
|
|
413
|
-
oneditprepare: function () {
|
|
414
|
-
buildFcList(this.mode, $("#node-input-fc"), this.fc);
|
|
415
|
-
toggleRuntimeUi(this.mode);
|
|
416
|
-
toggleValueUi(this.valueSource);
|
|
417
|
-
|
|
418
|
-
$("#node-input-mode").on("change", () => {
|
|
419
|
-
const mode = $("#node-input-mode").val();
|
|
420
|
-
buildFcList(mode, $("#node-input-fc"), $("#node-input-fc").val());
|
|
421
|
-
toggleRuntimeUi(mode);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
$("#node-input-valueSource").on("change", () => {
|
|
425
|
-
toggleValueUi($("#node-input-valueSource").val());
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
const $tbody = $("#st-items-tbody");
|
|
429
|
-
$tbody.empty();
|
|
430
|
-
|
|
431
|
-
let items = Array.isArray(this.items) ? this.items : [];
|
|
432
|
-
if (!items.length) items = [defaultItemRow()];
|
|
433
|
-
items.forEach(it => addItemRow($tbody, it, false));
|
|
434
|
-
|
|
435
|
-
$("#st-add-item").on("click", function (e) {
|
|
436
|
-
e.preventDefault();
|
|
437
|
-
addItemRow($tbody, defaultItemRow(), true);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
$("#st-clear-items").on("click", function (e) {
|
|
441
|
-
e.preventDefault();
|
|
442
|
-
$tbody.empty();
|
|
443
|
-
addItemRow($tbody, defaultItemRow(), false);
|
|
444
|
-
});
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
oneditsave: function () {
|
|
448
|
-
this.items = readItemsFromTable($("#st-items-tbody"));
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
})();
|
|
453
|
-
</script>
|
|
454
|
-
|
|
455
|
-
<!-- =========================
|
|
456
|
-
BUS CONFIG TEMPLATE
|
|
457
|
-
========================= -->
|
|
458
|
-
<script type="text/html" data-template-name="smithtek-mako-rf-bus">
|
|
459
|
-
<div class="form-row">
|
|
460
|
-
<label for="node-config-input-busName"><i class="fa fa-tag"></i> Bus name</label>
|
|
461
|
-
<input type="text" id="node-config-input-busName" placeholder="smithtek mako rf">
|
|
462
|
-
</div>
|
|
463
|
-
|
|
464
|
-
<div class="form-row">
|
|
465
|
-
<label for="node-config-input-serialPort"><i class="fa fa-plug"></i> Channel</label>
|
|
466
|
-
<select id="node-config-input-serialPort" style="width:260px;">
|
|
467
|
-
<option value="/dev/ttyAMA2">RF</option>
|
|
468
|
-
<option value="/dev/ttyAMA0">RS485-1</option>
|
|
469
|
-
<option value="/dev/ttyAMA1">RS485-2</option>
|
|
470
|
-
<option value="usb:auto">USB · Auto detect</option>
|
|
471
|
-
</select>
|
|
472
|
-
<button type="button" id="st-usb-refresh" class="red-ui-button">Refresh USB</button>
|
|
473
|
-
<p id="st-usb-status" class="form-tips"></p>
|
|
474
|
-
<p class="form-tips">Connect your Mako before using USB. Auto detect requires exactly one USB serial device. If several are connected, select yours. Match the PLC communication speed and format.</p>
|
|
475
|
-
</div>
|
|
476
|
-
|
|
477
|
-
<div class="form-row" id="st-row-baudRate">
|
|
478
|
-
<label for="node-config-input-baudRate"><i class="fa fa-cog"></i> Baud</label>
|
|
479
|
-
<input type="number" id="node-config-input-baudRate" style="width:140px;">
|
|
480
|
-
</div>
|
|
481
|
-
|
|
482
|
-
<div class="form-row" id="st-row-parity">
|
|
483
|
-
<label for="node-config-input-parity"><i class="fa fa-adjust"></i> Parity</label>
|
|
484
|
-
<select id="node-config-input-parity" style="width:160px;">
|
|
485
|
-
<option value="none">none</option>
|
|
486
|
-
<option value="even">even</option>
|
|
487
|
-
<option value="odd">odd</option>
|
|
488
|
-
<option value="mark">mark</option>
|
|
489
|
-
<option value="space">space</option>
|
|
490
|
-
</select>
|
|
491
|
-
</div>
|
|
492
|
-
|
|
493
|
-
<div class="form-row" id="st-row-stopBits">
|
|
494
|
-
<label for="node-config-input-stopBits"><i class="fa fa-pause"></i> Stop bits</label>
|
|
495
|
-
<input type="number" id="node-config-input-stopBits" style="width:140px;">
|
|
496
|
-
</div>
|
|
497
|
-
|
|
498
|
-
<hr />
|
|
499
|
-
|
|
500
|
-
<div id="st-rssi-wrap">
|
|
501
|
-
|
|
502
|
-
<div class="form-row">
|
|
503
|
-
<label for="node-config-input-appendRssi"><i class="fa fa-signal"></i> RSSI</label>
|
|
504
|
-
<input type="checkbox" id="node-config-input-appendRssi" style="width:auto;">
|
|
505
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
506
|
-
(Append RSSI in dBm. Diagnostics add one radio query per successful read.)
|
|
507
|
-
</span>
|
|
508
|
-
</div>
|
|
509
|
-
|
|
510
|
-
<div class="form-row">
|
|
511
|
-
<label for="node-config-input-rssiGuardMs"><i class="fa fa-clock-o"></i> RSSI guard (ms)</label>
|
|
512
|
-
<input type="number" id="node-config-input-rssiGuardMs" min="0" style="width:140px;">
|
|
513
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
514
|
-
(Older RF nodes only: pause before checking signal strength. Keep the default unless advised otherwise.)
|
|
515
|
-
</span>
|
|
516
|
-
</div>
|
|
517
|
-
|
|
518
|
-
<div class="form-row">
|
|
519
|
-
<label for="node-config-input-rssiTimeoutMs"><i class="fa fa-hourglass-end"></i> RSSI timeout (ms)</label>
|
|
520
|
-
<input type="number" id="node-config-input-rssiTimeoutMs" min="10" style="width:140px;">
|
|
521
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
522
|
-
(Older RF nodes only: how long to wait for signal strength. PassPort Connect does not use this reading.)
|
|
523
|
-
</span>
|
|
524
|
-
</div>
|
|
525
|
-
|
|
526
|
-
</div>
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
<div class="form-row">
|
|
530
|
-
<label for="node-config-input-timeout_s"><i class="fa fa-clock-o"></i> Timeout (seconds)</label>
|
|
531
|
-
<input type="number" id="node-config-input-timeout_s" style="width:140px;">
|
|
532
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
533
|
-
(How long to wait for a reply. A longer wait also delays the next message if a PLC is offline.)
|
|
534
|
-
</span>
|
|
535
|
-
</div>
|
|
536
|
-
|
|
537
|
-
<div class="form-row">
|
|
538
|
-
<label for="node-config-input-retries"><i class="fa fa-repeat"></i> Retries</label>
|
|
539
|
-
<input type="number" id="node-config-input-retries" style="width:140px;">
|
|
540
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
541
|
-
(Extra attempts after a failed read. PassPort Connect commands are not automatically repeated.)
|
|
542
|
-
</span>
|
|
543
|
-
</div>
|
|
544
|
-
|
|
545
|
-
<div class="form-row">
|
|
546
|
-
<label for="node-config-input-gap_s"><i class="fa fa-arrows-h"></i> Gap (seconds)</label>
|
|
547
|
-
<input type="number" id="node-config-input-gap_s" step="0.1" min="0" style="width:140px;">
|
|
548
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
549
|
-
(A short pause between messages. Keep the default to start.)
|
|
550
|
-
</span>
|
|
551
|
-
</div>
|
|
552
|
-
<div class="form-tips">
|
|
553
|
-
Choose RF for wireless, RS485 for a wired connection, or USB for a connected Mako.
|
|
554
|
-
</div>
|
|
555
|
-
<div class="form-row">
|
|
556
|
-
<label for="node-config-input-maxQueue"><i class="fa fa-list"></i> Max queue</label>
|
|
557
|
-
<input type="number" id="node-config-input-maxQueue" min="1" style="width:140px;">
|
|
558
|
-
</div>
|
|
559
|
-
<div class="form-tips">
|
|
560
|
-
Messages take turns. Max queue is the
|
|
561
|
-
</div>
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
</script>
|
|
565
|
-
|
|
566
|
-
<!-- =========================
|
|
567
|
-
MAIN NODE TEMPLATE
|
|
568
|
-
========================= -->
|
|
569
|
-
<script type="text/html" data-template-name="smithtek-mako-rf">
|
|
570
|
-
<div class="form-row">
|
|
571
|
-
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
572
|
-
<input type="text" id="node-input-name" placeholder="">
|
|
573
|
-
</div>
|
|
574
|
-
|
|
575
|
-
<div class="form-row">
|
|
576
|
-
<label for="node-input-prefix"><i class="fa fa-tag"></i> Prefix</label>
|
|
577
|
-
<input type="text" id="node-input-prefix" placeholder="e.g. b1">
|
|
578
|
-
<div class="form-tips">Added before each decoder name with a space: b1 + Pressure becomes b1 Pressure. Leave blank to keep existing names.</div>
|
|
579
|
-
</div>
|
|
580
|
-
|
|
581
|
-
<div class="form-row">
|
|
582
|
-
<label for="node-input-bus"><i class="fa fa-sitemap"></i> Bus</label>
|
|
583
|
-
<input type="text" id="node-input-bus" style="width: 25%;">
|
|
584
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
585
|
-
(Add or edit your Modbus channel here, RF is for the long range radio Coms (LoRa) )
|
|
586
|
-
</span>
|
|
587
|
-
</div>
|
|
588
|
-
|
|
589
|
-
<div class="form-row">
|
|
590
|
-
<label for="node-input-mode"><i class="fa fa-random"></i> Mode</label>
|
|
591
|
-
<select id="node-input-mode" style="width: 20%;">
|
|
592
|
-
<option value="read">Read</option>
|
|
593
|
-
<option value="write">Write</option>
|
|
594
|
-
</select>
|
|
595
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
596
|
-
(Read will poll the Mako's modbus, Write will send commands to it, digital BOOLS, values etc. )
|
|
597
|
-
</span>
|
|
598
|
-
</div>
|
|
599
|
-
|
|
600
|
-
<div class="form-row">
|
|
601
|
-
<label for="node-input-unitid"><i class="fa fa-hashtag"></i> Device ID</label>
|
|
602
|
-
<input type="number" id="node-input-unitid" min="1" max="247" style="width: 140px;">
|
|
603
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
604
|
-
(The ID of your Mako Device, via RF or RS485)
|
|
605
|
-
</span>
|
|
606
|
-
</div>
|
|
607
|
-
|
|
608
|
-
<div class="form-row">
|
|
609
|
-
<label for="node-input-fc"><i class="fa fa-code"></i> Function</label>
|
|
610
|
-
<select id="node-input-fc" style="width: 25%;"></select>
|
|
611
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
612
|
-
(Match the function code with your V-NET program, Input register, holding register and so on.)
|
|
613
|
-
</span>
|
|
614
|
-
</div>
|
|
615
|
-
|
|
616
|
-
<div class="form-row">
|
|
617
|
-
<label for="node-input-address"><i class="fa fa-crosshairs"></i> Address</label>
|
|
618
|
-
<input type="number" id="node-input-address" min="0" style="width: 140px;">
|
|
619
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
620
|
-
(The address of the first register you want to read,)
|
|
621
|
-
</span>
|
|
622
|
-
</div>
|
|
623
|
-
|
|
624
|
-
<div class="form-row">
|
|
625
|
-
<label for="node-input-quantity"><i class="fa fa-list-ol"></i> Quantity</label>
|
|
626
|
-
<input type="number" id="node-input-quantity" min="1" style="width: 100px;">
|
|
627
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
628
|
-
(Qty of registers to read. 16bit = 1 register, 32bit = 2 registers. example, 3* floats would = 6 registers)
|
|
629
|
-
</span>
|
|
630
|
-
</div>
|
|
631
|
-
|
|
632
|
-
<div id="st-rf-write" style="margin-top:10px; display:none;">
|
|
633
|
-
<div class="form-row">
|
|
634
|
-
<label for="node-input-valueSource"><i class="fa fa-pencil"></i> Value source</label>
|
|
635
|
-
<select id="node-input-valueSource" style="width: 70%;">
|
|
636
|
-
<option value="msg.payload">From msg.payload</option>
|
|
637
|
-
<option value="fixed">Fixed</option>
|
|
638
|
-
</select>
|
|
639
|
-
</div>
|
|
640
|
-
|
|
641
|
-
<div id="st-rf-fixedwrap" style="margin-top:6px;">
|
|
642
|
-
<div class="form-row">
|
|
643
|
-
<label for="node-input-valueType"><i class="fa fa-sliders"></i> Fixed type</label>
|
|
644
|
-
<select id="node-input-valueType" style="width: 70%;">
|
|
645
|
-
<option value="bool">Boolean (true/false)</option>
|
|
646
|
-
<option value="number">Number</option>
|
|
647
|
-
<option value="json">JSON (array/object)</option>
|
|
648
|
-
<option value="string">String</option>
|
|
649
|
-
</select>
|
|
650
|
-
</div>
|
|
651
|
-
|
|
652
|
-
<div class="form-row">
|
|
653
|
-
<label for="node-input-valueText"><i class="fa fa-edit"></i> Fixed value</label>
|
|
654
|
-
<input type="text" id="node-input-valueText" placeholder='e.g. true | 123 | [1,0,1] | [100,200]'>
|
|
655
|
-
</div>
|
|
656
|
-
</div>
|
|
657
|
-
|
|
658
|
-
<div class="form-row">
|
|
659
|
-
<label for="node-input-includeWriteQuantity"><i class="fa fa-check-square-o"></i> Include quantity</label>
|
|
660
|
-
<input type="checkbox" id="node-input-includeWriteQuantity" style="width:auto;">
|
|
661
|
-
</div>
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
<hr />
|
|
665
|
-
|
|
666
|
-
<div id="st-rf-decode-wrap">
|
|
667
|
-
<div class="form-row" style="margin-bottom:6px;">
|
|
668
|
-
<b>Decoder map</b>
|
|
669
|
-
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
670
|
-
(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)
|
|
671
|
-
</span>
|
|
672
|
-
<div style="margin-top:6px;">
|
|
673
|
-
<a href="#" class="editor-button" id="st-add-item"><i class="fa fa-plus"></i> Add row</a>
|
|
674
|
-
<a href="#" class="editor-button" id="st-clear-items" style="margin-left:8px;"><i class="fa fa-trash"></i> Reset</a>
|
|
675
|
-
</div>
|
|
676
|
-
</div>
|
|
677
|
-
|
|
678
|
-
<div class="form-row">
|
|
679
|
-
<div style="overflow-x:auto;"><table class="table table-condensed" style="width:100%; min-width:1100px; table-layout:fixed;">
|
|
680
|
-
<thead>
|
|
681
|
-
<tr>
|
|
682
|
-
<th style="width:20%;">Name </th>
|
|
683
|
-
<th style="width:145px;">Register data Type</th>
|
|
684
|
-
<th style="width:65px;">Offset</th>
|
|
685
|
-
<th style="width:565px;">Scaling</th>
|
|
686
|
-
<th style="width:42px;">Bit</th>
|
|
687
|
-
<th style="width:55px;">Action</th>
|
|
688
|
-
</tr>
|
|
689
|
-
</thead>
|
|
690
|
-
<tbody id="st-items-tbody"></tbody>
|
|
691
|
-
</table></div>
|
|
692
|
-
</div>
|
|
693
|
-
|
|
694
|
-
<div style="font-size:12px; color:#666; margin-top:6px;">
|
|
695
|
-
(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.
|
|
696
|
-
</div>
|
|
697
|
-
<div class="form-tips">None keeps the row compact and applies no scaling. Choose Math for an inline expression such as /100, or Range to map input low/high to output low/high. Use the values actually reported by the Mako: for example 4–20 to 0–100, or 4000–20000 to 0–100 if the register reports microamps. Range replaces the Math expression. Values outside the input range continue scaling unless Clamp is checked.</div>
|
|
698
|
-
</div>
|
|
699
|
-
</script>
|
|
700
|
-
|
|
701
|
-
<script type="text/markdown" data-help-name="smithtek-mako-rf">
|
|
702
|
-
Smithtek Mako RF / RS485.
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
<div class="form-row"><label for="
|
|
753
|
-
<
|
|
754
|
-
<
|
|
755
|
-
<
|
|
756
|
-
<
|
|
757
|
-
|
|
758
|
-
<div
|
|
759
|
-
<
|
|
760
|
-
<p>
|
|
761
|
-
<p>
|
|
762
|
-
|
|
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
|
+
scaleMode: "none",
|
|
30
|
+
inputLow: 4,
|
|
31
|
+
inputHigh: 20,
|
|
32
|
+
outputLow: 0,
|
|
33
|
+
outputHigh: 100,
|
|
34
|
+
clampRange: false,
|
|
35
|
+
offsetbit: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildTypeSelect(value) {
|
|
40
|
+
const $sel = $("<select></select>")
|
|
41
|
+
.addClass("st-item-type")
|
|
42
|
+
.css("width", "100%");
|
|
43
|
+
TYPE_OPTIONS.forEach(t => {
|
|
44
|
+
$sel.append($("<option></option>").attr("value", t).text(t));
|
|
45
|
+
});
|
|
46
|
+
$sel.val(String(value || "16 bit integer"));
|
|
47
|
+
return $sel;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applyTypeRules($tr) {
|
|
51
|
+
const t = String($tr.find(".st-item-type").val() || "");
|
|
52
|
+
const $lenHidden = $tr.find(".st-item-length"); // hidden
|
|
53
|
+
const $bit = $tr.find(".st-item-offsetbit");
|
|
54
|
+
const $bitCol = $tr.find(".st-bit-col");
|
|
55
|
+
|
|
56
|
+
// auto length (hidden but saved)
|
|
57
|
+
if (is32BitType(t)) $lenHidden.val(2);
|
|
58
|
+
else $lenHidden.val(1);
|
|
59
|
+
|
|
60
|
+
// bit only for digital unsigned
|
|
61
|
+
if (isDigitalUnsigned(t)) {
|
|
62
|
+
$bit.prop("disabled", false).show();
|
|
63
|
+
$bitCol.show();
|
|
64
|
+
} else {
|
|
65
|
+
$bit.val(0);
|
|
66
|
+
$bit.prop("disabled", true).hide();
|
|
67
|
+
$bitCol.show();
|
|
68
|
+
}
|
|
69
|
+
updateScaleUi($tr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updateScaleUi($tr) {
|
|
73
|
+
const mode = $tr.find('.st-scale-mode').val();
|
|
74
|
+
const range = mode === 'range';
|
|
75
|
+
const digital = isDigitalUnsigned($tr.find('.st-item-type').val());
|
|
76
|
+
$tr.find('.st-item-scale').toggle(mode === 'math');
|
|
77
|
+
$tr.find('.st-range-fields').toggle(range);
|
|
78
|
+
$tr.find('.st-range-note').toggle(range && digital);
|
|
79
|
+
const inputs = $tr.find('.st-range-number');
|
|
80
|
+
inputs.each(function () {
|
|
81
|
+
const raw = String($(this).val()).trim();
|
|
82
|
+
const bad = range && (raw === '' || !Number.isFinite(Number(raw)));
|
|
83
|
+
this.setCustomValidity(bad ? 'Enter a finite number.' : '');
|
|
84
|
+
$(this).attr('aria-invalid', bad ? 'true' : 'false');
|
|
85
|
+
});
|
|
86
|
+
const low = $tr.find('.st-inputLow'), high = $tr.find('.st-inputHigh');
|
|
87
|
+
if (range && String(low.val()).trim() !== '' && String(high.val()).trim() !== '' && Number(low.val()) === Number(high.val())) {
|
|
88
|
+
high[0].setCustomValidity('Input low and input high must differ.');
|
|
89
|
+
high.attr('aria-invalid', 'true');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validDecoderItems(items) {
|
|
94
|
+
return Array.isArray(items) && items.every(item => {
|
|
95
|
+
if (item.scaleMode !== 'range') return true;
|
|
96
|
+
if (isDigitalUnsigned(item.type) || item.type === 'bool') return false;
|
|
97
|
+
const values = ['inputLow','inputHigh','outputLow','outputHigh'].map(key => item[key]);
|
|
98
|
+
return values.every(value => (typeof value === 'string' || typeof value === 'number') && String(value).trim() !== '' && Number.isFinite(Number(value))) &&
|
|
99
|
+
Number(item.inputLow) !== Number(item.inputHigh) && Number.isFinite(Number(item.inputHigh)-Number(item.inputLow)) && Number.isFinite(Number(item.outputHigh)-Number(item.outputLow));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Determine next row behaviour based on the last row (REGS visible)
|
|
104
|
+
function nextRowFromLast($tbody) {
|
|
105
|
+
const $last = $tbody.find("tr").last();
|
|
106
|
+
if (!$last.length) {
|
|
107
|
+
return { offsetRegs: 0, type: "16 bit integer", offsetbit: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lastRegs = Number($last.find(".st-item-offset-regs").val());
|
|
111
|
+
const lastType = String($last.find(".st-item-type").val() || "");
|
|
112
|
+
const lastBit = Number($last.find(".st-item-offsetbit").val());
|
|
113
|
+
|
|
114
|
+
const safeRegs = Number.isFinite(lastRegs) ? lastRegs : 0;
|
|
115
|
+
const safeBit = Number.isFinite(lastBit) ? lastBit : 0;
|
|
116
|
+
|
|
117
|
+
// DIGITAL BIT-LADDER (same register, bit++, then next register)
|
|
118
|
+
if (isDigitalUnsigned(lastType)) {
|
|
119
|
+
if (safeBit < 15) {
|
|
120
|
+
return { offsetRegs: safeRegs, type: lastType, offsetbit: safeBit + 1 };
|
|
121
|
+
}
|
|
122
|
+
return { offsetRegs: safeRegs + 1, type: lastType, offsetbit: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Normal stepping (REGS)
|
|
126
|
+
const stepRegs = is32BitType(lastType) ? 2 : 1;
|
|
127
|
+
return { offsetRegs: safeRegs + stepRegs, type: lastType, offsetbit: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function addItemRow($tbody, item, isNew) {
|
|
131
|
+
const it = item || defaultItemRow();
|
|
132
|
+
const $tr = $("<tr></tr>");
|
|
133
|
+
const rowIndex = $tbody.find("tr").length + 1;
|
|
134
|
+
|
|
135
|
+
const $name = $("<input/>")
|
|
136
|
+
.attr("type", "text")
|
|
137
|
+
.addClass("st-item-name")
|
|
138
|
+
.css("width", "100%")
|
|
139
|
+
.val(it.name ?? (isNew ? `item${rowIndex}` : ""));
|
|
140
|
+
|
|
141
|
+
const $type = buildTypeSelect(it.type);
|
|
142
|
+
|
|
143
|
+
// Visible: regs
|
|
144
|
+
const existingBytes = Number(it.offset ?? 0);
|
|
145
|
+
const existingRegs = Number.isFinite(existingBytes) ? (existingBytes / 2) : 0;
|
|
146
|
+
|
|
147
|
+
const $offsetRegs = $("<input/>")
|
|
148
|
+
.attr("type", "number")
|
|
149
|
+
.addClass("st-item-offset-regs")
|
|
150
|
+
.css("width", "100%")
|
|
151
|
+
.val(existingRegs);
|
|
152
|
+
|
|
153
|
+
// Hidden: bytes (this is what the JS runtime expects)
|
|
154
|
+
const $offsetBytes = $("<input/>")
|
|
155
|
+
.attr("type", "hidden")
|
|
156
|
+
.addClass("st-item-offset")
|
|
157
|
+
.val(Number.isFinite(existingBytes) ? existingBytes : 0);
|
|
158
|
+
|
|
159
|
+
// keep hidden bytes synced when user edits regs
|
|
160
|
+
$offsetRegs.on("input", function () {
|
|
161
|
+
const r = Number($offsetRegs.val());
|
|
162
|
+
const regs = Number.isFinite(r) ? r : 0;
|
|
163
|
+
$offsetBytes.val(regs * 2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// hidden length field (kept in config, not shown)
|
|
167
|
+
const $lengthHidden = $("<input/>")
|
|
168
|
+
.attr("type", "hidden")
|
|
169
|
+
.addClass("st-item-length")
|
|
170
|
+
.val(it.length ?? 1);
|
|
171
|
+
|
|
172
|
+
const $scale = $("<input/>")
|
|
173
|
+
.attr("type", "text")
|
|
174
|
+
.addClass("st-item-scale")
|
|
175
|
+
.css("width", "100%")
|
|
176
|
+
.val(it.scale ?? it.scaler ?? it.scaleFactor ?? "");
|
|
177
|
+
|
|
178
|
+
const $scaleMode = $('<select></select>').addClass('st-scale-mode').attr('aria-label','Scaling mode').css({width:'82px',flex:'0 0 82px'})
|
|
179
|
+
.attr('title','None: unchanged; Math: expression such as /100; Range: input low/high to output low/high.')
|
|
180
|
+
.append($('<option value="none">None</option>'))
|
|
181
|
+
.append($('<option value="math">Math</option>'))
|
|
182
|
+
.append($('<option value="range">Range</option>'))
|
|
183
|
+
.val(it.scaleMode === 'none' ? 'none' : it.scaleMode === 'range' ? 'range' : String($scale.val()).trim() ? 'math' : 'none');
|
|
184
|
+
$scale.css({width:'100px',flex:'0 0 100px'}).attr('placeholder','/100 or +5').attr('aria-label','Math expression');
|
|
185
|
+
const $rangeFields = $('<div></div>').addClass('st-range-fields').css({display:'flex','align-items':'center',gap:'5px','flex-wrap':'nowrap'});
|
|
186
|
+
const $grid = $('<div></div>').css({display:'flex','align-items':'center',gap:'5px','flex-wrap':'nowrap'});
|
|
187
|
+
[['inputLow','In low',4],['inputHigh','In high',20],['outputLow','Out low',0],['outputHigh','Out high',100]].forEach(([key,label,fallback]) => {
|
|
188
|
+
const $input = $('<input/>').attr({type:'number',step:'any','aria-label':label,title:label.replace('In ', 'Input ').replace('Out ', 'Output ')}).addClass('st-range-number st-'+key).css({width:'62px','min-width':'62px',padding:'4px'}).val(it[key] ?? fallback);
|
|
189
|
+
const $label = $('<label></label>').css({display:'flex','align-items':'center',gap:'3px',width:'auto','white-space':'nowrap','font-size':'10px',margin:0}).append($('<span></span>').text(label)).append($input);
|
|
190
|
+
$input.on('input',() => updateScaleUi($tr));
|
|
191
|
+
$grid.append($label);
|
|
192
|
+
});
|
|
193
|
+
const $clamp = $('<input/>').attr('type','checkbox').addClass('st-clamp-range').prop('checked',it.clampRange === true).css({width:'auto',margin:'0 5px 0 0'});
|
|
194
|
+
$rangeFields.append($grid).append($('<label></label>').css({display:'flex','align-items':'center',width:'auto','white-space':'nowrap','font-size':'11px',margin:0}).append($clamp).append(document.createTextNode('Clamp')));
|
|
195
|
+
$rangeFields.append($('<div></div>').addClass('st-range-note').attr('title','Choose a numeric register type for range scaling.').css({color:'#b33','font-size':'11px'}).text('!'));
|
|
196
|
+
$scaleMode.on('change',() => updateScaleUi($tr));
|
|
197
|
+
|
|
198
|
+
const $offsetbit = $("<input/>")
|
|
199
|
+
.attr("type", "number")
|
|
200
|
+
.addClass("st-item-offsetbit")
|
|
201
|
+
.css("width", "100%")
|
|
202
|
+
.val(it.offsetbit ?? 0);
|
|
203
|
+
|
|
204
|
+
if (isNew) {
|
|
205
|
+
const next = nextRowFromLast($tbody);
|
|
206
|
+
|
|
207
|
+
$type.val(next.type);
|
|
208
|
+
|
|
209
|
+
// set regs, hidden bytes follows
|
|
210
|
+
$offsetRegs.val(next.offsetRegs).trigger("input");
|
|
211
|
+
|
|
212
|
+
// set bit for digital ladder
|
|
213
|
+
$offsetbit.val(next.offsetbit);
|
|
214
|
+
} else {
|
|
215
|
+
// ensure hidden offset is correct for existing rows
|
|
216
|
+
$offsetRegs.trigger("input");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const $del = $("<a href='#' style='color:#c00;'>Remove</a>").on("click", function (e) {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
$tr.remove();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
$tr.append($("<td></td>").append($name));
|
|
225
|
+
$tr.append($("<td></td>").append($type));
|
|
226
|
+
$tr.append($("<td></td>").append($offsetRegs).append($offsetBytes));
|
|
227
|
+
$tr.append($("<td></td>").append($("<div></div>").addClass("st-scaling-inline").css({display:"flex","align-items":"center",gap:"5px","flex-wrap":"nowrap"}).append($scaleMode).append($scale).append($rangeFields)));
|
|
228
|
+
|
|
229
|
+
const $bitTd = $("<td></td>").addClass("st-bit-col").append($offsetbit);
|
|
230
|
+
$tr.append($bitTd);
|
|
231
|
+
|
|
232
|
+
// attach hidden length to the row so it saves
|
|
233
|
+
$tr.append($("<td style='display:none;'></td>").append($lengthHidden));
|
|
234
|
+
|
|
235
|
+
$tr.append($("<td></td>").append($del));
|
|
236
|
+
|
|
237
|
+
$tbody.append($tr);
|
|
238
|
+
|
|
239
|
+
applyTypeRules($tr);
|
|
240
|
+
$type.on("change", function () {
|
|
241
|
+
applyTypeRules($tr);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readItemsFromTable($tbody) {
|
|
246
|
+
const items = [];
|
|
247
|
+
$tbody.find("tr").each(function () {
|
|
248
|
+
const $tr = $(this);
|
|
249
|
+
items.push({
|
|
250
|
+
name: String($tr.find(".st-item-name").val() ?? "").trim(),
|
|
251
|
+
type: String($tr.find(".st-item-type").val() ?? "16 bit integer").trim(),
|
|
252
|
+
offset: Number($tr.find(".st-item-offset").val()) || 0, // BYTES saved
|
|
253
|
+
length: Number($tr.find(".st-item-length").val()) || 1, // hidden but saved
|
|
254
|
+
scale: String($tr.find(".st-item-scale").val() ?? "").trim(),
|
|
255
|
+
scaleMode: $tr.find('.st-scale-mode').val() || 'none',
|
|
256
|
+
inputLow: String($tr.find('.st-inputLow').val() ?? '').trim(),
|
|
257
|
+
inputHigh: String($tr.find('.st-inputHigh').val() ?? '').trim(),
|
|
258
|
+
outputLow: String($tr.find('.st-outputLow').val() ?? '').trim(),
|
|
259
|
+
outputHigh: String($tr.find('.st-outputHigh').val() ?? '').trim(),
|
|
260
|
+
clampRange: $tr.find('.st-clamp-range').prop('checked') === true,
|
|
261
|
+
offsetbit: Number($tr.find(".st-item-offsetbit").val()) || 0
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
return items;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildFcList(mode, $fc, preferred) {
|
|
268
|
+
const isRead = mode === "read";
|
|
269
|
+
const keep = (preferred !== undefined && preferred !== null && preferred !== "")
|
|
270
|
+
? String(preferred)
|
|
271
|
+
: String($fc.val() || "");
|
|
272
|
+
|
|
273
|
+
$fc.empty();
|
|
274
|
+
|
|
275
|
+
const readFcs = [
|
|
276
|
+
{ v: 1, t: "1 - Read Coils" },
|
|
277
|
+
{ v: 2, t: "2 - Read Digital Inputs" },
|
|
278
|
+
{ v: 3, t: "3 - Read Holding Registers" },
|
|
279
|
+
{ v: 4, t: "4 - Read Input Registers" }
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const writeFcs = [
|
|
283
|
+
{ v: 5, t: "5 - Write Single Coil" },
|
|
284
|
+
{ v: 6, t: "6 - Write Single Register" },
|
|
285
|
+
{ v: 15, t: "15 - Write Multiple Coils" },
|
|
286
|
+
{ v: 16, t: "16 - Write Multiple Registers" }
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const list = isRead ? readFcs : writeFcs;
|
|
290
|
+
list.forEach(o => $fc.append($("<option></option>").attr("value", o.v).text(o.t)));
|
|
291
|
+
|
|
292
|
+
if (list.some(o => String(o.v) === keep)) $fc.val(keep);
|
|
293
|
+
else $fc.val(String(list[0].v));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function toggleRuntimeUi(mode) {
|
|
297
|
+
const isRead = mode === "read";
|
|
298
|
+
$("#st-rf-write").toggle(!isRead);
|
|
299
|
+
$("#st-rf-decode-wrap").toggle(isRead);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toggleValueUi(valueSource) {
|
|
303
|
+
const fixed = (valueSource || "msg.payload") === "fixed";
|
|
304
|
+
$("#st-rf-fixedwrap").toggle(fixed);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Bus UI: hide serial settings when RF channel selected
|
|
308
|
+
function updateSerialUiForChannel() {
|
|
309
|
+
const ch = $("#node-config-input-serialPort").val();
|
|
310
|
+
const isRF = (ch === "/dev/ttyAMA2");
|
|
311
|
+
|
|
312
|
+
$("#st-row-baudRate").toggle(!isRF);
|
|
313
|
+
$("#st-row-parity").toggle(!isRF);
|
|
314
|
+
$("#st-row-stopBits").toggle(!isRF);
|
|
315
|
+
|
|
316
|
+
// show RSSI options ONLY for RF
|
|
317
|
+
$("#st-rssi-wrap").toggle(isRF);
|
|
318
|
+
|
|
319
|
+
// Optional: if not RF, force RSSI off so it can't be accidentally enabled
|
|
320
|
+
if (!isRF) {
|
|
321
|
+
$("#node-config-input-appendRssi").prop("checked", false);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (isRF) {
|
|
325
|
+
$("#node-config-input-baudRate").val(9600);
|
|
326
|
+
$("#node-config-input-parity").val("none");
|
|
327
|
+
$("#node-config-input-stopBits").val(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// =========================
|
|
332
|
+
// Config node: smithtek-mako-rf-bus
|
|
333
|
+
// =========================
|
|
334
|
+
RED.nodes.registerType("smithtek-mako-rf-bus", {
|
|
335
|
+
category: "config",
|
|
336
|
+
defaults: {
|
|
337
|
+
busName: { value: "smithtek mako rf", required: true },
|
|
338
|
+
appendRssi: { value: false },
|
|
339
|
+
rssiGuardMs: { value: 10, validate: RED.validators.number() },
|
|
340
|
+
rssiTimeoutMs: { value: 100, validate: RED.validators.number() },
|
|
341
|
+
|
|
342
|
+
// Keep imported USB/by-id paths editable as well as the built-in ports.
|
|
343
|
+
serialPort: { value: "/dev/ttyAMA2", required: true },
|
|
344
|
+
|
|
345
|
+
baudRate: { value: 9600, required: true, validate: RED.validators.number() },
|
|
346
|
+
stopBits: { value: 1, required: true, validate: RED.validators.number() },
|
|
347
|
+
parity: { value: "none", required: true },
|
|
348
|
+
|
|
349
|
+
timeout_s: { value: 8, required: true, validate: RED.validators.number() },
|
|
350
|
+
retries: { value: 2, required: true, validate: RED.validators.number() },
|
|
351
|
+
gap_s: { value: 0, required: true, validate: RED.validators.number() },
|
|
352
|
+
maxQueue: { value: 50, required: true, validate: RED.validators.number() }
|
|
353
|
+
|
|
354
|
+
},
|
|
355
|
+
label: function () {
|
|
356
|
+
return this.busName || "smithtek mako rf";
|
|
357
|
+
},
|
|
358
|
+
oneditprepare: function () {
|
|
359
|
+
const select = $("#node-config-input-serialPort"), saved = this.serialPort || "/dev/ttyAMA2";
|
|
360
|
+
const builtins = ['/dev/ttyAMA2','/dev/ttyAMA0','/dev/ttyAMA1','usb:auto'];
|
|
361
|
+
if (!builtins.includes(saved)) $('<option>').val(saved).text('USB · saved device').appendTo(select);
|
|
362
|
+
select.val(saved);
|
|
363
|
+
const refresh = function () {
|
|
364
|
+
const current = select.val();
|
|
365
|
+
$('#st-usb-status').text('Checking USB connections…');
|
|
366
|
+
$.getJSON('smithtek-passport/usb').done(function(devices) {
|
|
367
|
+
select.find('option[data-detected]').remove();
|
|
368
|
+
devices.forEach(function(device) {
|
|
369
|
+
if (select.find('option').toArray().some(function(o){return o.value===device.value;})) return;
|
|
370
|
+
$('<option>').val(device.value).text(device.label).attr('data-detected','true').appendTo(select);
|
|
371
|
+
});
|
|
372
|
+
select.val(current);
|
|
373
|
+
$('#st-usb-status').text(devices.length ? 'USB devices found. Choose your device if more than one is connected.' : 'No USB devices found. Check power and the data cable.');
|
|
374
|
+
}).fail(function() { $('#st-usb-status').text('Unable to check USB connections. Try Refresh USB again.'); });
|
|
375
|
+
};
|
|
376
|
+
$('#st-usb-refresh').on('click',function(e){e.preventDefault();refresh();});
|
|
377
|
+
refresh();
|
|
378
|
+
updateSerialUiForChannel();
|
|
379
|
+
$("#node-config-input-serialPort").on("change", updateSerialUiForChannel);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// =========================
|
|
384
|
+
// Main node: smithtek-mako-rf
|
|
385
|
+
// =========================
|
|
386
|
+
RED.nodes.registerType("smithtek-mako-rf", {
|
|
387
|
+
category: "Smithtek",
|
|
388
|
+
color: "#B9B6B8",
|
|
389
|
+
defaults: {
|
|
390
|
+
name: { value: "" },
|
|
391
|
+
prefix: { value: "" },
|
|
392
|
+
bus: { type: "smithtek-mako-rf-bus", required: true },
|
|
393
|
+
|
|
394
|
+
mode: { value: "read" },
|
|
395
|
+
fc: { value: 3, required: true, validate: RED.validators.number() },
|
|
396
|
+
unitid: { value: 1, required: true, validate: RED.validators.number() },
|
|
397
|
+
address: { value: 0, required: true, validate: RED.validators.number() },
|
|
398
|
+
quantity: { value: 1, required: true, validate: RED.validators.number() },
|
|
399
|
+
|
|
400
|
+
valueSource: { value: "msg.payload" },
|
|
401
|
+
valueType: { value: "bool" },
|
|
402
|
+
valueText: { value: "" },
|
|
403
|
+
includeWriteQuantity: { value: false },
|
|
404
|
+
|
|
405
|
+
items: { value: [ defaultItemRow() ], validate: validDecoderItems }
|
|
406
|
+
},
|
|
407
|
+
inputs: 1,
|
|
408
|
+
outputs: 2,
|
|
409
|
+
outputLabels: ["Data / alarm", "Debug"],
|
|
410
|
+
icon: "bridge.svg",
|
|
411
|
+
label: function () { return this.name || "Smithtek Mako RF"; },
|
|
412
|
+
|
|
413
|
+
oneditprepare: function () {
|
|
414
|
+
buildFcList(this.mode, $("#node-input-fc"), this.fc);
|
|
415
|
+
toggleRuntimeUi(this.mode);
|
|
416
|
+
toggleValueUi(this.valueSource);
|
|
417
|
+
|
|
418
|
+
$("#node-input-mode").on("change", () => {
|
|
419
|
+
const mode = $("#node-input-mode").val();
|
|
420
|
+
buildFcList(mode, $("#node-input-fc"), $("#node-input-fc").val());
|
|
421
|
+
toggleRuntimeUi(mode);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
$("#node-input-valueSource").on("change", () => {
|
|
425
|
+
toggleValueUi($("#node-input-valueSource").val());
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const $tbody = $("#st-items-tbody");
|
|
429
|
+
$tbody.empty();
|
|
430
|
+
|
|
431
|
+
let items = Array.isArray(this.items) ? this.items : [];
|
|
432
|
+
if (!items.length) items = [defaultItemRow()];
|
|
433
|
+
items.forEach(it => addItemRow($tbody, it, false));
|
|
434
|
+
|
|
435
|
+
$("#st-add-item").on("click", function (e) {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
addItemRow($tbody, defaultItemRow(), true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
$("#st-clear-items").on("click", function (e) {
|
|
441
|
+
e.preventDefault();
|
|
442
|
+
$tbody.empty();
|
|
443
|
+
addItemRow($tbody, defaultItemRow(), false);
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
oneditsave: function () {
|
|
448
|
+
this.items = readItemsFromTable($("#st-items-tbody"));
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
})();
|
|
453
|
+
</script>
|
|
454
|
+
|
|
455
|
+
<!-- =========================
|
|
456
|
+
BUS CONFIG TEMPLATE
|
|
457
|
+
========================= -->
|
|
458
|
+
<script type="text/html" data-template-name="smithtek-mako-rf-bus">
|
|
459
|
+
<div class="form-row">
|
|
460
|
+
<label for="node-config-input-busName"><i class="fa fa-tag"></i> Bus name</label>
|
|
461
|
+
<input type="text" id="node-config-input-busName" placeholder="smithtek mako rf">
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div class="form-row">
|
|
465
|
+
<label for="node-config-input-serialPort"><i class="fa fa-plug"></i> Channel</label>
|
|
466
|
+
<select id="node-config-input-serialPort" style="width:260px;">
|
|
467
|
+
<option value="/dev/ttyAMA2">RF</option>
|
|
468
|
+
<option value="/dev/ttyAMA0">RS485-1</option>
|
|
469
|
+
<option value="/dev/ttyAMA1">RS485-2</option>
|
|
470
|
+
<option value="usb:auto">USB · Auto detect</option>
|
|
471
|
+
</select>
|
|
472
|
+
<button type="button" id="st-usb-refresh" class="red-ui-button">Refresh USB</button>
|
|
473
|
+
<p id="st-usb-status" class="form-tips"></p>
|
|
474
|
+
<p class="form-tips">Connect your Mako before using USB. Auto detect requires exactly one USB serial device. If several are connected, select yours. Match the PLC communication speed and format.</p>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<div class="form-row" id="st-row-baudRate">
|
|
478
|
+
<label for="node-config-input-baudRate"><i class="fa fa-cog"></i> Baud</label>
|
|
479
|
+
<input type="number" id="node-config-input-baudRate" style="width:140px;">
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<div class="form-row" id="st-row-parity">
|
|
483
|
+
<label for="node-config-input-parity"><i class="fa fa-adjust"></i> Parity</label>
|
|
484
|
+
<select id="node-config-input-parity" style="width:160px;">
|
|
485
|
+
<option value="none">none</option>
|
|
486
|
+
<option value="even">even</option>
|
|
487
|
+
<option value="odd">odd</option>
|
|
488
|
+
<option value="mark">mark</option>
|
|
489
|
+
<option value="space">space</option>
|
|
490
|
+
</select>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div class="form-row" id="st-row-stopBits">
|
|
494
|
+
<label for="node-config-input-stopBits"><i class="fa fa-pause"></i> Stop bits</label>
|
|
495
|
+
<input type="number" id="node-config-input-stopBits" style="width:140px;">
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<hr />
|
|
499
|
+
|
|
500
|
+
<div id="st-rssi-wrap">
|
|
501
|
+
|
|
502
|
+
<div class="form-row">
|
|
503
|
+
<label for="node-config-input-appendRssi"><i class="fa fa-signal"></i> RSSI</label>
|
|
504
|
+
<input type="checkbox" id="node-config-input-appendRssi" style="width:auto;">
|
|
505
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
506
|
+
(Append RSSI in dBm. Diagnostics add one radio query per successful read.)
|
|
507
|
+
</span>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div class="form-row">
|
|
511
|
+
<label for="node-config-input-rssiGuardMs"><i class="fa fa-clock-o"></i> RSSI guard (ms)</label>
|
|
512
|
+
<input type="number" id="node-config-input-rssiGuardMs" min="0" style="width:140px;">
|
|
513
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
514
|
+
(Older RF nodes only: pause before checking signal strength. Keep the default unless advised otherwise.)
|
|
515
|
+
</span>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div class="form-row">
|
|
519
|
+
<label for="node-config-input-rssiTimeoutMs"><i class="fa fa-hourglass-end"></i> RSSI timeout (ms)</label>
|
|
520
|
+
<input type="number" id="node-config-input-rssiTimeoutMs" min="10" style="width:140px;">
|
|
521
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
522
|
+
(Older RF nodes only: how long to wait for signal strength. PassPort Connect does not use this reading.)
|
|
523
|
+
</span>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
<div class="form-row">
|
|
530
|
+
<label for="node-config-input-timeout_s"><i class="fa fa-clock-o"></i> Timeout (seconds)</label>
|
|
531
|
+
<input type="number" id="node-config-input-timeout_s" style="width:140px;">
|
|
532
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
533
|
+
(How long to wait for a reply. A longer wait also delays the next message if a PLC is offline.)
|
|
534
|
+
</span>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<div class="form-row">
|
|
538
|
+
<label for="node-config-input-retries"><i class="fa fa-repeat"></i> Retries</label>
|
|
539
|
+
<input type="number" id="node-config-input-retries" style="width:140px;">
|
|
540
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
541
|
+
(Extra attempts after a failed read. PassPort Connect commands are not automatically repeated.)
|
|
542
|
+
</span>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div class="form-row">
|
|
546
|
+
<label for="node-config-input-gap_s"><i class="fa fa-arrows-h"></i> Gap (seconds)</label>
|
|
547
|
+
<input type="number" id="node-config-input-gap_s" step="0.1" min="0" style="width:140px;">
|
|
548
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
549
|
+
(A short pause between messages. Keep the default to start.)
|
|
550
|
+
</span>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="form-tips">
|
|
553
|
+
Choose RF for wireless, RS485 for a wired connection, or USB for a connected Mako.
|
|
554
|
+
</div>
|
|
555
|
+
<div class="form-row">
|
|
556
|
+
<label for="node-config-input-maxQueue"><i class="fa fa-list"></i> Max queue</label>
|
|
557
|
+
<input type="number" id="node-config-input-maxQueue" min="1" max="1000" step="1" style="width:140px;">
|
|
558
|
+
</div>
|
|
559
|
+
<div class="form-tips">
|
|
560
|
+
Messages take turns. When the waiting count reaches Max queue, this connection restarts and cancels all waiting messages and the active message. The active message is not counted towards the limit. Cancelled writes are never replayed; an already sent write may have taken effect. New messages are rejected while recovery runs. There is no five-minute recovery timer. PPC also has a separate limit of 16 unfinished messages per node; see Help.
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
</script>
|
|
565
|
+
|
|
566
|
+
<!-- =========================
|
|
567
|
+
MAIN NODE TEMPLATE
|
|
568
|
+
========================= -->
|
|
569
|
+
<script type="text/html" data-template-name="smithtek-mako-rf">
|
|
570
|
+
<div class="form-row">
|
|
571
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
572
|
+
<input type="text" id="node-input-name" placeholder="">
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<div class="form-row">
|
|
576
|
+
<label for="node-input-prefix"><i class="fa fa-tag"></i> Prefix</label>
|
|
577
|
+
<input type="text" id="node-input-prefix" placeholder="e.g. b1">
|
|
578
|
+
<div class="form-tips">Added before each decoder name with a space: b1 + Pressure becomes b1 Pressure. Leave blank to keep existing names.</div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<div class="form-row">
|
|
582
|
+
<label for="node-input-bus"><i class="fa fa-sitemap"></i> Bus</label>
|
|
583
|
+
<input type="text" id="node-input-bus" style="width: 25%;">
|
|
584
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
585
|
+
(Add or edit your Modbus channel here, RF is for the long range radio Coms (LoRa) )
|
|
586
|
+
</span>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
<div class="form-row">
|
|
590
|
+
<label for="node-input-mode"><i class="fa fa-random"></i> Mode</label>
|
|
591
|
+
<select id="node-input-mode" style="width: 20%;">
|
|
592
|
+
<option value="read">Read</option>
|
|
593
|
+
<option value="write">Write</option>
|
|
594
|
+
</select>
|
|
595
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
596
|
+
(Read will poll the Mako's modbus, Write will send commands to it, digital BOOLS, values etc. )
|
|
597
|
+
</span>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<div class="form-row">
|
|
601
|
+
<label for="node-input-unitid"><i class="fa fa-hashtag"></i> Device ID</label>
|
|
602
|
+
<input type="number" id="node-input-unitid" min="1" max="247" style="width: 140px;">
|
|
603
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
604
|
+
(The ID of your Mako Device, via RF or RS485)
|
|
605
|
+
</span>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div class="form-row">
|
|
609
|
+
<label for="node-input-fc"><i class="fa fa-code"></i> Function</label>
|
|
610
|
+
<select id="node-input-fc" style="width: 25%;"></select>
|
|
611
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
612
|
+
(Match the function code with your V-NET program, Input register, holding register and so on.)
|
|
613
|
+
</span>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div class="form-row">
|
|
617
|
+
<label for="node-input-address"><i class="fa fa-crosshairs"></i> Address</label>
|
|
618
|
+
<input type="number" id="node-input-address" min="0" style="width: 140px;">
|
|
619
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
620
|
+
(The address of the first register you want to read,)
|
|
621
|
+
</span>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div class="form-row">
|
|
625
|
+
<label for="node-input-quantity"><i class="fa fa-list-ol"></i> Quantity</label>
|
|
626
|
+
<input type="number" id="node-input-quantity" min="1" style="width: 100px;">
|
|
627
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
628
|
+
(Qty of registers to read. 16bit = 1 register, 32bit = 2 registers. example, 3* floats would = 6 registers)
|
|
629
|
+
</span>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<div id="st-rf-write" style="margin-top:10px; display:none;">
|
|
633
|
+
<div class="form-row">
|
|
634
|
+
<label for="node-input-valueSource"><i class="fa fa-pencil"></i> Value source</label>
|
|
635
|
+
<select id="node-input-valueSource" style="width: 70%;">
|
|
636
|
+
<option value="msg.payload">From msg.payload</option>
|
|
637
|
+
<option value="fixed">Fixed</option>
|
|
638
|
+
</select>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
<div id="st-rf-fixedwrap" style="margin-top:6px;">
|
|
642
|
+
<div class="form-row">
|
|
643
|
+
<label for="node-input-valueType"><i class="fa fa-sliders"></i> Fixed type</label>
|
|
644
|
+
<select id="node-input-valueType" style="width: 70%;">
|
|
645
|
+
<option value="bool">Boolean (true/false)</option>
|
|
646
|
+
<option value="number">Number</option>
|
|
647
|
+
<option value="json">JSON (array/object)</option>
|
|
648
|
+
<option value="string">String</option>
|
|
649
|
+
</select>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div class="form-row">
|
|
653
|
+
<label for="node-input-valueText"><i class="fa fa-edit"></i> Fixed value</label>
|
|
654
|
+
<input type="text" id="node-input-valueText" placeholder='e.g. true | 123 | [1,0,1] | [100,200]'>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<div class="form-row">
|
|
659
|
+
<label for="node-input-includeWriteQuantity"><i class="fa fa-check-square-o"></i> Include quantity</label>
|
|
660
|
+
<input type="checkbox" id="node-input-includeWriteQuantity" style="width:auto;">
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<hr />
|
|
665
|
+
|
|
666
|
+
<div id="st-rf-decode-wrap">
|
|
667
|
+
<div class="form-row" style="margin-bottom:6px;">
|
|
668
|
+
<b>Decoder map</b>
|
|
669
|
+
<span style="margin-left:8px; font-size:12px; color:#888;">
|
|
670
|
+
(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)
|
|
671
|
+
</span>
|
|
672
|
+
<div style="margin-top:6px;">
|
|
673
|
+
<a href="#" class="editor-button" id="st-add-item"><i class="fa fa-plus"></i> Add row</a>
|
|
674
|
+
<a href="#" class="editor-button" id="st-clear-items" style="margin-left:8px;"><i class="fa fa-trash"></i> Reset</a>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<div class="form-row">
|
|
679
|
+
<div style="overflow-x:auto;"><table class="table table-condensed" style="width:100%; min-width:1100px; table-layout:fixed;">
|
|
680
|
+
<thead>
|
|
681
|
+
<tr>
|
|
682
|
+
<th style="width:20%;">Name </th>
|
|
683
|
+
<th style="width:145px;">Register data Type</th>
|
|
684
|
+
<th style="width:65px;">Offset</th>
|
|
685
|
+
<th style="width:565px;">Scaling</th>
|
|
686
|
+
<th style="width:42px;">Bit</th>
|
|
687
|
+
<th style="width:55px;">Action</th>
|
|
688
|
+
</tr>
|
|
689
|
+
</thead>
|
|
690
|
+
<tbody id="st-items-tbody"></tbody>
|
|
691
|
+
</table></div>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
694
|
+
<div style="font-size:12px; color:#666; margin-top:6px;">
|
|
695
|
+
(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.
|
|
696
|
+
</div>
|
|
697
|
+
<div class="form-tips">None keeps the row compact and applies no scaling. Choose Math for an inline expression such as /100, or Range to map input low/high to output low/high. Use the values actually reported by the Mako: for example 4–20 to 0–100, or 4000–20000 to 0–100 if the register reports microamps. Range replaces the Math expression. Values outside the input range continue scaling unless Clamp is checked.</div>
|
|
698
|
+
</div>
|
|
699
|
+
</script>
|
|
700
|
+
|
|
701
|
+
<script type="text/markdown" data-help-name="smithtek-mako-rf">
|
|
702
|
+
Smithtek Mako RF / RS485.
|
|
703
|
+
|
|
704
|
+
**Automatic queue recovery (3.5.1):** Open this node and click the pencil beside Bus. Max queue counts waiting messages only. With Max queue 10, the tenth waiting message starts recovery. The active message and every waiting message are cancelled with EBUSRESET once through the existing alarm/debug outputs and Catch. New arrivals are rejected with EBUSRECOVERING. Cancelled writes are never replayed; an already sent write may have executed. Check the equipment and read back before resending. msg.modbus.writeMayHaveApplied and the technical error payload flag a possibly sent write.
|
|
705
|
+
|
|
706
|
+
The connection must close before a fresh connection opens. It waits for late replies to stop and clears old data before accepting new FIFO work. Other connections keep working. Failed recovery reports EBUSRECOVERYFAILED and blocks new work; a port whose closure cannot be confirmed stays blocked. Check the fault and connection before redeploying. This restarts only the affected connection inside the nodes, not Node-RED, PassPort, system drivers or the PLC. There is no five-minute recovery timer; ordinary response timeouts and retries still apply.
|
|
707
|
+
|
|
708
|
+
Each PPC input message uses one shared queue position and finishes its whole table before the next message. PPC retains its separate limit of 16 unfinished messages per node, including its active message. Extra messages report EQUEUEFULL through Catch without restarting the bus. For a single busy PPC node, Max queue 10 allows recovery before that separate limit. Slow down Inject repeats if the queue keeps filling.
|
|
709
|
+
|
|
710
|
+
Read mode outputs decoded JSON in msg.payload using the Decoder map rows. Numeric readings are rounded to at most six decimal places after Math or Range scaling, and remain numbers. Digital points (coils, discrete inputs and register bits) output numeric 1 for on and 0 for off.
|
|
711
|
+
Raw modbus response is kept in msg.modbus.raw.
|
|
712
|
+
|
|
713
|
+
Prefix adds text and one space before each decoder name. Leave blank to preserve names.
|
|
714
|
+
|
|
715
|
+
Output 1 (Data / alarm) carries successful readings and write responses, or the friendly named fault JSON on failure. Output 2 (Debug) carries technical error details: ok, error, code and req. Connect it to a local Debug node or leave it disconnected. Node-RED error logs and Catch handling remain enabled. A timeout alarm is sent after one poll exhausts its retries (retries 2 means 3 attempts), not after multiple bad polls. Permanent Modbus exceptions report immediately. Each failed poll emits one alarm and one debug message.
|
|
716
|
+
|
|
717
|
+
Failed requests output a named numeric alarm on output 1, for example {"b1 RF ID2 Not communicating — check power/RF; disable if out of service":1}. Details remain in msg.modbus, including ok, code, error and fault. Only final failures are reported; a successful retry or intentional shutdown does not emit an alarm. Successful reads resume normal data; no automatic alarm-clear value is sent.
|
|
718
|
+
|
|
719
|
+
Each numeric decoder row starts compact with None (unchanged). Choose Math or Range to show the inline settings and enable scaling. Selecting None hides both controls and disables both calculations, while retaining the entered settings. Range maps input low/high to output low/high using a straight line; for example 4–20 maps to 0–100, with 12 mapping to 50. Use raw register endpoints, not assumed electrical units. Input endpoints must differ. Reversed ranges and negative/decimal endpoints are supported. Clamp optionally limits output to the configured range. Existing rows without a scaling mode retain their Math expression.
|
|
720
|
+
</script>
|
|
721
|
+
|
|
722
|
+
<script type="text/javascript">
|
|
723
|
+
(function(){
|
|
724
|
+
const typeLabels={float32:'Float',uint32:'32-bit unsigned',int32:'32-bit signed',int16:'16-bit signed',uint16:'16-bit unsigned',boolean:'TRUE / FALSE'};
|
|
725
|
+
const width={float32:2,int32:2,uint32:2,int16:1,uint16:1,boolean:1};
|
|
726
|
+
function collect(){const channels=[],indices={analog:0,switches:0};$('#pl-rows tr').each(function(i){const tr=$(this),get=k=>tr.find('[data-field="'+k+'"]').val(),kind=get('kind');channels.push({...(tr.data('channel')||{}),slot:i,index:indices[kind]++,kind,name:get('name').trim(),dataType:kind==='switches'?'boolean':get('dataType'),address:Number(get('address')),pulseOnWrite:kind==='switches'&&!!(tr.data('channel')||{}).pulseOnWrite,scaleMode:kind==='switches'?'none':get('scaleMode'),scale:get('scale'),inputLow:get('inputLow'),inputHigh:get('inputHigh'),outputLow:get('outputLow'),outputHigh:get('outputHigh'),rangeCollapsed:!!tr.data('rangeCollapsed'),clampRange:tr.find('[data-field="clampRange"]').prop('checked')});});const map={schema:'smithtek.passport-channels',version:1,unitId:Number($('#pl-unit').val()),channels};map.mappingId='passport-link-v1:'+JSON.stringify([map.unitId,channels.map(c=>[c.kind,c.index,c.name,c.dataType,c.address,c.slot,!!c.pulseOnWrite,c.pulseMs||250])]);return map;}
|
|
727
|
+
function modeUi(){const write=$('#node-input-mode').val()==='write';$('.pl-write-only').toggle(write);$('.pl-read-only').toggle(!write);$('.pl-single-only').toggle(write&&$('#node-input-inputMode').val()==='single');$('.pl-table tr').each(function(){$(this).children().eq(4).toggle(!write);});}
|
|
728
|
+
function sync(){const map=collect();const choose=$('#node-input-writeChannel'),selected=choose.val()||choose.data('selected')||'';choose.empty().append($('<option>').val('').text('Choose channel…'));map.channels.forEach(c=>choose.append($('<option>').val(c.name).text(c.name+' · '+(typeLabels[c.dataType]||c.dataType))));if(selected&&!map.channels.some(c=>c.name===selected))choose.append($('<option>').val(selected).text(selected+' (not in table)'));choose.val(selected);modeUi();$('#node-input-mapping').val(JSON.stringify(map)).trigger('change');$('#pl-count').text(map.channels.length+' / 128 channels');$('#pl-add').prop('disabled',map.channels.length>=128);}
|
|
729
|
+
function row(c){const tr=$('<tr>').data('rangeCollapsed',!!c.rangeCollapsed).data('channel',c).appendTo('#pl-rows');function field(key,type,value){return $('<input>').attr({type:type||'text','data-field':key,'aria-label':key}).val(value??'');}function select(key,options,value){const el=$('<select>').attr({'data-field':key,'aria-label':key});options.forEach(o=>$('<option>').val(o[0]).text(o[1]).appendTo(el));return el.val(value);}
|
|
730
|
+
const name=field('name','text',c.name).attr('maxlength',40),kind=select('kind',[['analog','Analog'],['switches','Digital']],c.kind),type=select('dataType',Object.entries(typeLabels),c.dataType),address=field('address','number',c.address).attr({min:0,max:65535,step:1}),mode=select('scaleMode',[['none','None'],['math','Math'],['range','Range']],c.scaleMode||'none'),math=field('scale','text',c.scale).attr('placeholder','/100 or +5'),range=$('<div class="pl-range">');
|
|
731
|
+
for(const [key,label] of [['inputLow','Input low'],['inputHigh','Input high'],['outputLow','Output low'],['outputHigh','Output high']])$('<label>').text(label).append(field(key,'number',c[key]).attr('step','any')).appendTo(range);
|
|
732
|
+
$('<label>').text('Clamp ').append(field('clampRange','checkbox','').prop('checked',!!c.clampRange)).appendTo(range);
|
|
733
|
+
const toggle=$('<button type="button" class="red-ui-button">').on('click',()=>{tr.data('rangeCollapsed',!tr.data('rangeCollapsed'));visibility();sync();});
|
|
734
|
+
for(const control of [name,kind,type,address])$('<td>').append(control).appendTo(tr);$('<td>').append($('<div style="display:flex;gap:5px;align-items:center">').append(mode,toggle),math,range).appendTo(tr);
|
|
735
|
+
const buttons=$('<td>');for(const [label,action] of [['↑',()=>{tr.prev().before(tr);sync();}],['↓',()=>{tr.next().after(tr);sync();}],['×',()=>{tr.remove();sync();}]])$('<button type="button" class="red-ui-button">').text(label).attr('aria-label',label==='×'?'Remove channel':label==='↑'?'Move channel up':'Move channel down').on('click',action).appendTo(buttons);buttons.appendTo(tr);
|
|
736
|
+
function visibility(){const sw=kind.val()==='switches';if(sw){type.val('boolean');mode.val('none');}else if(type.val()==='boolean')type.val('float32');type.prop('disabled',sw);mode.prop('disabled',sw);math.toggle(mode.val()==='math');const isRange=mode.val()==='range',collapsed=!!tr.data('rangeCollapsed');range.toggle(isRange&&!collapsed);toggle.toggle(isRange).text(collapsed?'Show range':'Hide range').attr('aria-expanded',String(!collapsed)).css({whiteSpace:'nowrap',flexShrink:0});}
|
|
737
|
+
mode.on('change',()=>{if(mode.val()==='range')tr.data('rangeCollapsed',false);});
|
|
738
|
+
kind.on('change',()=>{const others=collect().channels.filter((_,i)=>i!==tr.index()).filter(c=>c.kind===kind.val());address.val(others.reduce((end,c)=>Math.max(end,c.address+(width[c.dataType]||1)),0));});
|
|
739
|
+
tr.on('input change','input,select',()=>{visibility();sync();});visibility();
|
|
740
|
+
}
|
|
741
|
+
function valid(value){try{const m=typeof value==='string'?JSON.parse(value):value;if(!m||!Number.isInteger(m.unitId)||m.unitId<1||m.unitId>247||!m.channels.length||m.channels.length>128)return false;const names=new Set(),used=new Set();return m.channels.every(c=>{if(!c.name||names.has(c.name)||['__proto__','constructor','prototype'].includes(c.name)||!width[c.dataType]||!Number.isInteger(c.address)||c.address<0||c.address+width[c.dataType]>65536)return false;names.add(c.name);for(let i=0;i<width[c.dataType];i++){const k=c.kind+':'+(c.address+i);if(used.has(k))return false;used.add(k);}if(c.scaleMode==='range'){const vals=['inputLow','inputHigh','outputLow','outputHigh'].map(k=>c[k]);if(vals.some(v=>v===''||!Number.isFinite(Number(v)))||Number(vals[0])===Number(vals[1]))return false;}if(c.scaleMode==='math'){const s=String(c.scale||'').trim();if(!s||(!Number.isFinite(Number(s))&&!/^([*\/+\-]|>>>|>>|<<|==|!=|>|<|%)\s*(-?\d+(?:\.\d+)?)$/.test(s))||/^[/%%]\s*-?0(?:\.0+)?$/.test(s))return false;}return true;});}catch(e){return false;}}
|
|
742
|
+
RED.nodes.registerType('smithtek-passport-link',{
|
|
743
|
+
category:'Smithtek',paletteLabel:'PassPort Connect',color:'#6D6D6D',defaults:{prefix:{value:''},writeChannel:{value:'',validate:function(v){if(this.mode!=='write'||this.inputMode!=='single')return true;try{return JSON.parse(this.mapping).channels.some(c=>c.name===v);}catch(e){return false;}}},name:{value:''},bus:{value:'',type:'smithtek-mako-rf-bus',required:true},mode:{value:'read',required:true},inputMode:{value:'single'},mapping:{value:'',required:true,validate:valid}},inputs:1,outputs:2,icon:'passport-connect-cropped.png',label:function(){return this.name||'PassPort Connect';},outputLabels:['Named data / write confirmed','Error'],
|
|
744
|
+
oneditprepare:function(){$('#node-input-writeChannel').data('selected',this.writeChannel||'').on('change',function(){$(this).data('selected',$(this).val());});$('#node-input-mode,#node-input-inputMode').on('change',modeUi);$('#node-input-inputMode').val(this.inputMode||'arrays');let map;try{map=typeof this.mapping==='string'?JSON.parse(this.mapping):this.mapping;}catch(e){}$('#pl-unit').val(map?.unitId||1);$('#pl-rows').empty();(map?.channels||[]).forEach(row);$('#pl-unit').on('input change',sync);$('#pl-add').on('click',function(){const channels=collect().channels,kind=$('#pl-add-kind').val(),label=kind==='switches'?'Digital':'Analog';let address=0;channels.filter(c=>c.kind===kind).forEach(c=>address=Math.max(address,c.address+width[c.dataType]));let i=1;while(channels.some(c=>c.name===label+' '+i))i++;row({name:label+' '+i,kind,dataType:kind==='switches'?'boolean':'float32',address});sync();});sync();},oneditsave:function(){sync();this.mapping=$('#node-input-mapping').val();}
|
|
745
|
+
});
|
|
746
|
+
})();
|
|
747
|
+
</script>
|
|
748
|
+
<style>
|
|
749
|
+
.pl-table{width:100%;border-collapse:collapse}.pl-table td,.pl-table th{padding:5px;text-align:left;vertical-align:top}.pl-table input,.pl-table select{width:100%;min-width:75px}.pl-table td:first-child{min-width:140px}.pl-table td:last-child{white-space:nowrap}.pl-table td:last-child button{padding:0 6px}.pl-range{display:grid;grid-template-columns:1fr 1fr;gap:5px;min-width:220px}.pl-range label{font-size:11px;margin:0!important;width:auto!important}.pl-range input[type=checkbox]{width:auto;min-width:0}.pl-table [data-field=scale]{margin-top:5px}
|
|
750
|
+
</style>
|
|
751
|
+
<script type="text/html" data-template-name="smithtek-passport-link">
|
|
752
|
+
<div class="form-row"><label for="node-input-name">Name</label><input id="node-input-name" type="text"></div>
|
|
753
|
+
<div class="form-row"><label for="node-input-bus">Connection</label><input id="node-input-bus"></div>
|
|
754
|
+
<div class="form-row"><label for="node-input-mode">Action</label><select id="node-input-mode"><option value="read">Read channels</option><option value="write">Write channels</option></select></div>
|
|
755
|
+
<div class="form-row pl-write-only"><label for="node-input-inputMode">Write input</label><select id="node-input-inputMode"><option value="single">Single channel</option><option value="arrays">Mapped arrays</option><option value="named">Named values</option></select></div>
|
|
756
|
+
<div class="form-row pl-single-only"><label for="node-input-writeChannel">Write channel</label><select id="node-input-writeChannel"></select></div>
|
|
757
|
+
<div class="form-row pl-read-only"><label for="node-input-prefix">Prefix</label><input id="node-input-prefix" type="text" placeholder="e.g. North Mako"></div>
|
|
758
|
+
<div class="form-row"><label for="pl-unit">Unit ID</label><input id="pl-unit" type="number" min="1" max="247"></div>
|
|
759
|
+
<input id="node-input-mapping" type="hidden">
|
|
760
|
+
<p><b>Separate Read and Write nodes:</b> copy and paste your complete Read node, then change the copy’s Action to Write. This copies the whole table. Choose the row you want to control. Later changes to one node do not update the other.</p>
|
|
761
|
+
<p class="pl-write-only"><b>Single channel:</b> select Pump, then inject a Boolean true/false into msg.payload; select a setpoint and inject a number. Only that channel is written. <b>Named values:</b> send {"Pump":true,"Setpoint":25}; only supplied names are written. Names are case-sensitive and use the table names without the read Prefix. <b>Mapped arrays:</b> send matching mappingId plus Analog and/or Digital arrays (JSON keys analog and switches) in table order. These are ordered values, not named objects.</p>
|
|
762
|
+
<p class="pl-read-only"><b>Prefix:</b> helps you label readings. North Mako plus Level appears as "North Mako Level". Commands still use the original row name, Level.</p>
|
|
763
|
+
|
|
764
|
+
<div style="overflow:auto"><table class="pl-table"><thead><tr><th>Name</th><th>Data type</th><th>Value type</th><th>Address</th><th>Read scaling</th><th>Order / remove</th></tr></thead><tbody id="pl-rows"></tbody></table></div>
|
|
765
|
+
<p><select id="pl-add-kind" aria-label="New channel kind" style="width:120px"><option value="analog">Analog</option><option value="switches">Digital</option></select> <button id="pl-add" type="button" class="red-ui-button">Add channel</button> <span id="pl-count"></span></p>
|
|
766
|
+
<p>Keep this table matched to the PLC. Moving a row does not change its address. Changes here do not change the PLC program. Export again from V-net after changing the PLC table.</p>
|
|
767
|
+
<p>Math and Range change named read values. Arrays in msg.channels keep the original values. Write inputs use device units; scaling is not applied to commands. Update matching nodes after changing the table.</p>
|
|
768
|
+
</script>
|
|
763
769
|
<script type="text/html" data-help-name="smithtek-passport-link">
|
|
764
770
|
<p>PassPort Connect (PPC) is Smithtek’s proprietary communication link between PassPort, Mako and Solaris. It lets your PassPort read measurements and send commands to a Mako or Solaris. For example, you can check a tank level, change a setting or start a pump.</p>
|
|
765
771
|
<p>The smithtek-mako-rf package includes two communication nodes. Original Mako RF is for basic Modbus reads and writes. PassPort Connect is for the matching PassPort Connect table in V-NET2. Choose PassPort Connect for this guide; the two nodes use different message formats.</p>
|
|
@@ -774,7 +780,7 @@ Each numeric decoder row starts compact with None (unchanged). Choose Math or Ra
|
|
|
774
780
|
<p>Keep the selected connection for PassPort messages. Do not also send ordinary Monitor text over it: mixed messages can stop readings and commands from working.</p>
|
|
775
781
|
<h3>2 · Make your channel table</h3>
|
|
776
782
|
<dl><dt>Float</dt><dd>A 32-bit floating-point number, such as 12.5 volts. Stores about seven significant digits. Large whole numbers can round: 123456789 becomes 123456792. Choose 32-bit unsigned or 32-bit signed when you need that whole number exactly.</dd><dt>16-bit signed</dt><dd>Whole numbers from −32768 to 32767.</dd><dt>16-bit unsigned</dt><dd>Whole numbers from 0 to 65535.</dd><dt>32-bit signed</dt><dd>Whole numbers from −2147483648 to 2147483647.</dd><dt>32-bit unsigned</dt><dd>Whole numbers from 0 to 4294967295.</dd><dt>Digital</dt><dd>On or off: true or false. Use this for switches and simple controls.</dd></dl>
|
|
777
|
-
<p>An Analog row holds a number. A Digital row holds on or off. Mako supports up to 64 rows; Solaris supports up to 16, with space also needed for the rest of your program.</p>
|
|
783
|
+
<p>An Analog row holds a number. A Digital row holds on or off. Mako supports up to 128 rows (use the updated PassPort Connect Node-RED package for tables over 64 rows); Solaris supports up to 16, with space also needed for the rest of your program.</p>
|
|
778
784
|
<p>Use a different name for each row, up to 40 characters. Capital letters matter: Pump and pump are different names. The editor will tell you if a name cannot be used.</p>
|
|
779
785
|
<p>V-NET2 works out the table addresses for you. An address is simply where a value is kept. Leave these alone when using an exported table.</p>
|
|
780
786
|
<p>Keep the PLC table and both Node-RED tables the same. After changing a table in V-NET2, upload the changed PLC program and export fresh nodes. Replace the old nodes so you do not accidentally read or send commands twice.</p>
|
|
@@ -819,7 +825,13 @@ Each numeric decoder row starts compact with None (unchanged). Choose Math or Ra
|
|
|
819
825
|
<p>Think of the connection as a single lane: messages take turns. A long table or a PLC that does not answer can hold up the next message, including a command.</p>
|
|
820
826
|
<p>Timeout is how long PassPort waits for an answer. Read retries is how many extra times it tries a failed read. The exported defaults wait up to 8 seconds and allow 2 extra read attempts. Commands do not use these extra attempts.</p>
|
|
821
827
|
<p>A queue is a waiting list of messages. If you ask for readings too quickly, the list grows and replies get late. Slow down the Inject repeat rate and remove duplicate triggers if this happens.</p>
|
|
822
|
-
<p>
|
|
828
|
+
<p>Automatic queue recovery is available in the Smithtek node package from version 3.5.1. Open a Read or Write node, then click the pencil beside Connection. Max queue sets how many messages may wait before that connection restarts. The message already being processed does not count. For example, with Max queue set to 10, the tenth waiting message starts recovery.</p>
|
|
829
|
+
<p>Recovery cancels the active message and every waiting message, including the one that reached the limit. Each cancelled message reports EBUSRESET once through the second output and Catch. Messages arriving during recovery are rejected with EBUSRECOVERING; they are not saved for later.</p>
|
|
830
|
+
<p>Cancelled writes are never replayed. A write already sent to the PLC may have taken effect, even if its reply was lost. Recovery does not undo it. Check the equipment and read the value back before deciding whether to send that command again. The error output includes writeMayHaveApplied to flag a command that may already have been sent.</p>
|
|
831
|
+
<p>The connection closes, opens fresh, waits for late replies to stop, then clears old data. New messages can take turns again when recovery finishes. Other connections keep working. This restarts only that connection inside the nodes; it does not restart Node-RED, PassPort or the PLC, and it does not reset system drivers.</p>
|
|
832
|
+
<p>If the old connection cannot be confirmed closed, recovery stops and reports EBUSRECOVERYFAILED. It will not open a second connection to the same port. Check the reported fault and the connection before redeploying. If the old port is still held open, a redeploy will remain blocked rather than risk two users of that port.</p>
|
|
833
|
+
<p>There is no five-minute recovery timer. Recovery starts only when Max queue is reached; normal response timeouts and read retries still apply. Slow down repeated Inject messages if recovery happens often. Recovery clears a backlog but does not fix a missing PLC or an overloaded connection.</p>
|
|
834
|
+
<p>Each PassPort Connect input message counts once in the shared waiting list, even when its table needs several requests. Its whole table finishes before the next message starts. Each Read or Write node also keeps its existing limit of 16 unfinished messages, including its active message. Any extra message reports EQUEUEFULL through Catch. This separate limit can be reached before Max queue; it does not restart the connection. For one busy PPC node, a Max queue of 10 lets bus recovery happen before that separate limit. Keep Debug on the second output and a Catch node connected while testing.</p>
|
|
823
835
|
<p>Start with one PLC and manual reads. Add regular reads and other PLCs gradually, checking how long replies take. Signal conditions and the amount of information affect the speed.</p>
|
|
824
836
|
<p>Signal-strength options used by the older RF node do not add a signal-strength reading to PassPort Connect.</p>
|
|
825
837
|
<h3>8 · If something is not working</h3>
|
|
@@ -845,4 +857,9 @@ Each numeric decoder row starts compact with None (unchanged). Choose Math or Ra
|
|
|
845
857
|
<p>Open Write, choose Single channel and select TestRelay. Do not leave Battery selected. Make one Inject with Boolean true and another with Boolean false. Leave automatic startup and Repeat off. Wire both to Write, Deploy, and test each button.</p>
|
|
846
858
|
<p>Check the relay itself or a feedback sensor, as well as the reply. For an Analog setpoint later, select that row and send a number in the original units.</p>
|
|
847
859
|
<p>For a ready-wired farm project, open Farmers quick templates. Choose the picture, enter a unique Unit ID and the required sensor settings, then choose Load & export Node-RED. The project uses PassPort Connect, includes battery voltage and exports the complete Read and Write pair onto your current flow.</p>
|
|
848
|
-
</
|
|
860
|
+
<h3>Remember table values on Mako</h3>
|
|
861
|
+
<p>Mako only: select the logic block, then open its table from the left-hand Properties panel. Tick Save on restart beside each row you want to remember and click Apply table. New rows start unticked. Only ticked rows restore their last saved numbers or ON/OFF commands; unticked rows start from their Initial value.</p>
|
|
862
|
+
<p>Live values update immediately. Save interval (minutes) starts at 5, which is the minimum. Enter 60 for one hour, 360 for six hours, or any whole number up to 1440 (24 hours). Changed values save at those checks, starting one full interval after startup. New messages, EEPROM writes, Clear EEPROM, reset and sleep commands do not force an early table save. The PLC EEPROM interval still controls the other saved features; each table has its own interval.</p>
|
|
863
|
+
<p>Power loss or a restart before the next save loses changes since the last successful save. A connected input takes over when its source publishes a value; leave remote-command rows unwired on the left. Restored measurements are old readings until refreshed. Pulse on write rows are excluded and need a new command. Uploading a new Mako program clears saved values.</p>
|
|
864
|
+
<p>PLC EEPROM Save pending and Save error include remembered tables. Clear EEPROM and Read saved still work on the separate EEPROM values bank, not on these tables. To see a table value, use its output pin or read it from PassPort. Changing which rows are saved changes the saved record. A record that no longer matches uses Initial values. Older programs with whole-table saving enabled open with their ordinary rows already ticked; you can untick any you do not need.</p>
|
|
865
|
+
</script>
|