@halsystems/red-bacnet 2.2.0-alpha.0 → 2.2.0-alpha.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,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## [2.2.0]
3
+ ## [2.2.0-alpha.1]
4
+ ### Changed
5
+ - `ConcorrentTasksWithNetworkAwareness` added `invoke-id` generator and tracking
6
+ - `writePoint` output `msg.payload` about failed write points after completion, and `msg.result` about written points statistics
7
+ - `discoverPoint` send output on every device discovery completed ([] if failed), removed `groupExportDeviceCount` feature and add `msg.device` to output
8
+ - refactored all nodes to retain original `msg` properties
9
+
10
+ ## [2.2.0-alpha.0]
4
11
  ### Changed
5
12
  - `ConcurrentTask` changed to `ConcorrentTasksWithNetworkAwareness` round robin distributing task according to IP address to reduce congestion on a particular network
6
13
 
package/common/bacnet.js CHANGED
@@ -10,7 +10,20 @@ const { EVENT_ERROR, EVENT_OUTPUT } = require('@root/common/core/constant.js')
10
10
  const { concurrentTasks } = require('@root/common/core/concurrent.js')
11
11
  const { getErrMsg } = require('@root/common/func.js')
12
12
 
13
+ // ---------------------------------- helper functions ----------------------------------
14
+ /**
15
+ * Apply invoke ID to options if getNextInvokeId function is provided
16
+ * @param {object} options - The options object to modify
17
+ * @param {Function} [getNextInvokeId] - Optional function to get the next invoke ID
18
+ */
19
+ function applyInvokeId(options, getNextInvokeId) {
20
+ if (getNextInvokeId && typeof getNextInvokeId === 'function') {
21
+ options.invokeId = getNextInvokeId();
22
+ }
23
+ }
24
+
13
25
  // ---------------------------------- type def ----------------------------------
26
+
14
27
  /**
15
28
  * @typedef {import('@root/ext/node-bacstack/dist/index.js').Client} BacnetClient
16
29
  */
@@ -32,17 +45,19 @@ module.exports = {
32
45
  * vendorId: 36,
33
46
  * deviceName: 'BMS'
34
47
  * }
48
+ * @param {Function} [getNextInvokeId]
49
+ * optional function to get the next invoke ID
35
50
  * @returns array of objects
36
51
  * eg: [{type: 12, value: {type: 8, instance: 123}, ...],
37
52
  * @async
38
53
  */
39
- readObjectList: async function (client, device) {
54
+ readObjectList: async function (client, device, getNextInvokeId) {
40
55
  const objectId = { type: baEnum.ObjectType.DEVICE, instance: device.deviceId };
41
56
  const propertyId = baEnum.PropertyIdentifier.OBJECT_LIST;
42
57
 
43
- let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId);
58
+ let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId, getNextInvokeId);
44
59
  if (result.length == 0) {
45
- result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
60
+ result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId, getNextInvokeId);
46
61
  }
47
62
 
48
63
  return result;
