@halsystems/red-bacnet 2.2.0-alpha.0 → 2.2.0-alpha2

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,18 @@
1
1
  # Changelog
2
2
 
3
- ## [2.2.0]
3
+ ## [2.2.0-alpha.2]
4
+ ### Changed
5
+ - all `nodes` retained original `msg` properties
6
+ - `writePoint` emit `payload` with failed points after completion
7
+
8
+ ## [2.2.0-alpha.1]
9
+ ### Changed
10
+ - `ConcorrentTasksWithNetworkAwareness` added `invoke-id` generator and tracking
11
+ - `writePoint` output `msg.payload` about failed write points after completion, and `msg.result` about written points statistics
12
+ - `discoverPoint` send output on every device discovery completed ([] if failed), removed `groupExportDeviceCount` feature and add `msg.device` to output
13
+ - refactored all nodes to retain original `msg` properties
14
+
15
+ ## [2.2.0-alpha.0]
4
16
  ### Changed
5
17
  - `ConcurrentTask` changed to `ConcorrentTasksWithNetworkAwareness` round robin distributing task according to IP address to reduce congestion on a particular network
6
18
 
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,10 +398,16 @@ 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
- eventEmitter.emit(EVENT_ERROR, { id: id, error: getErrMsg(err) });
405
+ eventEmitter.emit(EVENT_ERROR, {
406
+ point: {
407
+ [point.id]: point.value
408
+ },
409
+ error: getErrMsg(err)
410
+ });
380
411
  }
381
412
  return
382
413
  }
