@halsystems/red-bacnet 2.1.1 → 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 +11 -0
- package/common/bacnet.js +69 -21
- package/common/core/concurrent.js +150 -1
- package/common/job/discover_device.js +10 -5
- package/common/job/discover_point.js +51 -43
- package/common/job/read_point.js +14 -9
- package/common/job/write_point.js +44 -8
- package/package.json +1 -1
- package/red-bacnet/discover_device.js +4 -5
- package/red-bacnet/discover_point.html +0 -8
- package/red-bacnet/discover_point.js +5 -9
- package/red-bacnet/read_point.js +11 -15
- package/red-bacnet/write_point.html +1 -1
- package/red-bacnet/write_point.js +10 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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]
|
|
11
|
+
### Changed
|
|
12
|
+
- `ConcurrentTask` changed to `ConcorrentTasksWithNetworkAwareness` round robin distributing task according to IP address to reduce congestion on a particular network
|
|
13
|
+
|
|
3
14
|
## [2.1.1]
|
|
4
15
|
### Changed
|
|
5
16
|
- Reinstated `globalBroadcastAddress` filter in transport
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -54,4 +54,153 @@ 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
|
+
* Invoke IDs are automatically managed per-task to avoid collisions.
|
|
63
|
+
*
|
|
64
|
+
* @param {EventEmitter} eventEmitter - The event emitter to emit events to.
|
|
65
|
+
* @param {Array} tasks - The list of tasks to execute. Each task should have:
|
|
66
|
+
* { id, device: { ipAddress, ... }, task: async (getNextInvokeId) => {...} }
|
|
67
|
+
* @param {number} maxConcurrent - The maximum number of concurrent tasks.
|
|
68
|
+
* @param {number} concurrentTaskDelay - The delay between concurrent tasks.
|
|
69
|
+
* @returns {Promise<Array>} - A promise that resolves with an array of results.
|
|
70
|
+
* @async
|
|
71
|
+
*/
|
|
72
|
+
concurrentTasksWithNetworkAwareness: async function (
|
|
73
|
+
eventEmitter, tasks, maxConcurrent, concurrentTaskDelay = 50
|
|
74
|
+
) {
|
|
75
|
+
const executing = new Set();
|
|
76
|
+
const results = [];
|
|
77
|
+
const ipAddressInUse = new Set(); // Track which IPs are currently executing
|
|
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
|
+
|
|
83
|
+
// Pending tasks that have not been started yet.
|
|
84
|
+
// We keep them in a list and remove them once scheduled so that a
|
|
85
|
+
// task blocked by a busy IP never blocks later tasks with free IPs.
|
|
86
|
+
const pending = tasks.map(({ id, device, task }) => ({
|
|
87
|
+
id,
|
|
88
|
+
task,
|
|
89
|
+
// Extract IP address from device (handle "ip:port" format)
|
|
90
|
+
ipAddress: device?.ipAddress ? device.ipAddress.split(':')[0] : null,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Start a single task and wire up its lifecycle. When the task settles
|
|
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.
|
|
97
|
+
*
|
|
98
|
+
* @param {{id: any, task: Function, ipAddress: string|null}} item
|
|
99
|
+
*/
|
|
100
|
+
const startTask = (item) => {
|
|
101
|
+
const { id, task, ipAddress } = item;
|
|
102
|
+
if (ipAddress) ipAddressInUse.add(ipAddress);
|
|
103
|
+
|
|
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)
|
|
136
|
+
.then(result => {
|
|
137
|
+
eventEmitter.emit(EVENT_OUTPUT, { id, result });
|
|
138
|
+
results.push({ id, result });
|
|
139
|
+
})
|
|
140
|
+
.catch(error => {
|
|
141
|
+
eventEmitter.emit(EVENT_ERROR, { id, error });
|
|
142
|
+
results.push({ id, error });
|
|
143
|
+
})
|
|
144
|
+
.finally(() => {
|
|
145
|
+
executing.delete(promise);
|
|
146
|
+
// Release the IP address so waiting tasks can run
|
|
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
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
executing.add(promise);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Schedule as many pending tasks as possible, respecting both the
|
|
159
|
+
* global concurrency limit and per-IP exclusivity. Scans the whole
|
|
160
|
+
* pending list (not just the head) so a busy IP does not block later
|
|
161
|
+
* tasks targeting free IPs.
|
|
162
|
+
*
|
|
163
|
+
* @async
|
|
164
|
+
*/
|
|
165
|
+
const scheduleAvailable = async () => {
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < pending.length && executing.size < maxConcurrent) {
|
|
168
|
+
const candidate = pending[i];
|
|
169
|
+
|
|
170
|
+
// Skip (leave in queue) if this IP is currently in use
|
|
171
|
+
if (candidate.ipAddress && ipAddressInUse.has(candidate.ipAddress)) {
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Remove from pending and start it
|
|
177
|
+
pending.splice(i, 1);
|
|
178
|
+
startTask(candidate);
|
|
179
|
+
|
|
180
|
+
if (concurrentTaskDelay > 0)
|
|
181
|
+
await delay(concurrentTaskDelay);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Keep pumping until every task has been scheduled and finished.
|
|
186
|
+
// Each loop: schedule what we can, then wait for at least one task to
|
|
187
|
+
// settle (which frees a concurrency slot and/or an IP) before retrying.
|
|
188
|
+
while (pending.length > 0 || executing.size > 0) {
|
|
189
|
+
await scheduleAvailable();
|
|
190
|
+
|
|
191
|
+
if (executing.size > 0) {
|
|
192
|
+
// Wait for one in-flight task to settle before scheduling more.
|
|
193
|
+
await Promise.race(executing);
|
|
194
|
+
} else if (pending.length > 0) {
|
|
195
|
+
// Nothing running but tasks remain (all were blocked by busy
|
|
196
|
+
// IPs that just got released). Yield briefly and retry.
|
|
197
|
+
/* istanbul ignore next */
|
|
198
|
+
await delay(concurrentTaskDelay > 0 ? concurrentTaskDelay : 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Safety net: ensure all promises have fully settled.
|
|
203
|
+
await Promise.allSettled(executing);
|
|
204
|
+
return results;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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,
|
|
@@ -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,
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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) => {
|
|
@@ -170,13 +165,16 @@ module.exports = {
|
|
|
170
165
|
|
|
171
166
|
const tasks = this.devices.map((d, i) => ({
|
|
172
167
|
id: d.deviceName,
|
|
173
|
-
|
|
168
|
+
device: d,
|
|
169
|
+
task: async (getNextInvokeId) => {
|
|
174
170
|
void i
|
|
175
171
|
try {
|
|
176
|
-
const objectList = await readObjectList(this.client, d);
|
|
172
|
+
const objectList = await readObjectList(this.client, d, getNextInvokeId);
|
|
177
173
|
|
|
178
174
|
// istanbul ignore next
|
|
179
|
-
if (objectList == null)
|
|
175
|
+
if (objectList == null) {
|
|
176
|
+
return { device: d, points: [] }
|
|
177
|
+
}
|
|
180
178
|
|
|
181
179
|
let objectListFinal = objectList
|
|
182
180
|
.map(obj => {
|
|
@@ -205,33 +203,42 @@ module.exports = {
|
|
|
205
203
|
|
|
206
204
|
return await readPoints(
|
|
207
205
|
d, objectListFinal, discoverPointEvent, this.name, this.client, this.readMethod,
|
|
208
|
-
this.maxConcurrentSinglePointRead, this.concurrentTaskDelay
|
|
206
|
+
this.maxConcurrentSinglePointRead, this.concurrentTaskDelay, getNextInvokeId
|
|
209
207
|
);
|
|
210
208
|
} catch (error) {
|
|
211
209
|
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, `Error reading ${d.deviceName} points`, error));
|
|
210
|
+
return { device: d, points: [] }
|
|
212
211
|
}
|
|
213
212
|
}
|
|
214
213
|
}));
|
|
215
214
|
|
|
216
|
-
await
|
|
215
|
+
await concurrentTasksWithNetworkAwareness(discoverPointEvent, tasks, this.maxConcurrentDeviceRead);
|
|
217
216
|
}
|
|
218
217
|
|
|
219
|
-
#updateProgress(
|
|
220
|
-
|
|
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);
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
#exportPoints() {
|
|
230
|
+
#exportPoints(points, device) {
|
|
224
231
|
let exportPointsList = [];
|
|
225
|
-
for (let i = 0; i <
|
|
232
|
+
for (let i = 0; i < points.length; i++) {
|
|
226
233
|
const { error, value: result } = bacnetPointSchema.validate(
|
|
227
|
-
|
|
234
|
+
points[i], { stripUnknown: true }
|
|
228
235
|
);
|
|
229
236
|
// istanbul ignore next
|
|
230
237
|
{
|
|
231
238
|
if (error) {
|
|
232
239
|
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
233
240
|
this.name, ERR_SCHEMA_VALIDATION, {
|
|
234
|
-
bacnetPoint:
|
|
241
|
+
bacnetPoint: points[i],
|
|
235
242
|
error: error,
|
|
236
243
|
}));
|
|
237
244
|
continue
|
|
@@ -240,8 +247,10 @@ module.exports = {
|
|
|
240
247
|
exportPointsList.push(result);
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
250
|
+
this.msg.payload = exportPointsList;
|
|
251
|
+
this.msg.device = device;
|
|
252
|
+
delete this.msg.devices;
|
|
253
|
+
this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
|
|
245
254
|
}
|
|
246
255
|
}
|
|
247
256
|
}
|
|
@@ -249,7 +258,7 @@ module.exports = {
|
|
|
249
258
|
// ---------------------------------- functions ----------------------------------
|
|
250
259
|
const readPoints = async (
|
|
251
260
|
device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead,
|
|
252
|
-
concurrentTaskDelay
|
|
261
|
+
concurrentTaskDelay, getNextInvokeId
|
|
253
262
|
) => {
|
|
254
263
|
const points = [];
|
|
255
264
|
|
|
@@ -270,7 +279,7 @@ const readPoints = async (
|
|
|
270
279
|
try {
|
|
271
280
|
// First request - get basic properties
|
|
272
281
|
const result = await smartReadProperty(
|
|
273
|
-
client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay
|
|
282
|
+
client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay, getNextInvokeId
|
|
274
283
|
);
|
|
275
284
|
|
|
276
285
|
// Process basic properties first
|
|
@@ -291,7 +300,7 @@ const readPoints = async (
|
|
|
291
300
|
try {
|
|
292
301
|
// Force use single read on state text to reduce loss especially long text
|
|
293
302
|
const stateTextResult = await smartReadProperty(
|
|
294
|
-
client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay
|
|
303
|
+
client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay, getNextInvokeId
|
|
295
304
|
);
|
|
296
305
|
|
|
297
306
|
// Update points with STATE_TEXT
|
|
@@ -314,11 +323,10 @@ const readPoints = async (
|
|
|
314
323
|
));
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
|
-
|
|
318
|
-
return points;
|
|
319
326
|
} catch (error) {
|
|
320
327
|
eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
|
|
321
328
|
}
|
|
329
|
+
return { device: device, points: points }
|
|
322
330
|
};
|
|
323
331
|
|
|
324
332
|
const updatePointWithStateText = (point, stateTextData) => {
|
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
|
|
@@ -22,14 +22,15 @@ module.exports = {
|
|
|
22
22
|
devicePointSeparator = '.';
|
|
23
23
|
|
|
24
24
|
constructor(
|
|
25
|
-
client, eventEmitter,
|
|
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.
|
|
32
|
-
this.
|
|
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
|
|
@@ -231,15 +232,16 @@ module.exports = {
|
|
|
231
232
|
.filter(([_, v]) => Array.isArray(v.points) && v.points.length > 0) // eslint-disable-line
|
|
232
233
|
.map(([k, v]) => ({
|
|
233
234
|
id: k,
|
|
234
|
-
|
|
235
|
+
device: v.device,
|
|
236
|
+
task: async (getNextInvokeId) => {
|
|
235
237
|
return await smartReadProperty(
|
|
236
238
|
this.client, v.device, v.points, this.readMethod,
|
|
237
|
-
this.maxConcurrentSinglePointRead, 10, this.concurrentTaskDelay
|
|
239
|
+
this.maxConcurrentSinglePointRead, 10, this.concurrentTaskDelay, getNextInvokeId
|
|
238
240
|
);
|
|
239
241
|
}
|
|
240
242
|
}));
|
|
241
243
|
|
|
242
|
-
await
|
|
244
|
+
await concurrentTasksWithNetworkAwareness(smartReadEvent, tasks, this.maxConcurrentDeviceRead);
|
|
243
245
|
|
|
244
246
|
// format final result
|
|
245
247
|
this.points.forEach(p => {
|
|
@@ -310,7 +312,10 @@ module.exports = {
|
|
|
310
312
|
exportPoints[k] = result;
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
this.
|
|
315
|
+
this.msg.payload = exportPoints;
|
|
316
|
+
delete this.msg.devices;
|
|
317
|
+
delete this.msg.points;
|
|
318
|
+
this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
|
|
314
319
|
}
|
|
315
320
|
}
|
|
316
|
-
}
|
|
321
|
+
}
|
|
@@ -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,
|
|
@@ -23,7 +23,7 @@ module.exports = {
|
|
|
23
23
|
devicePointSeparator = '.';
|
|
24
24
|
|
|
25
25
|
constructor(
|
|
26
|
-
client, eventEmitter,
|
|
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.
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
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
|
|
@@ -286,10 +287,11 @@ module.exports = {
|
|
|
286
287
|
.filter(([_, v]) => (void _, Object.keys(v.writePoints).length > 0))
|
|
287
288
|
.map(([k, v]) => ({
|
|
288
289
|
id: k,
|
|
289
|
-
|
|
290
|
+
device: v.device,
|
|
291
|
+
task: async (getNextInvokeId) => {
|
|
290
292
|
return await smartWriteProperty(
|
|
291
293
|
this.client, v.device, v.writePoints, smartWriteEvent,
|
|
292
|
-
this.maxConcurrentPointWrite, this.concurrentTaskDelay
|
|
294
|
+
this.maxConcurrentPointWrite, this.concurrentTaskDelay, getNextInvokeId
|
|
293
295
|
);
|
|
294
296
|
}
|
|
295
297
|
}));
|
|
@@ -310,8 +312,42 @@ module.exports = {
|
|
|
310
312
|
));
|
|
311
313
|
});
|
|
312
314
|
|
|
313
|
-
await
|
|
315
|
+
await concurrentTasksWithNetworkAwareness(smartWriteEvent, tasks, this.maxConcurrentDeviceWrite);
|
|
316
|
+
|
|
317
|
+
// emit output with msg containing failed points and results
|
|
318
|
+
this.#exportWriteResults();
|
|
314
319
|
}
|
|
315
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
|
+
}
|
|
316
352
|
}
|
|
317
353
|
}
|
package/package.json
CHANGED
|
@@ -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, (
|
|
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
|
|
58
|
-
this.
|
|
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, (
|
|
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 ===
|
|
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:
|
|
81
|
+
this.status({ fill: 'yellow', shape: 'dot', text: `${msg}` });
|
|
86
82
|
});
|
|
87
83
|
|
|
88
84
|
this.#eventEmitter.on(EVENT_ERROR, (err) => {
|
package/red-bacnet/read_point.js
CHANGED
|
@@ -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
|
|
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 :
|
|
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, (
|
|
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, (
|
|
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
|
});
|