@halsystems/red-bacnet 1.0.23 → 1.1.1

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.1]
4
+ ### Added
5
+ - Unit test for BACnet string points
6
+
7
+ ### Fixed
8
+ - Reading offline device points run infinitely
9
+ - Unit test failure
10
+
11
+ ## [1.1.0]
12
+ ### Changed
13
+ - Implemented auto resize batch size when querying using readPropertyMultiple to improve performance
14
+ - Added readPropertyMultiple to read object list to reduce query speed
15
+ - Changed strategy when discovering device name, implemented delay instead of burst to reduce traffic congestion and yield better result
16
+ - Force use readProperty to read multistate object to improve result
17
+
18
+ ### Fixed
19
+ - Failure to discover device name for Forwarded NPDU devices
20
+ - Missed bacnet objects when using discover point
21
+
3
22
  ## [1.0.22]
4
23
  ### Fixed
5
24
  - Removed console.log in Read Point
package/common/bacnet.js CHANGED
@@ -18,7 +18,8 @@ const { getErrMsg } = require('@root/common/func.js')
18
18
  // ---------------------------------- export ----------------------------------
19
19
  module.exports = {
20
20
  /**
21
- * Reads the object list of a device.
21
+ * Reads the object list of a device using optimized batch reading.
22
+ * First tries readPropertyMultiple for faster performance, falls back to sequential reading if failed.
22
23
  * @param {BacnetClient} client
23
24
  * @param {object} device
24
25
  * eg:{
@@ -38,15 +39,97 @@ module.exports = {
38
39
  readObjectList: async function (client, device) {
39
40
  const objectId = { type: baEnum.ObjectType.DEVICE, instance: device.deviceId };
40
41
  const propertyId = baEnum.PropertyIdentifier.OBJECT_LIST;
41
- return await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
42
+
43
+ let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId);
44
+ if (result.length == 0) {
45
+ result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
46
+ }
47
+
48
+ return result;
49
+ },
50
+
51
+ /**
52
+ * Reads a property that returns an array using readPropertyMultiple for better performance.
53
+ * Attempts to read the property in batches to minimize round trips.
54
+ * @param {BacnetClient} client
55
+ * @param {object} device
56
+ * @param {object} objectId
57
+ * @param {number} propertyId
58
+ * @returns array of objects
59
+ * @async
60
+ */
61
+ readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId) {
62
+ let addressSet = device.ipAddress;
63
+ if (device.macAddress != null && device.network != null) {
64
+ addressSet = {
65
+ ip: device.ipAddress,
66
+ adr: device.macAddress,
67
+ net: device.network
68
+ };
69
+ }
70
+
71
+ const result = [];
72
+ let healthy = true;
73
+
74
+ // helper for single batch request
75
+ async function readBatch(arrayIndex) {
76
+ const reqArr = [{
77
+ objectId: objectId,
78
+ properties: [{
79
+ id: propertyId,
80
+ index: arrayIndex != null ? arrayIndex : baEnum.ASN1_ARRAY_ALL
81
+ }]
82
+ }];
83
+
84
+ return new Promise((resolve, reject) => {
85
+ client.readPropertyMultiple(
86
+ addressSet,
87
+ reqArr,
88
+ { maxApdu: device.maxApdu },
89
+ (err, value) => {
90
+ if (err) return reject(err);
91
+ resolve(value);
92
+ }
93
+ );
94
+ });
95
+ }
96
+
97
+ // try reading all at once first
98
+ try {
99
+ const value = await readBatch(baEnum.ASN1_ARRAY_ALL);
100
+ if (value?.values?.[0]?.values?.[0]?.value) {
101
+ return value.values[0].values[0].value;
102
+ }
103
+ } catch (err) {
104
+ void err
105
+ healthy = false;
106
+ }
107
+
108
+ // try using index read
109
+ if (!healthy) {
110
+ for (let i = 0; ; i++) {
111
+ try {
112
+ const res = await readBatch(i);
113
+ const item = res?.values?.[0]?.values?.[0]?.value?.[0];
114
+
115
+ if (item?.type === 105) break
116
+
117
+ result.push(item);
118
+ } catch (err) {
119
+ void err
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ return result;
42
126
  },
43
127
 
44
128
  /**
45
129
  * Smart read multiple properties from a BACnet device with concurrent read feature and various read methods
46
130
  * readMethod
47
131
  * 0: use readProperty only
48
- * 1: try readPropertyMultiple with consevative query size, fallback to readProperty if failed
49
- * 2: try readPropertyMultiple twice with high and conservative query size, fallback to readProperty if failed
132
+ * 1: try readPropertyMultiple, reduce query size if failed, and fallback to readProperty if query size reduced to 1
50
133
  * @param {BacnetClient} client
51
134
  * @param {object} device
52
135
  * eg:{
@@ -71,13 +154,15 @@ module.exports = {
71
154
  * maximum concurrent point to read in single read mode
72
155
  * @param {number} singleReadFailedRetry
73
156
  * retry times for single read failed
157
+ * @param {number} concurrentTaskDelay
158
+ * delay between concurrent tasks
74
159
  * @returns array of objects
75
160
  * eg: [{type: 12, value: {type: 8, instance: 123}, ...],
76
161
  * @async
77
162
  */
78
163
  smartReadProperty: async function (
79
164
  client, device, reqArr, readMethod = 1, maxConcurrentSinglePointRead = 5,
80
- singleReadFailedRetry = 5
165
+ singleReadFailedRetry = 5, concurrentTaskDelay = 50
81
166
  ) {
82
167
  /* reqArr example
83
168
  [{
@@ -95,16 +180,15 @@ module.exports = {
95
180
  if (readMethod > 0) {
96
181
  // calculate batch sizes
97
182
  if (device.maxApdu == null) // default batch size if null or undefined
98
- batchSizes.add(20).add(5);
183
+ batchSizes.add(20);
99
184
  else {
100
- const perValueBytes = [30, 50] // typical numeric point is 17 byte
101
- for (let x = 0; x < perValueBytes.length; x++)
102
- batchSizes.add(Math.trunc(device.maxApdu / perValueBytes[x]))
185
+ const perValueBytes = [30] // typical numeric point is 17 byte
186
+ for (let x = 0; x < perValueBytes.length; x++) {
187
+ let batchSize = Math.trunc(device.maxApdu / perValueBytes[x])
188
+ if (batchSize > 0)
189
+ batchSizes.add(batchSize)
190
+ }
103
191
  }
104
-
105
- // if readMethod === 1
106
- if (readMethod === 1 && batchSizes.size > 1)
107
- batchSizes = new Set([...batchSizes].slice(-1));
108
192
  }
109
193
 
110
194
  for (const batchSize of batchSizes) {
@@ -112,6 +196,7 @@ module.exports = {
112
196
  let first = true
113
197
  let reqArrBatch
114
198
  let batchCount
199
+ let currBatchSize = batchSize
115
200
 
116
201
  do { // use current batch size until query failed
117
202
  // get ready for next batch
@@ -132,7 +217,7 @@ module.exports = {
132
217
  if (reqArrIndexNext >= reqArr.length)
133
218
  break
134
219
 
135
- if (batchCount + reqArr[reqArrIndexNext].properties.length > batchSize)
220
+ if (batchCount + reqArr[reqArrIndexNext].properties.length > currBatchSize)
136
221
  break
137
222
 
138
223
  reqArrBatch.push(reqArr[reqArrIndexNext])
@@ -143,6 +228,14 @@ module.exports = {
143
228
  // read batch block
144
229
  const value = await module.exports.readPropertyMultple(client, device, reqArrBatch)
145
230
  .catch(() => {
231
+ // Reduce batch size by half (minimum 1)
232
+ currBatchSize = Math.max(1, Math.floor(currBatchSize / 2))
233
+
234
+ // If batch size is too small, mark as unhealthy
235
+ if (currBatchSize <= 1) {
236
+ healthy = false
237
+ }
238
+
146
239
  reqArrIndexNext = reqArrIndex
147
240
  healthy = false
148
241
  })
@@ -172,7 +265,6 @@ module.exports = {
172
265
  if (!success) {
173
266
  let failedCount = 0
174
267
  let result_single = []
175
-
176
268
  const dummyEventEmitter = new EventEmitter();
177
269
  const tasks = reqArr.slice(reqArrIndexNext).flatMap((req, x) =>
178
270
  req.properties.map((prop, y) => ({
@@ -196,7 +288,7 @@ module.exports = {
196
288
  return value;
197
289
  } catch (err) {
198
290
  failedCount++;
199
- if (readMethod < 2 && failedCount >= singleReadFailedRetry)
291
+ if (failedCount >= singleReadFailedRetry)
200
292
  throw err
201
293
  }
202
294
  }
@@ -204,7 +296,7 @@ module.exports = {
204
296
  );
205
297
 
206
298
  try {
207
- await concurrentTasks(dummyEventEmitter, tasks, maxConcurrentSinglePointRead);
299
+ await concurrentTasks(dummyEventEmitter, tasks, maxConcurrentSinglePointRead, concurrentTaskDelay);
208
300
  } catch (err) {
209
301
  void err
210
302
  }
@@ -253,9 +345,14 @@ module.exports = {
253
345
  * @param {EventEmitter} eventEmitter
254
346
  * @param {number} maxConcurrentWrite
255
347
  * maximum concurrent point to write
348
+ * @param {number} concurrentTaskDelay
349
+ * delay between concurrent tasks
256
350
  * @async
257
351
  */
258
- smartWriteProperty: async function (client, device, writePoints, eventEmitter, maxConcurrentWrite) {
352
+ smartWriteProperty: async function (
353
+ client, device, writePoints, eventEmitter, maxConcurrentWrite,
354
+ concurrentTaskDelay = 50
355
+ ) {
259
356
  const entries = Object.entries(writePoints);
260
357
 
261
358
  // current writePropertyMultiple will throw ERR_TIMEOUT if any of the write fails
@@ -281,7 +378,7 @@ module.exports = {
281
378
  }
282
379
  }));
283
380
 
284
- await concurrentTasks(eventEmitter, tasks, maxConcurrentWrite);
381
+ await concurrentTasks(eventEmitter, tasks, maxConcurrentWrite, concurrentTaskDelay);
285
382
 
286
383
  // write properties multiples example
287
384
  // const values = [
@@ -317,7 +414,7 @@ module.exports = {
317
414
  * eg: 85
318
415
  * @returns object
319
416
  * eg: { len: 11, objectId: { type: 19, instance: 1 },
320
- * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 } ]}
417
+ * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
321
418
  * @async
322
419
  */
323
420
  readProperty: async function (client, device, objectId, propertyId) {
@@ -485,4 +582,4 @@ module.exports = {
485
582
  });
486
583
  });
