@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 `
|
|
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 {
|
|
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
|
|
217
|
+
await concurrentTasksWithNetworkAwareness(discoverPointEvent, tasks, this.maxConcurrentDeviceRead);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
#updateProgress(progress) {
|
package/common/job/read_point.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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
|
|
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)({
|