@fa_yoshinobu/node-red-contrib-plc-comm-slmp 0.2.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/docsrc/assets/README.md +10 -0
- package/docsrc/assets/node-red-slmp.png +0 -0
- package/docsrc/index.md +11 -0
- package/docsrc/maintainer/ARCHITECTURE.md +36 -0
- package/docsrc/user/USER_GUIDE.md +238 -0
- package/docsrc/user/toc.yml +2 -0
- package/docsrc/validation/reports/README.md +15 -0
- package/examples/flows/README.md +24 -0
- package/examples/flows/slmp-array-string.json +185 -0
- package/examples/flows/slmp-basic-read-write.json +185 -0
- package/examples/flows/slmp-control-error.json +211 -0
- package/examples/flows/slmp-demo.json +260 -0
- package/examples/flows/slmp-device-matrix.json +514 -0
- package/examples/flows/slmp-routing.json +118 -0
- package/examples/flows/slmp-udp-read-write.json +185 -0
- package/lib/index.js +6 -0
- package/lib/slmp/client.js +642 -0
- package/lib/slmp/constants.js +121 -0
- package/lib/slmp/core.js +406 -0
- package/lib/slmp/errors.js +21 -0
- package/lib/slmp/high-level.js +911 -0
- package/lib/slmp/index.js +10 -0
- package/nodes/slmp-connection.html +142 -0
- package/nodes/slmp-connection.js +78 -0
- package/nodes/slmp-read.html +274 -0
- package/nodes/slmp-read.js +207 -0
- package/nodes/slmp-write.html +267 -0
- package/nodes/slmp-write.js +275 -0
- package/package.json +53 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
function slmpValidateIntegerInRange(value, min, max, options) {
|
|
3
|
+
const text = String(value == null ? "" : value).trim();
|
|
4
|
+
const base = options && options.base ? options.base : 10;
|
|
5
|
+
if (!text) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (base === 16 && !/^(?:0x)?[0-9a-f]+$/i.test(text)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (base === 10 && !/^\d+$/.test(text)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number.parseInt(text, base);
|
|
15
|
+
return Number.isInteger(parsed) && parsed >= min && parsed <= max;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
RED.nodes.registerType("slmp-connection", {
|
|
19
|
+
category: "config",
|
|
20
|
+
defaults: {
|
|
21
|
+
name: { value: "" },
|
|
22
|
+
host: { value: "", required: true },
|
|
23
|
+
port: {
|
|
24
|
+
value: 5000,
|
|
25
|
+
required: true,
|
|
26
|
+
validate: function (value) {
|
|
27
|
+
return slmpValidateIntegerInRange(value, 1, 65535);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
transport: { value: "tcp", required: true },
|
|
31
|
+
timeout: {
|
|
32
|
+
value: 3000,
|
|
33
|
+
required: true,
|
|
34
|
+
validate: function (value) {
|
|
35
|
+
return slmpValidateIntegerInRange(value, 1, 2147483647);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
plcSeries: { value: "ql", required: true },
|
|
39
|
+
frameType: { value: "4e", required: true },
|
|
40
|
+
monitoringTimer: {
|
|
41
|
+
value: 16,
|
|
42
|
+
required: true,
|
|
43
|
+
validate: function (value) {
|
|
44
|
+
return slmpValidateIntegerInRange(value, 0, 65535);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
network: {
|
|
48
|
+
value: "0",
|
|
49
|
+
validate: function (value) {
|
|
50
|
+
return slmpValidateIntegerInRange(value, 0, 255);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
station: {
|
|
54
|
+
value: "255",
|
|
55
|
+
validate: function (value) {
|
|
56
|
+
return slmpValidateIntegerInRange(value, 0, 255);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
moduleIO: {
|
|
60
|
+
value: "03FF",
|
|
61
|
+
validate: function (value) {
|
|
62
|
+
return slmpValidateIntegerInRange(value, 0, 65535, { base: 16 });
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
multidrop: {
|
|
66
|
+
value: "0",
|
|
67
|
+
validate: function (value) {
|
|
68
|
+
return slmpValidateIntegerInRange(value, 0, 255);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
label: function () {
|
|
73
|
+
return this.name || this.host + ":" + this.port;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<script type="text/html" data-template-name="slmp-connection">
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
81
|
+
<input type="text" id="node-config-input-name" />
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
|
|
85
|
+
<input type="text" id="node-config-input-host" placeholder="192.168.0.10" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-config-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
89
|
+
<input type="text" id="node-config-input-port" placeholder="1025" />
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-config-input-transport"><i class="fa fa-random"></i> Transport</label>
|
|
93
|
+
<select id="node-config-input-transport">
|
|
94
|
+
<option value="tcp">TCP</option>
|
|
95
|
+
<option value="udp">UDP</option>
|
|
96
|
+
</select>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="form-row">
|
|
99
|
+
<label for="node-config-input-timeout"><i class="fa fa-clock-o"></i> Timeout ms</label>
|
|
100
|
+
<input type="text" id="node-config-input-timeout" placeholder="3000" />
|
|
101
|
+
</div>
|
|
102
|
+
<div class="form-row">
|
|
103
|
+
<label for="node-config-input-plcSeries"><i class="fa fa-cog"></i> PLC series</label>
|
|
104
|
+
<select id="node-config-input-plcSeries">
|
|
105
|
+
<option value="ql">Q/L compatible</option>
|
|
106
|
+
<option value="iqr">iQ-R/iQ-L compatible</option>
|
|
107
|
+
</select>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="form-row">
|
|
110
|
+
<label for="node-config-input-frameType"><i class="fa fa-code"></i> Frame type</label>
|
|
111
|
+
<select id="node-config-input-frameType">
|
|
112
|
+
<option value="4e">4E binary</option>
|
|
113
|
+
<option value="3e">3E binary</option>
|
|
114
|
+
</select>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="form-row">
|
|
117
|
+
<label for="node-config-input-monitoringTimer"><i class="fa fa-hourglass-half"></i> Monitor timer</label>
|
|
118
|
+
<input type="text" id="node-config-input-monitoringTimer" placeholder="16" />
|
|
119
|
+
</div>
|
|
120
|
+
<div class="form-row">
|
|
121
|
+
<label for="node-config-input-network"><i class="fa fa-sitemap"></i> Network</label>
|
|
122
|
+
<input type="text" id="node-config-input-network" placeholder="0" />
|
|
123
|
+
</div>
|
|
124
|
+
<div class="form-row">
|
|
125
|
+
<label for="node-config-input-station"><i class="fa fa-sitemap"></i> Station</label>
|
|
126
|
+
<input type="text" id="node-config-input-station" placeholder="255" />
|
|
127
|
+
</div>
|
|
128
|
+
<div class="form-row">
|
|
129
|
+
<label for="node-config-input-moduleIO"><i class="fa fa-sitemap"></i> Module I/O</label>
|
|
130
|
+
<input type="text" id="node-config-input-moduleIO" placeholder="03FF" />
|
|
131
|
+
</div>
|
|
132
|
+
<div class="form-row">
|
|
133
|
+
<label for="node-config-input-multidrop"><i class="fa fa-sitemap"></i> Multidrop</label>
|
|
134
|
+
<input type="text" id="node-config-input-multidrop" placeholder="0" />
|
|
135
|
+
</div>
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<script type="text/html" data-help-name="slmp-connection">
|
|
139
|
+
<p>Reusable SLMP connection profile for Mitsubishi PLC access over binary 3E or 4E frames.</p>
|
|
140
|
+
<p>Choose the same frame type and PLC series you would use with <code>plc-comm-slmp-python</code>.</p>
|
|
141
|
+
<p><code>slmp-read</code> and <code>slmp-write</code> can send <code>connect</code>, <code>disconnect</code>, and <code>reinitialize</code> control messages through this shared connection.</p>
|
|
142
|
+
</script>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { SlmpClient } = require("../lib/slmp");
|
|
4
|
+
|
|
5
|
+
module.exports = function registerSlmpConnection(RED) {
|
|
6
|
+
function SlmpConnectionNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
|
|
9
|
+
this.name = config.name;
|
|
10
|
+
this.host = config.host;
|
|
11
|
+
this.port = Number(config.port || 5000);
|
|
12
|
+
this.transport = config.transport || "tcp";
|
|
13
|
+
this.timeout = Number(config.timeout || 3000);
|
|
14
|
+
this.plcSeries = config.plcSeries || "ql";
|
|
15
|
+
this.frameType = config.frameType || "4e";
|
|
16
|
+
this.monitoringTimer = Number(config.monitoringTimer || 0x0010);
|
|
17
|
+
this.target = {
|
|
18
|
+
network: config.network,
|
|
19
|
+
station: config.station,
|
|
20
|
+
moduleIO: config.moduleIO,
|
|
21
|
+
multidrop: config.multidrop,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.client = new SlmpClient({
|
|
25
|
+
host: this.host,
|
|
26
|
+
port: this.port,
|
|
27
|
+
transport: this.transport,
|
|
28
|
+
timeout: this.timeout,
|
|
29
|
+
plcSeries: this.plcSeries,
|
|
30
|
+
frameType: this.frameType,
|
|
31
|
+
monitoringTimer: this.monitoringTimer,
|
|
32
|
+
defaultTarget: this.target,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this._setState = (fill, shape, text) => {
|
|
36
|
+
this.status({ fill, shape, text });
|
|
37
|
+
};
|
|
38
|
+
this.getClient = () => this.client;
|
|
39
|
+
this.getProfile = () => ({
|
|
40
|
+
host: this.host,
|
|
41
|
+
port: this.port,
|
|
42
|
+
transport: this.transport,
|
|
43
|
+
frameType: this.client.frameType,
|
|
44
|
+
plcSeries: this.client.plcSeries,
|
|
45
|
+
target: this.client.defaultTarget,
|
|
46
|
+
});
|
|
47
|
+
this.connect = async () => {
|
|
48
|
+
this._setState("yellow", "ring", "connecting");
|
|
49
|
+
await this.client.connect();
|
|
50
|
+
this._setState("green", "dot", "connected");
|
|
51
|
+
};
|
|
52
|
+
this.disconnect = async () => {
|
|
53
|
+
this._setState("yellow", "ring", "disconnecting");
|
|
54
|
+
await this.client.close();
|
|
55
|
+
this._setState("red", "ring", "disconnected");
|
|
56
|
+
};
|
|
57
|
+
this.reinitialize = async () => {
|
|
58
|
+
this._setState("yellow", "ring", "reinitializing");
|
|
59
|
+
await this.client.close();
|
|
60
|
+
await this.client.connect();
|
|
61
|
+
this._setState("green", "dot", "connected");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this._setState("grey", "ring", "ready");
|
|
65
|
+
|
|
66
|
+
this.on("close", (_removed, done) => {
|
|
67
|
+
this.client
|
|
68
|
+
.close()
|
|
69
|
+
.catch(() => undefined)
|
|
70
|
+
.finally(() => {
|
|
71
|
+
this._setState("grey", "ring", "closed");
|
|
72
|
+
done();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
RED.nodes.registerType("slmp-connection", SlmpConnectionNode);
|
|
78
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
const SLMP_ADDRESS_TOKEN_RE = /[A-Z][A-Z0-9]*(?:\.[0-9A-F]+|:[A-Z]+)?(?:,\d+)?/iy;
|
|
3
|
+
|
|
4
|
+
function slmpGetCurrentType(typeSelector, fallback) {
|
|
5
|
+
const field = $(typeSelector);
|
|
6
|
+
if (field.length > 0 && field.val()) {
|
|
7
|
+
return field.val();
|
|
8
|
+
}
|
|
9
|
+
return fallback || "str";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function slmpValidateReferencePath(value, allowEmpty) {
|
|
13
|
+
const text = String(value == null ? "" : value).trim();
|
|
14
|
+
return allowEmpty ? true : Boolean(text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function slmpTokenizeAddressList(text) {
|
|
18
|
+
const result = [];
|
|
19
|
+
let index = 0;
|
|
20
|
+
const source = String(text || "");
|
|
21
|
+
|
|
22
|
+
while (index < source.length) {
|
|
23
|
+
while (index < source.length && /[\s,;]+/.test(source[index])) {
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
if (index >= source.length) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
SLMP_ADDRESS_TOKEN_RE.lastIndex = index;
|
|
30
|
+
const match = SLMP_ADDRESS_TOKEN_RE.exec(source);
|
|
31
|
+
if (!match || match.index !== index) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
result.push(match[0].trim());
|
|
35
|
+
index = SLMP_ADDRESS_TOKEN_RE.lastIndex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function slmpValidateAddressLiteral(value) {
|
|
42
|
+
const text = String(value == null ? "" : value).trim();
|
|
43
|
+
if (!text) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return Array.isArray(slmpTokenizeAddressList(text));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function slmpValidateIntegerString(value, min, max, base) {
|
|
50
|
+
const text = String(value == null ? "" : value).trim();
|
|
51
|
+
if (base === 16 && !/^(?:0x)?[0-9a-f]+$/i.test(text)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (base !== 16 && !/^\d+$/.test(text)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const parsed = Number.parseInt(text, base || 10);
|
|
58
|
+
return Number.isInteger(parsed) && parsed >= min && parsed <= max;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function slmpValidateTargetLiteral(value) {
|
|
62
|
+
const text = String(value == null ? "" : value).trim();
|
|
63
|
+
if (!text) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(text);
|
|
68
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const keys = Object.keys(parsed);
|
|
72
|
+
if (keys.length === 0) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const allowed = new Set(["network", "station", "moduleIO", "module_io", "multidrop"]);
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
if (!allowed.has(key)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (parsed.network !== undefined && !slmpValidateIntegerString(parsed.network, 0, 255)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (parsed.station !== undefined && !slmpValidateIntegerString(parsed.station, 0, 255)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const moduleIO = parsed.moduleIO !== undefined ? parsed.moduleIO : parsed.module_io;
|
|
88
|
+
if (moduleIO !== undefined && !slmpValidateIntegerString(moduleIO, 0, 65535, 16)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (parsed.multidrop !== undefined && !slmpValidateIntegerString(parsed.multidrop, 0, 255)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
} catch (_error) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function slmpValidateSourceField(value, typeSelector, literalValidator, fallbackType) {
|
|
101
|
+
const type = slmpGetCurrentType(typeSelector, fallbackType);
|
|
102
|
+
if (!type || type === "str") {
|
|
103
|
+
return literalValidator(value);
|
|
104
|
+
}
|
|
105
|
+
return slmpValidateReferencePath(value, false);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setupSlmpSourceField(sourceId, typeId, literalId, hiddenId) {
|
|
109
|
+
const sourceField = $(sourceId);
|
|
110
|
+
const typeField = $(typeId);
|
|
111
|
+
const literalField = $(literalId);
|
|
112
|
+
const literalRow = literalField.closest(".form-row");
|
|
113
|
+
|
|
114
|
+
sourceField.typedInput({
|
|
115
|
+
default: "str",
|
|
116
|
+
typeField: typeField,
|
|
117
|
+
types: ["msg", "flow", "global", "env", "str"]
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const refresh = function () {
|
|
121
|
+
const type = sourceField.typedInput("type");
|
|
122
|
+
const isLiteral = type === "str";
|
|
123
|
+
literalRow.toggle(isLiteral);
|
|
124
|
+
if (isLiteral) {
|
|
125
|
+
sourceField.typedInput("value", literalField.val());
|
|
126
|
+
}
|
|
127
|
+
$(hiddenId).val(isLiteral ? literalField.val() : sourceField.typedInput("value"));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
sourceField.on("change", refresh);
|
|
131
|
+
literalField.on("change keyup", refresh);
|
|
132
|
+
refresh();
|
|
133
|
+
return refresh;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
RED.nodes.registerType("slmp-read", {
|
|
137
|
+
category: "SLMP",
|
|
138
|
+
color: "#D9EAD3",
|
|
139
|
+
defaults: {
|
|
140
|
+
name: { value: "" },
|
|
141
|
+
connection: { value: "", type: "slmp-connection", required: true },
|
|
142
|
+
addresses: {
|
|
143
|
+
value: "",
|
|
144
|
+
validate: function (value) {
|
|
145
|
+
return slmpValidateSourceField(value, "#node-input-addressesType", slmpValidateAddressLiteral, this && this.addressesType);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
addressesType: { value: "str", required: true },
|
|
149
|
+
routeTarget: {
|
|
150
|
+
value: "",
|
|
151
|
+
validate: function (value) {
|
|
152
|
+
return slmpValidateSourceField(value, "#node-input-routeTargetType", slmpValidateTargetLiteral, this && this.routeTargetType);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
routeTargetType: { value: "str", required: true },
|
|
156
|
+
outputMode: { value: "object", required: true },
|
|
157
|
+
metadataMode: { value: "full", required: true },
|
|
158
|
+
errorHandling: { value: "throw", required: true },
|
|
159
|
+
outputs: { value: 1 }
|
|
160
|
+
},
|
|
161
|
+
inputs: 1,
|
|
162
|
+
outputs: 1,
|
|
163
|
+
icon: "font-awesome/fa-download",
|
|
164
|
+
label: function () {
|
|
165
|
+
return this.name || "slmp read";
|
|
166
|
+
},
|
|
167
|
+
oneditprepare: function () {
|
|
168
|
+
const sourceField = $("#node-input-addressesSource");
|
|
169
|
+
const literalField = $("#node-input-addressesLiteral");
|
|
170
|
+
literalField.val(this.addresses || "");
|
|
171
|
+
sourceField.val(this.addresses || "");
|
|
172
|
+
$("#node-input-addressesType").val(this.addressesType || "str");
|
|
173
|
+
const refreshAddresses = setupSlmpSourceField(
|
|
174
|
+
"#node-input-addressesSource",
|
|
175
|
+
"#node-input-addressesType",
|
|
176
|
+
"#node-input-addressesLiteral",
|
|
177
|
+
"#node-input-addresses"
|
|
178
|
+
);
|
|
179
|
+
const routeField = $("#node-input-routeTargetSource");
|
|
180
|
+
const routeLiteral = $("#node-input-routeTargetLiteral");
|
|
181
|
+
routeLiteral.val(this.routeTarget || "");
|
|
182
|
+
routeField.val(this.routeTarget || "");
|
|
183
|
+
$("#node-input-routeTargetType").val(this.routeTargetType || "str");
|
|
184
|
+
const refreshRoute = setupSlmpSourceField(
|
|
185
|
+
"#node-input-routeTargetSource",
|
|
186
|
+
"#node-input-routeTargetType",
|
|
187
|
+
"#node-input-routeTargetLiteral",
|
|
188
|
+
"#node-input-routeTarget"
|
|
189
|
+
);
|
|
190
|
+
refreshAddresses();
|
|
191
|
+
refreshRoute();
|
|
192
|
+
},
|
|
193
|
+
oneditsave: function () {
|
|
194
|
+
const sourceField = $("#node-input-addressesSource");
|
|
195
|
+
const type = sourceField.typedInput("type");
|
|
196
|
+
$("#node-input-addressesType").val(type);
|
|
197
|
+
$("#node-input-addresses").val(type === "str" ? $("#node-input-addressesLiteral").val() : sourceField.typedInput("value"));
|
|
198
|
+
const routeField = $("#node-input-routeTargetSource");
|
|
199
|
+
const routeType = routeField.typedInput("type");
|
|
200
|
+
$("#node-input-routeTargetType").val(routeType);
|
|
201
|
+
$("#node-input-routeTarget").val(
|
|
202
|
+
routeType === "str" ? $("#node-input-routeTargetLiteral").val() : routeField.typedInput("value")
|
|
203
|
+
);
|
|
204
|
+
this.outputs = $("#node-input-errorHandling").val() === "output2" ? 2 : 1;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<script type="text/html" data-template-name="slmp-read">
|
|
210
|
+
<div class="form-row">
|
|
211
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
212
|
+
<input type="text" id="node-input-name" />
|
|
213
|
+
</div>
|
|
214
|
+
<div class="form-row">
|
|
215
|
+
<label for="node-input-connection"><i class="fa fa-link"></i> Connection</label>
|
|
216
|
+
<input type="text" id="node-input-connection" />
|
|
217
|
+
</div>
|
|
218
|
+
<div class="form-row">
|
|
219
|
+
<label for="node-input-addressesSource"><i class="fa fa-sign-in"></i> Source</label>
|
|
220
|
+
<input type="hidden" id="node-input-addressesType" />
|
|
221
|
+
<input type="text" id="node-input-addressesSource" style="width: 70%;" />
|
|
222
|
+
<input type="hidden" id="node-input-addresses" />
|
|
223
|
+
</div>
|
|
224
|
+
<div class="form-row" id="node-input-addressesLiteralRow">
|
|
225
|
+
<label for="node-input-addressesLiteral"><i class="fa fa-list"></i> Addresses</label>
|
|
226
|
+
<textarea id="node-input-addressesLiteral" style="height: 140px;" placeholder="D100 D100,10 D200:F,4 D100:STR,10"></textarea>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="form-row">
|
|
229
|
+
<label for="node-input-routeTargetSource"><i class="fa fa-sitemap"></i> Route</label>
|
|
230
|
+
<input type="hidden" id="node-input-routeTargetType" />
|
|
231
|
+
<input type="text" id="node-input-routeTargetSource" style="width: 70%;" />
|
|
232
|
+
<input type="hidden" id="node-input-routeTarget" />
|
|
233
|
+
</div>
|
|
234
|
+
<div class="form-row" id="node-input-routeTargetLiteralRow">
|
|
235
|
+
<label for="node-input-routeTargetLiteral"><i class="fa fa-code"></i> Route JSON</label>
|
|
236
|
+
<textarea id="node-input-routeTargetLiteral" style="height: 90px;" placeholder="{"network":0,"station":255,"moduleIO":"03FF","multidrop":0}"></textarea>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="form-row">
|
|
239
|
+
<label for="node-input-outputMode"><i class="fa fa-sign-out"></i> Output</label>
|
|
240
|
+
<select id="node-input-outputMode">
|
|
241
|
+
<option value="object">Object payload</option>
|
|
242
|
+
<option value="array">Array in address order</option>
|
|
243
|
+
<option value="value">Single value when one address</option>
|
|
244
|
+
</select>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="form-row">
|
|
247
|
+
<label for="node-input-metadataMode"><i class="fa fa-database"></i> Metadata</label>
|
|
248
|
+
<select id="node-input-metadataMode">
|
|
249
|
+
<option value="full">Full msg.slmp</option>
|
|
250
|
+
<option value="minimal">Minimal msg.slmp</option>
|
|
251
|
+
<option value="off">Do not modify msg.slmp</option>
|
|
252
|
+
</select>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="form-row">
|
|
255
|
+
<label for="node-input-errorHandling"><i class="fa fa-exclamation"></i> Errors</label>
|
|
256
|
+
<select id="node-input-errorHandling">
|
|
257
|
+
<option value="throw">Throw</option>
|
|
258
|
+
<option value="msg">msg.error</option>
|
|
259
|
+
<option value="output2">Second output</option>
|
|
260
|
+
</select>
|
|
261
|
+
</div>
|
|
262
|
+
</script>
|
|
263
|
+
|
|
264
|
+
<script type="text/html" data-help-name="slmp-read">
|
|
265
|
+
<p>Reads one or more SLMP addresses and writes the result to <code>msg.payload</code>.</p>
|
|
266
|
+
<p>Supported forms include <code>D100</code>, <code>D100,10</code>, <code>D200:F</code>, <code>D200:F,4</code>, <code>D50.3</code>, <code>M1000</code>, <code>M1000,8</code>, <code>D100:STR,10</code>, and <code>DSTR100,10</code>.</p>
|
|
267
|
+
<p><code>,count</code> returns arrays for numeric and direct-bit reads. String reads use UTF-8 bytes packed two per word.</p>
|
|
268
|
+
<p>Output can be an object keyed by address, an array in address order, or a single scalar when exactly one address is requested.</p>
|
|
269
|
+
<p>Metadata can emit full <code>msg.slmp</code>, a minimal object with only the effective target and item count, or leave <code>msg.slmp</code> unchanged.</p>
|
|
270
|
+
<p>The configured source can be literal text, <code>msg</code>, <code>flow</code>, <code>global</code>, or <code>env</code>. At runtime, <code>msg.addresses</code> or a string/array in <code>msg.payload</code> still take priority.</p>
|
|
271
|
+
<p>Per-request routing can be supplied as <code>msg.target</code>, <code>msg.slmp.target</code>, or a configured route source. Route objects can include <code>network</code>, <code>station</code>, <code>moduleIO</code>, and <code>multidrop</code>.</p>
|
|
272
|
+
<p>Send <code>msg.connect</code>, <code>msg.disconnect</code>, or <code>msg.reinitialize</code> as <code>true</code>, or send the same words in <code>msg.topic</code>, to control the shared connection.</p>
|
|
273
|
+
<p>Error handling can throw, attach the error to <code>msg.error</code>, or emit the failed message on the second output.</p>
|
|
274
|
+
</script>
|