487
584
  },
488
- }
585
+ }
@@ -2,6 +2,7 @@
2
2
  require('./_alias.js');
3
3
 
4
4
  const { EVENT_OUTPUT, EVENT_ERROR } = require('@root/common/core/constant.js');
5
+ const { delay } = require('@root/common/core/util.js');
5
6
 
6
7
  // ---------------------------------- type def ----------------------------------
7
8
  /**
@@ -16,10 +17,13 @@ module.exports = {
16
17
  * @param {EventEmitter} eventEmitter - The event emitter to emit events to.
17
18
  * @param {Array} tasks - The list of tasks to execute.
18
19
  * @param {number} maxConcurrent - The maximum number of concurrent tasks.
20
+ * @param {number} concurrentTaskDelay - The delay between concurrent tasks.
19
21
  * @returns {Promise<Array>} - A promise that resolves with an array of results.
20
22
  * @async
21
23
  */
22
- concurrentTasks: async function (eventEmitter, tasks, maxConcurrent) {
24
+ concurrentTasks: async function (
25
+ eventEmitter, tasks, maxConcurrent, concurrentTaskDelay = 50
26
+ ) {
23
27
  const executing = new Set();
24
28
  const results = [];
25
29
 
@@ -43,8 +47,8 @@ module.exports = {
43
47
  await Promise.race(executing);
44
48
  }
45
49
 
46
- // need delay?
47
- // if (typeof delay === 'function') await delay(1);
50
+ if (concurrentTaskDelay > 0)
51
+ await delay(concurrentTaskDelay)
48
52
  }
49
53
 
50
54
  await Promise.allSettled(executing);
@@ -31,10 +31,9 @@ module.exports = {
31
31
  * @param {number} lowLimit - Low limit of the network range.
32
32
  * @param {number} highLimit - High limit of the network range.
33
33
  * @param {number} whoIsTimeout - Who is request timeout in milliseconds.
34
- * @param {number} readDeviceNameTimeout - Read device name timeout in milliseconds.
35
34
  */
36
35
  constructor(
37
- client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout, readDeviceNameTimeout,
36
+ client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout,
38
37
  name = 'discover device'
39
38
  ) {
40
39
  super();
@@ -44,7 +43,6 @@ module.exports = {
44
43
  this.lowLimit = lowLimit
45
44
  this.highLimit = highLimit
46
45
  this.whoIsTimeout = whoIsTimeout
47
- this.readDeviceNameTimeout = readDeviceNameTimeout
48
46
  this.name = name
49
47
  }
50
48
 
@@ -108,7 +106,6 @@ module.exports = {
108
106
  * @returns {Promise<void>}
109
107
  */
110
108
  async #readDevices() {
111
- const chunkSize = 100;
112
109
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
113
110
 
114
111
  const readDeviceName = (d, addressSet) => {
@@ -138,26 +135,22 @@ module.exports = {
138
135
  });
139
136
  };
140
137
 
141
- for (let i = 0; i < this.discoverList.length; i += chunkSize) {
142
- const chunk = this.discoverList.slice(i, i + chunkSize);
138
+ for (let i = 0; i < this.discoverList.length; i++) {
139
+ const d = this.discoverList[i];
143
140
 
144
- const promises = chunk.map(d => {
145
- let addressSet = d.address;
146
- if (d.adr !== undefined && d.net !== undefined) {
147
- addressSet = { ip: d.address, adr: d.adr, net: d.net };
148
- }
149
-
150
- return readDeviceName(d, addressSet)
151
- .then(deviceInfo => this.discoveredDevices.push(deviceInfo));
152
- });
141
+ let addressSet = d.address;
142
+ if (d.adr !== undefined && d.net !== undefined) {
143
+ addressSet = { ip: d.address, adr: d.adr, net: d.net };
144
+ }
153
145
 
154
- await Promise.allSettled(promises);
155
- await delay(this.readDeviceNameTimeout);
146
+ const deviceInfo = await readDeviceName(d, addressSet);
147
+ this.discoveredDevices.push(deviceInfo);
156
148
 
157
- // update progress
149
+ // throttle to avoid bursts
150
+ await delay(75); // tweak between 50–150ms
158
151
  this.#updateProgress(
159
- Math.min(90, Math.round((80 / this.discoverList.length) * (i + chunkSize) + 10))
160
- )
152
+ Math.min(90, Math.round((80 / this.discoverList.length) * (i + 1) + 10))
153
+ );
161
154
  }
162
155
 
163
156
  this.#updateProgress(90)
@@ -53,7 +53,8 @@ module.exports = {
53
53
 
54
54
  constructor(
55
55
  client, eventEmitter, inputDevices, discoverMode, readMethod, groupExportDeviceCount,
56
- maxConcurrentDeviceRead, maxConcurrentSinglePointRead, name = 'discover point'
56
+ maxConcurrentDeviceRead, maxConcurrentSinglePointRead, concurrentTaskDelay = 50,
57
+ name = 'discover point'
57
58
  ) {
58
59
  super();
59
60
  this.client = client
@@ -64,6 +65,7 @@ module.exports = {
64
65
  this.groupExportDeviceCount = groupExportDeviceCount
65
66
  this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
66
67
  this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
68
+ this.concurrentTaskDelay = concurrentTaskDelay
67
69
  this.name = name
68
70
  }
69
71
 
@@ -194,7 +196,7 @@ module.exports = {
194
196
 
195
197
  return await readPoints(
196
198
  d, objectListFinal, discoverPointEvent, this.name, this.client, this.readMethod,
197
- this.maxConcurrentSinglePointRead
199
+ this.maxConcurrentSinglePointRead, this.concurrentTaskDelay
198
200
  );
199
201
  } catch (error) {
200
202
  this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, `Error reading ${d.deviceName} points`, error));
@@ -237,9 +239,12 @@ module.exports = {
237
239
 
238
240
  // ---------------------------------- functions ----------------------------------
239
241
  const readPoints = async (
240
- device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead
242
+ device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead,
243
+ concurrentTaskDelay
241
244
  ) => {
242
245
  const points = [];
246
+
247
+ // First query - get basic properties (excluding STATE_TEXT)
243
248
  const reqArr = objects.map(obj => ({
244
249
  objectId: { type: obj.value.type, instance: obj.value.instance },
245
250
  properties: [
@@ -250,38 +255,80 @@ const readPoints = async (
250
255
  { id: baEnum.PropertyIdentifier.INACTIVE_TEXT },
251
256
  { id: baEnum.PropertyIdentifier.ACTIVE_TEXT }
252
257
  ] : []),
253
- ...(multiStateObjectTypes.includes(obj.value.type) ? [{ id: baEnum.PropertyIdentifier.STATE_TEXT }] : []),
254
- // ...(scheduleObjectTypes.includes(obj.value.type) ? [
255
- // { id: baEnum.PropertyIdentifier.WEEKLY_SCHEDULE },
256
- // { id: baEnum.PropertyIdentifier.EXCEPTION_SCHEDULE },
257
- // { id: baEnum.PropertyIdentifier.SCHEDULE_DEFAULT }
258
- // ] : [])
259
258
  ]
260
259
  }));
261
260
 
262
261
  try {
262
+ // First request - get basic properties
263
263
  const result = await smartReadProperty(
264
- client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50
264
+ client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay
265
265
  );
266
+
267
+ // Process basic properties first
266
268
  result.forEach(i => {
267
- /** i example
268
- {
269
- objectId: { type: 1, instance: 0 },
270
- values: [
271
- { id: 85, index: 4294967295, value: [Array] },
272
- { id: 77, index: 4294967295, value: [Array] }
273
- ]
274
- }
275
- */
276
269
  const point = processPoint(i, device.deviceName);
277
270
  points.push(point);
278
271
  });
279
- return points
272
+
273
+ // Second query - get STATE_TEXT for multistate objects only
274
+ const multistateObjects = objects.filter(obj => multiStateObjectTypes.includes(obj.value.type));
275
+
276
+ if (multistateObjects.length > 0) {
277
+ const stateTextReqArr = multistateObjects.map(obj => ({
278
+ objectId: { type: obj.value.type, instance: obj.value.instance },
279
+ properties: [{ id: baEnum.PropertyIdentifier.STATE_TEXT }]
280
+ }));
281
+
282
+ try {
283
+ // Force use single read on state text to reduce loss especially long text
284
+ const stateTextResult = await smartReadProperty(
285
+ client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay
286
+ );
287
+
288
+ // Update points with STATE_TEXT
289
+ stateTextResult.forEach(stateTextData => {
290
+ const matchingPoint = points.find(point =>
291
+ point.bacType === stateTextData.objectId.type &&
292
+ point.bacInstance === stateTextData.objectId.instance
293
+ );
294
+
295
+ if (matchingPoint) {
296
+ updatePointWithStateText(matchingPoint, stateTextData);
297
+ }
298
+ });
299
+ } catch (stateTextError) {
300
+ // Log the error but don't fail the entire operation
301
+ eventEmitter.emit(EVENT_ERROR, errMsg(
302
+ name,
303
+ 'Warning: Failed to read STATE_TEXT properties',
304
+ stateTextError
305
+ ));
306
+ }
307
+ }
308
+
309
+ return points;
280
310
  } catch (error) {
281
311
  eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
282
312
  }
283
313
  };
284
314
 
315
+ const updatePointWithStateText = (point, stateTextData) => {
316
+ const stateTextProp = stateTextData.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT);
317
+ const states = stateTextProp?.value;
318
+
319
+ if (Array.isArray(states)) {
320
+ // Update the facets with state text information
321
+ const stateTextFacet = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
322
+
323
+ // If point already has facets, append; otherwise, set new facets
324
+ if (point.facets) {
325
+ point.facets += `;${stateTextFacet}`;
326
+ } else {
327
+ point.facets = stateTextFacet;
328
+ }
329
+ }
330
+ };
331
+
285
332
  const processPoint = (i, deviceName) => {
286
333
  const point = {
287
334
  deviceName,
@@ -310,12 +357,8 @@ const setPointFacets = (point, i) => {
310
357
  const inactive = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.INACTIVE_TEXT)?.value?.[0]?.value;
311
358
  const active = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.ACTIVE_TEXT)?.value?.[0]?.value;
312
359
  point.facets = `falseText:${inactive ? inactive : 'false'};trueText:${active ? active : 'true'}`;
313
- } else if (multiStateObjectTypes.includes(point.bacType)) {
314
- const states = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT)?.value;
315
- if (Array.isArray(states)) {
316
- point.facets = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
317
- }
318
360
  }
361
+ // STATE_TEXT handling removed from here - now handled separately
319
362
  };
