node-red-contrib-uos-nats 1.3.52 → 1.3.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/lib/payloads.js +8 -2
- package/nodes/datahub-input.js +76 -108
- package/nodes/datahub-output.js +1 -1
- package/nodes/uos-config.html +7 -0
- package/nodes/uos-config.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
**Unofficial Node-RED Package for Weidmüller u-OS Data Hub**
|
|
3
2
|
|
|
4
3
|
Read, write, and provide variables via NATS protocol using **OAuth2 authentication**.
|
|
5
4
|
Optimized for high performance and real-time updates.
|
|
6
5
|
|
|
6
|
+
> **IMPORTANT:**
|
|
7
|
+
> These nodes **MUST** run directly on the **u-OS device** (e.g. as a simplified App or Snap).
|
|
8
|
+
> The system's NATS server is **NOT accessible from the outside** (blocked by firewall/binding).
|
|
9
|
+
> You cannot use this package from a remote Node-RED instance (e.g. on your Laptop) to connect to the device.
|
|
10
|
+
|
|
7
11
|
Maintained by [IoTUeli](https://iotueli.ch). Source: [GitHub](https://github.com/uiff/nats-NodeRed-Node-uc20)
|
|
8
12
|
|
|
9
13
|
---
|
package/lib/payloads.js
CHANGED
|
@@ -79,8 +79,14 @@ export function buildReadVariablesQuery(ids) {
|
|
|
79
79
|
const builder = new flatbuffers.Builder(128);
|
|
80
80
|
let idsOffset = 0;
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Safety: Filter invalid IDs to prevent crashes in C++ provider
|
|
83
|
+
// FlatBuffers requires strict types.
|
|
84
|
+
const validIds = (ids || [])
|
|
85
|
+
.map(id => Number(id))
|
|
86
|
+
.filter(id => Number.isInteger(id) && id >= 0);
|
|
87
|
+
|
|
88
|
+
if (validIds.length > 0) {
|
|
89
|
+
idsOffset = ReadVariablesQueryRequest.createIdsVector(builder, validIds);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
ReadVariablesQueryRequest.startReadVariablesQueryRequest(builder);
|
package/nodes/datahub-input.js
CHANGED
|
@@ -168,125 +168,100 @@ module.exports = function (RED) {
|
|
|
168
168
|
return; // Stop here, do not start Interval or Subscription
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
performSnapshot = async (
|
|
171
|
+
performSnapshot = async () => {
|
|
172
|
+
// Debugging connection state
|
|
172
173
|
if (!nc || nc.isClosed()) {
|
|
173
174
|
this.warn('Snapshot skipped: Connection is closed or not ready.');
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (overrideItems) {
|
|
182
|
-
// Dynamic Mode: Resolve each item
|
|
183
|
-
for (const item of overrideItems) {
|
|
184
|
-
let pId = (typeof item === 'object' && item.provider) ? item.provider : this.providerId;
|
|
185
|
-
let key = (typeof item === 'object') ? item.key : item; // String or Object.key
|
|
186
|
-
|
|
187
|
-
if (!key) continue;
|
|
178
|
+
try {
|
|
179
|
+
// Resolve requested variable names to IDs
|
|
180
|
+
let targetIds = [];
|
|
181
|
+
let isWildcard = (this.variables.length === 0);
|
|
188
182
|
|
|
189
|
-
try {
|
|
190
|
-
// Use Config Node Resolution (Cache)
|
|
191
|
-
// This handles fetching definitions for ANY provider on demand
|
|
192
|
-
const resolved = await connection.resolveVariableId(pId, key);
|
|
193
|
-
|
|
194
|
-
if (!requests.has(pId)) requests.set(pId, new Set());
|
|
195
|
-
requests.get(pId).add(resolved.id);
|
|
196
|
-
} catch (e) {
|
|
197
|
-
this.warn(`Dynamic Read: Skipping '${key}' on '${pId}': ${e.message}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
// Static Mode: Use local defMap for configured provider
|
|
202
|
-
const targetIds = new Set();
|
|
203
|
-
const isWildcard = (this.variables.length === 0);
|
|
204
183
|
if (!isWildcard) {
|
|
184
|
+
// Reverse lookup: Find Def by Key
|
|
205
185
|
const requestedKeys = new Set(this.variables);
|
|
186
|
+
|
|
206
187
|
for (const def of defMap.values()) {
|
|
207
|
-
if (requestedKeys.has(def.key))
|
|
188
|
+
if (requestedKeys.has(def.key)) {
|
|
189
|
+
targetIds.push(Number(def.id));
|
|
190
|
+
}
|
|
208
191
|
}
|
|
209
|
-
|
|
210
|
-
|
|
192
|
+
|
|
193
|
+
// CRITICAL FIX: If user requested specific variables but we found NONE,
|
|
194
|
+
// we MUST NOT send an empty list, because that would interpret as "Read All".
|
|
195
|
+
if (targetIds.length === 0) {
|
|
196
|
+
// Already handled by Gate above mostly, but good for safety
|
|
197
|
+
this.warn(`Snapshot Aborted: None of the ${this.variables.length} requested variables could be resolved to IDs.`);
|
|
211
198
|
return;
|
|
212
199
|
}
|
|
213
200
|
}
|
|
214
|
-
requests.set(this.providerId, targetIds); // Empty Set = Wildcard
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (requests.size === 0) {
|
|
218
|
-
this.warn("Snapshot Aborted: No valid variables resolved.");
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
201
|
|
|
222
|
-
|
|
223
|
-
|
|
202
|
+
// If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
|
|
203
|
+
// If Specific (targetIds has items) -> Request specific.
|
|
204
|
+
// Use serialRequest via Config Node to prevent concurrency issues on the connection
|
|
205
|
+
// Simple Snapshot Logic using Config Node Semaphore
|
|
206
|
+
// The Config Node now handles concurrency (max 3 parallel), preventing 503s naturally.
|
|
207
|
+
// We use a simple retry wrapper just in case.
|
|
208
|
+
let lastError;
|
|
209
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
210
|
+
try {
|
|
211
|
+
let snapshotMsg;
|
|
212
|
+
// If config node has serialRequest (Semaphore), use it.
|
|
213
|
+
if (typeof connection.serialRequest === 'function') {
|
|
214
|
+
snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
|
|
215
|
+
} else {
|
|
216
|
+
// Fallback to direct request (unsafe)
|
|
217
|
+
snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
|
|
218
|
+
}
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
221
|
+
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
222
|
+
const states = payloads.decodeVariableList(snapshotObj.variables());
|
|
223
|
+
|
|
224
|
+
const filteredSnapshot = processStates(states);
|
|
225
|
+
if (filteredSnapshot.length > 0) {
|
|
226
|
+
this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
|
|
227
|
+
this.status({ fill: 'green', shape: 'dot', text: 'active' });
|
|
228
|
+
} else {
|
|
229
|
+
// Handle empty/partial...
|
|
230
|
+
this.status({ fill: 'green', shape: 'ring', text: 'active (empty)' });
|
|
231
|
+
}
|
|
232
|
+
return true; // Success
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
} catch (err) {
|
|
235
|
+
lastError = err;
|
|
236
|
+
// If semaphore is full or timeout, wait a bit
|
|
237
|
+
await new Promise(r => setTimeout(r, 1000 * attempt));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (lastError) {
|
|
241
|
+
const msg = lastError.message || '';
|
|
242
|
+
if (msg.includes('503') || msg.includes('no responders')) {
|
|
243
|
+
this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
|
|
244
|
+
this.status({ fill: 'yellow', shape: 'dot', text: 'provider offline' });
|
|
245
|
+
} else if (msg.includes('Cooldown')) {
|
|
246
|
+
this.debug(`Snapshot skipped (Cooldown): ${msg}`);
|
|
247
|
+
this.status({ fill: 'yellow', shape: 'ring', text: 'cooldown (10s)' });
|
|
248
|
+
} else if (msg.includes('Authorization') || msg.includes('Permission')) {
|
|
249
|
+
this.debug(`Snapshot skipped (Auth): ${msg}`);
|
|
250
|
+
this.status({ fill: 'yellow', shape: 'ring', text: 'auth failed' });
|
|
235
251
|
} else {
|
|
236
|
-
|
|
252
|
+
this.warn(`Snapshot failed: ${msg}`);
|
|
253
|
+
this.status({ fill: 'red', shape: 'ring', text: 'snapshot error' });
|
|
237
254
|
}
|
|
238
|
-
|
|
239
|
-
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
240
|
-
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
241
|
-
const states = payloads.decodeVariableList(snapshotObj.variables());
|
|
242
|
-
|
|
243
|
-
// Add Provider Info to Result?
|
|
244
|
-
// ProcessStates adds keys, but we might want to know which provider it came from if mixed?
|
|
245
|
-
// The current `processStates` maps IDs back to Keys using `defMap`.
|
|
246
|
-
// ISSUE: `defMap` only has local provider defs.
|
|
247
|
-
// If we read from foreign provider, `processStates` will return IDs or "Unknown".
|
|
248
|
-
// We need `resolveVariableId` to also START caching the reverse lookup or we assume result has IDs?
|
|
249
|
-
// Wait, `decodeVariableList` returns {id, value}.
|
|
250
|
-
// `processStates` tries to find Key.
|
|
251
|
-
|
|
252
|
-
// QUICK FIX for Dynamic Multi-Provider:
|
|
253
|
-
// If pId !== this.providerId, we don't have local defMap.
|
|
254
|
-
// But `resolveVariableId` (in config) caches definitions.
|
|
255
|
-
// We can ask config to resolve ID back to Key? Or just return ID?
|
|
256
|
-
// Better: We know the Keys we asked for.
|
|
257
|
-
|
|
258
|
-
// Let's modify processStates to accept a custom Map?
|
|
259
|
-
// Or simple: Just return the raw objects with IDs if we can't map them?
|
|
260
|
-
// Actually, for Dynamic Read, users might accept {id, value} or we try to enrich.
|
|
261
|
-
|
|
262
|
-
const enriched = states.map(s => {
|
|
263
|
-
// Try local defMap
|
|
264
|
-
if (pId === this.providerId && defMap.has(s.id)) {
|
|
265
|
-
return { ...s, key: defMap.get(s.id).key, provider: pId };
|
|
266
|
-
}
|
|
267
|
-
// Try Config Node Cache (it has all fetched defs)
|
|
268
|
-
const cachedDef = connection.getProviderVariable(pId, s.id);
|
|
269
|
-
if (cachedDef) {
|
|
270
|
-
return { ...s, key: cachedDef.key, provider: pId };
|
|
271
|
-
}
|
|
272
|
-
return { ...s, key: `id:${s.id}`, provider: pId };
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
allResults.push(...enriched);
|
|
276
|
-
|
|
277
|
-
} catch (err) {
|
|
278
|
-
errors.push(`${pId}: ${err.message}`);
|
|
279
255
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.status({ fill: 'green', shape: 'ring', text: 'empty' });
|
|
256
|
+
} catch (e) {
|
|
257
|
+
const msg = e.message || '';
|
|
258
|
+
if (msg.includes('503') || msg.includes('no responders')) {
|
|
259
|
+
this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
|
|
260
|
+
} else if (msg.includes('Cooldown')) {
|
|
261
|
+
// Ignore cooldown errors in outer catch
|
|
262
|
+
} else {
|
|
263
|
+
this.warn(`Snapshot failed: ${msg}`);
|
|
264
|
+
}
|
|
290
265
|
}
|
|
291
266
|
};
|
|
292
267
|
|
|
@@ -359,14 +334,7 @@ module.exports = function (RED) {
|
|
|
359
334
|
};
|
|
360
335
|
|
|
361
336
|
this.on('input', (msg, send, done) => {
|
|
362
|
-
|
|
363
|
-
if (Array.isArray(msg.payload) && msg.payload.length > 0) {
|
|
364
|
-
// Pass the raw array items (String or Object {provider, key})
|
|
365
|
-
// performSnapshot will handle filtering
|
|
366
|
-
overrideItems = msg.payload;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
performSnapshot(overrideItems)
|
|
337
|
+
performSnapshot()
|
|
370
338
|
.then(() => done())
|
|
371
339
|
.catch((err) => done(err));
|
|
372
340
|
});
|
package/nodes/datahub-output.js
CHANGED
|
@@ -154,7 +154,7 @@ module.exports = function (RED) {
|
|
|
154
154
|
const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
|
|
155
155
|
const subject = modSubjects.providerDefinitionChanged(this.providerId);
|
|
156
156
|
|
|
157
|
-
console.log(`[DataHub Output] Publishing definition for Provider '${this.providerId}' to '${subject}'`);
|
|
157
|
+
// console.log(`[DataHub Output] Publishing definition for Provider '${this.providerId}' to '${subject}'`);
|
|
158
158
|
|
|
159
159
|
fingerprint = fp;
|
|
160
160
|
await nc.publish(subject, payload);
|
package/nodes/uos-config.html
CHANGED
|
@@ -230,6 +230,13 @@
|
|
|
230
230
|
|
|
231
231
|
<h3>Troubleshooting</h3>
|
|
232
232
|
|
|
233
|
+
<p style="color:var(--red-ui-text-color-error); font-weight:bold; border:1px solid var(--red-ui-text-color-error); padding:5px; border-radius:3px;">
|
|
234
|
+
⚠️ IMPORTANT:<br>
|
|
235
|
+
These nodes MUST run directly on the u-OS device (App/Snap).
|
|
236
|
+
The NATS server is NOT accessible from the outside.
|
|
237
|
+
You cannot use this node from a remote instance (e.g. Laptop).
|
|
238
|
+
</p>
|
|
239
|
+
|
|
233
240
|
<h4>Test Connection fails</h4>
|
|
234
241
|
<ul>
|
|
235
242
|
<li>Check Host/Port are correct and device is reachable</li>
|
package/nodes/uos-config.js
CHANGED
|
@@ -197,7 +197,7 @@ module.exports = function (RED) {
|
|
|
197
197
|
// REVERT: Use Configured Client Name for Connection.
|
|
198
198
|
// Using UUID caused "Authorization Violation" for some users.
|
|
199
199
|
name: this.clientName,
|
|
200
|
-
inboxPrefix: `_INBOX.${this.clientName}`,
|
|
200
|
+
inboxPrefix: `_INBOX.${this.clientName}`, // RESTORED: Spec requires strictly this format.
|
|
201
201
|
maxReconnectAttempts: -1, // Infinite reconnects
|
|
202
202
|
reconnectTimeWait: 2000,
|
|
203
203
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.61",
|
|
4
4
|
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|