@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bacnet_client.js +181 -28
  3. package/bacnet_device.js +16 -15
  4. package/bacnet_gateway.html +82 -0
  5. package/bacnet_gateway.js +3 -1
  6. package/bacnet_write.html +2 -1
  7. package/common.js +8 -1
  8. package/package.json +1 -1
  9. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -41
  10. package/resources/node-bacstack-ts/dist/index.js +0 -3
  11. package/resources/node-bacstack-ts/dist/lib/apdu.js +0 -193
  12. package/resources/node-bacstack-ts/dist/lib/asn1.js +0 -1671
  13. package/resources/node-bacstack-ts/dist/lib/bvlc.js +0 -47
  14. package/resources/node-bacstack-ts/dist/lib/client.js +0 -1553
  15. package/resources/node-bacstack-ts/dist/lib/enum.js +0 -2114
  16. package/resources/node-bacstack-ts/dist/lib/npdu.js +0 -112
  17. package/resources/node-bacstack-ts/dist/lib/services/add-list-element.js +0 -58
  18. package/resources/node-bacstack-ts/dist/lib/services/alarm-acknowledge.js +0 -93
  19. package/resources/node-bacstack-ts/dist/lib/services/alarm-summary.js +0 -42
  20. package/resources/node-bacstack-ts/dist/lib/services/atomic-read-file.js +0 -157
  21. package/resources/node-bacstack-ts/dist/lib/services/atomic-write-file.js +0 -136
  22. package/resources/node-bacstack-ts/dist/lib/services/cov-notify.js +0 -119
  23. package/resources/node-bacstack-ts/dist/lib/services/create-object.js +0 -104
  24. package/resources/node-bacstack-ts/dist/lib/services/delete-object.js +0 -21
  25. package/resources/node-bacstack-ts/dist/lib/services/device-communication-control.js +0 -46
  26. package/resources/node-bacstack-ts/dist/lib/services/error.js +0 -27
  27. package/resources/node-bacstack-ts/dist/lib/services/event-information.js +0 -100
  28. package/resources/node-bacstack-ts/dist/lib/services/event-notify-data.js +0 -219
  29. package/resources/node-bacstack-ts/dist/lib/services/get-enrollment-summary.js +0 -172
  30. package/resources/node-bacstack-ts/dist/lib/services/get-event-information.js +0 -135
  31. package/resources/node-bacstack-ts/dist/lib/services/i-am-broadcast.js +0 -59
  32. package/resources/node-bacstack-ts/dist/lib/services/i-have-broadcast.js +0 -34
  33. package/resources/node-bacstack-ts/dist/lib/services/index.js +0 -32
  34. package/resources/node-bacstack-ts/dist/lib/services/life-safety-operation.js +0 -40
  35. package/resources/node-bacstack-ts/dist/lib/services/private-transfer.js +0 -43
  36. package/resources/node-bacstack-ts/dist/lib/services/read-property-multiple.js +0 -44
  37. package/resources/node-bacstack-ts/dist/lib/services/read-property.js +0 -122
  38. package/resources/node-bacstack-ts/dist/lib/services/read-range.js +0 -201
  39. package/resources/node-bacstack-ts/dist/lib/services/reinitialize-device.js +0 -35
  40. package/resources/node-bacstack-ts/dist/lib/services/subscribe-cov.js +0 -55
  41. package/resources/node-bacstack-ts/dist/lib/services/subscribe-property.js +0 -93
  42. package/resources/node-bacstack-ts/dist/lib/services/time-sync.js +0 -31
  43. package/resources/node-bacstack-ts/dist/lib/services/who-has.js +0 -56
  44. package/resources/node-bacstack-ts/dist/lib/services/who-is.js +0 -45
  45. package/resources/node-bacstack-ts/dist/lib/services/write-property-multiple.js +0 -105
  46. package/resources/node-bacstack-ts/dist/lib/services/write-property.js +0 -90
  47. package/resources/node-bacstack-ts/dist/lib/transport.js +0 -86
  48. 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
- 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,
@@ -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
- // If not, proceed to read individual properties
1455
- readPropertiesIndividually(resolve, reject);
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
- // On error, proceed to read individual properties
1460
- readPropertiesIndividually(resolve, reject);
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: point.meta.objectId.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: options.appTag,
1576
- value: 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.logOut(err);
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
- throw new Error("Unable to build network tree, empty point list");
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
- if (newPoint) {
274
- let foundIndex = this.pointsList.findIndex(ele => ele.value.type == newPoint.value.type && ele.value.instance == newPoint.value.instance);
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
- this.pointsList = this.pointsList.filter((point) =>
283
- point.value.type == 8 || //DEVICE
284
- point.value.type == 0 || //AI
285
- point.value.type == 1 || //AV
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() {
@@ -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") {
package/bacnet_write.html CHANGED
@@ -8,7 +8,7 @@
8
8
  color: "#00aeef",
9
9
  defaults: {
10
10
  name: { value: "" },
11
- applicationTag: { value: "4" },
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.6.7",
3
+ "version": "1.6.9",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -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.