@bitpoolos/edge-bacnet 1.6.6 → 1.6.8
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 +27 -0
- package/bacnet_client.js +230 -82
- package/bacnet_device.js +27 -0
- 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 +4 -2
- package/resources/node-bacstack-ts/dist/lib/asn1.js +2 -2
- package/resources/node-bacstack-ts/dist/lib/client.js +198 -72
- package/resources/node-bacstack-ts/dist/lib/npdu.js +1 -1
- package/resources/style.css +7 -2
- package/treeBuilder.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.8] - 10-03-2026
|
|
4
|
+
|
|
5
|
+
Bug fix:
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
- Fixed byteLength errors
|
|
10
|
+
|
|
11
|
+
- Merged github PR's 36 and 37:
|
|
12
|
+
- Fix startup error spam: empty point list during initialization
|
|
13
|
+
- Fix boolean write failures with auto application tag detection
|
|
14
|
+
|
|
15
|
+
Minor update:
|
|
16
|
+
|
|
17
|
+
- Added concurrency management. Max Concurrent Requests option in the gateway node, which throttles how many concurrent requests can be made at any given point.
|
|
18
|
+
|
|
19
|
+
## [1.6.7] - 15-01-2026
|
|
20
|
+
|
|
21
|
+
Bug fix:
|
|
22
|
+
|
|
23
|
+
- Fix UI tree rendering issue with MSTP folders
|
|
24
|
+
|
|
25
|
+
Minor update:
|
|
26
|
+
|
|
27
|
+
- Styling
|
|
28
|
+
- Added properties to device points and minor change to device point polling
|
|
29
|
+
|
|
3
30
|
## [1.6.6] - 20-11-2025
|
|
4
31
|
|
|
5
32
|
Minor update:
|
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,
|
|
@@ -362,7 +432,7 @@ class BacnetClient extends EventEmitter {
|
|
|
362
432
|
let that = this;
|
|
363
433
|
let address = device.getAddress().address;
|
|
364
434
|
let deviceId = device.getDeviceId();
|
|
365
|
-
let foundParentIndex = that.deviceList.findIndex((ele) => that.getDeviceAddress(ele) == address);
|
|
435
|
+
let foundParentIndex = that.deviceList.findIndex((ele) => that.getDeviceAddress(ele) == address && !ele.getIsMstpDevice());
|
|
366
436
|
if (foundParentIndex !== -1) {
|
|
367
437
|
that.deviceList[foundParentIndex].addChildDevice(deviceId);
|
|
368
438
|
device.setParentDeviceId(that.deviceList[foundParentIndex].getDeviceId());
|
|
@@ -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
|
{
|
|
@@ -1368,8 +1454,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1368
1454
|
port: device.getPort(),
|
|
1369
1455
|
};
|
|
1370
1456
|
|
|
1371
|
-
// Define
|
|
1372
|
-
const
|
|
1457
|
+
// Define default properties for non-device objects
|
|
1458
|
+
const defaultProperties = [
|
|
1373
1459
|
{ id: baEnum.PropertyIdentifier.PRESENT_VALUE },
|
|
1374
1460
|
{ id: baEnum.PropertyIdentifier.DESCRIPTION },
|
|
1375
1461
|
{ id: baEnum.PropertyIdentifier.UNITS },
|
|
@@ -1384,8 +1470,66 @@ class BacnetClient extends EventEmitter {
|
|
|
1384
1470
|
{ id: baEnum.PropertyIdentifier.VENDOR_NAME },
|
|
1385
1471
|
];
|
|
1386
1472
|
|
|
1473
|
+
// Use device-specific properties for type 8, otherwise use default
|
|
1474
|
+
const propertiesToRead = type === 8 ? BacnetDevice.getDeviceObjectProperties() : defaultProperties;
|
|
1475
|
+
|
|
1476
|
+
// Function to read properties individually
|
|
1477
|
+
const readPropertiesIndividually = (resolve, reject) => {
|
|
1478
|
+
const promises = propertiesToRead.map(
|
|
1479
|
+
(property) =>
|
|
1480
|
+
new Promise((propertyResolve) => {
|
|
1481
|
+
that.client.readProperty(
|
|
1482
|
+
addressObject,
|
|
1483
|
+
{ type: type, instance: instance },
|
|
1484
|
+
property.id,
|
|
1485
|
+
readIndividualPropsOptions,
|
|
1486
|
+
(err, value) => {
|
|
1487
|
+
if (err) {
|
|
1488
|
+
propertyResolve(null);
|
|
1489
|
+
} else {
|
|
1490
|
+
propertyResolve({
|
|
1491
|
+
id: property.id,
|
|
1492
|
+
index: value.property.index,
|
|
1493
|
+
value: value.values,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
);
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1500
|
+
|
|
1501
|
+
Promise.all(promises)
|
|
1502
|
+
.then((resultArray) => {
|
|
1503
|
+
// Filter out null results
|
|
1504
|
+
const validResults = resultArray.filter((result) => result !== null);
|
|
1505
|
+
|
|
1506
|
+
resolve({
|
|
1507
|
+
error: null,
|
|
1508
|
+
value: {
|
|
1509
|
+
values: [
|
|
1510
|
+
{
|
|
1511
|
+
objectId: {
|
|
1512
|
+
type: type,
|
|
1513
|
+
instance: instance,
|
|
1514
|
+
},
|
|
1515
|
+
values: validResults,
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
})
|
|
1521
|
+
.catch(reject);
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1387
1524
|
return new Promise((resolve, reject) => {
|
|
1388
|
-
//
|
|
1525
|
+
// For Device objects (type 8), skip ALL attempt - many MSTP devices don't support it
|
|
1526
|
+
// Go straight to reading individual properties for better reliability
|
|
1527
|
+
if (type === 8) {
|
|
1528
|
+
readPropertiesIndividually(resolve, reject);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// For other object types, try to read all properties at once first
|
|
1389
1533
|
that
|
|
1390
1534
|
._readObject(addressObject, type, instance, [{ id: baEnum.PropertyIdentifier.ALL }], readOptions)
|
|
1391
1535
|
.then((result) => {
|
|
@@ -1394,61 +1538,13 @@ class BacnetClient extends EventEmitter {
|
|
|
1394
1538
|
resolve(result);
|
|
1395
1539
|
} else {
|
|
1396
1540
|
// If not, proceed to read individual properties
|
|
1397
|
-
readPropertiesIndividually();
|
|
1541
|
+
readPropertiesIndividually(resolve, reject);
|
|
1398
1542
|
}
|
|
1399
1543
|
})
|
|
1400
1544
|
.catch(() => {
|
|
1401
1545
|
// On error, proceed to read individual properties
|
|
1402
|
-
readPropertiesIndividually();
|
|
1546
|
+
readPropertiesIndividually(resolve, reject);
|
|
1403
1547
|
});
|
|
1404
|
-
|
|
1405
|
-
// Function to read properties individually
|
|
1406
|
-
const readPropertiesIndividually = () => {
|
|
1407
|
-
const promises = allProperties.map(
|
|
1408
|
-
(property, index) =>
|
|
1409
|
-
new Promise((propertyResolve) => {
|
|
1410
|
-
that.client.readProperty(
|
|
1411
|
-
addressObject,
|
|
1412
|
-
{ type: type, instance: instance },
|
|
1413
|
-
property.id,
|
|
1414
|
-
readIndividualPropsOptions,
|
|
1415
|
-
(err, value) => {
|
|
1416
|
-
if (err) {
|
|
1417
|
-
propertyResolve(null);
|
|
1418
|
-
} else {
|
|
1419
|
-
propertyResolve({
|
|
1420
|
-
id: property.id,
|
|
1421
|
-
index: value.property.index,
|
|
1422
|
-
value: value.values,
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
);
|
|
1427
|
-
})
|
|
1428
|
-
);
|
|
1429
|
-
|
|
1430
|
-
Promise.all(promises)
|
|
1431
|
-
.then((resultArray) => {
|
|
1432
|
-
// Filter out null results
|
|
1433
|
-
const validResults = resultArray.filter((result) => result !== null);
|
|
1434
|
-
|
|
1435
|
-
resolve({
|
|
1436
|
-
error: null,
|
|
1437
|
-
value: {
|
|
1438
|
-
values: [
|
|
1439
|
-
{
|
|
1440
|
-
objectId: {
|
|
1441
|
-
type: type,
|
|
1442
|
-
instance: instance,
|
|
1443
|
-
},
|
|
1444
|
-
values: validResults,
|
|
1445
|
-
},
|
|
1446
|
-
],
|
|
1447
|
-
},
|
|
1448
|
-
});
|
|
1449
|
-
})
|
|
1450
|
-
.catch(reject);
|
|
1451
|
-
};
|
|
1452
1548
|
});
|
|
1453
1549
|
}
|
|
1454
1550
|
|
|
@@ -1549,10 +1645,14 @@ class BacnetClient extends EventEmitter {
|
|
|
1549
1645
|
port: device.getPort(),
|
|
1550
1646
|
};
|
|
1551
1647
|
|
|
1648
|
+
let objectType = point.meta.objectId.type;
|
|
1649
|
+
let resolvedAppTag = that._resolveAppTag(objectType, options.appTag);
|
|
1650
|
+
let resolvedValue = that._coerceWriteValue(value, resolvedAppTag);
|
|
1651
|
+
|
|
1552
1652
|
let writeObject = {
|
|
1553
1653
|
address: addressObject,
|
|
1554
1654
|
objectId: {
|
|
1555
|
-
type:
|
|
1655
|
+
type: objectType,
|
|
1556
1656
|
instance: point.meta.objectId.instance,
|
|
1557
1657
|
},
|
|
1558
1658
|
values: {
|
|
@@ -1562,8 +1662,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1562
1662
|
},
|
|
1563
1663
|
value: [
|
|
1564
1664
|
{
|
|
1565
|
-
type:
|
|
1566
|
-
value:
|
|
1665
|
+
type: resolvedAppTag,
|
|
1666
|
+
value: resolvedValue,
|
|
1567
1667
|
},
|
|
1568
1668
|
],
|
|
1569
1669
|
},
|
|
@@ -1596,7 +1696,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1596
1696
|
point.options,
|
|
1597
1697
|
(err, value) => {
|
|
1598
1698
|
if (err) {
|
|
1599
|
-
that.
|
|
1699
|
+
let objType = that.getObjectType(point.objectId.type) || point.objectId.type;
|
|
1700
|
+
that.logOut("writeProperty error for " + objType + ":" + point.objectId.instance + " - ", err);
|
|
1600
1701
|
}
|
|
1601
1702
|
}
|
|
1602
1703
|
);
|
|
@@ -1928,7 +2029,7 @@ class BacnetClient extends EventEmitter {
|
|
|
1928
2029
|
const promiseArray = [];
|
|
1929
2030
|
|
|
1930
2031
|
if (typeof pointList === "undefined" || pointList.length === 0) {
|
|
1931
|
-
|
|
2032
|
+
return { deviceList: this.deviceList, pointList: this.networkTree };
|
|
1932
2033
|
}
|
|
1933
2034
|
|
|
1934
2035
|
for (const point of pointList) {
|
|
@@ -2102,10 +2203,23 @@ class BacnetClient extends EventEmitter {
|
|
|
2102
2203
|
}
|
|
2103
2204
|
break;
|
|
2104
2205
|
case baEnum.PropertyIdentifier.VENDOR_NAME:
|
|
2105
|
-
if (object.value) {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2206
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2207
|
+
values[objectId].vendorName = object.value[0].value;
|
|
2208
|
+
}
|
|
2209
|
+
break;
|
|
2210
|
+
case baEnum.PropertyIdentifier.MODEL_NAME:
|
|
2211
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2212
|
+
values[objectId].modelName = object.value[0].value;
|
|
2213
|
+
}
|
|
2214
|
+
break;
|
|
2215
|
+
case baEnum.PropertyIdentifier.FIRMWARE_REVISION:
|
|
2216
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2217
|
+
values[objectId].firmwareRevision = object.value[0].value;
|
|
2218
|
+
}
|
|
2219
|
+
break;
|
|
2220
|
+
case baEnum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION:
|
|
2221
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2222
|
+
values[objectId].applicationSoftwareVersion = object.value[0].value;
|
|
2109
2223
|
}
|
|
2110
2224
|
break;
|
|
2111
2225
|
}
|
|
@@ -2253,6 +2367,40 @@ class BacnetClient extends EventEmitter {
|
|
|
2253
2367
|
}
|
|
2254
2368
|
}
|
|
2255
2369
|
|
|
2370
|
+
_resolveAppTag(objectType, userAppTag) {
|
|
2371
|
+
// If user explicitly set an app tag (not auto), use it
|
|
2372
|
+
if (userAppTag !== -1) return userAppTag;
|
|
2373
|
+
|
|
2374
|
+
// Auto-detect based on BACnet object type
|
|
2375
|
+
switch (objectType) {
|
|
2376
|
+
case 3: // BI
|
|
2377
|
+
case 4: // BO
|
|
2378
|
+
case 5: // BV
|
|
2379
|
+
return 9; // ENUMERATED
|
|
2380
|
+
case 13: // MI
|
|
2381
|
+
case 14: // MO
|
|
2382
|
+
case 19: // MV
|
|
2383
|
+
return 2; // UNSIGNED_INT
|
|
2384
|
+
default:
|
|
2385
|
+
return 4; // REAL
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
_coerceWriteValue(value, appTag) {
|
|
2390
|
+
switch (appTag) {
|
|
2391
|
+
case 9: // ENUMERATED - binary objects expect 0 (inactive) or 1 (active)
|
|
2392
|
+
if (value === true || value === 1 || value === "1" ||
|
|
2393
|
+
value === "true" || value === "active" || value === "on") {
|
|
2394
|
+
return 1;
|
|
2395
|
+
}
|
|
2396
|
+
return 0;
|
|
2397
|
+
case 2: // UNSIGNED_INT - multistate objects expect positive integers
|
|
2398
|
+
return parseInt(value) || 0;
|
|
2399
|
+
default:
|
|
2400
|
+
return value;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2256
2404
|
getObjectType(objectId) {
|
|
2257
2405
|
switch (objectId) {
|
|
2258
2406
|
case 0:
|
package/bacnet_device.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
const bacnet = require("./resources/node-bacstack-ts/dist/index.js");
|
|
2
|
+
const baEnum = bacnet.enum;
|
|
3
|
+
|
|
4
|
+
// Device Object (type 8) properties - critical properties guaranteed per ASHRAE 135
|
|
5
|
+
// These are the minimum required properties that all BACnet devices must support
|
|
6
|
+
const DEVICE_OBJECT_PROPERTIES = [
|
|
7
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_IDENTIFIER },
|
|
8
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_NAME },
|
|
9
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_TYPE },
|
|
10
|
+
{ id: baEnum.PropertyIdentifier.SYSTEM_STATUS },
|
|
11
|
+
{ id: baEnum.PropertyIdentifier.VENDOR_NAME },
|
|
12
|
+
{ id: baEnum.PropertyIdentifier.VENDOR_IDENTIFIER },
|
|
13
|
+
{ id: baEnum.PropertyIdentifier.MODEL_NAME },
|
|
14
|
+
{ id: baEnum.PropertyIdentifier.FIRMWARE_REVISION },
|
|
15
|
+
{ id: baEnum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION },
|
|
16
|
+
{ id: baEnum.PropertyIdentifier.DESCRIPTION },
|
|
17
|
+
];
|
|
18
|
+
|
|
1
19
|
class BacnetDevice {
|
|
20
|
+
/**
|
|
21
|
+
* Returns the standardized list of properties to read for Device objects (type 8).
|
|
22
|
+
* These are critical properties guaranteed to exist on all BACnet devices per ASHRAE 135.
|
|
23
|
+
* @returns {Array} Array of property identifier objects
|
|
24
|
+
*/
|
|
25
|
+
static getDeviceObjectProperties() {
|
|
26
|
+
return DEVICE_OBJECT_PROPERTIES;
|
|
27
|
+
}
|
|
28
|
+
|
|
2
29
|
constructor(fromImport, config) {
|
|
3
30
|
let that = this;
|
|
4
31
|
|
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") {
|