@bitpoolos/edge-bacnet 1.6.4 → 1.6.6

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.6] - 20-11-2025
4
+
5
+ Minor update:
6
+
7
+ - Added all stored point properties as columns to "Export point list" CSV in read node Properties tab.
8
+ - Adjustment to point polling, setting stricter maxApdu sizes
9
+
10
+ ## [1.6.5] - 09-10-2025
11
+
12
+ Bug fix:
13
+
14
+ - Specific users found issues of spiking values. Results after a read query are now force ordered. An adjustment made to the bacnet stack _getInvokeId function as it was running out of array space to process a high volume of request responses.
15
+
16
+
3
17
  ## [1.6.4] - 02-09-2025
4
18
 
5
19
  Minor feature:
package/bacnet_client.js CHANGED
@@ -222,9 +222,31 @@ class BacnetClient extends EventEmitter {
222
222
  port: port,
223
223
  };
224
224
 
225
+ // Try to find the device to use device-specific options
226
+ let device = null;
227
+ if (type === 8) {
228
+ // Device object - instance is the device ID
229
+ device = that.deviceList.find(ele => ele.getDeviceId() === instance);
230
+ } else {
231
+ // For non-device objects, we can't determine the device from just address/instance
232
+ // This is a limitation of testFunction's current signature
233
+ }
234
+
235
+ // Use device-specific options if we found the device, otherwise use safer defaults
236
+ let readOptions;
237
+ if (device) {
238
+ readOptions = that.getDeviceSpecificOptions(device);
239
+ } else {
240
+ // Conservative defaults for unknown devices (assume small MSTP)
241
+ readOptions = {
242
+ maxSegments: 0, // No segmentation
243
+ maxApdu: 2 // 206 octets - safe for most MSTP devices
244
+ };
245
+ }
246
+
225
247
  const propertiesArray = [{ objectId: { type: type, instance: instance }, properties: [{ id: property }] }];
226
248
 
