@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.
@@ -0,0 +1,514 @@
1
+ [
2
+ {
3
+ "id": "tab-slmp-device-matrix",
4
+ "type": "tab",
5
+ "label": "SLMP Device Matrix",
6
+ "disabled": false,
7
+ "info": "High-level one-by-one sample for every supported device code. Edit the catalog in the function node before deploy if your PLC uses different ranges."
8
+ },
9
+ {
10
+ "id": "comment-slmp-device-matrix-main",
11
+ "type": "comment",
12
+ "z": "tab-slmp-device-matrix",
13
+ "name": "High-level one-by-one read/write sample with persistent JSONL logging and timeout tracking",
14
+ "info": "",
15
+ "x": 350,
16
+ "y": 40,
17
+ "wires": [
18
+
19
+ ]
20
+ },
21
+ {
22
+ "id": "comment-slmp-device-matrix-edit",
23
+ "type": "comment",
24
+ "z": "tab-slmp-device-matrix",
25
+ "name": "Edit the catalog in \u0027Prepare next device sample\u0027 before deploy. Completed results append to logs/slmp-device-matrix-\u003csession\u003e.jsonl",
26
+ "info": "",
27
+ "x": 400,
28
+ "y": 80,
29
+ "wires": [
30
+
31
+ ]
32
+ },
33
+ {
34
+ "id": "inject-device-matrix-reset",
35
+ "type": "inject",
36
+ "z": "tab-slmp-device-matrix",
37
+ "name": "Reset sequence + history",
38
+ "props": [
39
+ {
40
+ "p": "mode",
41
+ "v": "reset",
42
+ "vt": "str"
43
+ }
44
+ ],
45
+ "repeat": "",
46
+ "crontab": "",
47
+ "once": false,
48
+ "onceDelay": 0.1,
49
+ "topic": "",
50
+ "x": 140,
51
+ "y": 140,
52
+ "wires": [
53
+ [
54
+ "function-device-matrix-router"
55
+ ]
56
+ ]
57
+ },
58
+ {
59
+ "id": "inject-device-matrix-catalog",
60
+ "type": "inject",
61
+ "z": "tab-slmp-device-matrix",
62
+ "name": "Show catalog",
63
+ "props": [
64
+ {
65
+ "p": "mode",
66
+ "v": "catalog",
67
+ "vt": "str"
68
+ }
69
+ ],
70
+ "repeat": "",
71
+ "crontab": "",
72
+ "once": false,
73
+ "onceDelay": 0.1,
74
+ "topic": "",
75
+ "x": 110,
76
+ "y": 180,
77
+ "wires": [
78
+ [
79
+ "function-device-matrix-router"
80
+ ]
81
+ ]
82
+ },
83
+ {
84
+ "id": "inject-device-matrix-read",
85
+ "type": "inject",
86
+ "z": "tab-slmp-device-matrix",
87
+ "name": "Read next device",
88
+ "props": [
89
+ {
90
+ "p": "mode",
91
+ "v": "readNext",
92
+ "vt": "str"
93
+ }
94
+ ],
95
+ "repeat": "",
96
+ "crontab": "",
97
+ "once": false,
98
+ "onceDelay": 0.1,
99
+ "topic": "",
100
+ "x": 120,
101
+ "y": 220,
102
+ "wires": [
103
+ [
104
+ "function-device-matrix-router"
105
+ ]
106
+ ]
107
+ },
108
+ {
109
+ "id": "inject-device-matrix-write",
110
+ "type": "inject",
111
+ "z": "tab-slmp-device-matrix",
112
+ "name": "Write next device",
113
+ "props": [
114
+ {
115
+ "p": "mode",
116
+ "v": "writeNext",
117
+ "vt": "str"
118
+ }
119
+ ],
120
+ "repeat": "",
121
+ "crontab": "",
122
+ "once": false,
123
+ "onceDelay": 0.1,
124
+ "topic": "",
125
+ "x": 120,
126
+ "y": 260,
127
+ "wires": [
128
+ [
129
+ "function-device-matrix-router"
130
+ ]
131
+ ]
132
+ },
133
+ {
134
+ "id": "function-device-matrix-router",
135
+ "type": "function",
136
+ "z": "tab-slmp-device-matrix",
137
+ "name": "Prepare next device sample (edit catalog here)",
138
+ "func": "const catalog = [\n { code: \u0027SM\u0027, address: \u0027SM10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027SD\u0027, address: \u0027SD10\u0027, value: 1, note: \u0027word decimal\u0027 },\n { code: \u0027X\u0027, address: \u0027X10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027Y\u0027, address: \u0027Y10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027M\u0027, address: \u0027M10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027L\u0027, address: \u0027L10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027F\u0027, address: \u0027F10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027V\u0027, address: \u0027V10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027B\u0027, address: \u0027B10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027D\u0027, address: \u0027D30\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027W\u0027, address: \u0027W10\u0027, value: 123, note: \u0027word hexadecimal\u0027 },\n { code: \u0027TS\u0027, address: \u0027TS10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027TC\u0027, address: \u0027TC10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027TN\u0027, address: \u0027TN10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027LTS\u0027, address: \u0027LTS10\u0027, value: true, note: \u0027long timer contact\u0027 },\n { code: \u0027LTC\u0027, address: \u0027LTC10\u0027, value: true, note: \u0027long timer coil\u0027 },\n { code: \u0027LTN\u0027, address: \u0027LTN10\u0027, value: 123456, note: \u0027long timer current dword\u0027 },\n { code: \u0027STS\u0027, address: \u0027STS10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027STC\u0027, address: \u0027STC10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027STN\u0027, address: \u0027STN10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027LSTS\u0027, address: \u0027LSTS10\u0027, value: true, note: \u0027long retentive timer contact\u0027 },\n { code: \u0027LSTC\u0027, address: \u0027LSTC10\u0027, value: true, note: \u0027long retentive timer coil\u0027 },\n { code: \u0027LSTN\u0027, address: \u0027LSTN10\u0027, value: 123456, note: \u0027long retentive timer current dword\u0027 },\n { code: \u0027CS\u0027, address: \u0027CS10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027CC\u0027, address: \u0027CC10\u0027, value: true, note: \u0027bit decimal\u0027 },\n { code: \u0027CN\u0027, address: \u0027CN10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027LCS\u0027, address: \u0027LCS10\u0027, value: true, note: \u0027long counter contact\u0027 },\n { code: \u0027LCC\u0027, address: \u0027LCC10\u0027, value: true, note: \u0027long counter coil\u0027 },\n { code: \u0027LCN\u0027, address: \u0027LCN10\u0027, value: 123456, note: \u0027long counter current dword\u0027 },\n { code: \u0027SB\u0027, address: \u0027SB10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027SW\u0027, address: \u0027SW10\u0027, value: 123, note: \u0027word hexadecimal\u0027 },\n { code: \u0027DX\u0027, address: \u0027DX10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027DY\u0027, address: \u0027DY10\u0027, value: true, note: \u0027bit hexadecimal\u0027 },\n { code: \u0027Z\u0027, address: \u0027Z10\u0027, value: 1, note: \u0027word decimal\u0027 },\n { code: \u0027LZ\u0027, address: \u0027LZ10\u0027, value: 1, note: \u0027word decimal\u0027 },\n { code: \u0027R\u0027, address: \u0027R10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027ZR\u0027, address: \u0027ZR10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027RD\u0027, address: \u0027RD10\u0027, value: 123, note: \u0027word decimal\u0027 },\n { code: \u0027G\u0027, address: \u0027G10\u0027, value: 123, note: \u0027word decimal via U0\\\\G10 equivalent target\u0027, target: { moduleIO: \u00270000\u0027 } },\n { code: \u0027HG\u0027, address: \u0027HG20\u0027, value: 123, note: \u0027word decimal via U3E0\\\\HG20 equivalent target\u0027, target: { moduleIO: \u002703E0\u0027 } }\n];\n\nconst total = catalog.length;\nconst readKey = \u0027slmpDeviceMatrixReadIndex\u0027;\nconst writeKey = \u0027slmpDeviceMatrixWriteIndex\u0027;\nconst resultsKey = \u0027slmpDeviceMatrixResults\u0027;\nconst pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst sessionKey = \u0027slmpDeviceMatrixSessionId\u0027;\nconst logPathKey = \u0027slmpDeviceMatrixLogPath\u0027;\nconst timeoutKey = \u0027slmpDeviceMatrixTimeoutMs\u0027;\nconst totalKey = \u0027slmpDeviceMatrixTotalCatalog\u0027;\nconst action = String(msg.mode || \u0027catalog\u0027);\n\nflow.set(totalKey, total);\nflow.set(\u0027slmpDeviceMatrixCatalog\u0027, catalog);\nif (!flow.get(timeoutKey)) {\n flow.set(timeoutKey, 5000);\n}\n\nfunction makeSessionId() {\n return new Date().toISOString().replace(/[:.]/g, \u0027-\u0027);\n}\n\nfunction ensureSessionId(reset) {\n let sessionId = flow.get(sessionKey);\n if (reset || !sessionId) {\n sessionId = makeSessionId();\n flow.set(sessionKey, sessionId);\n }\n return sessionId;\n}\n\nfunction ensureLogPath(sessionId, reset) {\n let logPath = flow.get(logPathKey);\n if (reset || !logPath) {\n logPath = \u0027logs/slmp-device-matrix-\u0027 + sessionId + \u0027.jsonl\u0027;\n flow.set(logPathKey, logPath);\n }\n return logPath;\n}\n\nfunction getPendingMap() {\n return Object.assign({}, flow.get(pendingKey) || {});\n}\n\nfunction getResults() {\n return Array.from(flow.get(resultsKey) || []);\n}\n\nfunction buildSummary() {\n const results = getResults();\n const pending = getPendingMap();\n const summary = {\n kind: \u0027summary\u0027,\n sessionId: ensureSessionId(false),\n logPath: ensureLogPath(ensureSessionId(false), false),\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending)\n };\n for (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n }\n return summary;\n}\n\nfunction registerPending(kind, index, entry) {\n const sessionId = ensureSessionId(false);\n const logPath = ensureLogPath(sessionId, false);\n const now = new Date().toISOString();\n const requestId = kind + \u0027-\u0027 + entry.code + \u0027-\u0027 + String(index + 1) + \u0027-\u0027 + String(Date.now());\n const pending = getPendingMap();\n const record = {\n kind: \u0027result\u0027,\n requestId: requestId,\n sessionId: sessionId,\n logPath: logPath,\n operation: kind,\n action: kind === \u0027write\u0027 ? \u0027write-readback\u0027 : \u0027read\u0027,\n deviceCode: entry.code,\n address: entry.address,\n target: entry.target || null,\n note: entry.note,\n sequence: {\n index: index + 1,\n total: total\n },\n status: \u0027PENDING\u0027,\n phase: kind === \u0027write\u0027 ? \u0027WRITE_PENDING\u0027 : \u0027READ_PENDING\u0027,\n ok: null,\n expectedValue: kind === \u0027write\u0027 ? entry.value : null,\n writeValue: kind === \u0027write\u0027 ? entry.value : null,\n writeStatus: kind === \u0027write\u0027 ? \u0027PENDING\u0027 : \u0027SKIPPED\u0027,\n readValue: null,\n readStatus: \u0027PENDING\u0027,\n readbackStatus: kind === \u0027write\u0027 ? \u0027PENDING\u0027 : \u0027SKIPPED\u0027,\n error: null,\n startedAt: now,\n updatedAt: now,\n finishedAt: null\n };\n pending[requestId] = record;\n flow.set(pendingKey, pending);\n return record;\n}\n\nif (action === \u0027reset\u0027) {\n const sessionId = ensureSessionId(true);\n const logPath = ensureLogPath(sessionId, true);\n flow.set(readKey, 0);\n flow.set(writeKey, 0);\n flow.set(resultsKey, []);\n flow.set(pendingKey, {});\n return [null, null, {\n payload: {\n kind: \u0027reset\u0027,\n sessionId: sessionId,\n logPath: logPath,\n totalCatalog: total,\n timeoutMs: flow.get(timeoutKey),\n note: \u0027In-memory history cleared. Completed results append to the JSONL log file.\u0027\n }\n }];\n}\n\nif (action === \u0027catalog\u0027) {\n const sessionId = ensureSessionId(false);\n const logPath = ensureLogPath(sessionId, false);\n return [null, null, {\n payload: {\n kind: \u0027catalog\u0027,\n sessionId: sessionId,\n logPath: logPath,\n totalCatalog: total,\n timeoutMs: flow.get(timeoutKey),\n entries: catalog,\n summary: buildSummary()\n }\n }];\n}\n\nif (action === \u0027readNext\u0027) {\n const index = Number(flow.get(readKey) || 0) % total;\n const entry = catalog[index];\n flow.set(readKey, index + 1);\n const record = registerPending(\u0027read\u0027, index, entry);\n return [\n {\n topic: \u0027read:\u0027 + entry.code,\n action: \u0027read\u0027,\n requestId: record.requestId,\n deviceSample: entry,\n sequence: record.sequence,\n startedAt: record.startedAt,\n addresses: [entry.address],\n target: entry.target || undefined\n },\n null,\n { payload: record }\n ];\n}\n\nif (action === \u0027writeNext\u0027) {\n const index = Number(flow.get(writeKey) || 0) % total;\n const entry = catalog[index];\n flow.set(writeKey, index + 1);\n const record = registerPending(\u0027write\u0027, index, entry);\n const updates = {};\n updates[entry.address] = entry.value;\n return [\n null,\n {\n topic: \u0027write:\u0027 + entry.code,\n action: \u0027write\u0027,\n requestId: record.requestId,\n deviceSample: entry,\n sequence: record.sequence,\n startedAt: record.startedAt,\n payload: updates,\n target: entry.target || undefined\n },\n { payload: record }\n ];\n}\n\nnode.error(\u0027Unknown mode: \u0027 + action, msg);\nreturn null;",
139
+ "outputs": 3,
140
+ "noerr": 0,
141
+ "initialize": "",
142
+ "finalize": "",
143
+ "libs": [
144
+
145
+ ],
146
+ "x": 450,
147
+ "y": 200,
148
+ "wires": [
149
+ [
150
+ "read-device-matrix"
151
+ ],
152
+ [
153
+ "write-device-matrix"
154
+ ],
155
+ [
156
+ "debug-device-matrix-status"
157
+ ]
158
+ ]
159
+ },
160
+ {
161
+ "id": "read-device-matrix",
162
+ "type": "slmp-read",
163
+ "z": "tab-slmp-device-matrix",
164
+ "name": "Read one sample",
165
+ "connection": "cfg-slmp-device-matrix",
166
+ "addresses": "addresses",
167
+ "addressesType": "msg",
168
+ "outputMode": "value",
169
+ "errorHandling": "output2",
170
+ "x": 760,
171
+ "y": 180,
172
+ "wires": [
173
+ [
174
+ "function-device-matrix-format-read-ok"
175
+ ],
176
+ [
177
+ "function-device-matrix-format-read-error"
178
+ ]
179
+ ]
180
+ },
181
+ {
182
+ "id": "write-device-matrix",
183
+ "type": "slmp-write",
184
+ "z": "tab-slmp-device-matrix",
185
+ "name": "Write one sample",
186
+ "connection": "cfg-slmp-device-matrix",
187
+ "updates": "payload",
188
+ "updatesType": "msg",
189
+ "errorHandling": "output2",
190
+ "x": 760,
191
+ "y": 280,
192
+ "wires": [
193
+ [
194
+ "function-device-matrix-format-write-ok"
195
+ ],
196
+ [
197
+ "function-device-matrix-format-write-error"
198
+ ]
199
+ ]
200
+ },
201
+ {
202
+ "id": "function-device-matrix-format-read-ok",
203
+ "type": "function",
204
+ "z": "tab-slmp-device-matrix",
205
+ "name": "Finalize read result",
206
+ "func": "const resultsKey = \u0027slmpDeviceMatrixResults\u0027;\nconst pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst totalKey = \u0027slmpDeviceMatrixTotalCatalog\u0027;\nconst sessionKey = \u0027slmpDeviceMatrixSessionId\u0027;\nconst logPathKey = \u0027slmpDeviceMatrixLogPath\u0027;\nconst sample = msg.deviceSample || {};\nconst pending = Object.assign({}, flow.get(pendingKey) || {});\nconst results = Array.from(flow.get(resultsKey) || []);\nconst total = Number(flow.get(totalKey) || 0);\nconst now = new Date().toISOString();\nconst address = sample.address || ((msg.slmp \u0026\u0026 Array.isArray(msg.slmp.addresses) \u0026\u0026 msg.slmp.addresses[0]) || null);\n\nfunction valuesMatch(expected, actual) {\n if (typeof expected === \u0027boolean\u0027) {\n if (typeof actual === \u0027boolean\u0027) {\n return expected === actual;\n }\n if (actual === 0 || actual === 1) {\n return Boolean(actual) === expected;\n }\n if (actual === \u0027true\u0027 || actual === \u0027false\u0027) {\n return (actual === \u0027true\u0027) === expected;\n }\n }\n return JSON.stringify(actual) === JSON.stringify(expected);\n}\n\nfunction buildSummary() {\n const summary = {\n kind: \u0027summary\u0027,\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending)\n };\n for (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n }\n return summary;\n}\n\nconst record = Object.assign({\n kind: \u0027result\u0027,\n requestId: msg.requestId || (\u0027read-\u0027 + (sample.code || \u0027unknown\u0027) + \u0027-\u0027 + String(Date.now())),\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n operation: msg.action === \u0027write-readback\u0027 ? \u0027write\u0027 : \u0027read\u0027,\n action: msg.action || \u0027read\u0027,\n deviceCode: sample.code || null,\n address: address,\n note: sample.note || \u0027\u0027,\n sequence: msg.sequence || null,\n status: \u0027PENDING\u0027,\n phase: \u0027DONE\u0027,\n ok: null,\n expectedValue: null,\n writeValue: null,\n writeStatus: msg.action === \u0027write-readback\u0027 ? \u0027UNKNOWN\u0027 : \u0027SKIPPED\u0027,\n readValue: null,\n readStatus: \u0027PENDING\u0027,\n readbackStatus: msg.action === \u0027write-readback\u0027 ? \u0027PENDING\u0027 : \u0027SKIPPED\u0027,\n error: null,\n startedAt: msg.startedAt || now,\n updatedAt: now,\n finishedAt: now\n}, pending[msg.requestId] || {});\n\nrecord.updatedAt = now;\nrecord.finishedAt = now;\nrecord.phase = \u0027DONE\u0027;\nrecord.address = address;\nrecord.deviceCode = sample.code || record.deviceCode;\nrecord.note = sample.note || record.note || \u0027\u0027;\nrecord.sequence = msg.sequence || record.sequence || null;\nrecord.readValue = msg.payload;\nrecord.error = null;\n\nif (msg.action === \u0027write-readback\u0027) {\n const matched = valuesMatch(record.expectedValue, msg.payload);\n record.writeStatus = record.writeStatus === \u0027OK\u0027 ? \u0027OK\u0027 : \u0027UNKNOWN\u0027;\n record.readStatus = matched ? \u0027OK\u0027 : \u0027MISMATCH\u0027;\n record.readbackStatus = matched ? \u0027OK\u0027 : \u0027MISMATCH\u0027;\n record.status = matched ? \u0027OK\u0027 : \u0027MISMATCH\u0027;\n record.ok = matched;\n if (!matched) {\n record.error = \u0027Readback mismatch\u0027;\n }\n} else {\n record.writeStatus = \u0027SKIPPED\u0027;\n record.readStatus = \u0027OK\u0027;\n record.readbackStatus = \u0027SKIPPED\u0027;\n record.status = \u0027OK\u0027;\n record.ok = true;\n}\n\nif (record.requestId \u0026\u0026 Object.prototype.hasOwnProperty.call(pending, record.requestId)) {\n delete pending[record.requestId];\n}\nresults.push(record);\nflow.set(pendingKey, pending);\nflow.set(resultsKey, results);\n\nreturn [\n { payload: record },\n { payload: results },\n { payload: buildSummary() },\n { filename: flow.get(logPathKey), payload: JSON.stringify(record) }\n];",
207
+ "outputs": 4,
208
+ "noerr": 0,
209
+ "initialize": "",
210
+ "finalize": "",
211
+ "libs": [
212
+
213
+ ],
214
+ "x": 1030,
215
+ "y": 160,
216
+ "wires": [
217
+ [
218
+ "debug-device-matrix-latest"
219
+ ],
220
+ [
221
+ "debug-device-matrix-history"
222
+ ],
223
+ [
224
+ "debug-device-matrix-summary"
225
+ ],
226
+ [
227
+ "file-device-matrix-log"
228
+ ]
229
+ ]
230
+ },
231
+ {
232
+ "id": "function-device-matrix-format-read-error",
233
+ "type": "function",
234
+ "z": "tab-slmp-device-matrix",
235
+ "name": "Finalize read error",
236
+ "func": "const resultsKey = \u0027slmpDeviceMatrixResults\u0027;\nconst pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst totalKey = \u0027slmpDeviceMatrixTotalCatalog\u0027;\nconst sessionKey = \u0027slmpDeviceMatrixSessionId\u0027;\nconst logPathKey = \u0027slmpDeviceMatrixLogPath\u0027;\nconst sample = msg.deviceSample || {};\nconst pending = Object.assign({}, flow.get(pendingKey) || {});\nconst results = Array.from(flow.get(resultsKey) || []);\nconst total = Number(flow.get(totalKey) || 0);\nconst now = new Date().toISOString();\nconst address = sample.address || ((msg.slmp \u0026\u0026 Array.isArray(msg.slmp.addresses) \u0026\u0026 msg.slmp.addresses[0]) || null);\n\nfunction buildSummary() {\n const summary = {\n kind: \u0027summary\u0027,\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending)\n };\n for (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n }\n return summary;\n}\n\nconst record = Object.assign({\n kind: \u0027result\u0027,\n requestId: msg.requestId || (\u0027read-\u0027 + (sample.code || \u0027unknown\u0027) + \u0027-\u0027 + String(Date.now())),\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n operation: msg.action === \u0027write-readback\u0027 ? \u0027write\u0027 : \u0027read\u0027,\n action: msg.action || \u0027read\u0027,\n deviceCode: sample.code || null,\n address: address,\n note: sample.note || \u0027\u0027,\n sequence: msg.sequence || null,\n status: \u0027NG\u0027,\n phase: \u0027DONE\u0027,\n ok: false,\n expectedValue: null,\n writeValue: null,\n writeStatus: msg.action === \u0027write-readback\u0027 ? \u0027UNKNOWN\u0027 : \u0027SKIPPED\u0027,\n readValue: null,\n readStatus: \u0027NG\u0027,\n readbackStatus: msg.action === \u0027write-readback\u0027 ? \u0027NG\u0027 : \u0027SKIPPED\u0027,\n error: null,\n startedAt: msg.startedAt || now,\n updatedAt: now,\n finishedAt: now\n}, pending[msg.requestId] || {});\n\nrecord.updatedAt = now;\nrecord.finishedAt = now;\nrecord.phase = \u0027DONE\u0027;\nrecord.address = address;\nrecord.deviceCode = sample.code || record.deviceCode;\nrecord.note = sample.note || record.note || \u0027\u0027;\nrecord.sequence = msg.sequence || record.sequence || null;\nrecord.ok = false;\nrecord.status = \u0027NG\u0027;\nrecord.readValue = null;\nrecord.error = msg.error \u0026\u0026 msg.error.message ? msg.error.message : String(msg.error || \u0027Unknown read error\u0027);\n\nif (msg.action === \u0027write-readback\u0027) {\n record.writeStatus = record.writeStatus === \u0027OK\u0027 ? \u0027OK\u0027 : \u0027UNKNOWN\u0027;\n record.readStatus = \u0027NG\u0027;\n record.readbackStatus = \u0027NG\u0027;\n} else {\n record.writeStatus = \u0027SKIPPED\u0027;\n record.readStatus = \u0027NG\u0027;\n record.readbackStatus = \u0027SKIPPED\u0027;\n}\n\nif (record.requestId \u0026\u0026 Object.prototype.hasOwnProperty.call(pending, record.requestId)) {\n delete pending[record.requestId];\n}\nresults.push(record);\nflow.set(pendingKey, pending);\nflow.set(resultsKey, results);\n\nreturn [\n { payload: record },\n { payload: results },\n { payload: buildSummary() },\n { filename: flow.get(logPathKey), payload: JSON.stringify(record) }\n];",
237
+ "outputs": 4,
238
+ "noerr": 0,
239
+ "initialize": "",
240
+ "finalize": "",
241
+ "libs": [
242
+
243
+ ],
244
+ "x": 1030,
245
+ "y": 200,
246
+ "wires": [
247
+ [
248
+ "debug-device-matrix-latest"
249
+ ],
250
+ [
251
+ "debug-device-matrix-history"
252
+ ],
253
+ [
254
+ "debug-device-matrix-summary"
255
+ ],
256
+ [
257
+ "file-device-matrix-log"
258
+ ]
259
+ ]
260
+ },
261
+ {
262
+ "id": "function-device-matrix-format-write-ok",
263
+ "type": "function",
264
+ "z": "tab-slmp-device-matrix",
265
+ "name": "Mark write ok + read back",
266
+ "func": "const pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst pending = Object.assign({}, flow.get(pendingKey) || {});\nconst now = new Date().toISOString();\nconst sample = msg.deviceSample || {};\nconst updates = (msg.slmp \u0026\u0026 msg.slmp.updates) || msg.payload || {};\nconst address = sample.address || Object.keys(updates)[0] || null;\nconst writeValue = address \u0026\u0026 Object.prototype.hasOwnProperty.call(updates, address) ? updates[address] : null;\nconst record = pending[msg.requestId];\n\nif (record) {\n record.phase = \u0027READBACK_PENDING\u0027;\n record.writeStatus = \u0027OK\u0027;\n record.writeValue = writeValue;\n record.expectedValue = writeValue;\n record.updatedAt = now;\n pending[msg.requestId] = record;\n flow.set(pendingKey, pending);\n}\n\nmsg.writeRequest = updates;\nmsg.action = \u0027write-readback\u0027;\nmsg.addresses = [address];\ndelete msg.payload;\nreturn msg;",
267
+ "outputs": 1,
268
+ "noerr": 0,
269
+ "initialize": "",
270
+ "finalize": "",
271
+ "libs": [
272
+
273
+ ],
274
+ "x": 1030,
275
+ "y": 240,
276
+ "wires": [
277
+ [
278
+ "read-device-matrix"
279
+ ]
280
+ ]
281
+ },
282
+ {
283
+ "id": "function-device-matrix-format-write-error",
284
+ "type": "function",
285
+ "z": "tab-slmp-device-matrix",
286
+ "name": "Finalize write error",
287
+ "func": "const resultsKey = \u0027slmpDeviceMatrixResults\u0027;\nconst pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst totalKey = \u0027slmpDeviceMatrixTotalCatalog\u0027;\nconst sessionKey = \u0027slmpDeviceMatrixSessionId\u0027;\nconst logPathKey = \u0027slmpDeviceMatrixLogPath\u0027;\nconst sample = msg.deviceSample || {};\nconst pending = Object.assign({}, flow.get(pendingKey) || {});\nconst results = Array.from(flow.get(resultsKey) || []);\nconst total = Number(flow.get(totalKey) || 0);\nconst now = new Date().toISOString();\nconst updates = (msg.slmp \u0026\u0026 msg.slmp.updates) || msg.payload || {};\nconst address = sample.address || Object.keys(updates)[0] || null;\n\nfunction buildSummary() {\n const summary = {\n kind: \u0027summary\u0027,\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending)\n };\n for (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n }\n return summary;\n}\n\nconst record = Object.assign({\n kind: \u0027result\u0027,\n requestId: msg.requestId || (\u0027write-\u0027 + (sample.code || \u0027unknown\u0027) + \u0027-\u0027 + String(Date.now())),\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n operation: \u0027write\u0027,\n action: \u0027write-readback\u0027,\n deviceCode: sample.code || null,\n address: address,\n note: sample.note || \u0027\u0027,\n sequence: msg.sequence || null,\n status: \u0027NG\u0027,\n phase: \u0027DONE\u0027,\n ok: false,\n expectedValue: address \u0026\u0026 Object.prototype.hasOwnProperty.call(updates, address) ? updates[address] : null,\n writeValue: address \u0026\u0026 Object.prototype.hasOwnProperty.call(updates, address) ? updates[address] : null,\n writeStatus: \u0027NG\u0027,\n readValue: null,\n readStatus: \u0027SKIPPED\u0027,\n readbackStatus: \u0027SKIPPED\u0027,\n error: null,\n startedAt: msg.startedAt || now,\n updatedAt: now,\n finishedAt: now\n}, pending[msg.requestId] || {});\n\nrecord.updatedAt = now;\nrecord.finishedAt = now;\nrecord.phase = \u0027DONE\u0027;\nrecord.address = address;\nrecord.deviceCode = sample.code || record.deviceCode;\nrecord.note = sample.note || record.note || \u0027\u0027;\nrecord.sequence = msg.sequence || record.sequence || null;\nrecord.ok = false;\nrecord.status = \u0027NG\u0027;\nrecord.writeStatus = \u0027NG\u0027;\nrecord.readStatus = \u0027SKIPPED\u0027;\nrecord.readbackStatus = \u0027SKIPPED\u0027;\nrecord.error = msg.error \u0026\u0026 msg.error.message ? msg.error.message : String(msg.error || \u0027Unknown write error\u0027);\n\nif (record.requestId \u0026\u0026 Object.prototype.hasOwnProperty.call(pending, record.requestId)) {\n delete pending[record.requestId];\n}\nresults.push(record);\nflow.set(pendingKey, pending);\nflow.set(resultsKey, results);\n\nreturn [\n { payload: record },\n { payload: results },\n { payload: buildSummary() },\n { filename: flow.get(logPathKey), payload: JSON.stringify(record) }\n];",
288
+ "outputs": 4,
289
+ "noerr": 0,
290
+ "initialize": "",
291
+ "finalize": "",
292
+ "libs": [
293
+
294
+ ],
295
+ "x": 1030,
296
+ "y": 320,
297
+ "wires": [
298
+ [
299
+ "debug-device-matrix-latest"
300
+ ],
301
+ [
302
+ "debug-device-matrix-history"
303
+ ],
304
+ [
305
+ "debug-device-matrix-summary"
306
+ ],
307
+ [
308
+ "file-device-matrix-log"
309
+ ]
310
+ ]
311
+ },
312
+ {
313
+ "id": "debug-device-matrix-status",
314
+ "type": "debug",
315
+ "z": "tab-slmp-device-matrix",
316
+ "name": "Catalog / pending",
317
+ "active": true,
318
+ "tosidebar": true,
319
+ "console": false,
320
+ "tostatus": false,
321
+ "complete": "payload",
322
+ "targetType": "msg",
323
+ "x": 780,
324
+ "y": 120,
325
+ "wires": [
326
+
327
+ ]
328
+ },
329
+ {
330
+ "id": "debug-device-matrix-latest",
331
+ "type": "debug",
332
+ "z": "tab-slmp-device-matrix",
333
+ "name": "Latest device result",
334
+ "active": true,
335
+ "tosidebar": true,
336
+ "console": false,
337
+ "tostatus": false,
338
+ "complete": "payload",
339
+ "targetType": "msg",
340
+ "x": 1300,
341
+ "y": 220,
342
+ "wires": [
343
+
344
+ ]
345
+ },
346
+ {
347
+ "id": "debug-device-matrix-history",
348
+ "type": "debug",
349
+ "z": "tab-slmp-device-matrix",
350
+ "name": "Completed result history",
351
+ "active": true,
352
+ "tosidebar": true,
353
+ "console": false,
354
+ "tostatus": false,
355
+ "complete": "payload",
356
+ "targetType": "msg",
357
+ "x": 1290,
358
+ "y": 280,
359
+ "wires": [
360
+
361
+ ]
362
+ },
363
+ {
364
+ "id": "cfg-slmp-device-matrix",
365
+ "type": "slmp-connection",
366
+ "name": "PLC TCP 4E",
367
+ "host": "192.168.250.100",
368
+ "port": "1025",
369
+ "transport": "tcp",
370
+ "timeout": "3000",
371
+ "plcSeries": "iqr",
372
+ "frameType": "4e",
373
+ "monitoringTimer": "16",
374
+ "network": "0",
375
+ "station": "255",
376
+ "moduleIO": "03FF",
377
+ "multidrop": "0"
378
+ },
379
+ {
380
+ "id": "inject-device-matrix-summary",
381
+ "type": "inject",
382
+ "z": "tab-slmp-device-matrix",
383
+ "name": "Show summary",
384
+ "props": [
385
+ {
386
+ "p": "mode",
387
+ "v": "summary",
388
+ "vt": "str"
389
+ }
390
+ ],
391
+ "repeat": "",
392
+ "crontab": "",
393
+ "once": false,
394
+ "onceDelay": 0.1,
395
+ "topic": "",
396
+ "x": 100,
397
+ "y": 300,
398
+ "wires": [
399
+ [
400
+ "function-device-matrix-summary"
401
+ ]
402
+ ]
403
+ },
404
+ {
405
+ "id": "inject-device-matrix-timeout",
406
+ "type": "inject",
407
+ "z": "tab-slmp-device-matrix",
408
+ "name": "Timeout sweep (auto 1s)",
409
+ "props": [
410
+ {
411
+ "p": "mode",
412
+ "v": "timeoutSweep",
413
+ "vt": "str"
414
+ }
415
+ ],
416
+ "repeat": "1",
417
+ "crontab": "",
418
+ "once": false,
419
+ "onceDelay": 0.1,
420
+ "topic": "",
421
+ "x": 120,
422
+ "y": 340,
423
+ "wires": [
424
+ [
425
+ "function-device-matrix-timeout"
426
+ ]
427
+ ]
428
+ },
429
+ {
430
+ "id": "function-device-matrix-summary",
431
+ "type": "function",
432
+ "z": "tab-slmp-device-matrix",
433
+ "name": "Show current matrix summary",
434
+ "func": "const results = Array.from(flow.get(\u0027slmpDeviceMatrixResults\u0027) || []);\nconst pending = Object.assign({}, flow.get(\u0027slmpDeviceMatrixPendingById\u0027) || {});\nconst total = Number(flow.get(\u0027slmpDeviceMatrixTotalCatalog\u0027) || 0);\nconst summary = {\n kind: \u0027summary\u0027,\n sessionId: flow.get(\u0027slmpDeviceMatrixSessionId\u0027) || null,\n logPath: flow.get(\u0027slmpDeviceMatrixLogPath\u0027) || null,\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending),\n pendingEntries: Object.values(pending)\n};\nfor (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n}\nmsg.payload = summary;\nreturn msg;",
435
+ "outputs": 1,
436
+ "noerr": 0,
437
+ "initialize": "",
438
+ "finalize": "",
439
+ "libs": [
440
+
441
+ ],
442
+ "x": 360,
443
+ "y": 300,
444
+ "wires": [
445
+ [
446
+ "debug-device-matrix-summary"
447
+ ]
448
+ ]
449
+ },
450
+ {
451
+ "id": "function-device-matrix-timeout",
452
+ "type": "function",
453
+ "z": "tab-slmp-device-matrix",
454
+ "name": "Finalize timed out operations",
455
+ "func": "const resultsKey = \u0027slmpDeviceMatrixResults\u0027;\nconst pendingKey = \u0027slmpDeviceMatrixPendingById\u0027;\nconst totalKey = \u0027slmpDeviceMatrixTotalCatalog\u0027;\nconst sessionKey = \u0027slmpDeviceMatrixSessionId\u0027;\nconst logPathKey = \u0027slmpDeviceMatrixLogPath\u0027;\nconst timeoutMs = Number(flow.get(\u0027slmpDeviceMatrixTimeoutMs\u0027) || 5000);\nconst pending = Object.assign({}, flow.get(pendingKey) || {});\nconst results = Array.from(flow.get(resultsKey) || []);\nconst total = Number(flow.get(totalKey) || 0);\nconst nowMs = Date.now();\nconst timedOut = [];\n\nfunction buildSummary() {\n const summary = {\n kind: \u0027summary\u0027,\n sessionId: flow.get(sessionKey) || null,\n logPath: flow.get(logPathKey) || null,\n totalCatalog: total,\n completed: results.length,\n ok: 0,\n ng: 0,\n mismatch: 0,\n timeout: 0,\n pending: Object.keys(pending).length,\n pendingIds: Object.keys(pending)\n };\n for (const entry of results) {\n if (entry.status === \u0027OK\u0027) {\n summary.ok += 1;\n } else {\n summary.ng += 1;\n if (entry.status === \u0027MISMATCH\u0027) {\n summary.mismatch += 1;\n }\n if (entry.status === \u0027TIMEOUT\u0027) {\n summary.timeout += 1;\n }\n }\n }\n return summary;\n}\n\nfor (const requestId of Object.keys(pending)) {\n const record = pending[requestId];\n const started = Date.parse(record.updatedAt || record.startedAt || \u0027\u0027);\n if (!Number.isFinite(started)) {\n continue;\n }\n if ((nowMs - started) \u003c timeoutMs) {\n continue;\n }\n\n const finishedAt = new Date().toISOString();\n const timedOutRecord = Object.assign({}, record, {\n status: \u0027TIMEOUT\u0027,\n phase: \u0027DONE\u0027,\n ok: false,\n error: \u0027No completion within \u0027 + String(timeoutMs) + \u0027 ms\u0027,\n updatedAt: finishedAt,\n finishedAt: finishedAt\n });\n\n if (timedOutRecord.operation === \u0027write\u0027) {\n if (timedOutRecord.writeStatus === \u0027PENDING\u0027) {\n timedOutRecord.writeStatus = \u0027TIMEOUT\u0027;\n timedOutRecord.readStatus = \u0027SKIPPED\u0027;\n timedOutRecord.readbackStatus = \u0027SKIPPED\u0027;\n } else {\n timedOutRecord.readStatus = \u0027TIMEOUT\u0027;\n timedOutRecord.readbackStatus = \u0027TIMEOUT\u0027;\n }\n } else {\n timedOutRecord.writeStatus = \u0027SKIPPED\u0027;\n timedOutRecord.readStatus = \u0027TIMEOUT\u0027;\n timedOutRecord.readbackStatus = \u0027SKIPPED\u0027;\n }\n\n delete pending[requestId];\n results.push(timedOutRecord);\n timedOut.push(timedOutRecord);\n}\n\nif (timedOut.length === 0) {\n return null;\n}\n\nflow.set(pendingKey, pending);\nflow.set(resultsKey, results);\n\nreturn [\n timedOut.map(function (entry) { return { payload: entry }; }),\n [{ payload: results }],\n [{ payload: buildSummary() }],\n timedOut.map(function (entry) {\n return {\n filename: flow.get(logPathKey),\n payload: JSON.stringify(entry)\n };\n })\n];",
456
+ "outputs": 4,
457
+ "noerr": 0,
458
+ "initialize": "",
459
+ "finalize": "",
460
+ "libs": [
461
+
462
+ ],
463
+ "x": 380,
464
+ "y": 340,
465
+ "wires": [
466
+ [
467
+ "debug-device-matrix-latest"
468
+ ],
469
+ [
470
+ "debug-device-matrix-history"
471
+ ],
472
+ [
473
+ "debug-device-matrix-summary"
474
+ ],
475
+ [
476
+ "file-device-matrix-log"
477
+ ]
478
+ ]
479
+ },
480
+ {
481
+ "id": "debug-device-matrix-summary",
482
+ "type": "debug",
483
+ "z": "tab-slmp-device-matrix",
484
+ "name": "Run summary",
485
+ "active": true,
486
+ "tosidebar": true,
487
+ "console": false,
488
+ "tostatus": false,
489
+ "complete": "payload",
490
+ "targetType": "msg",
491
+ "x": 1290,
492
+ "y": 340,
493
+ "wires": [
494
+
495
+ ]
496
+ },
497
+ {
498
+ "id": "file-device-matrix-log",
499
+ "type": "file",
500
+ "z": "tab-slmp-device-matrix",
501
+ "name": "Append JSONL result log",
502
+ "filename": "filename",
503
+ "filenameType": "msg",
504
+ "appendNewline": true,
505
+ "createDir": true,
506
+ "overwriteFile": "false",
507
+ "encoding": "none",
508
+ "x": 1310,
509
+ "y": 400,
510
+ "wires": [
511
+
512
+ ]
513
+ }
514
+ ]
@@ -0,0 +1,118 @@
1
+ [
2
+ {
3
+ "id": "tab-slmp-routing",
4
+ "type": "tab",
5
+ "label": "SLMP Per Request Routing",
6
+ "disabled": false,
7
+ "info": "Update the host, port, and route object before deploy."
8
+ },
9
+ {
10
+ "id": "comment-slmp-routing",
11
+ "type": "comment",
12
+ "z": "tab-slmp-routing",
13
+ "name": "Read with msg-provided addresses and per-request route target",
14
+ "info": "",
15
+ "x": 300,
16
+ "y": 40,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "inject-routing-read",
21
+ "type": "inject",
22
+ "z": "tab-slmp-routing",
23
+ "name": "Read with route override",
24
+ "props": [
25
+ {
26
+ "p": "lookup",
27
+ "v": "D300,4\nDSTR320,10",
28
+ "vt": "str"
29
+ },
30
+ {
31
+ "p": "target",
32
+ "v": "{\"network\":0,\"station\":255,\"moduleIO\":\"03FF\",\"multidrop\":0}",
33
+ "vt": "json"
34
+ }
35
+ ],
36
+ "repeat": "",
37
+ "crontab": "",
38
+ "once": false,
39
+ "onceDelay": 0.1,
40
+ "topic": "",
41
+ "x": 140,
42
+ "y": 140,
43
+ "wires": [
44
+ [
45
+ "read-routing"
46
+ ]
47
+ ]
48
+ },
49
+ {
50
+ "id": "read-routing",
51
+ "type": "slmp-read",
52
+ "z": "tab-slmp-routing",
53
+ "name": "Read from msg.lookup + msg.target",
54
+ "connection": "cfg-slmp-routing",
55
+ "addresses": "lookup",
56
+ "addressesType": "msg",
57
+ "routeTarget": "",
58
+ "routeTargetType": "str",
59
+ "outputMode": "array",
60
+ "errorHandling": "output2",
61
+ "x": 430,
62
+ "y": 140,
63
+ "wires": [
64
+ [
65
+ "debug-routing-ok"
66
+ ],
67
+ [
68
+ "debug-routing-error"
69
+ ]
70
+ ]
71
+ },
72
+ {
73
+ "id": "debug-routing-ok",
74
+ "type": "debug",
75
+ "z": "tab-slmp-routing",
76
+ "name": "Read OK",
77
+ "active": true,
78
+ "tosidebar": true,
79
+ "console": false,
80
+ "tostatus": false,
81
+ "complete": "true",
82
+ "targetType": "full",
83
+ "x": 690,
84
+ "y": 120,
85
+ "wires": []
86
+ },
87
+ {
88
+ "id": "debug-routing-error",
89
+ "type": "debug",
90
+ "z": "tab-slmp-routing",
91
+ "name": "Read Error",
92
+ "active": true,
93
+ "tosidebar": true,
94
+ "console": false,
95
+ "tostatus": false,
96
+ "complete": "true",
97
+ "targetType": "full",
98
+ "x": 700,
99
+ "y": 160,
100
+ "wires": []
101
+ },
102
+ {
103
+ "id": "cfg-slmp-routing",
104
+ "type": "slmp-connection",
105
+ "name": "PLC TCP 4E",
106
+ "host": "192.168.250.100",
107
+ "port": "1025",
108
+ "transport": "tcp",
109
+ "timeout": "3000",
110
+ "plcSeries": "iqr",
111
+ "frameType": "4e",
112
+ "monitoringTimer": "16",
113
+ "network": "0",
114
+ "station": "255",
115
+ "moduleIO": "03FF",
116
+ "multidrop": "0"
117
+ }
118
+ ]