320
363
 
321
364
  const setPointValues = (point, i) => {
@@ -334,7 +377,7 @@ const setPointValues = (point, i) => {
334
377
 
335
378
  const processValue = (value, facets) => {
336
379
  if (value?.errorClass != null && value?.errorCode != null) return null;
337
- const match = facets.match(/precision:(\d+)/);
380
+ const match = facets?.match(/precision:(\d+)/);
338
381
  const precision = match ? +match[1] : null;
339
382
 
340
383
  if (typeof value === 'number')
@@ -342,4 +385,4 @@ const processValue = (value, facets) => {
342
385
  else
343
386
  return value
344
387
 
345
- };
388
+ };
@@ -23,7 +23,7 @@ module.exports = {
23
23
 
24
24
  constructor(
25
25
  client, eventEmitter, devices, points, readMethod, maxConcurrentDeviceRead = 2,
26
- maxConcurrentSinglePointRead = 5, name = 'read point'
26
+ maxConcurrentSinglePointRead = 5, concurrentTaskDelay = 50, name = 'read point'
27
27
  ) {
28
28
  super();
29
29
  this.client = client
@@ -33,6 +33,7 @@ module.exports = {
33
33
  this.readMethod = readMethod
34
34
  this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
35
35
  this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
36
+ this.concurrentTaskDelay = concurrentTaskDelay
36
37
  this.name = name
37
38
  }
38
39
 
@@ -219,7 +220,8 @@ module.exports = {
219
220
  id: k,
220
221
  task: async () => {
221
222
  return await smartReadProperty(
222
- this.client, v.device, v.points, this.readMethod, this.maxConcurrentSinglePointRead
223
+ this.client, v.device, v.points, this.readMethod,
224
+ this.maxConcurrentSinglePointRead, 5, this.concurrentTaskDelay
223
225
  );
224
226
  }
225
227
  }));
@@ -25,6 +25,7 @@ module.exports = {
25
25
  constructor(
26
26
  client, eventEmitter, devices, points, writePoints,
27
27
  maxConcurrentDeviceWrite = 2, maxConcurrentPointWrite = 1,
28
+ concurrentTaskDelay = 50,
28
29
  name = 'write point'
29
30
  ) {
30
31
  super();
@@ -35,6 +36,7 @@ module.exports = {
35
36
  this.writePoints = writePoints
36
37
  this.maxConcurrentDeviceWrite = maxConcurrentDeviceWrite
37
38
  this.maxConcurrentPointWrite = maxConcurrentPointWrite
39
+ this.concurrentTaskDelay = concurrentTaskDelay
38
40
  this.name = name
39
41
  }
40
42
 
@@ -277,7 +279,7 @@ module.exports = {
277
279
  task: async () => {
278
280
  return await smartWriteProperty(
279
281
  this.client, v.device, v.writePoints, smartWriteEvent,
280
- this.maxConcurrentPointWrite
282
+ this.maxConcurrentPointWrite, this.concurrentTaskDelay
281
283
  );
282
284
  }
283
285
  }));
@@ -12,8 +12,10 @@ const encode = (buffer, func, msgLength) => {
12
12
  exports.encode = encode;
13
13
  const decode = (buffer, offset) => {
14
14
  let len;
15
+ let linkAddress;
15
16
  const func = buffer[1];
16
17
  const msgLength = (buffer[2] << 8) | (buffer[3] << 0);
18
+
17
19
  if (buffer[0] !== baEnum.BVLL_TYPE_BACNET_IP || buffer.length !== msgLength)
18
20
  return;
19
21
  switch (func) {
@@ -25,6 +27,27 @@ const decode = (buffer, offset) => {
25
27
  break;
26
28
  case baEnum.BvlcResultPurpose.FORWARDED_NPDU:
27
29
  len = 10;
30
+ // HAL modified. Extract BVLC IP and port from buffer
31
+ if (buffer.length >= 10) {
32
+ const ipParts = [buffer[4], buffer[5], buffer[6], buffer[7]];
33
+ const port = (buffer[8] << 8) | buffer[9];
34
+
35
+ // Validate IP octets (0–255 only)
36
+ const isValidIp = ipParts.every(octet => Number.isInteger(octet) && octet >= 0 && octet <= 255);
37
+
38
+ // Validate port (1–65535; BACnet default is 47808)
39
+ const isValidPort = Number.isInteger(port) && port > 0 && port <= 65535;
40
+
41
+ // Extra IP rules
42
+ const isBroadcast = ipParts.every(octet => octet === 255); // 255.255.255.255
43
+ const isMulticast = ipParts[0] >= 224 && ipParts[0] <= 239; // 224.0.0.0 – 239.255.255.255
44
+ const isZero = ipParts.every(octet => octet === 0); // 0.0.0.0
45
+
46
+ if (isValidIp && isValidPort && !isBroadcast && !isMulticast && !isZero) {
47
+ const ip = ipParts.join(".");
48
+ linkAddress = (port === 47808) ? ip : `${ip}:${port}`;
49
+ }
50
+ }
28
51
  break;
29
52
  case baEnum.BvlcResultPurpose.REGISTER_FOREIGN_DEVICE:
30
53
  case baEnum.BvlcResultPurpose.READ_FOREIGN_DEVICE_TABLE:
@@ -41,7 +64,8 @@ const decode = (buffer, offset) => {
41
64
  return {
42
65
  len: len,
43
66
  func: func,
44
- msgLength: msgLength
67
+ msgLength: msgLength,
68
+ linkAddress: linkAddress
45
69
  };
46
70
  };
47
71
  exports.decode = decode;
@@ -458,6 +458,8 @@ class Client extends events_1.EventEmitter {
458
458
  this._handlePdu(remoteAddress, apduType, buffer, offset, msgLength, addressObject, destAddress);
459
459
  }
460
460
  _receiveData(buffer, remoteAddress) {
461
+ let srcAddress = remoteAddress
462
+
461
463
  // Check data length
462
464
  if (buffer.length < baEnum.BVLC_HEADER_LENGTH)
463
465
  return debug('Received invalid data -> Drop package');
@@ -465,9 +467,14 @@ class Client extends events_1.EventEmitter {
465
467
  const result = baBvlc.decode(buffer, 0);
466
468
  if (!result)
467
469
  return debug('Received invalid BVLC header -> Drop package');
468
- // Check BVLC function
470
+ // Check BVLC virtual link controller address if defined
469
471
  if (result.func === baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU || result.func === baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU || result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU) {
470
- this._handleNpdu(buffer, result.len, buffer.length - result.len, remoteAddress);
472
+ // HAL modified. Use BACnet link address if exists
473
+ if (result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU && result.linkAddress) {
474
+ srcAddress = result.linkAddress
475
+ }
476
+
477
+ this._handleNpdu(buffer, result.len, buffer.length - result.len, srcAddress);
471
478
  }
472
479
  else {
473
480
  debug('Received unknown BVLC function -> Drop package');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "1.0.23",
3
+ "version": "1.1.1",
4
4
  "description": "NodeRED BACnet IP client",
5
5
  "email": "open_source@halsystems.com.au",
6
6
  "repository": {
@@ -9,7 +9,6 @@
9
9
  lowLimit: { value: 0, required: true, validate: RED.validators.number() },
10
10
  highLimit: { value: 4194304, required: true, validate: RED.validators.number() },
11
11
  whoIsTimeout: { value: 3000, required: true, validate: RED.validators.number() },
12
- readDeviceTimeout: { value: 1000, required: true, validate: RED.validators.number() },
13
12
  },
14
13
  inputs: 1,
15
14
  outputs: 1,
@@ -46,10 +45,6 @@
46
45
  <label for="node-input-whoIsTimeout"><i class="fa fa-tag"></i> Who Is Timeout (ms)</label>
47
46
  <input type="number" id="node-input-whoIsTimeout" placeholder="3000", min="100", max="3600000">
48
47
  </div>
49
- <div class="form-row">
50
- <label for="node-input-readDeviceTimeout"><i class="fa fa-tag"></i> Read Device Timeout (ms)</label>
51
- <input type="number" id="node-input-readDeviceTimeout" placeholder="1000", min="100", max="3600000">
52
- </div>
53
48
  </script>
54
49
 
55
50
  <script type="text/html" data-help-name="discover device">
@@ -28,7 +28,6 @@ module.exports = function (RED) {
28
28
  this.lowLimit = +config.lowLimit
29
29
  this.highLimit = +config.highLimit
30
30
  this.whoIsTimeout = +config.whoIsTimeout
31
- this.readDeviceTimeout = +config.readDeviceTimeout
32
31
 
33
32
  // events
34
33
  this.#subscribeListeners();
@@ -45,7 +44,6 @@ module.exports = function (RED) {
45
44
  this.lowLimit,
46
45
  this.highLimit,
47
46
  this.whoIsTimeout,
48
- this.readDeviceTimeout
49
47
  );
50
48
  this.task.onStart();
51
49
  }
@@ -41,8 +41,7 @@
41
41
  <label for="node-input-readMethod"><i class="fa fa-list"></i> Read Method</label>
42
42
  <select id="node-input-readMethod">
43
43
  <option value="0">Read Single Only</option>
44
- <option value="1">Read Multiple Fallback Single</option>
45
- <option value="2">Comprehensive Read</option>
44
+ <option value="1">Comprehensive Read</option>
46
45
  </select>
47
46
  </div>
48
47
  <div class="form-row">
@@ -73,8 +72,7 @@
73
72
 
74
73
  <dt>readMethod<span class="property-type">number</span></dt>
75
74
  <dd> <b>Read Single Only</b>: use <code>readProperty</code></dd>
76
- <dd> <b>Read Multiple Fallback Single</b>: try <code>readPropertyMultiple</code> with consevative query size, fallback to <code>readProperty</code> if failed</dd>
77
- <dd> <b>Comprehensive Read</b>: try <code>readPropertyMultiple</code> twice with high and conservative query size, fallback to <code>readProperty</code> if failed</dd>
75
+ <dd> <b>Comprehensive Read</b>: try <code>readPropertyMultiple</code>, reduce query size if failed, and fallback to <code>readProperty</code> if query size reduced to 1</dd>
78
76
 
79
77
  <dt>groupExportDeviceCount<span class="property-type">number</span></dt>
80
78
  <dd> Emit <code>msg.payload</code> once discovered devices count reaches this limit</dd>