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

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,9 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0]
4
+ ### Changed
5
+ - `ConcurrentTask` changed to `ConcorrentTasksWithNetworkAwareness` round robin distributing task according to IP address to reduce congestion on a particular network
6
+
7
+ ## [2.1.1]
8
+ ### Changed
9
+ - Reinstated `globalBroadcastAddress` filter in transport
10
+
3
11
  ## [2.1.0]
4
12
  ### Changed
5
13
  - Matched v2.1.0-alpha.0 release
6
- - Removed `broadcastAddress` filter list from transport
14
+ - Removed `globalBroadcastAddress` filter from transport
7
15
 
8
16
  ## [2.1.0-alpha.0]
9
17
  ### Added
@@ -54,4 +54,112 @@ module.exports = {
54
54
  await Promise.allSettled(executing);
55
55
  return results;
56
56
  },
57
- };
57
+
58
+ /**
59
+ * Executes a list of tasks concurrently with IP address awareness.
60
+ * Tasks targeting the same IP address will execute sequentially.
61
+ * Tasks targeting different IP addresses can execute in parallel.
62
+ *
63
+ * @param {EventEmitter} eventEmitter - The event emitter to emit events to.
64
+ * @param {Array} tasks - The list of tasks to execute. Each task should have:
65
+ * { id, device: { ipAddress, ... }, task: async () => {...} }
66
+ * @param {number} maxConcurrent - The maximum number of concurrent tasks.
67
+ * @param {number} concurrentTaskDelay - The delay between concurrent tasks.
68
+ * @returns {Promise<Array>} - A promise that resolves with an array of results.
69
+ * @async
70
+ */
71
+ concurrentTasksWithNetworkAwareness: async function (
72
+ eventEmitter, tasks, maxConcurrent, concurrentTaskDelay = 50
73
+ ) {
74
+ const executing = new Set();
75
+ const results = [];
76
+ const ipAddressInUse = new Set(); // Track which IPs are currently executing
77
+
78
+ // Pending tasks that have not been started yet.
79
+ // We keep them in a list and remove them once scheduled so that a
80
+ // task blocked by a busy IP never blocks later tasks with free IPs.
81
+ const pending = tasks.map(({ id, device, task }) => ({
82
+ id,
83
+ task,
84
+ // Extract IP address from device (handle "ip:port" format)
85
+ ipAddress: device?.ipAddress ? device.ipAddress.split(':')[0] : null,
86
+ }));
87
+
88
+ /**
89
+ * Start a single task and wire up its lifecycle. When the task settles
90
+ * it releases its IP so waiting tasks targeting the same IP can run.
91
+ *
92
+ * @param {{id: any, task: Function, ipAddress: string|null}} item
93
+ */
94
+ const startTask = (item) => {
95
+ const { id, task, ipAddress } = item;
96
+ if (ipAddress) ipAddressInUse.add(ipAddress);
97
+
98
+ const promise = task()
99
+ .then(result => {
100
+ eventEmitter.emit(EVENT_OUTPUT, { id, result });
101
+ results.push({ id, result });
102
+ })
103
+ .catch(error => {
104
+ eventEmitter.emit(EVENT_ERROR, { id, error });
105
+ results.push({ id, error });
106
+ })
107
+ .finally(() => {
108
+ executing.delete(promise);
109
+ // Release the IP address so waiting tasks can run
110
+ if (ipAddress) ipAddressInUse.delete(ipAddress);
111
+ });
112
+
113
+ executing.add(promise);
114
+ };
115
+
116
+ /**
117
+ * Schedule as many pending tasks as possible, respecting both the
118
+ * global concurrency limit and per-IP exclusivity. Scans the whole
119
+ * pending list (not just the head) so a busy IP does not block later
120
+ * tasks targeting free IPs.
121
+ *
122
+ * @async
123
+ */
124
+ const scheduleAvailable = async () => {
125
+ let i = 0;
126
+ while (i < pending.length && executing.size < maxConcurrent) {
127
+ const candidate = pending[i];
128
+
129
+ // Skip (leave in queue) if this IP is currently in use
130
+ if (candidate.ipAddress && ipAddressInUse.has(candidate.ipAddress)) {
131
+ i++;
132
+ continue;
133
+ }
134
+
135
+ // Remove from pending and start it
136
+ pending.splice(i, 1);
137
+ startTask(candidate);
138
+
139
+ if (concurrentTaskDelay > 0)
140
+ await delay(concurrentTaskDelay);
141
+ }
142
+ };
143
+
144
+ // Keep pumping until every task has been scheduled and finished.
145
+ // Each loop: schedule what we can, then wait for at least one task to
146
+ // settle (which frees a concurrency slot and/or an IP) before retrying.
147
+ while (pending.length > 0 || executing.size > 0) {
148
+ await scheduleAvailable();
149
+
150
+ if (executing.size > 0) {
151
+ // Wait for one in-flight task to settle before scheduling more.
152
+ await Promise.race(executing);
153
+ } else if (pending.length > 0) {
154
+ // Nothing running but tasks remain (all were blocked by busy
155
+ // IPs that just got released). Yield briefly and retry.
156
+ /* istanbul ignore next */
157
+ await delay(concurrentTaskDelay > 0 ? concurrentTaskDelay : 1);
158
+ }
159
+ }
160
+
161
+ // Safety net: ensure all promises have fully settled.
162
+ await Promise.allSettled(executing);
163
+ return results;
164
+ },
165
+ };
@@ -13,7 +13,7 @@ const { readObjectList, smartReadProperty } = require('@root/common/bacnet.js')
13
13
  const { bacnetUnitMap } = require('@root/common/bacnet_unit.js')
14
14
  const { EG_BACNET_DEVICES } = require('@root/common/example')