@@ -55,10 +70,12 @@ module.exports = {
55
70
  * @param {object} device
56
71
  * @param {object} objectId
57
72
  * @param {number} propertyId
73
+ * @param {Function} [getNextInvokeId]
74
+ * optional function to get the next invoke ID
58
75
  * @returns array of objects
59
76
  * @async
60
77
  */
61
- readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId) {
78
+ readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId, getNextInvokeId) {
62
79
  let addressSet = device.ipAddress;
63
80
  if (device.macAddress != null && device.network != null) {
64
81
  addressSet = {
@@ -82,10 +99,14 @@ module.exports = {
82
99
  }];
83
100
 
84
101
  return new Promise((resolve, reject) => {
102
+ const options = { maxApdu: device.maxApdu };
103
+ applyInvokeId(options, getNextInvokeId);
104
+
85
105
  client.readPropertyMultiple(
106
+
86
107
  addressSet,
87
108
  reqArr,
88
- { maxApdu: device.maxApdu },
109
+ options,
89
110
  (err, value) => {
90
111
  if (err) return reject(err);
91
112
  resolve(value);
@@ -156,13 +177,15 @@ module.exports = {
156
177
  * retry times for single read failed
157
178
  * @param {number} concurrentTaskDelay
158
179
  * delay between concurrent tasks
180
+ * @param {Function} [getNextInvokeId]
181
+ * optional function to get the next invoke ID
159
182
  * @returns array of objects
160
183
  * eg: [{type: 12, value: {type: 8, instance: 123}, ...],
161
184
  * @async
162
185
  */
163
186
  smartReadProperty: async function (
164
187
  client, device, reqArr, readMethod = 1, maxConcurrentSinglePointRead = 5,
165
- singleReadFailedRetry = 5, concurrentTaskDelay = 50
188
+ singleReadFailedRetry = 5, concurrentTaskDelay = 50, getNextInvokeId
166
189
  ) {
167
190
  /* reqArr example
168
191
  [{
@@ -226,7 +249,7 @@ module.exports = {
226
249
  } while (!batchFull)
227
250
 
228
251
  // read batch block
229
- const value = await module.exports.readPropertyMultple(client, device, reqArrBatch)
252
+ const value = await module.exports.readPropertyMultple(client, device, reqArrBatch, getNextInvokeId)
230
253
  .catch(() => {
231
254
  // Reduce batch size by half (minimum 1)
232
255
  currBatchSize = Math.max(1, Math.floor(currBatchSize / 2))
@@ -274,7 +297,7 @@ module.exports = {
274
297
  id: `${x}-${y}`,
275
298
  task: async () => {
276
299
  try {
277
- let value = await module.exports.readProperty(client, device, req.objectId, prop.id);
300
+ let value = await module.exports.readProperty(client, device, req.objectId, prop.id, getNextInvokeId);
278
301
 
279
302
  if (value == null) {
280
303
  value = {
@@ -351,11 +374,13 @@ module.exports = {
351
374
  * maximum concurrent point to write
352
375
  * @param {number} concurrentTaskDelay
353
376
  * delay between concurrent tasks
377
+ * @param {Function} [getNextInvokeId]
378
+ * optional function to get the next invoke ID
354
379
  * @async
355
380
  */
356
381
  smartWriteProperty: async function (
357
382
  client, device, writePoints, eventEmitter, maxConcurrentWrite,
358
- concurrentTaskDelay = 50
383
+ concurrentTaskDelay = 50, getNextInvokeId
359
384
  ) {
360
385
  const entries = Object.entries(writePoints);
361
386
 
@@ -373,7 +398,8 @@ module.exports = {
373
398
  { type: point.bacType, instance: point.bacInstance },
374
399
  point.bacProp,
375
400
  [{ type: point.valueType, value: point.value }],
376
- point.priority
401
+ point.priority,
402
+ getNextInvokeId
377
403
  );
378
404
  } catch (err) {
379
405
  eventEmitter.emit(EVENT_ERROR, { id: id, error: getErrMsg(err) });
@@ -416,12 +442,14 @@ module.exports = {
416
442
  * eg:{ type: 2, instance: 1 }
417
443
  * @param {number} propertyId
418
444
  * eg: 85
445
+ * @param {Function} [getNextInvokeId]
446
+ * optional function to get the next invoke ID
419
447
  * @returns object
420
448
  * eg: { len: 11, objectId: { type: 19, instance: 1 },
421
449
  * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
422
450
  * @async
423
451
  */
424
- readProperty: async function (client, device, objectId, propertyId) {
452
+ readProperty: async function (client, device, objectId, propertyId, getNextInvokeId) {
425
453
  let addressSet = device.ipAddress
426
454
  if (device.macAddress != null && device.network != null) {
427
455
  addressSet = {
@@ -432,17 +460,21 @@ module.exports = {
432
460
  }
433
461
 
434
462
  return new Promise((resolve, reject) => {
463
+ const options = { maxApdu: device.maxApdu };
464
+ applyInvokeId(options, getNextInvokeId);
465
+
435
466
  client.readProperty(
436
467
  addressSet,
437
468
  objectId,
438
469
  propertyId,
439
- { maxApdu: device.maxApdu },
470
+ options,
440
471
  (err, value) => {
441
472
  if (err)
442
473
  reject(err);
443
474
  else
444
475
  resolve(value);
445
- });
476
+ }
477
+ );
446
478
  });
447
479
  },
448
480
 
@@ -456,11 +488,13 @@ module.exports = {
456
488
  * eg:{ type: 2, instance: 1 }
457
489
  * @param {number} propertyId
458
490
  * eg: 85
491
+ * @param {Function} [getNextInvokeId]
492
+ * optional function to get the next invoke ID
459
493
  * @returns array of objects
460
494
  * eg: [{type: 12, value: {type: 8, instance: 123}, ...],
461
495
  * @async
462
496
  */
463
- readPropertyReturnArr: async function (client, device, objectId, propertyId) {
497
+ readPropertyReturnArr: async function (client, device, objectId, propertyId, getNextInvokeId) {
464
498
  const result = [];
465
499
 
466
500
  let addressSet = device.ipAddress
@@ -474,10 +508,14 @@ module.exports = {
474
508
 
475
509
  async function readPropertyPart(index) {
476
510
  return new Promise((resolve, reject) => {
477
- client.readProperty(addressSet,
511
+ const options = { maxApdu: device.maxApdu, arrayIndex: index };
512
+ applyInvokeId(options, getNextInvokeId);
513
+
514
+ client.readProperty(
515
+ addressSet,
478
516
  objectId,
479
517
  propertyId,
480
- { maxApdu: device.maxApdu, arrayIndex: index },
518
+ options,
481
519
  (err, value) => {
482
520
  if (err) {
483
521
  if (index === 1)
@@ -493,7 +531,8 @@ module.exports = {
493
531
  readPropertyPart(index + 1).then(resolve).catch(reject);
494
532
  }
495
533
  }
496
- });
534
+ }
535
+ );
497
536
  });
498
537
  }
499
538
 
@@ -512,6 +551,8 @@ module.exports = {
512
551
  * properties: [ { id: 85 }, ... ]
513
552
  * },
514
553
  * ...]
554
+ * @param {Function} [getNextInvokeId]
555
+ * optional function to get the next invoke ID
515
556
  * @returns object
516
557
  * eg: {"len":1937,"values":[{
517
558
  * "objectId":{"type":3,"instance":0},
@@ -519,7 +560,7 @@ module.exports = {
519
560
  * ...}]}
520
561
  * @async
521
562
  */
522
- readPropertyMultple: async function (client, device, reqArr) {
563
+ readPropertyMultple: async function (client, device, reqArr, getNextInvokeId) {
523
564
  let addressSet = device.ipAddress
524
565
  if (device.macAddress != null && device.network != null) {
525
566
  addressSet = {
@@ -530,16 +571,20 @@ module.exports = {
530
571
  }
531
572
 
532
573
  return new Promise((resolve, reject) => {
574
+ const options = { maxApdu: device.maxApdu };
575
+ applyInvokeId(options, getNextInvokeId);
576
+
533
577
  client.readPropertyMultiple(
534
578
  addressSet,
535
579
  reqArr,
536
- { maxApdu: device.maxApdu },
580
+ options,
537
581
  (err, value) => {
538
582
  if (err)
539
583
  reject(err);
540
584
  else
541
585
  resolve(value);
542
- });
586
+ }
587
+ );
543
588
  });
544
589
  },
545
590
 
@@ -557,10 +602,12 @@ module.exports = {
557
602
  * eg:[{type: 2, value: 3}]
558
603
  * @param {number} priority
559
604
  * eg: 8
605
+ * @param {Function} [getNextInvokeId]
606
+ * optional function to get the next invoke ID
560
607
  * @returns true / undefined: true if success, else undefined
561
608
  * @async
562
609
  */
563
- writeProperty: async function (client, device, objectId, propertyId, writeValue, priority) {
610
+ writeProperty: async function (client, device, objectId, propertyId, writeValue, priority, getNextInvokeId) {
564
611
  let addressSet = device.ipAddress
565
612
  if (device.macAddress != null && device.network != null) {
566
613
  addressSet = {
@@ -576,6 +623,7 @@ module.exports = {
576
623
  if (priority !== null && priority !== undefined) {
577
624
  options.priority = priority;
578
625
  }
626
+ applyInvokeId(options, getNextInvokeId);
579
627
 
580
628
  client.writeProperty(
581
629
  addressSet,
@@ -59,10 +59,11 @@ module.exports = {
59
59
  * Executes a list of tasks concurrently with IP address awareness.
60
60
  * Tasks targeting the same IP address will execute sequentially.
61
61
  * Tasks targeting different IP addresses can execute in parallel.
62
+ * Invoke IDs are automatically managed per-task to avoid collisions.
62
63
  *
63
64
  * @param {EventEmitter} eventEmitter - The event emitter to emit events to.
64
65
  * @param {Array} tasks - The list of tasks to execute. Each task should have:
65
- * { id, device: { ipAddress, ... }, task: async () => {...} }
66
+ * { id, device: { ipAddress, ... }, task: async (getNextInvokeId) => {...} }
66
67
  * @param {number} maxConcurrent - The maximum number of concurrent tasks.
67
68
  * @param {number} concurrentTaskDelay - The delay between concurrent tasks.
68
69
  * @returns {Promise<Array>} - A promise that resolves with an array of results.
@@ -75,6 +76,10 @@ module.exports = {
75
76
  const results = [];
76
77
  const ipAddressInUse = new Set(); // Track which IPs are currently executing
77
78
 
79
+ // Create a sequential invoke ID generator with tracking
80
+ let invokeIdCounter = 1;
81
+ const invokeIdsInUse = new Set(); // Track which invoke IDs are currently in use
82
+
78
83
  // Pending tasks that have not been started yet.
79
84
  // We keep them in a list and remove them once scheduled so that a
80
85
  // task blocked by a busy IP never blocks later tasks with free IPs.
@@ -88,6 +93,7 @@ module.exports = {
88
93
  /**
89
94
  * Start a single task and wire up its lifecycle. When the task settles
90
95
  * it releases its IP so waiting tasks targeting the same IP can run.
96
+ * Creates a per-task invoke ID generator that tracks and releases IDs.
91
97
  *
92
98
  * @param {{id: any, task: Function, ipAddress: string|null}} item
93
99
  */
@@ -95,7 +101,38 @@ module.exports = {
95
101
  const { id, task, ipAddress } = item;
96
102
  if (ipAddress) ipAddressInUse.add(ipAddress);
97
103
 
98
- const promise = task()
104
+ // Track invoke IDs issued to this specific task
105
+ const taskInvokeIds = new Set();
106
+
107
+ // Create a task-scoped getNextInvokeId function that:
108
+ // 1. Finds the next available invoke ID (1-255)
109
+ // 2. Skips IDs already in use
110
+ // 3. Tracks the ID for later release
111
+ const taskGetNextInvokeId = () => {
112
+ let attempts = 0;
113
+ while (attempts < 255) {
114
+ const candidateId = invokeIdCounter++;
115
+ if (invokeIdCounter > 255) invokeIdCounter = 1;
116
+
117
+ // If this ID is not in use, claim it
118
+ if (!invokeIdsInUse.has(candidateId)) {
119
+ invokeIdsInUse.add(candidateId);
120
+ taskInvokeIds.add(candidateId);
121
+ return candidateId;
122
+ }
123
+ attempts++;
124
+ }
125
+
126
+ // Fallback: if all 255 IDs are exhausted (shouldn't happen with proper concurrency limits),
127
+ // just return the next ID anyway
128
+ const fallbackId = invokeIdCounter++;
129
+ if (invokeIdCounter > 255) invokeIdCounter = 1;
130
+ invokeIdsInUse.add(fallbackId);
131
+ taskInvokeIds.add(fallbackId);
132
+ return fallbackId;
133
+ };
134
+
135
+ const promise = task(taskGetNextInvokeId)
99
136
  .then(result => {
100
137
  eventEmitter.emit(EVENT_OUTPUT, { id, result });
101
138
  results.push({ id, result });
@@ -108,6 +145,10 @@ module.exports = {
108
145
  executing.delete(promise);
109
146
  // Release the IP address so waiting tasks can run
110
147
  if (ipAddress) ipAddressInUse.delete(ipAddress);
148
+ // Release all invoke IDs that were issued to this task
149
+ for (const invokeId of taskInvokeIds) {
150
+ invokeIdsInUse.delete(invokeId);
151
+ }
111
152
  });
112
153
 
113
154
  executing.add(promise);
@@ -27,18 +27,20 @@ module.exports = {
27
27
  * Construct a DiscoverDeviceJob.
28
28
  * @param {BacnetClient} client - Bacnet client to use for the discovery.
29
29
  * @param {EventEmitter} eventEmitter - Status and error event emitter.
30
+ * @param {Object} msg - Original incoming message.
30
31
  * @param {number} network - Network to discover devices on, eg: 65535 / 1 / 1001
31
32
  * @param {number} lowLimit - Low limit of the network range.
32
33
  * @param {number} highLimit - High limit of the network range.
33
34
  * @param {number} whoIsTimeout - Who is request timeout in milliseconds.
34
35
  */
35
36
  constructor(
36
- client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout,
37
+ client, eventEmitter, msg, network, lowLimit, highLimit, whoIsTimeout,
37
38
  name = 'discover device'
38
39
  ) {
39
40
  super();
40
41
  this.client = client
41
42
  this.eventEmitter = eventEmitter
43
+ this.msg = msg
42
44
  this.network = network
43
45
  this.lowLimit = lowLimit
44
46
  this.highLimit = highLimit
@@ -86,7 +88,11 @@ module.exports = {
86
88
  validatedDevices.push(result);
87
89
  }
88
90
 
89
- this.eventEmitter.emit(EVENT_OUTPUT, validatedDevices);
91
+ this.msg.payload = validatedDevices;
92
+ delete this.msg.network;
93
+ delete this.msg.lowLimit;
94
+ delete this.msg.highLimit;
95
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
90
96
  this.#updateProgress(100)
91
97
  resolve();
92
98
  }).catch(error => {
@@ -146,8 +152,7 @@ module.exports = {
146
152
  const deviceInfo = await readDeviceName(d, addressSet);
147
153
  this.discoveredDevices.push(deviceInfo);
148
154
 
149
- // throttle to avoid bursts
150
- await delay(75); // tweak between 50–150ms
155
+ await delay(50);
151
156
  this.#updateProgress(
152
157
  Math.min(90, Math.round((80 / this.discoverList.length) * (i + 1) + 10))
153
158
  );
@@ -188,4 +193,4 @@ module.exports = {
188
193
  this.eventEmitter.emit(EVENT_UPDATE_STATUS, progress);
189
194
  }
190
195
  },
191
- }
196
+ }
@@ -53,20 +53,19 @@ const supportedObjectTypes = [
53
53
  module.exports = {
54
54
  DiscoverPointJob: class DiscoverPointJob extends BaseJob {
55
55
  devices = [];
56
- pointsList = [];
57
56
 
58
57
  constructor(
59
- client, eventEmitter, inputDevices, discoverMode, readMethod, groupExportDeviceCount,
58
+ client, eventEmitter, msg, discoverMode, readMethod,
60
59
  maxConcurrentDeviceRead, maxConcurrentSinglePointRead, concurrentTaskDelay = 50,
61
60
  name = 'discover point'
62
61
  ) {
63
62
  super();
64
63
  this.client = client
65
64
  this.eventEmitter = eventEmitter
66
- this.inputDevices = inputDevices
65
+ this.msg = msg
66
+ this.inputDevices = msg?.devices
67
67
  this.discoverMode = discoverMode
68
68
  this.readMethod = readMethod
69
- this.groupExportDeviceCount = groupExportDeviceCount
70
69
  this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
71
70
  this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
72
71
  this.concurrentTaskDelay = concurrentTaskDelay
@@ -74,7 +73,7 @@ module.exports = {
74
73
  }
75
74
 
76
75
  async execute() {
77
- this.#updateProgress(0)
76
+ this.#updateProgress('discovering')
78
77
 
79
78
  return new Promise((resolve) => {
80
79
  try {
@@ -88,7 +87,7 @@ module.exports = {
88
87
  example: EG_BACNET_DEVICES
89
88
  })
90
89
  )
91
- this.#updateProgress(100)
90
+ this.#updateProgress('completed')
92
91
  resolve();
93
92
  return
94
93
  }
@@ -124,14 +123,16 @@ module.exports = {
124
123
  this.devices.push(result);
125
124
  }
126
125
 
127
- this.#updateProgress(10)
126
+ // If no valid devices, complete immediately
127
+ if (this.devices.length === 0) {
128
+ this.#updateProgress('completed')
129
+ resolve();
130
+ return
131
+ }
128
132
 
129
133
  // read all device points
130
134
  this.#readAllDevicePoints().then(() => {
131
- if (this.pointsList.length > 0)
132
- this.#exportPoints();
133
-
134
- this.#updateProgress(100)
135
+ this.#updateProgress('completed')
135
136
  resolve();
136
137
  });
137
138
  } catch (err) {
@@ -145,23 +146,17 @@ module.exports = {
145
146
  const discoverPointEvent = new EventEmitter();
146
147
  const size = this.devices.length;
147
148
  let count = 0;
148
- let discoveredDevices = 0
149
149
 
150
150
  discoverPointEvent.on(EVENT_OUTPUT, (data) => {
151
- this.#updateProgress(
152
- Math.round((85 / size) * (count + 1) + 10)
153
- )
154
-
155
- if (Array.isArray(data.result))
156
- this.pointsList.push(...data.result);
157
-
158
151
  count++;
159
- discoveredDevices++;
160
-
161
- if (discoveredDevices >= this.groupExportDeviceCount) {
162
- this.#exportPoints();
163
- discoveredDevices = 0
152
+ // Export points immediately after each device is discovered
153
+ if (Array.isArray(data.result?.points)) {
154
+ this.#exportPoints(data.result?.points, data.result?.device);
155
+ } else {
156
+ // Export empty array if no points discovered
157
+ this.#exportPoints([], data.result?.device);
164
158
  }
159
+ this.#updateProgress(count, size);
165
160
  });
166
161
 
167
162
  discoverPointEvent.on(EVENT_ERROR, (data) => {
@@ -171,13 +166,15 @@ module.exports = {
171
166
  const tasks = this.devices.map((d, i) => ({
172
167
  id: d.deviceName,
173
168
  device: d,
174
- task: async () => {
169
+ task: async (getNextInvokeId) => {
175
170
  void i
176
171
  try {
177
- const objectList = await readObjectList(this.client, d);
172
+ const objectList = await readObjectList(this.client, d, getNextInvokeId);
178
173
 
179
174
  // istanbul ignore next
180
- if (objectList == null) return;
175
+ if (objectList == null) {
176
+ return { device: d, points: [] }
177
+ }
181
178
 
182
179
  let objectListFinal = objectList
183
180
  .map(obj => {
@@ -206,10 +203,11 @@ module.exports = {
206
203
 
207
204
  return await readPoints(
208
205
  d, objectListFinal, discoverPointEvent, this.name, this.client, this.readMethod,
209
- this.maxConcurrentSinglePointRead, this.concurrentTaskDelay
206
+ this.maxConcurrentSinglePointRead, this.concurrentTaskDelay, getNextInvokeId
210
207
  );
211
208
  } catch (error) {
212
209
  this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, `Error reading ${d.deviceName} points`, error));
210
+ return { device: d, points: [] }
213
211
  }
214
212
  }
215
213
  }));
@@ -217,22 +215,30 @@ module.exports = {
217
215
  await concurrentTasksWithNetworkAwareness(discoverPointEvent, tasks, this.maxConcurrentDeviceRead);
218
216
  }
219
217
 
220
- #updateProgress(progress) {
221
- this.eventEmitter.emit(EVENT_UPDATE_STATUS, progress);
218
+ #updateProgress(discovered, total) {
219
+ let message;
220
+ if (typeof discovered === 'string') {
221
+ // discovered is a status message like 'initializing' or 'completed'
222
+ message = discovered;
223
+ } else {
224
+ // discovered and total are numbers
225
+ message = `discovered: ${discovered}/${total}`;
226
+ }
227
+ this.eventEmitter.emit(EVENT_UPDATE_STATUS, message);
222
228
  }
223
229
 
224
- #exportPoints() {
230
+ #exportPoints(points, device) {
225
231
  let exportPointsList = [];
226
- for (let i = 0; i < this.pointsList.length; i++) {
232
+ for (let i = 0; i < points.length; i++) {
227
233
  const { error, value: result } = bacnetPointSchema.validate(
228
- this.pointsList[i], { stripUnknown: true }
234
+ points[i], { stripUnknown: true }
229
235
  );
230
236
  // istanbul ignore next
231
237
  {
232
238
  if (error) {
233
239
  this.eventEmitter.emit(EVENT_ERROR, errMsg(
234
240
  this.name, ERR_SCHEMA_VALIDATION, {
235
- bacnetPoint: this.pointsList[i],
241
+ bacnetPoint: points[i],
236
242
  error: error,
237
243
  }));
238
244
  continue
@@ -241,8 +247,10 @@ module.exports = {
241
247
  exportPointsList.push(result);
242
248
  }
243
249
 
244
- this.eventEmitter.emit(EVENT_OUTPUT, exportPointsList);
245
- this.pointsList = [];
250
+ this.msg.payload = exportPointsList;
251
+ this.msg.device = device;
252
+ delete this.msg.devices;
253
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
246
254
  }
247
255
  }
248
256
  }
@@ -250,7 +258,7 @@ module.exports = {
250
258
  // ---------------------------------- functions ----------------------------------
251
259
  const readPoints = async (
252
260
  device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead,
253
- concurrentTaskDelay
261
+ concurrentTaskDelay, getNextInvokeId
254
262
  ) => {
255
263
  const points = [];
256
264
 
@@ -271,7 +279,7 @@ const readPoints = async (
271
279
  try {
272
280
  // First request - get basic properties
273
281
  const result = await smartReadProperty(
274
- client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay
282
+ client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay, getNextInvokeId
275
283
  );
276
284
 
277
285
  // Process basic properties first
@@ -292,7 +300,7 @@ const readPoints = async (
292
300
  try {
293
301
  // Force use single read on state text to reduce loss especially long text
294
302
  const stateTextResult = await smartReadProperty(
295
- client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay
303
+ client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay, getNextInvokeId
296
304
  );
297
305
 
298
306
  // Update points with STATE_TEXT
@@ -315,11 +323,10 @@ const readPoints = async (
315
323
  ));
316
324
  }
317
325
  }
318
-
319
- return points;
320
326
  } catch (error) {
321
327
  eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
322
328
  }
329
+ return { device: device, points: points }
323
330
  };
324
331
 
325
332
  const updatePointWithStateText = (point, stateTextData) => {
@@ -22,14 +22,15 @@ module.exports = {
22
22
  devicePointSeparator = '.';
23
23
 
24
24
  constructor(
25
- client, eventEmitter, devices, points, readMethod, maxConcurrentDeviceRead = 2,
25
+ client, eventEmitter, msg, readMethod, maxConcurrentDeviceRead = 2,
26
26
  maxConcurrentSinglePointRead = 5, concurrentTaskDelay = 50, name = 'read point'
27
27
  ) {
28
28
  super();
29
29
  this.client = client
30
30
  this.eventEmitter = eventEmitter
31
- this.devices = devices
32
- this.points = points
31
+ this.msg = msg
32
+ this.devices = msg?.devices
33
+ this.points = msg?.points
33
34
  this.readMethod = readMethod
34
35
  this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
35
36
  this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
@@ -232,10 +233,10 @@ module.exports = {
232
233
  .map(([k, v]) => ({
233
234
  id: k,
234
235
  device: v.device,
235
- task: async () => {
236
+ task: async (getNextInvokeId) => {
236
237
  return await smartReadProperty(
237
238
  this.client, v.device, v.points, this.readMethod,
238
- this.maxConcurrentSinglePointRead, 10, this.concurrentTaskDelay
239
+ this.maxConcurrentSinglePointRead, 10, this.concurrentTaskDelay, getNextInvokeId
239
240
  );
240
241
  }
241
242
  }));
@@ -311,7 +312,10 @@ module.exports = {
311
312
  exportPoints[k] = result;
312
313
  }
313
314
 
314
- this.eventEmitter.emit(EVENT_OUTPUT, exportPoints);
315
+ this.msg.payload = exportPoints;
316
+ delete this.msg.devices;
317
+ delete this.msg.points;
318
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
315
319
  }
316
320
  }
317
- }
321
+ }
@@ -23,7 +23,7 @@ module.exports = {
23
23
  devicePointSeparator = '.';
24
24
 
25
25
  constructor(
26
- client, eventEmitter, devices, points, writePoints,
26
+ client, eventEmitter, msg,
27
27
  maxConcurrentDeviceWrite = 2, maxConcurrentPointWrite = 1,
28
28
  concurrentTaskDelay = 50,
29
29
  name = 'write point'
@@ -31,9 +31,10 @@ module.exports = {
31
31
  super();
32
32
  this.client = client
33
33
  this.eventEmitter = eventEmitter
34
- this.devices = devices
35
- this.points = points
36
- this.writePoints = writePoints
34
+ this.msg = msg
35
+ this.devices = msg?.devices
36
+ this.points = msg?.points
37
+ this.writePoints = msg?.writePoints
37
38
  this.maxConcurrentDeviceWrite = maxConcurrentDeviceWrite
38
39
  this.maxConcurrentPointWrite = maxConcurrentPointWrite
39
40
  this.concurrentTaskDelay = concurrentTaskDelay
@@ -287,10 +288,10 @@ module.exports = {
287
288
  .map(([k, v]) => ({
288
289
  id: k,
289
290
  device: v.device,
290
- task: async () => {
291
+ task: async (getNextInvokeId) => {
291
292
  return await smartWriteProperty(
292
293
  this.client, v.device, v.writePoints, smartWriteEvent,
293
- this.maxConcurrentPointWrite, this.concurrentTaskDelay
294
+ this.maxConcurrentPointWrite, this.concurrentTaskDelay, getNextInvokeId
294
295
  );
295
296
  }
296
297
  }));
@@ -312,7 +313,41 @@ module.exports = {
312
313
  });
313
314
 
314
315
  await concurrentTasksWithNetworkAwareness(smartWriteEvent, tasks, this.maxConcurrentDeviceWrite);
316
+
317
+ // emit output with msg containing failed points and results
318
+ this.#exportWriteResults();
315
319
  }
316
320
 
321
+ #exportWriteResults() {
322
+ // Separate successful and failed points
323
+ const failedPoints = {};
324
+ let totalPoints = 0;
325
+ let successfulPoints = 0;
326
+
327
+ Object.entries(this.writePointDetails).forEach(([key, value]) => {
328
+ totalPoints++;
329
+ // Check if point has an error property (would be set if write failed)
330
+ if (value.error) {
331
+ failedPoints[key] = value;
332
+ } else {
333
+ successfulPoints++;
334
+ }
335
+ });
336
+
337
+ // Update msg with results
338
+ this.msg.payload = failedPoints;
339
+ this.msg.result = {
340
+ written: successfulPoints,
341
+ failed: Object.keys(failedPoints).length,
342
+ total: totalPoints
343
+ };
344
+
345
+ // Remove input properties from msg
346
+ delete this.msg.devices;
347
+ delete this.msg.points;
348
+ delete this.msg.writePoints;
349
+
350
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
351
+ }
317
352
  }
318
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "2.2.0-alpha.0",
3
+ "version": "2.2.0-alpha.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -42,6 +42,7 @@ module.exports = function (RED) {
42
42
  this.task = new DiscoverDeviceJob(
43
43
  this.client,
44
44
  this.#eventEmitter,
45
+ null,
45
46
  this.network,
46
47
  this.lowLimit,
47
48
  this.highLimit,
@@ -72,12 +73,14 @@ module.exports = function (RED) {
72
73
  this.task.network = (msg.network === undefined) ? this.network : msg.network;
73
74
  this.task.lowLimit = (msg.lowLimit === undefined) ? this.lowLimit : msg.lowLimit;
74
75
  this.task.highLimit = (msg.highLimit === undefined) ? this.highLimit : msg.highLimit;
76
+ this.task.msg = msg;
75
77
 
76
78
  this.job.addJob({
77
79
  id: jobId,
78
80
  task: this.task,
79
81
  priority: Number.isFinite(msg.priority) ? msg.priority : 5,
80
82
  });
83
+
81
84
  // @ts-ignore
82
85
  this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
83
86
  });
@@ -87,10 +90,7 @@ module.exports = function (RED) {
87
90
  this.task.onStop();
88
91
  })
89
92
 
90
- this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
91
- const msg = {
92
- payload: data
93
- };
93
+ this.#eventEmitter.on(EVENT_OUTPUT, (msg) => {
94
94
  // @ts-ignore
95
95
  this.send(msg);
96
96
  });
@@ -114,4 +114,3 @@ module.exports = function (RED) {
114
114
  // ----- register node -----
115
115
  RED.nodes.registerType('discover device', DiscoverDevice);
116
116
  }
117
-
@@ -7,7 +7,6 @@
7
7
  client: { type: "bacnet client", required: true },
8
8
  discoverMode: { value: 0, required: true, validate: RED.validators.number() },
9
9
  readMethod: { value: 2, required: true, validate: RED.validators.number() },
10
- groupExportDeviceCount: { value: 30000, required: true, validate: RED.validators.number() },
11
10
  maxConcurrentDeviceRead: { value: 1, required: true, validate: RED.validators.number() },
12
11
  maxConcurrentSinglePointRead: { value: 10, required: true, validate: RED.validators.number() },
13
12
  concurrentTaskDelay: { value: 0, required: true, validate: RED.validators.number() },
@@ -46,10 +45,6 @@
46
45
  <option value="1">Comprehensive Read</option>
47
46
  </select>
48
47
  </div>
49
- <div class="form-row">
50
- <label for="node-input-groupExportDeviceCount"><i class="fa fa-tag"></i> Group Export Device Count</label>
51
- <input type="number" id="node-input-groupExportDeviceCount" placeholder="50", min="10", max="1000">
52
- </div>
53
48
  <div class="form-row">
54
49
  <label for="node-input-maxConcurrentDeviceRead"><i class="fa fa-tag"></i> Max Concurrent Device Read</label>
55
50
  <input type="number" id="node-input-maxConcurrentDeviceRead" placeholder="1" , min="1" , max="1000">
@@ -81,9 +76,6 @@
81
76
  <dd> <b>Read Single Only</b>: use <code>readProperty</code></dd>
82
77
  <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>
83
78
 
84
- <dt>groupExportDeviceCount<span class="property-type">number</span></dt>
85
- <dd> Emit <code>msg.payload</code> once discovered devices count reaches this limit</dd>
86
-
87
79
  <dt>maxConcurrentDeviceRead<span class="property-type">number</span></dt>
88
80
  <dd> Devices count to discover simultaneously.</dd>
89
81
  <dd> <b> USE WITH CAUTION!</b> Reduce to low value on a slow network</dd>
@@ -24,7 +24,6 @@ module.exports = function (RED) {
24
24
  this.client = RED.nodes.getNode(config.client).instance;
25
25
  this.discoverMode = +config.discoverMode
26
26
  this.readMethod = +config.readMethod
27
- this.groupExportDeviceCount = +config.groupExportDeviceCount
28
27
  this.maxConcurrentDeviceRead = +config.maxConcurrentDeviceRead
29
28
  this.maxConcurrentSinglePointRead = +config.maxConcurrentSinglePointRead
30
29
  this.concurrentTaskDelay = +config.concurrentTaskDelay
@@ -54,8 +53,8 @@ module.exports = function (RED) {
54
53
  }
55
54
 
56
55
  const task = new DiscoverPointJob(
57
- this.client, this.#eventEmitter, msg?.devices, this.discoverMode, this.readMethod,
58
- this.groupExportDeviceCount, this.maxConcurrentDeviceRead,
56
+ this.client, this.#eventEmitter, msg, this.discoverMode, this.readMethod,
57
+ this.maxConcurrentDeviceRead,
59
58
  this.maxConcurrentSinglePointRead, this.concurrentTaskDelay
60
59
  );
61
60
 
@@ -68,21 +67,18 @@ module.exports = function (RED) {
68
67
  this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
69
68
  });
70
69
 
71
- this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
72
- const msg = {
73
- payload: data
74
- };
70
+ this.#eventEmitter.on(EVENT_OUTPUT, (msg) => {
75
71
  // @ts-ignore
76
72
  this.send(msg);
77
73
  });
78
74
 
79
75
  this.#eventEmitter.on(EVENT_UPDATE_STATUS, (msg) => {
80
- if (msg === 100)
76
+ if (msg === 'completed')
81
77
  // @ts-ignore
82
78
  this.status({ fill: 'green', shape: 'dot', text: `completed: ${nowFormatted()}` });
83
79
  else
84
80
  // @ts-ignore
85
- this.status({ fill: 'yellow', shape: 'dot', text: `progress: ${msg} %` });
81
+ this.status({ fill: 'yellow', shape: 'dot', text: `${msg}` });
86
82
  });
87
83
 
88
84
  this.#eventEmitter.on(EVENT_ERROR, (err) => {
@@ -44,38 +44,34 @@ module.exports = function (RED) {
44
44
  */
45
45
  // @ts-ignore
46
46
  this.on(EVENT_INPUT, async function (msg) {
47
+ const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : this.jobId;
48
+
49
+ if (this.job.queue.map(item => item.id).includes(jobId)) {
50
+ // @ts-ignore
51
+ print(`Coalesced job: ${jobId}`, true)
52
+ return;
53
+ }
54
+
47
55
  const task = new ReadPointJob(
48
56
  this.client,
49
57
  this.#eventEmitter,
50
- msg.devices,
51
- msg.points,
58
+ msg,
52
59
  this.readMethod,
53
60
  this.maxConcurrentDeviceRead,
54
61
  this.maxConcurrentSinglePointRead,
55
62
  this.concurrentTaskDelay
56
63
  );
57
64
 
58
- const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : this.jobId;
59
-
60
- if (this.job.queue.map(item => item.id).includes(jobId)) {
61
- // @ts-ignore
62
- print(`Coalesced job: ${jobId}`, true)
63
- return;
64
- }
65
-
66
65
  this.job.addJob({
67
66
  id: jobId,
68
67
  task: task,
69
- priority: Number.isFinite(msg.priority) ? msg.priority : 2,
68
+ priority: Number.isFinite(msg.priority) ? msg.priority : 3,
70
69
  });
71
70
  // @ts-ignore
72
71
  this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
73
72
  });
74
73
 
75
- this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
76
- const msg = {
77
- payload: data
78
- };
74
+ this.#eventEmitter.on(EVENT_OUTPUT, (msg) => {
79
75
  // @ts-ignore
80
76
  this.send(msg);
81
77
  });
@@ -37,7 +37,7 @@
37
37
  <input type="number" id="node-input-maxConcurrentPointWrite" placeholder="5" , min="1" , max="1000">
38
38
  </div>
39
39
  <div class="form-row">
40
- <label for="node-input-concurrentTaskDelay"><i class="fa fa-tag"></i> Concurrent Task Delay</label>
40
+ <label for="node-input-concurrentTaskDelay"><i class="fa fa-tag"></i> Concurrent Task Delay (ms)</label>
41
41
  <input type="number" id="node-input-concurrentTaskDelay" placeholder="0" , min="0" , max="10000">
42
42
  </div>
43
43
  </script>
@@ -43,16 +43,6 @@ module.exports = function (RED) {
43
43
  */
44
44
  // @ts-ignore
45
45
  this.on(EVENT_INPUT, async function (msg) {
46
- const task = new WritePointJob(
47
- this.client,
48
- this.#eventEmitter,
49
- msg.devices,
50
- msg.points,
51
- msg.writePoints,
52
- this.maxConcurrentDeviceWrite,
53
- this.maxConcurrentPointWrite,
54
- this.concurrentTaskDelay
55
- );
56
46
  const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : this.jobId;
57
47
 
58
48
  if (this.job.queue.map(item => item.id).includes(jobId)) {
@@ -61,6 +51,15 @@ module.exports = function (RED) {
61
51
  return;
62
52
  }
63
53
 
54
+ const task = new WritePointJob(
55
+ this.client,
56
+ this.#eventEmitter,
57
+ msg,
58
+ this.maxConcurrentDeviceWrite,
59
+ this.maxConcurrentPointWrite,
60
+ this.concurrentTaskDelay
61
+ );
62
+
64
63
  this.job.addJob({
65
64
  id: jobId,
66
65
  task: task,
@@ -70,10 +69,7 @@ module.exports = function (RED) {
70
69
  this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
71
70
  });
72
71
 
73
- this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
74
- const msg = {
75
- payload: data
76
- };
72
+ this.#eventEmitter.on(EVENT_OUTPUT, (msg) => {
77
73
  // @ts-ignore
78
74
  this.send(msg);
79
75
  });