@@ -416,12 +447,14 @@ module.exports = {
416
447
  * eg:{ type: 2, instance: 1 }
417
448
  * @param {number} propertyId
418
449
  * eg: 85
450
+ * @param {Function} [getNextInvokeId]
451
+ * optional function to get the next invoke ID
419
452
  * @returns object
420
453
  * eg: { len: 11, objectId: { type: 19, instance: 1 },
421
454
  * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
422
455
  * @async
423
456
  */
424
- readProperty: async function (client, device, objectId, propertyId) {
457
+ readProperty: async function (client, device, objectId, propertyId, getNextInvokeId) {
425
458
  let addressSet = device.ipAddress
426
459
  if (device.macAddress != null && device.network != null) {
427
460
  addressSet = {
@@ -432,17 +465,21 @@ module.exports = {
432
465
  }
433
466
 
434
467
  return new Promise((resolve, reject) => {
468
+ const options = { maxApdu: device.maxApdu };
469
+ applyInvokeId(options, getNextInvokeId);
470
+
435
471
  client.readProperty(
436
472
  addressSet,
437
473
  objectId,
438
474
  propertyId,
439
- { maxApdu: device.maxApdu },
475
+ options,
440
476
  (err, value) => {
441
477
  if (err)
442
478
  reject(err);
443
479
  else
444
480
  resolve(value);
445
- });
481
+ }
482
+ );
446
483
  });
447
484
  },
448
485
 
@@ -456,11 +493,13 @@ module.exports = {
456
493
  * eg:{ type: 2, instance: 1 }
457
494
  * @param {number} propertyId
458
495
  * eg: 85
496
+ * @param {Function} [getNextInvokeId]
497
+ * optional function to get the next invoke ID
459
498
  * @returns array of objects
460
499
  * eg: [{type: 12, value: {type: 8, instance: 123}, ...],
461
500
  * @async
462
501
  */
463
- readPropertyReturnArr: async function (client, device, objectId, propertyId) {
502
+ readPropertyReturnArr: async function (client, device, objectId, propertyId, getNextInvokeId) {
464
503
  const result = [];
465
504
 
466
505
  let addressSet = device.ipAddress
@@ -474,10 +513,14 @@ module.exports = {
474
513
 
475
514
  async function readPropertyPart(index) {
476
515
  return new Promise((resolve, reject) => {
477
- client.readProperty(addressSet,
516
+ const options = { maxApdu: device.maxApdu, arrayIndex: index };
517
+ applyInvokeId(options, getNextInvokeId);
518
+
519
+ client.readProperty(
520
+ addressSet,
478
521
  objectId,
479
522
  propertyId,
480
- { maxApdu: device.maxApdu, arrayIndex: index },
523
+ options,
481
524
  (err, value) => {
482
525
  if (err) {
483
526
  if (index === 1)
@@ -493,7 +536,8 @@ module.exports = {
493
536
  readPropertyPart(index + 1).then(resolve).catch(reject);
494
537
  }
495
538
  }
496
- });
539
+ }
540
+ );
497
541
  });
498
542
  }
499
543
 
@@ -512,6 +556,8 @@ module.exports = {
512
556
  * properties: [ { id: 85 }, ... ]
513
557
  * },
514
558
  * ...]
559
+ * @param {Function} [getNextInvokeId]
560
+ * optional function to get the next invoke ID
515
561
  * @returns object
516
562
  * eg: {"len":1937,"values":[{
517
563
  * "objectId":{"type":3,"instance":0},
@@ -519,7 +565,7 @@ module.exports = {
519
565
  * ...}]}
520
566
  * @async
521
567
  */
522
- readPropertyMultple: async function (client, device, reqArr) {
568
+ readPropertyMultple: async function (client, device, reqArr, getNextInvokeId) {
523
569
  let addressSet = device.ipAddress
524
570
  if (device.macAddress != null && device.network != null) {
525
571
  addressSet = {
@@ -530,16 +576,20 @@ module.exports = {
530
576
  }
531
577
 
532
578
  return new Promise((resolve, reject) => {
579
+ const options = { maxApdu: device.maxApdu };
580
+ applyInvokeId(options, getNextInvokeId);
581
+
533
582
  client.readPropertyMultiple(
534
583
  addressSet,
535
584
  reqArr,
536
- { maxApdu: device.maxApdu },
585
+ options,
537
586
  (err, value) => {
538
587
  if (err)
539
588
  reject(err);
540
589
  else
541
590
  resolve(value);
542
- });
591
+ }
592
+ );
543
593
  });
544
594
  },
545
595
 
@@ -557,10 +607,12 @@ module.exports = {
557
607
  * eg:[{type: 2, value: 3}]
558
608
  * @param {number} priority
559
609
  * eg: 8
610
+ * @param {Function} [getNextInvokeId]
611
+ * optional function to get the next invoke ID
560
612
  * @returns true / undefined: true if success, else undefined
561
613
  * @async
562
614
  */
563
- writeProperty: async function (client, device, objectId, propertyId, writeValue, priority) {
615
+ writeProperty: async function (client, device, objectId, propertyId, writeValue, priority, getNextInvokeId) {
564
616
  let addressSet = device.ipAddress
565
617
  if (device.macAddress != null && device.network != null) {
566
618
  addressSet = {
@@ -576,6 +628,7 @@ module.exports = {
576
628
  if (priority !== null && priority !== undefined) {
577
629
  options.priority = priority;
578
630
  }
631
+ applyInvokeId(options, getNextInvokeId);
579
632
 
580
633
  client.writeProperty(
581
634
  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,8 @@ module.exports = {
86
88
  validatedDevices.push(result);
87
89
  }
88
90
 
89
- this.eventEmitter.emit(EVENT_OUTPUT, validatedDevices);
91
+ this.msg.payload = validatedDevices;
92
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
90
93
  this.#updateProgress(100)
91
94
  resolve();
92
95
  }).catch(error => {
@@ -146,8 +149,7 @@ module.exports = {
146
149
  const deviceInfo = await readDeviceName(d, addressSet);
147
150
  this.discoveredDevices.push(deviceInfo);
148
151
 
149
- // throttle to avoid bursts
150
- await delay(75); // tweak between 50–150ms
152
+ await delay(50);
151
153
  this.#updateProgress(
152
154
  Math.min(90, Math.round((80 / this.discoverList.length) * (i + 1) + 10))
153
155
  );
@@ -188,4 +190,4 @@ module.exports = {
188
190
  this.eventEmitter.emit(EVENT_UPDATE_STATUS, progress);
189
191
  }
190
192
  },
191
- }
193
+ }
@@ -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,9 @@ 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
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
246
253
  }
247
254
  }
248
255
  }
@@ -250,7 +257,7 @@ module.exports = {
250
257
  // ---------------------------------- functions ----------------------------------
251
258
  const readPoints = async (
252
259
  device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead,
253
- concurrentTaskDelay
260
+ concurrentTaskDelay, getNextInvokeId
254
261
  ) => {
255
262
  const points = [];
256
263
 
@@ -271,7 +278,7 @@ const readPoints = async (
271
278
  try {
272
279
  // First request - get basic properties
273
280
  const result = await smartReadProperty(
274
- client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay
281
+ client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay, getNextInvokeId
275
282
  );
276
283
 
277
284
  // Process basic properties first
@@ -292,7 +299,7 @@ const readPoints = async (
292
299
  try {
293
300
  // Force use single read on state text to reduce loss especially long text
294
301
  const stateTextResult = await smartReadProperty(
295
- client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay
302
+ client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay, getNextInvokeId
296
303
  );
297
304
 
298
305
  // Update points with STATE_TEXT
@@ -315,11 +322,10 @@ const readPoints = async (
315
322
  ));
316
323
  }
317
324
  }
318
-
319
- return points;
320
325
  } catch (error) {
321
326
  eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
322
327
  }
328
+ return { device: device, points: points }
323
329
  };
324
330
 
325
331
  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,8 @@ module.exports = {
311
312
  exportPoints[k] = result;
312
313
  }
313
314
 
314
- this.eventEmitter.emit(EVENT_OUTPUT, exportPoints);
315
+ this.msg.payload = exportPoints;
316
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
315
317
  }
316
318
  }
317
- }
319
+ }
@@ -20,10 +20,11 @@ const {
20
20
  module.exports = {
21
21
  WritePointJob: class WritePointJob extends BaseJob {
22
22
  writePointDetails = {}
23
+ failedPoints = {}
23
24
  devicePointSeparator = '.';
24
25
 
25
26
  constructor(
26
- client, eventEmitter, devices, points, writePoints,
27
+ client, eventEmitter, msg,
27
28
  maxConcurrentDeviceWrite = 2, maxConcurrentPointWrite = 1,
28
29
  concurrentTaskDelay = 50,
29
30
  name = 'write point'
@@ -31,9 +32,10 @@ module.exports = {
31
32
  super();
32
33
  this.client = client
33
34
  this.eventEmitter = eventEmitter
34
- this.devices = devices
35
- this.points = points
36
- this.writePoints = writePoints
35
+ this.msg = msg
36
+ this.devices = msg?.devices
37
+ this.points = msg?.points
38
+ this.writePoints = msg?.writePoints
37
39
  this.maxConcurrentDeviceWrite = maxConcurrentDeviceWrite
38
40
  this.maxConcurrentPointWrite = maxConcurrentPointWrite
39
41
  this.concurrentTaskDelay = concurrentTaskDelay
@@ -175,14 +177,7 @@ module.exports = {
175
177
  }
176
178
 
177
179
  // add point details
178
- pointDetails[id] = {
179
- deviceName: p.deviceName,
180
- bacType: p.bacType,
181
- bacInstance: p.bacInstance,
182
- bacProp: p.bacProp,
183
- valueType: p.valueType,
184
- priority: p.priority
185
- }
180
+ pointDetails[id] = { ...p };
186
181
  });
187
182
 
188
183
  if (pointSchemaErrList.length > 0) {
@@ -287,10 +282,10 @@ module.exports = {
287
282
  .map(([k, v]) => ({
288
283
  id: k,
289
284
  device: v.device,
290
- task: async () => {
285
+ task: async (getNextInvokeId) => {
291
286
  return await smartWriteProperty(
292
287
  this.client, v.device, v.writePoints, smartWriteEvent,
293
- this.maxConcurrentPointWrite, this.concurrentTaskDelay
288
+ this.maxConcurrentPointWrite, this.concurrentTaskDelay, getNextInvokeId
294
289
  );
295
290
  }
296
291
  }));
@@ -306,13 +301,22 @@ module.exports = {
306
301
  });
307
302
 
308
303
  smartWriteEvent.on(EVENT_ERROR, (data) => {
304
+ Object.assign(this.failedPoints, data.point);
309
305
  this.eventEmitter.emit(EVENT_ERROR, errMsg(
310
306
  this.name, ERR_WRITING_POINT, data
311
307
  ));
312
308
  });
313
309
 
314
310
  await concurrentTasksWithNetworkAwareness(smartWriteEvent, tasks, this.maxConcurrentDeviceWrite);
311
+
312
+ // emit output with msg containing failed points and results
313
+ this.#exportWriteResults();
315
314
  }
316
315
 
316
+ #exportWriteResults() {
317
+ // Update msg with results
318
+ this.msg.payload = this.failedPoints;
319
+ this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
320
+ }
317
321
  }
318
322
  }
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-alpha2",
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>
@@ -103,7 +95,7 @@
103
95
  <dt class="optional">msg.id <span class="property-type">string | number</span></dt>
104
96
  <dd> Job ID. Default to <code>discoverPoints</code> if not provided.</dd>
105
97
  <dt class="optional">msg.priority <span class="property-type">number</span></dt>
106
- <dd> Job priority in queue. Default to <code>5</code> if not provided.</dd>
98
+ <dd> Job priority in queue. Default to <code>5</code> if not provided.</dd>
107
99
  </dl>
108
100
 
109
101
  <h3>Outputs</h3>
@@ -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) => {
@@ -91,7 +91,7 @@
91
91
  <dd> Job ID. Default to <code>readPoints</code> if not provided.</dd>
92
92
 
93
93
  <dt class="optional">msg.priority <span class="property-type">number</span></dt>
94
- <dd> Job priority in queue. Default to <code>2</code> if not provided.</dd>
94
+ <dd> Job priority in queue. Default to <code>3</code> if not provided.</dd>
95
95
  </dl>
96
96
 
97
97
  <h3>Outputs</h3>
@@ -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>
@@ -82,6 +82,12 @@
82
82
  <dd> Job priority in queue. Default to <code>2</code> if not provided.</dd>
83
83
  </dl>
84
84
 
85
+ <h3>Outputs</h3>
86
+ <dl class="message-properties">
87
+ <dt>msg.payload <span class="property-type">object</span></dt>
88
+ <dd>Failed to write points.</dd>
89
+ </dl>
90
+
85
91
  <h3>Details</h3>
86
92
  <p>If node is triggered while existing job is running, new job will be coalesced if <code>msg.id</code> are identical, else new job will be put in queue until current job is completed.</p>
87
93
  </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
  });