15
15
  const { errMsg, getErrMsg } = require('@root/common/func.js')
16
- const { concurrentTasks } = require('@root/common/core/concurrent.js')
16
+ const { concurrentTasksWithNetworkAwareness } = require('@root/common/core/concurrent.js')
17
17
  const {
18
18
  ERR_GENERIC, ERR_SCHEMA_VALIDATION, ERR_INVALID_DATA_TYPE, ERR_IGNORE_DUPLICATED_DEVICE_NAME,
19
19
  ERR_READING_POINTS,
@@ -170,6 +170,7 @@ module.exports = {
170
170
 
171
171
  const tasks = this.devices.map((d, i) => ({
172
172
  id: d.deviceName,
173
+ device: d,
173
174
  task: async () => {
174
175
  void i
175
176
  try {
@@ -213,7 +214,7 @@ module.exports = {
213
214
  }
214
215
  }));
215
216
 
216
- await concurrentTasks(discoverPointEvent, tasks, this.maxConcurrentDeviceRead);
217
+ await concurrentTasksWithNetworkAwareness(discoverPointEvent, tasks, this.maxConcurrentDeviceRead);
217
218
  }
218
219
 
219
220
  #updateProgress(progress) {
@@ -9,7 +9,7 @@ const { EVENT_OUTPUT, EVENT_UPDATE_STATUS, EVENT_ERROR } = require('@root/common
9
9
  const { smartReadProperty } = require('@root/common/bacnet.js')
10
10
  const { EG_BACNET_DEVICES, EG_BACNET_POINTS } = require('@root/common/example')
11
11
  const { facetsStrToObj, errMsg, getErrMsg } = require('@root/common/func.js')
12
- const { concurrentTasks } = require('@root/common/core/concurrent.js')
12
+ const { concurrentTasksWithNetworkAwareness } = require('@root/common/core/concurrent.js')
13
13
  const {
14
14
  ERR_GENERIC, ERR_SCHEMA_VALIDATION, ERR_INVALID_DATA_TYPE, ERR_DUPLICATED_DEVICE_NAME,
15
15
  ERR_DUPLICATED_POINT_ID, ERR_POINT_NOT_ATTACHED_TO_DEVICE, ERR_READING_POINT
@@ -231,6 +231,7 @@ module.exports = {
231
231
  .filter(([_, v]) => Array.isArray(v.points) && v.points.length > 0) // eslint-disable-line
232
232
  .map(([k, v]) => ({
233
233
  id: k,
234
+ device: v.device,
234
235
  task: async () => {
235
236
  return await smartReadProperty(
236
237
  this.client, v.device, v.points, this.readMethod,
@@ -239,7 +240,7 @@ module.exports = {
239
240
  }
240
241
  }));
241
242
 
242
- await concurrentTasks(smartReadEvent, tasks, this.maxConcurrentDeviceRead);
243
+ await concurrentTasksWithNetworkAwareness(smartReadEvent, tasks, this.maxConcurrentDeviceRead);
243
244
 
244
245
  // format final result
245
246
  this.points.forEach(p => {
@@ -9,7 +9,7 @@ const { EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_OUTPUT } = require('@root/common
9
9
  const { smartWriteProperty } = require('@root/common/bacnet.js')
10
10
  const { EG_BACNET_DEVICES, EG_BACNET_POINTS, EG_BACNET_WRITE_POINTS } = require('@root/common/example')
11
11
  const { errMsg, getErrMsg } = require('@root/common/func.js')
12
- const { concurrentTasks } = require('@root/common/core/concurrent.js')
12
+ const { concurrentTasksWithNetworkAwareness } = require('@root/common/core/concurrent.js')
13
13
  const {
14
14
  ERR_GENERIC, ERR_SCHEMA_VALIDATION, ERR_INVALID_DATA_TYPE, ERR_DUPLICATED_DEVICE_NAME,
15
15
  ERR_DUPLICATED_POINT_ID, ERR_POINT_NOT_ATTACHED_TO_DEVICE, ERR_WRITE_POINT_NOT_FOUND,
@@ -286,6 +286,7 @@ module.exports = {
286
286
  .filter(([_, v]) => (void _, Object.keys(v.writePoints).length > 0))
287
287
  .map(([k, v]) => ({
288
288
  id: k,
289
+ device: v.device,
289
290
  task: async () => {
290
291
  return await smartWriteProperty(
291
292
  this.client, v.device, v.writePoints, smartWriteEvent,
@@ -310,7 +311,7 @@ module.exports = {
310
311
  ));
311
312
  });
312
313
 
313
- await concurrentTasks(smartWriteEvent, tasks, this.maxConcurrentDeviceWrite);
314
+ await concurrentTasksWithNetworkAwareness(smartWriteEvent, tasks, this.maxConcurrentDeviceWrite);
314
315
  }
315
316
 
316
317
  }
@@ -4,6 +4,7 @@ exports.Transport = void 0;
4
4
  const dgram_1 = require("dgram");
5
5
  const events_1 = require("events");
6
6
  const DEFAULT_BACNET_PORT = 47808;
7
+ const GLOBAL_BROADCAST_ADDRESS = "255.255.255.255";
7
8
  const UDP_TYPE = "udp4";
8
9
 
9
10
  // HAL modified
@@ -23,6 +24,7 @@ class Transport extends events_1.EventEmitter {
23
24
 
24
25
  if (
25
26
  this._settings.broadcastAddress &&
27
+ this._settings.broadcastAddress !== GLOBAL_BROADCAST_ADDRESS &&
26
28
  this._settings.broadcastAddress !== this._settings.interface
27
29
  ) {
28
30
  this._broadcastServer = (0, dgram_1.createSocket)({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "2.1.0",
3
+ "version": "2.2.0-alpha.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"