@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 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
- if (that.config.cacheFileEnabled) {
198
- const cachedData = await Read_Config_Async();
199
- const parsedData = JSON.parse(cachedData);
200
- if (parsedData && typeof parsedData == "object") {
201
- // renderList is no longer cached - will be rebuilt by tree builder
202
- // if (parsedData.renderList) that.renderList = parsedData.renderList;
203
- if (parsedData.deviceList) {
204
- parsedData.deviceList.forEach(function (device) {
205
- let newBacnetDevice = new BacnetDevice(true, device);
206
- that.deviceList.push(newBacnetDevice);
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 all properties to be read
1372
- const allProperties = [
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
- // Try to read all properties at once
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: point.meta.objectId.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: options.appTag,
1566
- value: 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.logOut(err);
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
- throw new Error("Unable to build network tree, empty point list");
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
- if (object.value[0].value && typeof object.value[0].value == "string") {
2107
- values[objectId].vendorName = object.value[0].value;
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
 
@@ -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 &amp; 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") {