node-red-contrib-uos-nats 1.3.68 → 1.3.71
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 +11 -0
- package/lib/payloads.js +8 -1
- package/nodes/datahub-input.html +105 -10
- package/nodes/datahub-input.js +40 -3
- package/nodes/datahub-write.html +107 -11
- package/nodes/uos-config.js +22 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -109,6 +109,17 @@ Publishes your own data to the Data Hub.
|
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
+
## Performance & Reliability
|
|
113
|
+
- **High-Speed Decoding (Filter-on-Decode):** The node now intelligently filters incoming data at the byte-level. Even if a provider sends thousands of variables, Node-RED only decodes the ones you have selected. This massively reduces CPU usage.
|
|
114
|
+
- **Large Buffers:** The internal NATS buffer has been increased (10MB) to handle event bursts (e.g. rapid switching).
|
|
115
|
+
- **Slow Consumer Warning:** If Node-RED cannot keep up, a "SLOW CONSUMER" warning will appear in the debug log to alert you of dropped messages.
|
|
116
|
+
|
|
117
|
+
## UI Features
|
|
118
|
+
- **Grouped Variables:** Variables are automatically grouped by folder (prefix) in the selection list (e.g. `ur20._4com...`).
|
|
119
|
+
- **Smart Filtering:**
|
|
120
|
+
- **Read Node** shows all variables.
|
|
121
|
+
- **Write Node** automatically hides Read-Only variables to prevent errors.
|
|
122
|
+
|
|
112
123
|
## Troubleshooting
|
|
113
124
|
|
|
114
125
|
- **Provider not visible?** Ensure **Provider ID** matches your **Client ID**. Easiest way: Leave Provider ID empty in the node.
|
package/lib/payloads.js
CHANGED
|
@@ -218,7 +218,7 @@ export function decodeWriteVariablesCommand(buffer) {
|
|
|
218
218
|
return decodeVariableList(cmd.variables());
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
export function decodeVariableList(list) {
|
|
221
|
+
export function decodeVariableList(list, whitelistIds = null) {
|
|
222
222
|
if (!list)
|
|
223
223
|
return [];
|
|
224
224
|
const result = [];
|
|
@@ -226,6 +226,13 @@ export function decodeVariableList(list) {
|
|
|
226
226
|
const item = list.items(i);
|
|
227
227
|
if (!item)
|
|
228
228
|
continue;
|
|
229
|
+
|
|
230
|
+
// Optimization: Filter-on-Decode
|
|
231
|
+
// If whitelist is provided and this ID is NOT in it, skip decoding
|
|
232
|
+
if (whitelistIds && !whitelistIds.has(item.id())) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
let decoded;
|
|
230
237
|
switch (item.valueType()) {
|
|
231
238
|
case VariableValue.Int64: {
|
package/nodes/datahub-input.html
CHANGED
|
@@ -155,34 +155,123 @@
|
|
|
155
155
|
|
|
156
156
|
const selectedMap = getSelectedMap();
|
|
157
157
|
|
|
158
|
+
// Grouping Logic
|
|
159
|
+
const groups = {};
|
|
160
|
+
const noGroup = [];
|
|
161
|
+
|
|
158
162
|
vars.forEach(v => {
|
|
163
|
+
const key = v.key || '';
|
|
164
|
+
const parts = key.split('.');
|
|
165
|
+
if (parts.length > 1) {
|
|
166
|
+
const groupName = parts[0];
|
|
167
|
+
if (!groups[groupName]) groups[groupName] = [];
|
|
168
|
+
groups[groupName].push(v);
|
|
169
|
+
} else {
|
|
170
|
+
noGroup.push(v);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Helper to render a single row
|
|
175
|
+
const createRow = (v) => {
|
|
159
176
|
let rawId = (v.id !== undefined && v.id !== null) ? v.id : v.Id;
|
|
160
177
|
const safeId = (rawId !== undefined && rawId !== null) ? rawId : 'ERR';
|
|
161
178
|
const isSelected = selectedMap.has(v.key);
|
|
162
179
|
|
|
163
180
|
const row = $('<div>', { class: 'var-row', style: rowStyle });
|
|
164
|
-
|
|
165
181
|
const cbContainer = $('<div>', { style: 'text-align:center;' });
|
|
166
182
|
const cb = $('<input type="checkbox" class="var-checkbox">')
|
|
167
183
|
.prop('checked', isSelected)
|
|
168
184
|
.data('key', v.key)
|
|
169
185
|
.data('id', safeId);
|
|
170
186
|
cbContainer.append(cb);
|
|
171
|
-
|
|
172
187
|
const label = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
|
|
173
|
-
|
|
174
188
|
row.append(cbContainer).append(label);
|
|
175
|
-
|
|
189
|
+
return row;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Render Groups
|
|
193
|
+
Object.keys(groups).sort().forEach(groupName => {
|
|
194
|
+
const groupVars = groups[groupName];
|
|
195
|
+
|
|
196
|
+
// Header
|
|
197
|
+
const header = $('<div>', {
|
|
198
|
+
style: 'background:#eee; padding:5px 10px; font-weight:bold; cursor:pointer; border-bottom:1px solid #ddd; display:flex; align-items:center;'
|
|
199
|
+
});
|
|
200
|
+
const icon = $('<i class="fa fa-caret-right" style="margin-right:5px; width:10px;"></i>');
|
|
201
|
+
header.append(icon).append($('<span>').text(groupName + ` (${groupVars.length})`));
|
|
202
|
+
|
|
203
|
+
// Container for items
|
|
204
|
+
const itemContainer = $('<div>', { style: 'display:none; padding-left:0;' });
|
|
205
|
+
|
|
206
|
+
groupVars.forEach(v => {
|
|
207
|
+
itemContainer.append(createRow(v));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Toggle Logic
|
|
211
|
+
header.click(function () {
|
|
212
|
+
const isVisible = itemContainer.is(':visible');
|
|
213
|
+
itemContainer.toggle(!isVisible);
|
|
214
|
+
icon.removeClass(isVisible ? 'fa-caret-down' : 'fa-caret-right').addClass(isVisible ? 'fa-caret-right' : 'fa-caret-down');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Auto-expand if any child is selected OR if filtering
|
|
218
|
+
const hasSelection = groupVars.some(v => selectedMap.has(v.key));
|
|
219
|
+
if (hasSelection) {
|
|
220
|
+
header.click(); // Expand
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$listContainer.append(header).append(itemContainer);
|
|
176
224
|
});
|
|
225
|
+
|
|
226
|
+
// Render Ungrouped
|
|
227
|
+
if (noGroup.length > 0) {
|
|
228
|
+
const header = $('<div>', {
|
|
229
|
+
style: 'background:#eee; padding:5px 10px; font-weight:bold; border-bottom:1px solid #ddd; color:#666;'
|
|
230
|
+
}).text('Other Variables');
|
|
231
|
+
$listContainer.append(header);
|
|
232
|
+
|
|
233
|
+
noGroup.forEach(v => {
|
|
234
|
+
$listContainer.append(createRow(v));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
177
237
|
};
|
|
178
238
|
|
|
239
|
+
// Adjusted filter to open groups when searching
|
|
179
240
|
const filterList = (term) => {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
const termLower = term.toLowerCase();
|
|
242
|
+
|
|
243
|
+
// Show all containers first to filter their rows
|
|
244
|
+
$listContainer.children().show();
|
|
245
|
+
|
|
246
|
+
// Iterate groups (header + div container pairs)
|
|
247
|
+
// Group Headers are usually direct children?
|
|
248
|
+
// Actually, logic needs to be robust.
|
|
249
|
+
|
|
250
|
+
// Simplest: Re-render? No, state/checkboxes lost.
|
|
251
|
+
// Hiding logic:
|
|
252
|
+
$listContainer.find('.var-row').each(function () {
|
|
253
|
+
const text = $(this).text().toLowerCase();
|
|
254
|
+
const match = text.indexOf(termLower) > -1;
|
|
255
|
+
$(this).toggle(match);
|
|
256
|
+
|
|
257
|
+
// If matching, ensure parent group container is visible
|
|
258
|
+
if (match) {
|
|
259
|
+
$(this).parent().show();
|
|
260
|
+
// Ensure header icon is correct (caret down)
|
|
261
|
+
const container = $(this).parent();
|
|
262
|
+
const header = container.prev();
|
|
263
|
+
if (header.find('.fa-caret-right').length) {
|
|
264
|
+
header.find('.fa').removeClass('fa-caret-right').addClass('fa-caret-down');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
185
267
|
});
|
|
268
|
+
|
|
269
|
+
// Hide empty group headers/containers?
|
|
270
|
+
// Let's keep it simple for now: Open all if search term exists
|
|
271
|
+
if (termLower.length > 0) {
|
|
272
|
+
$listContainer.find('div[style*="display:none"]').show();
|
|
273
|
+
$listContainer.find('.fa-caret-right').removeClass('fa-caret-right').addClass('fa-caret-down');
|
|
274
|
+
}
|
|
186
275
|
};
|
|
187
276
|
|
|
188
277
|
$searchInput.on('keyup', function () {
|
|
@@ -218,7 +307,7 @@
|
|
|
218
307
|
// We generally don't. But fetch providers works if deployed.
|
|
219
308
|
|
|
220
309
|
$.ajax({
|
|
221
|
-
url: 'uos/providers/' + configNodeId + '/' + providerId + '/variables',
|
|
310
|
+
url: 'uos/providers/' + configNodeId + '/' + providerId + '/variables?mode=read',
|
|
222
311
|
success: function (data) {
|
|
223
312
|
$fetchBtn.prop('disabled', false);
|
|
224
313
|
$statusMsg.text('');
|
|
@@ -342,6 +431,12 @@
|
|
|
342
431
|
|
|
343
432
|
<script type="text/html" data-help-name="datahub-input">
|
|
344
433
|
<p><b>DataHub - Read</b> reads variables from u-OS Data Hub providers.</p>
|
|
434
|
+
<h3>Features</h3>
|
|
435
|
+
<ul>
|
|
436
|
+
<li><b>Smart Filtering:</b> Only selected variables are decoded (Filter-on-Decode), significantly reducing CPU usage.</li>
|
|
437
|
+
<li><b>Grouped Selection:</b> Variables are organized by prefix (e.g., Device Name) for easier navigation.</li>
|
|
438
|
+
<li><b>High Performance:</b> Enhanced buffering handles rapid data bursts reliably.</li>
|
|
439
|
+
</ul>
|
|
345
440
|
<h3>Configuration</h3>
|
|
346
441
|
<ol>
|
|
347
442
|
<li><b>Config:</b> Select your u-OS connection.</li>
|
package/nodes/datahub-input.js
CHANGED
|
@@ -219,7 +219,9 @@ module.exports = function (RED) {
|
|
|
219
219
|
|
|
220
220
|
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
221
221
|
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
222
|
-
|
|
222
|
+
// Optimization: Pass activeWhitelist to decoder
|
|
223
|
+
// Although snapshot usually contains only requested IDs, this adds safety/speed for full snapshots
|
|
224
|
+
const states = payloads.decodeVariableList(snapshotObj.variables(), activeWhitelist);
|
|
223
225
|
|
|
224
226
|
const filteredSnapshot = processStates(states);
|
|
225
227
|
if (filteredSnapshot.length > 0) {
|
|
@@ -267,6 +269,20 @@ module.exports = function (RED) {
|
|
|
267
269
|
|
|
268
270
|
|
|
269
271
|
|
|
272
|
+
// Create Whitelist Set for Optimized Decoding
|
|
273
|
+
const whitelistIds = new Set();
|
|
274
|
+
// Resolve keys to IDs and populate whitelist
|
|
275
|
+
if (this.variables.length > 0) {
|
|
276
|
+
const requestedKeys = new Set(this.variables);
|
|
277
|
+
for (const def of defMap.values()) {
|
|
278
|
+
if (requestedKeys.has(def.key)) {
|
|
279
|
+
whitelistIds.add(Number(def.id));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// If whitelist is empty (Wildcard mode), pass null to decoder to read ALL.
|
|
284
|
+
const activeWhitelist = whitelistIds.size > 0 ? whitelistIds : null;
|
|
285
|
+
|
|
270
286
|
await performSnapshot();
|
|
271
287
|
|
|
272
288
|
// RECONNECT LOGIC
|
|
@@ -284,12 +300,21 @@ module.exports = function (RED) {
|
|
|
284
300
|
}
|
|
285
301
|
|
|
286
302
|
this.log(`Subscribing to changes for ${this.providerId}...`);
|
|
287
|
-
|
|
303
|
+
|
|
304
|
+
// Optimization: Increase maxMessages/maxBytes pending limits
|
|
305
|
+
// Default is often small (e.g. 65k bytes or related msg count). Raising to prevent SlowConsumer on bursts.
|
|
306
|
+
sub = nc.subscribe(subjects.varsChangedEvent(this.providerId), {
|
|
307
|
+
maxMessages: 10000,
|
|
308
|
+
maxBytes: 10 * 1024 * 1024 // 10MB Puffer
|
|
309
|
+
});
|
|
310
|
+
|
|
288
311
|
(async () => {
|
|
289
312
|
for await (const msg of sub) {
|
|
290
313
|
const eventBB = new flatbuffers.ByteBuffer(msg.data);
|
|
291
314
|
const event = VariablesChangedEvent.getRootAsVariablesChangedEvent(eventBB);
|
|
292
|
-
|
|
315
|
+
// Optimization: Pass activeWhitelist to decoder
|
|
316
|
+
const changeStates = payloads.decodeVariableList(event.changedVariables(), activeWhitelist);
|
|
317
|
+
|
|
293
318
|
const filtered = processStates(changeStates);
|
|
294
319
|
if (filtered.length === 0) {
|
|
295
320
|
continue;
|
|
@@ -309,6 +334,18 @@ module.exports = function (RED) {
|
|
|
309
334
|
this.warn(`subscription error: ${err.message}`);
|
|
310
335
|
}
|
|
311
336
|
});
|
|
337
|
+
|
|
338
|
+
// Optimization: Monitor for Slow Consumer / Dropped Messages via status iterator
|
|
339
|
+
(async () => {
|
|
340
|
+
if (sub && sub.status) {
|
|
341
|
+
for await (const s of sub.status()) {
|
|
342
|
+
if (s.type === 'slow_consumer') {
|
|
343
|
+
this.warn(`SLOW CONSUMER DETECTED! Use less variables or faster CPU. Dropped: ${s.data}`);
|
|
344
|
+
this.status({ fill: 'red', shape: 'dot', text: 'slow consumer (drops)' });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
})().catch(() => { }); // Ignore status loop errors
|
|
312
349
|
};
|
|
313
350
|
|
|
314
351
|
connection.on('reconnected', () => {
|
package/nodes/datahub-write.html
CHANGED
|
@@ -85,7 +85,24 @@
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Grouping Logic
|
|
89
|
+
const groups = {};
|
|
90
|
+
const noGroup = [];
|
|
91
|
+
|
|
88
92
|
vars.forEach(v => {
|
|
93
|
+
const key = v.key || '';
|
|
94
|
+
const parts = key.split('.');
|
|
95
|
+
if (parts.length > 1) {
|
|
96
|
+
const groupName = parts[0];
|
|
97
|
+
if (!groups[groupName]) groups[groupName] = [];
|
|
98
|
+
groups[groupName].push(v);
|
|
99
|
+
} else {
|
|
100
|
+
noGroup.push(v);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Helper to render a single row
|
|
105
|
+
const createRow = (v) => {
|
|
89
106
|
const safeId = (v.id !== undefined) ? v.id : (v.Id !== undefined ? v.Id : 'ERR');
|
|
90
107
|
const isSelected = String(v.key) === String($inputKey.val()); // Match by Key primarily now
|
|
91
108
|
|
|
@@ -97,6 +114,7 @@
|
|
|
97
114
|
row.append(cbContainer).append(keyCol);
|
|
98
115
|
|
|
99
116
|
const selectRow = () => {
|
|
117
|
+
// Radio Behavior for Write Node (Single Select)
|
|
100
118
|
$('.var-checkbox').prop('checked', false);
|
|
101
119
|
cb.prop('checked', true);
|
|
102
120
|
$inputId.val(safeId !== 'ERR' ? safeId : '');
|
|
@@ -109,20 +127,94 @@
|
|
|
109
127
|
row.on('mouseenter', function () { $(this).css('background', '#f7f7f7'); });
|
|
110
128
|
row.on('mouseleave', function () { $(this).css('background', 'transparent'); });
|
|
111
129
|
|
|
112
|
-
|
|
130
|
+
return row;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Render Groups
|
|
134
|
+
Object.keys(groups).sort().forEach(groupName => {
|
|
135
|
+
const groupVars = groups[groupName];
|
|
136
|
+
|
|
137
|
+
// Header
|
|
138
|
+
const header = $('<div>', {
|
|
139
|
+
style: 'background:#eee; padding:5px 10px; font-weight:bold; cursor:pointer; border-bottom:1px solid #ddd; display:flex; align-items:center;'
|
|
140
|
+
});
|
|
141
|
+
const icon = $('<i class="fa fa-caret-right" style="margin-right:5px; width:10px;"></i>');
|
|
142
|
+
header.append(icon).append($('<span>').text(groupName + ` (${groupVars.length})`));
|
|
143
|
+
|
|
144
|
+
// Container for items
|
|
145
|
+
const itemContainer = $('<div>', { style: 'display:none; padding-left:0;' });
|
|
146
|
+
|
|
147
|
+
groupVars.forEach(v => {
|
|
148
|
+
itemContainer.append(createRow(v));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Toggle Logic
|
|
152
|
+
header.click(function () {
|
|
153
|
+
const isVisible = itemContainer.is(':visible');
|
|
154
|
+
itemContainer.toggle(!isVisible);
|
|
155
|
+
icon.removeClass(isVisible ? 'fa-caret-down' : 'fa-caret-right').addClass(isVisible ? 'fa-caret-right' : 'fa-caret-down');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Auto-expand if child is selected
|
|
159
|
+
const hasSelection = groupVars.some(v => String(v.key) === String($inputKey.val()));
|
|
160
|
+
if (hasSelection) {
|
|
161
|
+
header.click(); // Expand
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
$listContainer.append(header).append(itemContainer);
|
|
113
165
|
});
|
|
114
166
|
|
|
115
|
-
//
|
|
167
|
+
// Render Ungrouped
|
|
168
|
+
if (noGroup.length > 0) {
|
|
169
|
+
const header = $('<div>', {
|
|
170
|
+
style: 'background:#eee; padding:5px 10px; font-weight:bold; border-bottom:1px solid #ddd; color:#666;'
|
|
171
|
+
}).text('Other Variables');
|
|
172
|
+
$listContainer.append(header);
|
|
173
|
+
|
|
174
|
+
noGroup.forEach(v => {
|
|
175
|
+
$listContainer.append(createRow(v));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Scroll to selection (simple attempt)
|
|
116
180
|
const selectedCb = $listContainer.find('.var-checkbox:checked');
|
|
117
|
-
if (selectedCb.length)
|
|
181
|
+
if (selectedCb.length) {
|
|
182
|
+
// Try to scroll parent container
|
|
183
|
+
// This is harder with hidden groups, but auto-expand handles visibility
|
|
184
|
+
// Scroll logic might need delay or calculating offsets of visible items.
|
|
185
|
+
// Skipping precise scroll for now.
|
|
186
|
+
}
|
|
118
187
|
};
|
|
119
188
|
|
|
120
|
-
|
|
121
|
-
|
|
189
|
+
// Adjusted filter to open groups when searching
|
|
190
|
+
const filterList = (term) => {
|
|
191
|
+
const termLower = term.toLowerCase();
|
|
192
|
+
$listContainer.children().show();
|
|
193
|
+
|
|
122
194
|
$listContainer.find('.var-row').each(function () {
|
|
123
|
-
const text = $(this).
|
|
124
|
-
|
|
195
|
+
const text = $(this).text().toLowerCase();
|
|
196
|
+
const match = text.indexOf(termLower) > -1;
|
|
197
|
+
$(this).toggle(match);
|
|
198
|
+
|
|
199
|
+
if (match) {
|
|
200
|
+
const container = $(this).parent();
|
|
201
|
+
container.show();
|
|
202
|
+
// Ensure header icon matches state
|
|
203
|
+
const header = container.prev();
|
|
204
|
+
if (header.find('.fa-caret-right').length) {
|
|
205
|
+
header.find('.fa').removeClass('fa-caret-right').addClass('fa-caret-down');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
125
208
|
});
|
|
209
|
+
|
|
210
|
+
if (termLower.length > 0) {
|
|
211
|
+
$listContainer.find('div[style*="display:none"]').show();
|
|
212
|
+
$listContainer.find('.fa-caret-right').removeClass('fa-caret-right').addClass('fa-caret-down');
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
$searchInput.on('keyup', function () {
|
|
217
|
+
filterList($(this).val());
|
|
126
218
|
});
|
|
127
219
|
|
|
128
220
|
$fetchBtn.on('click', function () {
|
|
@@ -136,7 +228,10 @@
|
|
|
136
228
|
$statusMsg.text('Fetching...').css('color', 'blue');
|
|
137
229
|
$listContainer.html('<div style="padding:20px; text-align:center;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
|
|
138
230
|
|
|
139
|
-
|
|
231
|
+
$listContainer.html('<div style="padding:20px; text-align:center;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
|
|
232
|
+
|
|
233
|
+
// Request filtering from server
|
|
234
|
+
$.getJSON('uos/providers/' + configNodeId + '/' + providerId + '/variables?mode=write', function (data) {
|
|
140
235
|
$fetchBtn.prop('disabled', false);
|
|
141
236
|
$statusMsg.text('');
|
|
142
237
|
|
|
@@ -242,11 +337,12 @@
|
|
|
242
337
|
</script>
|
|
243
338
|
|
|
244
339
|
<script type="text/html" data-help-name="datahub-write">
|
|
245
|
-
<p><b>DataHub - Write</b> sends write commands to external Data Hub providers
|
|
340
|
+
<p><b>DataHub - Write</b> sends write commands to external Data Hub providers.
|
|
341
|
+
It now features <b>Smart Filtering</b> to show only writable variables and <b>Grouped Lists</b> for better organization.</p>
|
|
246
342
|
<h3>Configuration</h3>
|
|
247
343
|
<ol>
|
|
248
|
-
<li><b>Provider ID:</b> Target provider (e.g
|
|
249
|
-
<li><b>Variable:</b> Select a target variable. <b>Only
|
|
344
|
+
<li><b>Provider ID:</b> Target provider (e.g., <code>u_os_adm</code>). Use the search button to find it.</li>
|
|
345
|
+
<li><b>Variable:</b> Select a target variable. <b>Only variables with Write access are displayed.</b></li>
|
|
250
346
|
</ol>
|
|
251
347
|
<p>If no variable is selected, the node works in <b>Batch Mode</b> or <b>Dynamic Mode</b>.</p>
|
|
252
348
|
<ul>
|
package/nodes/uos-config.js
CHANGED
|
@@ -708,6 +708,28 @@ module.exports = function (RED) {
|
|
|
708
708
|
});
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
// Filter by Mode
|
|
712
|
+
const mode = req.query.mode; // 'read', 'write', or defaults to all
|
|
713
|
+
|
|
714
|
+
if (mode && Array.isArray(variables)) {
|
|
715
|
+
variables = variables.filter(v => {
|
|
716
|
+
// Safe Access Type check (defaults to READ_ONLY if missing)
|
|
717
|
+
// access is usually: 'READ_ONLY' or 'READ_WRITE'
|
|
718
|
+
const access = String(v.access || v.accessType || 'READ_ONLY').toUpperCase();
|
|
719
|
+
|
|
720
|
+
if (mode === 'write') {
|
|
721
|
+
// WRITE requires READ_WRITE (or WRITE_ONLY if that existed)
|
|
722
|
+
return access.includes('WRITE');
|
|
723
|
+
} else if (mode === 'read') {
|
|
724
|
+
// READ allows everything (READ_ONLY and READ_WRITE)
|
|
725
|
+
// So theoretically we return everything.
|
|
726
|
+
// But maybe the user wants to filter OUT Write-Only? (Not common in u-OS)
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
return true;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
711
733
|
res.json(variables);
|
|
712
734
|
}
|
|
713
735
|
}
|
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.71",
|
|
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",
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"datahub-write": "nodes/datahub-write.js"
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build:offline": "node scripts/build-offline.js",
|
|
64
|
+
"generate:fbs": "bash scripts/generate-fbs.sh",
|
|
65
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
66
|
+
},
|
|
62
67
|
"devDependencies": {
|
|
63
68
|
"dotenv": "^17.2.3"
|
|
64
69
|
}
|