@bitpoolos/edge-bacnet 1.6.7 → 1.6.9
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 +26 -0
- package/bacnet_client.js +181 -28
- package/bacnet_device.js +16 -15
- package/bacnet_gateway.html +82 -0
- package/bacnet_gateway.js +3 -1
- package/bacnet_write.html +2 -1
- package/common.js +8 -1
- package/package.json +1 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -41
- package/resources/node-bacstack-ts/dist/index.js +0 -3
- package/resources/node-bacstack-ts/dist/lib/apdu.js +0 -193
- package/resources/node-bacstack-ts/dist/lib/asn1.js +0 -1671
- package/resources/node-bacstack-ts/dist/lib/bvlc.js +0 -47
- package/resources/node-bacstack-ts/dist/lib/client.js +0 -1553
- package/resources/node-bacstack-ts/dist/lib/enum.js +0 -2114
- package/resources/node-bacstack-ts/dist/lib/npdu.js +0 -112
- package/resources/node-bacstack-ts/dist/lib/services/add-list-element.js +0 -58
- package/resources/node-bacstack-ts/dist/lib/services/alarm-acknowledge.js +0 -93
- package/resources/node-bacstack-ts/dist/lib/services/alarm-summary.js +0 -42
- package/resources/node-bacstack-ts/dist/lib/services/atomic-read-file.js +0 -157
- package/resources/node-bacstack-ts/dist/lib/services/atomic-write-file.js +0 -136
- package/resources/node-bacstack-ts/dist/lib/services/cov-notify.js +0 -119
- package/resources/node-bacstack-ts/dist/lib/services/create-object.js +0 -104
- package/resources/node-bacstack-ts/dist/lib/services/delete-object.js +0 -21
- package/resources/node-bacstack-ts/dist/lib/services/device-communication-control.js +0 -46
- package/resources/node-bacstack-ts/dist/lib/services/error.js +0 -27
- package/resources/node-bacstack-ts/dist/lib/services/event-information.js +0 -100
- package/resources/node-bacstack-ts/dist/lib/services/event-notify-data.js +0 -219
- package/resources/node-bacstack-ts/dist/lib/services/get-enrollment-summary.js +0 -172
- package/resources/node-bacstack-ts/dist/lib/services/get-event-information.js +0 -135
- package/resources/node-bacstack-ts/dist/lib/services/i-am-broadcast.js +0 -59
- package/resources/node-bacstack-ts/dist/lib/services/i-have-broadcast.js +0 -34
- package/resources/node-bacstack-ts/dist/lib/services/index.js +0 -32
- package/resources/node-bacstack-ts/dist/lib/services/life-safety-operation.js +0 -40
- package/resources/node-bacstack-ts/dist/lib/services/private-transfer.js +0 -43
- package/resources/node-bacstack-ts/dist/lib/services/read-property-multiple.js +0 -44
- package/resources/node-bacstack-ts/dist/lib/services/read-property.js +0 -122
- package/resources/node-bacstack-ts/dist/lib/services/read-range.js +0 -201
- package/resources/node-bacstack-ts/dist/lib/services/reinitialize-device.js +0 -35
- package/resources/node-bacstack-ts/dist/lib/services/subscribe-cov.js +0 -55
- package/resources/node-bacstack-ts/dist/lib/services/subscribe-property.js +0 -93
- package/resources/node-bacstack-ts/dist/lib/services/time-sync.js +0 -31
- package/resources/node-bacstack-ts/dist/lib/services/who-has.js +0 -56
- package/resources/node-bacstack-ts/dist/lib/services/who-is.js +0 -45
- package/resources/node-bacstack-ts/dist/lib/services/write-property-multiple.js +0 -105
- package/resources/node-bacstack-ts/dist/lib/services/write-property.js +0 -90
- package/resources/node-bacstack-ts/dist/lib/transport.js +0 -86
- package/resources/node-bacstack-ts/dist/lib/types.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.9] - 01-07-2026
|
|
4
|
+
|
|
5
|
+
Bug fix:
|
|
6
|
+
|
|
7
|
+
- Recover missing points during discovery on small MSTP devices (e.g. RC FlexOne) that reject ReadProperty(ALL). When the "read all" attempt fails, discovery now tries a single targeted ReadPropertyMultiple for the required properties before falling back to per-property reads. This avoids the request storm that could cause the device to reject reads and silently drop Analog/Binary Output points from the model.
|
|
8
|
+
|
|
9
|
+
- Fixed object-type whitelist filter leaking non-whitelisted objects (e.g. trend-logs) into the model. A malformed object-list entry could throw and abort the filter, leaving the point list unfiltered. The filter is now null-safe, so it always applies and only whitelisted object types are kept.
|
|
10
|
+
|
|
11
|
+
- Added a discovery log when an object is dropped because no OBJECT_NAME was returned (read likely rejected), to aid diagnosis.
|
|
12
|
+
|
|
13
|
+
## [1.6.8] - 10-03-2026
|
|
14
|
+
|
|
15
|
+
Bug fix:
|
|
16
|
+
|
|
17
|
+
- Spiking values. Large polling sites were experiencing intermittent spiking values, due to a overloaded invokeId stack. This stack has been refactored to be per device, instead of global.
|
|
18
|
+
|
|
19
|
+
- Fixed byteLength errors
|
|
20
|
+
|
|
21
|
+
- Merged github PR's 36 and 37:
|
|
22
|
+
- Fix startup error spam: empty point list during initialization
|
|
23
|
+
- Fix boolean write failures with auto application tag detection
|
|
24
|
+
|
|
25
|
+
Minor update:
|
|
26
|
+
|
|
27
|
+
- Added concurrency management. Max Concurrent Requests option in the gateway node, which throttles how many concurrent requests can be made at any given point.
|
|
28
|
+
|
|
3
29
|
## [1.6.7] - 15-01-2026
|
|
4
30
|
|
|
5
31
|
Bug fix:
|
package/bacnet_client.js
CHANGED
|
@@ -36,9 +36,13 @@ class BacnetClient extends EventEmitter {
|
|
|
36
36
|
that.manualMutex = new Mutex();
|
|
37
37
|
that.pollInProgress = false;
|
|
38
38
|
that.buildJsonInProgress = false;
|
|
39
|
+
that.cacheLoaded = false;
|
|
39
40
|
that.scanMatrix = [];
|
|
40
41
|
that.renderListCount = 0;
|
|
41
42
|
that.portRangeMatrix = config.portRangeMatrix;
|
|
43
|
+
that._requestQueue = []; // Queue of waiting request resolvers
|
|
44
|
+
that._processingQueue = false; // Flag to prevent concurrent queue processing
|
|
45
|
+
that._maxQueueSize = 10000; // Maximum queued requests before rejecting (sized for large sites)
|
|
42
46
|
|
|
43
47
|
try {
|
|
44
48
|
that.roundDecimal = config.roundDecimal;
|
|
@@ -67,6 +71,7 @@ class BacnetClient extends EventEmitter {
|
|
|
67
71
|
port: config.port,
|
|
68
72
|
broadcastAddress: config.broadCastAddr,
|
|
69
73
|
portRangeMatrix: config.portRangeMatrix,
|
|
74
|
+
maxConcurrentRequests: config.maxConcurrentRequests,
|
|
70
75
|
});
|
|
71
76
|
that.setMaxListeners(1);
|
|
72
77
|
|
|
@@ -80,6 +85,8 @@ class BacnetClient extends EventEmitter {
|
|
|
80
85
|
|
|
81
86
|
//query device task
|
|
82
87
|
const queryDevices = new Task("simple task", () => {
|
|
88
|
+
if (!that.cacheLoaded) return;
|
|
89
|
+
|
|
83
90
|
if (!that.pollInProgress && that.enable_device_discovery) {
|
|
84
91
|
that.queryDevices();
|
|
85
92
|
}
|
|
@@ -192,24 +199,83 @@ class BacnetClient extends EventEmitter {
|
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Waits until a request slot is available (throttling).
|
|
204
|
+
* Uses a queue to ensure only one waiter proceeds per available slot.
|
|
205
|
+
* Rejects if the queue is full (backpressure mechanism).
|
|
206
|
+
*
|
|
207
|
+
* The previous implementation used a `while` loop inside `processQueue` which
|
|
208
|
+
* called `nextResolve()` multiple times synchronously. Because Promise
|
|
209
|
+
* resolutions are scheduled as microtasks, none of those continuations ran
|
|
210
|
+
* before the next `canSendRequest()` check — so the while-loop could release
|
|
211
|
+
* dozens of waiters simultaneously even when only one slot was free. The fix
|
|
212
|
+
* is to release exactly ONE waiter per `requestComplete` event, letting the
|
|
213
|
+
* microtask queue drain before the next slot check.
|
|
214
|
+
*/
|
|
215
|
+
_waitForRequestSlot() {
|
|
216
|
+
let that = this;
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
// Fast path: a slot is available right now
|
|
219
|
+
if (that.client.canSendRequest()) {
|
|
220
|
+
resolve();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Backpressure: refuse to queue more work than the limit allows
|
|
225
|
+
if (that._requestQueue.length >= that._maxQueueSize) {
|
|
226
|
+
reject(new Error('ERR_REQUEST_QUEUE_FULL: Too many pending requests. Reduce polling frequency or increase Max Concurrent Requests.'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Park this request until a slot opens
|
|
231
|
+
that._requestQueue.push(resolve);
|
|
232
|
+
|
|
233
|
+
if (!that._processingQueue) {
|
|
234
|
+
that._processingQueue = true;
|
|
235
|
+
|
|
236
|
+
const processQueue = () => {
|
|
237
|
+
// Release exactly ONE waiter per call. The 'requestComplete' event
|
|
238
|
+
// fires each time a slot is freed, so we will naturally be called
|
|
239
|
+
// again once the released request registers its own callback in the
|
|
240
|
+
// invoke store and eventually completes.
|
|
241
|
+
if (that._requestQueue.length > 0 && that.client.canSendRequest()) {
|
|
242
|
+
const nextResolve = that._requestQueue.shift();
|
|
243
|
+
nextResolve();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (that._requestQueue.length === 0) {
|
|
247
|
+
that._processingQueue = false;
|
|
248
|
+
that.client.removeListener('requestComplete', processQueue);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
that.client.on('requestComplete', processQueue);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
195
257
|
async readCachedFile() {
|
|
196
258
|
let that = this;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
parsedData.deviceList
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
259
|
+
try {
|
|
260
|
+
if (that.config.cacheFileEnabled) {
|
|
261
|
+
const cachedData = await Read_Config_Async();
|
|
262
|
+
const parsedData = JSON.parse(cachedData);
|
|
263
|
+
if (parsedData && typeof parsedData == "object") {
|
|
264
|
+
// renderList is no longer cached - will be rebuilt by tree builder
|
|
265
|
+
// if (parsedData.renderList) that.renderList = parsedData.renderList;
|
|
266
|
+
if (parsedData.deviceList) {
|
|
267
|
+
parsedData.deviceList.forEach(function (device) {
|
|
268
|
+
let newBacnetDevice = new BacnetDevice(true, device);
|
|
269
|
+
that.deviceList.push(newBacnetDevice);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (parsedData.pointList) that.networkTree = parsedData.pointList;
|
|
273
|
+
// renderListCount is no longer cached - will be recalculated by tree builder
|
|
274
|
+
// if (parsedData.renderListCount) that.renderListCount = parsedData.renderListCount;
|
|
208
275
|
}
|
|
209
|
-
if (parsedData.pointList) that.networkTree = parsedData.pointList;
|
|
210
|
-
// renderListCount is no longer cached - will be recalculated by tree builder
|
|
211
|
-
// if (parsedData.renderListCount) that.renderListCount = parsedData.renderListCount;
|
|
212
276
|
}
|
|
277
|
+
} finally {
|
|
278
|
+
that.cacheLoaded = true;
|
|
213
279
|
}
|
|
214
280
|
}
|
|
215
281
|
|
|
@@ -331,7 +397,7 @@ class BacnetClient extends EventEmitter {
|
|
|
331
397
|
}
|
|
332
398
|
}
|
|
333
399
|
|
|
334
|
-
getProtocolSupported(device) {
|
|
400
|
+
async getProtocolSupported(device) {
|
|
335
401
|
//return protocols support for device
|
|
336
402
|
let that = this;
|
|
337
403
|
let addressObject = {
|
|
@@ -339,6 +405,10 @@ class BacnetClient extends EventEmitter {
|
|
|
339
405
|
port: device.getPort(),
|
|
340
406
|
};
|
|
341
407
|
const readOptions = that.getDeviceSpecificOptions(device);
|
|
408
|
+
|
|
409
|
+
// Wait for a request slot before proceeding
|
|
410
|
+
await that._waitForRequestSlot();
|
|
411
|
+
|
|
342
412
|
return new Promise((resolve, reject) => {
|
|
343
413
|
that.client.readProperty(
|
|
344
414
|
addressObject,
|
|
@@ -674,6 +744,8 @@ class BacnetClient extends EventEmitter {
|
|
|
674
744
|
|
|
675
745
|
// //query device task
|
|
676
746
|
const queryDevices = new Task("simple task", () => {
|
|
747
|
+
if (!that.cacheLoaded) return;
|
|
748
|
+
|
|
677
749
|
if (!that.pollInProgress && that.enable_device_discovery) {
|
|
678
750
|
that.queryDevices();
|
|
679
751
|
}
|
|
@@ -1046,7 +1118,7 @@ class BacnetClient extends EventEmitter {
|
|
|
1046
1118
|
}
|
|
1047
1119
|
|
|
1048
1120
|
//used in the doRead querying work flow
|
|
1049
|
-
updatePoint(device, point) {
|
|
1121
|
+
async updatePoint(device, point) {
|
|
1050
1122
|
let that = this;
|
|
1051
1123
|
let addressObject = {
|
|
1052
1124
|
address: device.getAddress(),
|
|
@@ -1056,6 +1128,9 @@ class BacnetClient extends EventEmitter {
|
|
|
1056
1128
|
// Use device-specific options
|
|
1057
1129
|
const settings = that.getDeviceSpecificOptions(device);
|
|
1058
1130
|
|
|
1131
|
+
// Wait for a request slot before proceeding
|
|
1132
|
+
await that._waitForRequestSlot();
|
|
1133
|
+
|
|
1059
1134
|
return new Promise((resolve, reject) => {
|
|
1060
1135
|
that.client.readProperty(
|
|
1061
1136
|
addressObject,
|
|
@@ -1226,13 +1301,16 @@ class BacnetClient extends EventEmitter {
|
|
|
1226
1301
|
|
|
1227
1302
|
send(index);
|
|
1228
1303
|
|
|
1229
|
-
function send(index) {
|
|
1304
|
+
async function send(index) {
|
|
1230
1305
|
let readOptions = {
|
|
1231
1306
|
maxSegments: that.readPropertyMultipleOptions.maxSegments,
|
|
1232
1307
|
maxApdu: that.readPropertyMultipleOptions.maxApdu,
|
|
1233
1308
|
arrayIndex: index,
|
|
1234
1309
|
};
|
|
1235
1310
|
|
|
1311
|
+
// Wait for a request slot before proceeding
|
|
1312
|
+
await that._waitForRequestSlot();
|
|
1313
|
+
|
|
1236
1314
|
that.client.readProperty(
|
|
1237
1315
|
addressObject,
|
|
1238
1316
|
{ type: baEnum.ObjectType.DEVICE, instance: deviceId },
|
|
@@ -1254,12 +1332,16 @@ class BacnetClient extends EventEmitter {
|
|
|
1254
1332
|
});
|
|
1255
1333
|
}
|
|
1256
1334
|
|
|
1257
|
-
_readObjectWithRequestArray(device, requestArray, readOptions) {
|
|
1335
|
+
async _readObjectWithRequestArray(device, requestArray, readOptions) {
|
|
1258
1336
|
let that = this;
|
|
1259
1337
|
let addressObject = {
|
|
1260
1338
|
address: device.getAddress(),
|
|
1261
1339
|
port: device.getPort(),
|
|
1262
1340
|
};
|
|
1341
|
+
|
|
1342
|
+
// Wait for a request slot before proceeding
|
|
1343
|
+
await that._waitForRequestSlot();
|
|
1344
|
+
|
|
1263
1345
|
return new Promise((resolve, reject) => {
|
|
1264
1346
|
that.client.readPropertyMultiple(addressObject, requestArray, readOptions, (error, value) => {
|
|
1265
1347
|
if (value && value.values) {
|
|
@@ -1335,8 +1417,12 @@ class BacnetClient extends EventEmitter {
|
|
|
1335
1417
|
}
|
|
1336
1418
|
}
|
|
1337
1419
|
|
|
1338
|
-
_readObject(addressObject, type, instance, properties, readOptions) {
|
|
1420
|
+
async _readObject(addressObject, type, instance, properties, readOptions) {
|
|
1339
1421
|
let that = this;
|
|
1422
|
+
|
|
1423
|
+
// Wait for a request slot before proceeding
|
|
1424
|
+
await that._waitForRequestSlot();
|
|
1425
|
+
|
|
1340
1426
|
return new Promise((resolve, reject) => {
|
|
1341
1427
|
const requestArray = [
|
|
1342
1428
|
{
|
|
@@ -1435,6 +1521,26 @@ class BacnetClient extends EventEmitter {
|
|
|
1435
1521
|
.catch(reject);
|
|
1436
1522
|
};
|
|
1437
1523
|
|
|
1524
|
+
// Targeted middle tier: request the needed properties in ONE ReadPropertyMultiple.
|
|
1525
|
+
// Used when ALL is rejected (e.g. RC FlexOne / small MSTP that don't support reading
|
|
1526
|
+
// the ALL pseudo-property). Same property set as the per-property fallback, so stored
|
|
1527
|
+
// data is identical - just one request instead of ~12. Falls through to the
|
|
1528
|
+
// per-property reads only if this targeted read also fails.
|
|
1529
|
+
const readTargetedMultiple = (resolve, reject) => {
|
|
1530
|
+
that
|
|
1531
|
+
._readObject(addressObject, type, instance, propertiesToRead, readOptions)
|
|
1532
|
+
.then((result) => {
|
|
1533
|
+
if (result.value) {
|
|
1534
|
+
resolve(result);
|
|
1535
|
+
} else {
|
|
1536
|
+
readPropertiesIndividually(resolve, reject);
|
|
1537
|
+
}
|
|
1538
|
+
})
|
|
1539
|
+
.catch(() => {
|
|
1540
|
+
readPropertiesIndividually(resolve, reject);
|
|
1541
|
+
});
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1438
1544
|
return new Promise((resolve, reject) => {
|
|
1439
1545
|
// For Device objects (type 8), skip ALL attempt - many MSTP devices don't support it
|
|
1440
1546
|
// Go straight to reading individual properties for better reliability
|
|
@@ -1451,13 +1557,15 @@ class BacnetClient extends EventEmitter {
|
|
|
1451
1557
|
// If the result has value, resolve the promise
|
|
1452
1558
|
resolve(result);
|
|
1453
1559
|
} else {
|
|
1454
|
-
//
|
|
1455
|
-
|
|
1560
|
+
// ALL returned no value - try the targeted multi-property RPM before
|
|
1561
|
+
// falling back to the per-property storm.
|
|
1562
|
+
readTargetedMultiple(resolve, reject);
|
|
1456
1563
|
}
|
|
1457
1564
|
})
|
|
1458
1565
|
.catch(() => {
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1566
|
+
// ALL errored - try the targeted multi-property RPM before falling back
|
|
1567
|
+
// to the per-property storm.
|
|
1568
|
+
readTargetedMultiple(resolve, reject);
|
|
1461
1569
|
});
|
|
1462
1570
|
});
|
|
1463
1571
|
}
|
|
@@ -1559,10 +1667,14 @@ class BacnetClient extends EventEmitter {
|
|
|
1559
1667
|
port: device.getPort(),
|
|
1560
1668
|
};
|
|
1561
1669
|
|
|
1670
|
+
let objectType = point.meta.objectId.type;
|
|
1671
|
+
let resolvedAppTag = that._resolveAppTag(objectType, options.appTag);
|
|
1672
|
+
let resolvedValue = that._coerceWriteValue(value, resolvedAppTag);
|
|
1673
|
+
|
|
1562
1674
|
let writeObject = {
|
|
1563
1675
|
address: addressObject,
|
|
1564
1676
|
objectId: {
|
|
1565
|
-
type:
|
|
1677
|
+
type: objectType,
|
|
1566
1678
|
instance: point.meta.objectId.instance,
|
|
1567
1679
|
},
|
|
1568
1680
|
values: {
|
|
@@ -1572,8 +1684,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1572
1684
|
},
|
|
1573
1685
|
value: [
|
|
1574
1686
|
{
|
|
1575
|
-
type:
|
|
1576
|
-
value:
|
|
1687
|
+
type: resolvedAppTag,
|
|
1688
|
+
value: resolvedValue,
|
|
1577
1689
|
},
|
|
1578
1690
|
],
|
|
1579
1691
|
},
|
|
@@ -1606,7 +1718,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1606
1718
|
point.options,
|
|
1607
1719
|
(err, value) => {
|
|
1608
1720
|
if (err) {
|
|
1609
|
-
that.
|
|
1721
|
+
let objType = that.getObjectType(point.objectId.type) || point.objectId.type;
|
|
1722
|
+
that.logOut("writeProperty error for " + objType + ":" + point.objectId.instance + " - ", err);
|
|
1610
1723
|
}
|
|
1611
1724
|
}
|
|
1612
1725
|
);
|
|
@@ -1938,7 +2051,7 @@ class BacnetClient extends EventEmitter {
|
|
|
1938
2051
|
const promiseArray = [];
|
|
1939
2052
|
|
|
1940
2053
|
if (typeof pointList === "undefined" || pointList.length === 0) {
|
|
1941
|
-
|
|
2054
|
+
return { deviceList: this.deviceList, pointList: this.networkTree };
|
|
1942
2055
|
}
|
|
1943
2056
|
|
|
1944
2057
|
for (const point of pointList) {
|
|
@@ -2146,6 +2259,12 @@ class BacnetClient extends EventEmitter {
|
|
|
2146
2259
|
that.logOut("issue resolving bacnet payload, see error: ", e);
|
|
2147
2260
|
reject(e);
|
|
2148
2261
|
}
|
|
2262
|
+
} else {
|
|
2263
|
+
that.logOut(
|
|
2264
|
+
"[discovery] dropped " + bac_obj + ":" + pointProperty.objectId.instance +
|
|
2265
|
+
" on device " + device.getDeviceId() +
|
|
2266
|
+
" - no OBJECT_NAME returned (read likely rejected)"
|
|
2267
|
+
);
|
|
2149
2268
|
}
|
|
2150
2269
|
});
|
|
2151
2270
|
} else {
|
|
@@ -2276,6 +2395,40 @@ class BacnetClient extends EventEmitter {
|
|
|
2276
2395
|
}
|
|
2277
2396
|
}
|
|
2278
2397
|
|
|
2398
|
+
_resolveAppTag(objectType, userAppTag) {
|
|
2399
|
+
// If user explicitly set an app tag (not auto), use it
|
|
2400
|
+
if (userAppTag !== -1) return userAppTag;
|
|
2401
|
+
|
|
2402
|
+
// Auto-detect based on BACnet object type
|
|
2403
|
+
switch (objectType) {
|
|
2404
|
+
case 3: // BI
|
|
2405
|
+
case 4: // BO
|
|
2406
|
+
case 5: // BV
|
|
2407
|
+
return 9; // ENUMERATED
|
|
2408
|
+
case 13: // MI
|
|
2409
|
+
case 14: // MO
|
|
2410
|
+
case 19: // MV
|
|
2411
|
+
return 2; // UNSIGNED_INT
|
|
2412
|
+
default:
|
|
2413
|
+
return 4; // REAL
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
_coerceWriteValue(value, appTag) {
|
|
2418
|
+
switch (appTag) {
|
|
2419
|
+
case 9: // ENUMERATED - binary objects expect 0 (inactive) or 1 (active)
|
|
2420
|
+
if (value === true || value === 1 || value === "1" ||
|
|
2421
|
+
value === "true" || value === "active" || value === "on") {
|
|
2422
|
+
return 1;
|
|
2423
|
+
}
|
|
2424
|
+
return 0;
|
|
2425
|
+
case 2: // UNSIGNED_INT - multistate objects expect positive integers
|
|
2426
|
+
return parseInt(value) || 0;
|
|
2427
|
+
default:
|
|
2428
|
+
return value;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2279
2432
|
getObjectType(objectId) {
|
|
2280
2433
|
switch (objectId) {
|
|
2281
2434
|
case 0:
|
package/bacnet_device.js
CHANGED
|
@@ -268,10 +268,20 @@ class BacnetDevice {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
setPointsList(newPoints) {
|
|
271
|
+
// Whitelisted object types kept in the model:
|
|
272
|
+
// DEVICE(8) AI(0) AO(1) AV(2) BI(3) BO(4) BV(5) MSI(13) MSO(14) MSV(19) CS(40)
|
|
273
|
+
const allowedTypes = new Set([8, 0, 1, 2, 3, 4, 5, 13, 14, 19, 40]);
|
|
274
|
+
|
|
271
275
|
for (let index = 0; index < newPoints.length; index++) {
|
|
272
276
|
let newPoint = newPoints[index];
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
// Guard against malformed entries (e.g. a bad object-list read with no .value):
|
|
278
|
+
// skip them here so they never enter pointsList and can't throw the dedup below.
|
|
279
|
+
if (newPoint && newPoint.value && newPoint.value.type !== undefined) {
|
|
280
|
+
let foundIndex = this.pointsList.findIndex(
|
|
281
|
+
ele => ele && ele.value &&
|
|
282
|
+
ele.value.type == newPoint.value.type &&
|
|
283
|
+
ele.value.instance == newPoint.value.instance
|
|
284
|
+
);
|
|
275
285
|
if (foundIndex == -1) {
|
|
276
286
|
//not found
|
|
277
287
|
this.pointsList.push(newPoint);
|
|
@@ -279,19 +289,10 @@ class BacnetDevice {
|
|
|
279
289
|
}
|
|
280
290
|
}
|
|
281
291
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
point.value.type == 2 || //AO
|
|
287
|
-
point.value.type == 3 || //BI
|
|
288
|
-
point.value.type == 4 || //BV
|
|
289
|
-
point.value.type == 5 || //BO
|
|
290
|
-
point.value.type == 13 || //MSI
|
|
291
|
-
point.value.type == 14 || //MSO
|
|
292
|
-
point.value.type == 19 || //MSV
|
|
293
|
-
point.value.type == 40 //CS
|
|
294
|
-
);
|
|
292
|
+
// Optional chaining so a malformed element is EXCLUDED (allowedTypes.has(undefined) === false)
|
|
293
|
+
// instead of throwing and aborting the filter, which previously left pointsList unfiltered
|
|
294
|
+
// and leaked non-whitelisted types (trend-logs etc.) into the model.
|
|
295
|
+
this.pointsList = this.pointsList.filter(point => allowedTypes.has(point?.value?.type));
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
getDevicePoints() {
|
package/bacnet_gateway.html
CHANGED
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
local_device_address: { value: "", required: true },
|
|
166
166
|
local_interface_name: { value: "", required: false },
|
|
167
167
|
apduTimeout: { value: 6000 },
|
|
168
|
+
maxConcurrentRequests: { value: 250 },
|
|
168
169
|
roundDecimal: { value: 2 },
|
|
169
170
|
local_device_port: { value: 47808, required: true },
|
|
170
171
|
apduSize: { value: "5", required: true },
|
|
@@ -837,6 +838,13 @@
|
|
|
837
838
|
oneditsave: function () {
|
|
838
839
|
let node = this;
|
|
839
840
|
|
|
841
|
+
// Validate and clamp maxConcurrentRequests (1-250)
|
|
842
|
+
// BACnet protocol limits invoke IDs to 256, so 250 is the safe max
|
|
843
|
+
let maxConcurrent = parseInt(document.getElementById("node-input-maxConcurrentRequests").value) || 250;
|
|
844
|
+
if (maxConcurrent < 1) maxConcurrent = 1;
|
|
845
|
+
if (maxConcurrent > 250) maxConcurrent = 250;
|
|
846
|
+
document.getElementById("node-input-maxConcurrentRequests").value = maxConcurrent;
|
|
847
|
+
|
|
840
848
|
document.getElementById("node-input-discover_polling_schedule").value = getTimePeriodInSeconds(
|
|
841
849
|
document.getElementById("node-input-discover_polling_schedule_value").value,
|
|
842
850
|
document.getElementById("node-input-discover_polling_schedule_options").value
|
|
@@ -1090,6 +1098,13 @@
|
|
|
1090
1098
|
<input type="text" id="node-input-apduTimeout" placeholder="10000" />
|
|
1091
1099
|
</div>
|
|
1092
1100
|
|
|
1101
|
+
<div class="form-row bp-row">
|
|
1102
|
+
<label for="node-input-maxConcurrentRequests"
|
|
1103
|
+
><i class="icon-tag"></i> <span data-i18n="bitpool-bacnet.label.maxConcurrentRequests"></span>Max Concurrent Requests</label
|
|
1104
|
+
>
|
|
1105
|
+
<input type="number" id="node-input-maxConcurrentRequests" placeholder="250" min="1" max="250" />
|
|
1106
|
+
</div>
|
|
1107
|
+
|
|
1093
1108
|
<div class="form-row bp-row" style="display: none;">
|
|
1094
1109
|
<label for="node-input-apduSize"
|
|
1095
1110
|
><i class="icon-tag"></i> <span data-i18n="bitpool-bacnet.label.apduSize"></span>Max Apdu Size</label
|
|
@@ -1310,6 +1325,7 @@
|
|
|
1310
1325
|
<h3><strong>Discovery Tab</strong></h3>
|
|
1311
1326
|
<ul class="node-ports">
|
|
1312
1327
|
<li>APDU Timeout - BACnet msg timeout option</li>
|
|
1328
|
+
<li>Max Concurrent Requests - Maximum number of simultaneous BACnet requests (1-250). Lower values reduce invoke ID exhaustion on large MSTP networks. Default: 250</li>
|
|
1313
1329
|
<li>Max APDU Size - BACnet max apdu size</li>
|
|
1314
1330
|
<li>Max Segments - BACnet max segments</li>
|
|
1315
1331
|
<li>
|
|
@@ -1341,6 +1357,72 @@
|
|
|
1341
1357
|
<li>Clear Server Points - a schedule for the locally generated BACnet points to get cleared from the node object store</li>
|
|
1342
1358
|
</ul>
|
|
1343
1359
|
|
|
1360
|
+
<h3><strong>Performance Tuning & Throttling</strong></h3>
|
|
1361
|
+
<p>
|
|
1362
|
+
For large BACnet networks, especially those with many MS/TP devices, proper tuning is essential to prevent
|
|
1363
|
+
communication issues like value spiking, timeouts, and data corruption.
|
|
1364
|
+
</p>
|
|
1365
|
+
|
|
1366
|
+
<h4>Understanding the BACnet Invoke ID Limit</h4>
|
|
1367
|
+
<p>
|
|
1368
|
+
The BACnet protocol uses an 8-bit "invoke ID" to match requests with responses. This limits the number of
|
|
1369
|
+
simultaneous outstanding requests to <strong>256</strong>. When polling many devices/points rapidly, this
|
|
1370
|
+
limit can be exhausted, causing responses to be matched with the wrong requests (value spiking).
|
|
1371
|
+
</p>
|
|
1372
|
+
|
|
1373
|
+
<h4>Key Settings for Large Networks</h4>
|
|
1374
|
+
<table style="width:100%; border-collapse: collapse; margin: 10px 0;">
|
|
1375
|
+
<tr style="background-color: #f0f0f0;">
|
|
1376
|
+
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Setting</th>
|
|
1377
|
+
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Recommended</th>
|
|
1378
|
+
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Description</th>
|
|
1379
|
+
</tr>
|
|
1380
|
+
<tr>
|
|
1381
|
+
<td style="border: 1px solid #ddd; padding: 8px;"><strong>Max Concurrent Requests</strong></td>
|
|
1382
|
+
<td style="border: 1px solid #ddd; padding: 8px;">64-100 (MSTP)<br>150-200 (IP only)</td>
|
|
1383
|
+
<td style="border: 1px solid #ddd; padding: 8px;">Limits simultaneous in-flight requests. Lower for slow MSTP networks.</td>
|
|
1384
|
+
</tr>
|
|
1385
|
+
<tr>
|
|
1386
|
+
<td style="border: 1px solid #ddd; padding: 8px;"><strong>APDU Timeout</strong></td>
|
|
1387
|
+
<td style="border: 1px solid #ddd; padding: 8px;">8000-15000ms (MSTP)<br>3000-6000ms (IP only)</td>
|
|
1388
|
+
<td style="border: 1px solid #ddd; padding: 8px;">How long to wait for a response. MSTP token-passing adds latency.</td>
|
|
1389
|
+
</tr>
|
|
1390
|
+
</table>
|
|
1391
|
+
|
|
1392
|
+
<h4>Tuning Guidelines by Network Type</h4>
|
|
1393
|
+
<ul class="node-ports">
|
|
1394
|
+
<li>
|
|
1395
|
+
<strong>BACnet/IP Only (few devices):</strong> Default settings usually work. Max Concurrent: 200-250, Timeout: 3000-6000ms
|
|
1396
|
+
</li>
|
|
1397
|
+
<li>
|
|
1398
|
+
<strong>BACnet/IP with MS/TP routers (moderate):</strong> Max Concurrent: 100-150, Timeout: 6000-10000ms
|
|
1399
|
+
</li>
|
|
1400
|
+
<li>
|
|
1401
|
+
<strong>Large MS/TP Networks (100+ devices):</strong> Max Concurrent: 64-100, Timeout: 10000-15000ms
|
|
1402
|
+
</li>
|
|
1403
|
+
<li>
|
|
1404
|
+
<strong>Very Large Networks (500+ devices):</strong> Max Concurrent: 50-75, Timeout: 12000-20000ms.
|
|
1405
|
+
Consider segmenting polling across multiple read nodes with staggered schedules.
|
|
1406
|
+
</li>
|
|
1407
|
+
</ul>
|
|
1408
|
+
|
|
1409
|
+
<h4>Symptoms of Poor Tuning</h4>
|
|
1410
|
+
<ul class="node-ports">
|
|
1411
|
+
<li><strong>Value Spiking:</strong> Random incorrect values appearing briefly → Lower Max Concurrent Requests</li>
|
|
1412
|
+
<li><strong>Many Timeouts:</strong> Points frequently showing offline → Increase APDU Timeout</li>
|
|
1413
|
+
<li><strong>Slow Discovery:</strong> Devices/points taking too long to appear → May need higher Max Concurrent (if not MSTP-bound)</li>
|
|
1414
|
+
<li><strong>Network Congestion:</strong> Other BACnet traffic affected → Lower Max Concurrent Requests and increase polling intervals</li>
|
|
1415
|
+
</ul>
|
|
1416
|
+
|
|
1417
|
+
<h4>Best Practices</h4>
|
|
1418
|
+
<ul class="node-ports">
|
|
1419
|
+
<li>Start with conservative settings (lower Max Concurrent, higher Timeout) and tune up gradually</li>
|
|
1420
|
+
<li>For MS/TP networks, remember that token-passing adds 50-200ms latency per device hop</li>
|
|
1421
|
+
<li>Use longer polling intervals (5-15 minutes) for stable values; shorter (30s-1min) for critical points only</li>
|
|
1422
|
+
<li>Monitor for timeout errors in the console (enable "Log BACnet Errors to Console")</li>
|
|
1423
|
+
<li>If experiencing issues, reduce Max Concurrent Requests by 25% and test again</li>
|
|
1424
|
+
</ul>
|
|
1425
|
+
|
|
1344
1426
|
<h3><strong>Examples</strong></h3>
|
|
1345
1427
|
<p>
|
|
1346
1428
|
For example flows, please use the examples section for this node. These examples can be found at: Node-red hamburger menu on
|
package/bacnet_gateway.js
CHANGED
|
@@ -38,6 +38,7 @@ module.exports = function (RED) {
|
|
|
38
38
|
this.cacheFileEnabled = config.cacheFileEnabled;
|
|
39
39
|
this.sanitise_device_schedule = config.sanitise_device_schedule;
|
|
40
40
|
this.enable_device_discovery = config.enable_device_discovery;
|
|
41
|
+
this.maxConcurrentRequests = config.maxConcurrentRequests;
|
|
41
42
|
|
|
42
43
|
//client and config store
|
|
43
44
|
this.bacnetConfig = nodeContext.get("bacnetConfig");
|
|
@@ -67,7 +68,8 @@ module.exports = function (RED) {
|
|
|
67
68
|
node.cacheFileEnabled,
|
|
68
69
|
node.sanitise_device_schedule,
|
|
69
70
|
node.portRangeRegisters.filter((ele) => ele.enabled === true),
|
|
70
|
-
node.enable_device_discovery
|
|
71
|
+
node.enable_device_discovery,
|
|
72
|
+
node.maxConcurrentRequests
|
|
71
73
|
);
|
|
72
74
|
|
|
73
75
|
if (typeof node.bacnetClient !== "undefined") {
|
package/bacnet_write.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
color: "#00aeef",
|
|
9
9
|
defaults: {
|
|
10
10
|
name: { value: "" },
|
|
11
|
-
applicationTag: { value: "
|
|
11
|
+
applicationTag: { value: "-1" },
|
|
12
12
|
priority: { value: "16" },
|
|
13
13
|
pointsToWrite: { value: [] },
|
|
14
14
|
writeDevices: { value: [] },
|
|
@@ -943,6 +943,7 @@
|
|
|
943
943
|
><i class="icon-tag"></i><span data-i18n="bitpool-bacnet.label.applicationTag"></span> Application Tag</label
|
|
944
944
|
>
|
|
945
945
|
<select id="node-input-applicationTag">
|
|
946
|
+
<option value="-1">AUTO</option>
|
|
946
947
|
<option value="0">NULL</option>
|
|
947
948
|
<option value="1">BOOLEAN</option>
|
|
948
949
|
<option value="2">UNSIGNED_INT</option>
|
package/common.js
CHANGED
|
@@ -67,7 +67,8 @@ class BacnetClientConfig {
|
|
|
67
67
|
cacheFileEnabled,
|
|
68
68
|
sanitise_device_schedule,
|
|
69
69
|
portRangeMatrix,
|
|
70
|
-
enable_device_discovery
|
|
70
|
+
enable_device_discovery,
|
|
71
|
+
maxConcurrentRequests
|
|
71
72
|
) {
|
|
72
73
|
this.apduTimeout = apduTimeout;
|
|
73
74
|
this.localIpAdrress = localIpAdrress;
|
|
@@ -87,6 +88,12 @@ class BacnetClientConfig {
|
|
|
87
88
|
this.sanitise_device_schedule = sanitise_device_schedule;
|
|
88
89
|
this.portRangeMatrix = this.generatePortRangeArray(portRangeMatrix);
|
|
89
90
|
this.enable_device_discovery = enable_device_discovery;
|
|
91
|
+
// Clamp maxConcurrentRequests between 1 and 250
|
|
92
|
+
// BACnet protocol limits invoke IDs to 256, so 250 is the safe maximum
|
|
93
|
+
let clampedMaxConcurrent = parseInt(maxConcurrentRequests) || 250;
|
|
94
|
+
if (clampedMaxConcurrent < 1) clampedMaxConcurrent = 1;
|
|
95
|
+
if (clampedMaxConcurrent > 250) clampedMaxConcurrent = 250;
|
|
96
|
+
this.maxConcurrentRequests = clampedMaxConcurrent;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
generatePortRangeArray(rangeMatrix) {
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug report
|
|
3
|
-
about: Create a report to help us improve
|
|
4
|
-
title: ''
|
|
5
|
-
labels: ''
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
**Describe the bug**
|
|
10
|
-
A clear and concise description of what the bug is.
|
|
11
|
-
|
|
12
|
-
**To Reproduce**
|
|
13
|
-
Steps to reproduce the behavior:
|
|
14
|
-
1. Go to '...'
|
|
15
|
-
2. Click on '....'
|
|
16
|
-
3. Scroll down to '....'
|
|
17
|
-
4. See error
|
|
18
|
-
|
|
19
|
-
**Expected behavior**
|
|
20
|
-
A clear and concise description of what you expected to happen.
|
|
21
|
-
|
|
22
|
-
**Screenshots**
|
|
23
|
-
If applicable, add screenshots to help explain your problem.
|
|
24
|
-
|
|
25
|
-
**System Information (please complete the following information):**
|
|
26
|
-
- OS: [e.g. iOS]
|
|
27
|
-
- Browser [e.g. chrome, safari]
|
|
28
|
-
- Version [e.g. 1.2]
|
|
29
|
-
- Node-RED version:
|
|
30
|
-
- NodeJS version:
|
|
31
|
-
|
|
32
|
-
**Are you running Node-RED in a docker container or directly on the operating system or virtual machine?**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
**Please provide a dump of the Node-RED error log with the Error logging and Device Found check box options enabled (found in _gateway_ node - _Discovery_ tab) if applicable:**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
**Additional context**
|
|
41
|
-
Add any other context about the problem here.
|