227
- that.client.readPropertyMultiple(addressObject, propertiesArray, that.readPropertyMultipleOptions, (err, value) => {
249
+ that.client.readPropertyMultiple(addressObject, propertiesArray, readOptions, (err, value) => {
228
250
  console.log("1 - readPropertyMultiple: ");
229
251
 
230
252
  console.log(value);
@@ -248,7 +270,7 @@ class BacnetClient extends EventEmitter {
248
270
  addressObject,
249
271
  { type: type, instance: instance },
250
272
  property,
251
- that.readPropertyMultipleOptions,
273
+ readOptions,
252
274
  (err, value) => {
253
275
  console.log("2 - readProperty: ");
254
276
 
@@ -316,12 +338,13 @@ class BacnetClient extends EventEmitter {
316
338
  address: device.getAddress(),
317
339
  port: device.getPort(),
318
340
  };
341
+ const readOptions = that.getDeviceSpecificOptions(device);
319
342
  return new Promise((resolve, reject) => {
320
343
  that.client.readProperty(
321
344
  addressObject,
322
345
  { type: baEnum.ObjectType.DEVICE, instance: device.getDeviceId() },
323
346
  baEnum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED,
324
- that.readPropertyMultipleOptions,
347
+ readOptions,
325
348
  (err, value) => {
326
349
  if (err) {
327
350
  reject(err);
@@ -339,7 +362,7 @@ class BacnetClient extends EventEmitter {
339
362
  let that = this;
340
363
  let address = device.getAddress().address;
341
364
  let deviceId = device.getDeviceId();
342
- let foundParentIndex = that.deviceList.findIndex((ele) => ele.getAddress() == address);
365
+ let foundParentIndex = that.deviceList.findIndex((ele) => that.getDeviceAddress(ele) == address);
343
366
  if (foundParentIndex !== -1) {
344
367
  that.deviceList[foundParentIndex].addChildDevice(deviceId);
345
368
  device.setParentDeviceId(that.deviceList[foundParentIndex].getDeviceId());
@@ -827,16 +850,12 @@ class BacnetClient extends EventEmitter {
827
850
  };
828
851
 
829
852
  // Process the results of the batch
830
- results.value.values.forEach((pointResult) => {
831
- const cacheRef = requestArray.find(
832
- (ele) =>
833
- ele.pointRef.meta.objectId.type === pointResult.objectId.type &&
834
- ele.pointRef.meta.objectId.instance === pointResult.objectId.instance
835
- );
853
+ results.value.values.forEach((pointResult, index) => {
854
+ const cacheRef = requestArray[index];
855
+ const pointRef = cacheRef.pointRef;
856
+ const pointNameRef = cacheRef.pointName;
836
857
 
837
- if (cacheRef) {
838
- const pointRef = cacheRef.pointRef;
839
- const pointNameRef = cacheRef.pointName;
858
+ if (pointResult.values[0].value.length > 0) {
840
859
  const val = pointResult.values[0].value[0].value;
841
860
 
842
861
  if (isNumber(val)) {
@@ -865,13 +884,12 @@ class BacnetClient extends EventEmitter {
865
884
  pointRef.status = "online";
866
885
  }
867
886
  }
868
-
869
- pointRef.meta["device"] = deviceMetaInfo;
870
- pointRef.timestamp = Date.now();
871
-
872
- // Store the point data in results
873
- bacnetResults[deviceName][pointNameRef] = pointRef;
874
887
  }
888
+ pointRef.meta["device"] = deviceMetaInfo;
889
+ pointRef.timestamp = Date.now();
890
+
891
+ // Store the point data in results
892
+ bacnetResults[deviceName][pointNameRef] = pointRef;
875
893
  });
876
894
  } catch (err) {
877
895
  that.logOut("Error processing batch:", err);
@@ -944,13 +962,46 @@ class BacnetClient extends EventEmitter {
944
962
 
945
963
  async updateManyPoints(device, points) {
946
964
  try {
947
- const results = await this._readObjectWithRequestArray(device, points, this.readPropertyMultipleOptions);
965
+ // Use device-specific options instead of global options
966
+ const deviceOptions = this.getDeviceSpecificOptions(device);
967
+ const results = await this._readObjectWithRequestArray(device, points, deviceOptions);
948
968
  return results;
949
969
  } catch (error) {
950
970
  throw error;
951
971
  }
952
972
  }
953
973
 
974
+ getDeviceSpecificOptions(device) {
975
+ let maxSegments = this.readPropertyMultipleOptions.maxSegments;
976
+ let maxApdu = this.readPropertyMultipleOptions.maxApdu;
977
+
978
+ // Adjust for devices with no segmentation support
979
+ if (device.getSegmentation() == 3) {
980
+ maxSegments = 0;
981
+ }
982
+
983
+ // Adjust maxApdu based on device capability
984
+ const deviceMaxApdu = device.getMaxApdu();
985
+ if (deviceMaxApdu <= 50) {
986
+ maxApdu = 0; // 50 octets
987
+ } else if (deviceMaxApdu <= 128) {
988
+ maxApdu = 1; // 128 octets
989
+ } else if (deviceMaxApdu <= 206) {
990
+ maxApdu = 2; // 206 octets
991
+ } else if (deviceMaxApdu <= 480) {
992
+ maxApdu = 3; // 480 octets
993
+ } else if (deviceMaxApdu <= 1024) {
994
+ maxApdu = 4; // 1024 octets
995
+ } else {
996
+ maxApdu = 5; // 1476 octets
997
+ }
998
+
999
+ return {
1000
+ maxSegments: maxSegments,
1001
+ maxApdu: maxApdu
1002
+ };
1003
+ }
1004
+
954
1005
  updatePointWithRetry(device, point, retryCount = 1) {
955
1006
  let that = this;
956
1007
  const tryUpdate = (retriesLeft) => {
@@ -1002,21 +1053,8 @@ class BacnetClient extends EventEmitter {
1002
1053
  port: device.getPort(),
1003
1054
  };
1004
1055
 
1005
- let maxSegments = that.readPropertyMultipleOptions.maxSegments;
1006
- let maxApdu = that.readPropertyMultipleOptions.maxApdu;
1007
-
1008
- if (device.getSegmentation() == 3) {
1009
- maxSegments = 0;
1010
- }
1011
-
1012
- if (device.getMaxApdu() == 480) {
1013
- maxApdu = 3;
1014
- }
1015
-
1016
- let settings = {
1017
- maxSegments: maxSegments,
1018
- maxApdu: maxApdu,
1019
- };
1056
+ // Use device-specific options
1057
+ const settings = that.getDeviceSpecificOptions(device);
1020
1058
 
1021
1059
  return new Promise((resolve, reject) => {
1022
1060
  that.client.readProperty(
@@ -1037,8 +1075,15 @@ class BacnetClient extends EventEmitter {
1037
1075
  }
1038
1076
 
1039
1077
  estimateMaxObjectSize(apduSize) {
1040
- if (apduSize < 500) {
1041
- return 20;
1078
+ // Be more conservative for very small MSTP devices
1079
+ if (apduSize <= 50) {
1080
+ return 1; // Only 1 object at a time for 50-byte devices
1081
+ } else if (apduSize <= 128) {
1082
+ return 3; // 3 objects for 128-byte devices
1083
+ } else if (apduSize <= 206) {
1084
+ return 5; // 5 objects for 206-byte devices
1085
+ } else if (apduSize < 500) {
1086
+ return 10; // Reduced from 20 for safety
1042
1087
  } else if (apduSize > 500 && apduSize < 1000) {
1043
1088
  //return Math.round(((apduSize - 30) / 7));
1044
1089
  return 50;
@@ -1217,6 +1262,32 @@ class BacnetClient extends EventEmitter {
1217
1262
  };
1218
1263
  return new Promise((resolve, reject) => {
1219
1264
  that.client.readPropertyMultiple(addressObject, requestArray, readOptions, (error, value) => {
1265
+ if (value && value.values) {
1266
+ const reorderedValues = requestArray.map((req) => {
1267
+ const foundValue = value.values.find(
1268
+ (val) => val.objectId.type === req.objectId.type && val.objectId.instance === req.objectId.instance
1269
+ );
1270
+ return (
1271
+ foundValue || {
1272
+ objectId: req.objectId,
1273
+ values: [
1274
+ {
1275
+ value: [
1276
+ {
1277
+ value: {
1278
+ errorClass: baEnum.ErrorClass.PROPERTY,
1279
+ errorCode: baEnum.ErrorCode.UNKNOWN_PROPERTY,
1280
+ },
1281
+ },
1282
+ ],
1283
+ },
1284
+ ],
1285
+ }
1286
+ );
1287
+ });
1288
+ value.values = reorderedValues;
1289
+ }
1290
+
1220
1291
  resolve({
1221
1292
  error: error,
1222
1293
  value: value,
@@ -1233,12 +1304,13 @@ class BacnetClient extends EventEmitter {
1233
1304
  port: device.getPort(),
1234
1305
  };
1235
1306
  let deviceId = device.getDeviceId();
1307
+ const readOptions = that.getDeviceSpecificOptions(device);
1236
1308
 
1237
1309
  that.client.readProperty(
1238
1310
  addressObject,
1239
1311
  { type: baEnum.ObjectType.DEVICE, instance: deviceId },
1240
1312
  baEnum.PropertyIdentifier.OBJECT_NAME,
1241
- that.readPropertyMultipleOptions,
1313
+ readOptions,
1242
1314
  callback
1243
1315
  );
1244
1316
  }
@@ -1283,10 +1355,8 @@ class BacnetClient extends EventEmitter {
1283
1355
 
1284
1356
  _readObjectFull(device, type, instance) {
1285
1357
  const that = this;
1286
- const readOptions = {
1287
- maxSegments: that.readPropertyMultipleOptions.maxSegments,
1288
- maxApdu: that.readPropertyMultipleOptions.maxApdu,
1289
- };
1358
+ // Use device-specific options for reading all properties
1359
+ const readOptions = that.getDeviceSpecificOptions(device);
1290
1360
 
1291
1361
  const readIndividualPropsOptions = {
1292
1362
  maxSegments: 0,
@@ -1384,10 +1454,8 @@ class BacnetClient extends EventEmitter {
1384
1454
 
1385
1455
  _readObjectLite(device, type, instance) {
1386
1456
  const that = this;
1387
- const readOptions = {
1388
- maxSegments: that.readPropertyMultipleOptions.maxSegments,
1389
- maxApdu: that.readPropertyMultipleOptions.maxApdu,
1390
- };
1457
+ // Use device-specific options
1458
+ const readOptions = that.getDeviceSpecificOptions(device);
1391
1459
 
1392
1460
  const readIndividualPropsOptions = {
1393
1461
  maxSegments: 0,
@@ -1552,10 +1620,8 @@ class BacnetClient extends EventEmitter {
1552
1620
  scanDevice(device) {
1553
1621
  let that = this;
1554
1622
  return new Promise((resolve, reject) => {
1555
- const readOptions = {
1556
- maxSegments: that.readPropertyMultipleOptions.maxSegments,
1557
- maxApdu: that.readPropertyMultipleOptions.maxApdu,
1558
- };
1623
+ // Use device-specific options
1624
+ const readOptions = that.getDeviceSpecificOptions(device);
1559
1625
  this._readObjectList(device, readOptions, (err, result) => {
1560
1626
  if (!err) {
1561
1627
  try {
package/bacnet_gateway.js CHANGED
@@ -212,14 +212,10 @@ module.exports = function (RED) {
212
212
  }
213
213
  }
214
214
 
215
- if (
216
- node.bacnetServerEnabled == true &&
217
- node.bacnetClient &&
218
- node.bacnetServer
219
- ) {
215
+ if (node.bacnetServerEnabled == true && node.bacnetClient && node.bacnetServer) {
220
216
  try {
221
217
  // Clean up any existing listeners to prevent stale references
222
- node.bacnetServer.removeAllListeners('writeProperty');
218
+ node.bacnetServer.removeAllListeners("writeProperty");
223
219
 
224
220
  // Store the event handler function so we can clean it up later
225
221
  node.writePropertyHandler = (topic, newValue) => {
@@ -269,7 +265,7 @@ module.exports = function (RED) {
269
265
  } else if (msg.doUpdatePriorityDevices == true && msg.priorityDevices !== null) {
270
266
  node.bacnetClient
271
267
  .updatePriorityQueue(msg.priorityDevices)
272
- .then(function (result) { })
268
+ .then(function (result) {})
273
269
  .catch(function (error) {
274
270
  logOut("Error updating priorityQueue: ", error);
275
271
  });
@@ -283,7 +279,7 @@ module.exports = function (RED) {
283
279
 
284
280
  node.bacnetClient
285
281
  .applyDisplayNames(msg.pointsToRead)
286
- .then(function (result) { })
282
+ .then(function (result) {})
287
283
  .catch(function (error) {
288
284
  logOut("Error in applyDisplayNames: ", error);
289
285
  });
@@ -306,7 +302,7 @@ module.exports = function (RED) {
306
302
  node.on("close", function () {
307
303
  // Clean up the writeProperty event listener
308
304
  if (node.bacnetServer && node.writePropertyHandler) {
309
- node.bacnetServer.removeListener('writeProperty', node.writePropertyHandler);
305
+ node.bacnetServer.removeListener("writeProperty", node.writePropertyHandler);
310
306
  node.writePropertyHandler = null;
311
307
  }
312
308
 
@@ -794,7 +790,6 @@ module.exports = function (RED) {
794
790
  if (points[point] && "presentValue" in points[point]) {
795
791
  let pointName = getPointName(points[point], point);
796
792
  let topic = getTopicString("sendSimpleWithStatus", useDeviceName, readNodeName, device, pointName);
797
-
798
793
  msgg.topic = topic;
799
794
  let payload = {
800
795
  presentValue: points[point]["presentValue"],
package/bacnet_read.html CHANGED
@@ -433,8 +433,20 @@
433
433
  },
434
434
  exportPointListCsv() {
435
435
  let app = this;
436
- let csvContent = "ipAddress,deviceId,deviceName,pointName,objectType" + "\r\n";
436
+ let csvContent = "ipAddress,deviceId,deviceName,pointName,objectType,presentValue,description,units,objectName,displayName,systemStatus,modificationDate,programState,recordCount,hasPriorityArray,vendorName" + "\r\n";
437
437
  const keys = Object.keys(app.pointList);
438
+
439
+ // Helper function to safely get property value and escape for CSV
440
+ const getCsvValue = (value) => {
441
+ if (value === null || value === undefined) return "";
442
+ const stringValue = String(value);
443
+ // Escape double quotes and wrap in quotes if contains comma, newline, or double quote
444
+ if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) {
445
+ return '"' + stringValue.replace(/"/g, '""') + '"';
446
+ }
447
+ return stringValue;
448
+ };
449
+
438
450
  for (key in keys) {
439
451
  const guid = keys[key];
440
452
  const ipAddress = guid.split("-")[0];
@@ -452,16 +464,39 @@
452
464
  const points = Object.keys(deviceObject);
453
465
  if (points.length > 0) {
454
466
  for (point in points) {
467
+ const pointData = deviceObject[points[point]] || {};
455
468
  csvContent +=
456
- ipAddress +
469
+ getCsvValue(ipAddress) +
470
+ "," +
471
+ getCsvValue(deviceId) +
472
+ "," +
473
+ getCsvValue(deviceName) +
474
+ "," +
475
+ getCsvValue(points[point]) +
476
+ "," +
477
+ getCsvValue(pointData.meta?.objectId?.type) +
457
478
  "," +
458
- deviceId +
479
+ getCsvValue(pointData.presentValue) +
459
480
  "," +
460
- deviceName +
481
+ getCsvValue(pointData.description) +
461
482
  "," +
462
- points[point] +
483
+ getCsvValue(pointData.units) +
463
484
  "," +
464
- deviceObject[points[point]].meta.objectId.type +
485
+ getCsvValue(pointData.objectName) +
486
+ "," +
487
+ getCsvValue(pointData.displayName) +
488
+ "," +
489
+ getCsvValue(pointData.systemStatus) +
490
+ "," +
491
+ getCsvValue(pointData.modificationDate) +
492
+ "," +
493
+ getCsvValue(pointData.programState) +
494
+ "," +
495
+ getCsvValue(pointData.recordCount) +
496
+ "," +
497
+ getCsvValue(pointData.hasPriorityArray) +
498
+ "," +
499
+ getCsvValue(pointData.vendorName) +
465
500
  "\r\n";
466
501
 
467
502
  if (parseInt(point) == points.length - 1 && parseInt(key) == keys.length - 1) {
@@ -687,14 +722,14 @@
687
722
  let app = this;
688
723
  let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
689
724
  if (device) {
690
- app.nodeService.purgeDevice(device).then(function (result) {});
725
+ app.nodeService.purgeDevice(device).then(function (result) { });
691
726
  }
692
727
  },
693
728
  updatePointsForDevice(slotProps) {
694
729
  let app = this;
695
730
  let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
696
731
  if (device) {
697
- app.nodeService.updatePointsForDevice(device).then(function (result) {});
732
+ app.nodeService.updatePointsForDevice(device).then(function (result) { });
698
733
  }
699
734
  },
700
735
  updatePoint(slotProps) {
@@ -705,7 +740,7 @@
705
740
  });
706
741
  const deviceKey = `${app.getDeviceAddress(device.address)}-${device.deviceId}`;
707
742
  if (device) {
708
- app.nodeService.updatePoint(deviceKey, pointKey).then(function (result) {});
743
+ app.nodeService.updatePoint(deviceKey, pointKey).then(function (result) { });
709
744
  }
710
745
  },
711
746
  setDeviceName() {
@@ -1708,4 +1743,4 @@
1708
1743
  <li><a href="https://wiki.bitpool.com/">wiki.bitpool.com</a> - find more documentation.</li>
1709
1744
  <li><a href="https://bacnet.org/">BACnet</a> - find more about the protocol.</li>
1710
1745
  </ul>
1711
- </script>
1746
+ </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -54,4 +54,4 @@
54
54
  "type": "github",
55
55
  "url": "git+https://github.com/bitpool/edge-bacnet.git"
56
56
  }
57
- }
57
+ }
@@ -67,9 +67,23 @@ class Client extends events_1.EventEmitter {
67
67
  }
68
68
  // Helper utils
69
69
  _getInvokeId() {
70
+ // Try up to 256 times to find an unused invoke ID
71
+ for (let attempts = 0; attempts < 256; attempts++) {
72
+ const id = this._invokeCounter++;
73
+ if (id >= 256) this._invokeCounter = 1;
74
+
75
+ const invokeId = id - 1;
76
+
77
+ // If this invoke ID is not currently in use, return it
78
+ if (!this._invokeStore[invokeId]) {
79
+ return invokeId;
80
+ }
81
+ }
82
+
83
+ // Edge case: if all 256 invoke IDs are in use, fall back to original behavior
84
+ // This prevents infinite loops while maintaining backwards compatibility
70
85
  const id = this._invokeCounter++;
71
- if (id >= 256)
72
- this._invokeCounter = 1;
86
+ if (id >= 256) this._invokeCounter = 1;
73
87
  return id - 1;
74
88
  }
75
89
  _invokeCallback(id, err, result) {