@halsystems/red-bacnet 1.0.23 → 1.1.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 +19 -0
- package/common/bacnet.js +118 -21
- package/common/core/concurrent.js +7 -3
- package/common/job/discover_device.js +13 -20
- package/common/job/discover_point.js +70 -27
- package/common/job/read_point.js +4 -2
- package/common/job/write_point.js +3 -1
- package/ext/node-bacstack/dist/lib/bvlc.js +25 -1
- package/ext/node-bacstack/dist/lib/client.js +9 -2
- package/package.json +1 -1
- package/red-bacnet/discover_device.html +0 -5
- package/red-bacnet/discover_device.js +0 -2
- package/red-bacnet/discover_point.html +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.1]
|
|
4
|
+
### Added
|
|
5
|
+
- Unit test for BACnet string points
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Reading offline device points run infinitely
|
|
9
|
+
- Unit test failure
|
|
10
|
+
|
|
11
|
+
## [1.1.0]
|
|
12
|
+
### Changed
|
|
13
|
+
- Implemented auto resize batch size when querying using readPropertyMultiple to improve performance
|
|
14
|
+
- Added readPropertyMultiple to read object list to reduce query speed
|
|
15
|
+
- Changed strategy when discovering device name, implemented delay instead of burst to reduce traffic congestion and yield better result
|
|
16
|
+
- Force use readProperty to read multistate object to improve result
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Failure to discover device name for Forwarded NPDU devices
|
|
20
|
+
- Missed bacnet objects when using discover point
|
|
21
|
+
|
|
3
22
|
## [1.0.22]
|
|
4
23
|
### Fixed
|
|
5
24
|
- Removed console.log in Read Point
|
package/common/bacnet.js
CHANGED
|
@@ -18,7 +18,8 @@ const { getErrMsg } = require('@root/common/func.js')
|
|
|
18
18
|
// ---------------------------------- export ----------------------------------
|
|
19
19
|
module.exports = {
|
|
20
20
|
/**
|
|
21
|
-
* Reads the object list of a device.
|
|
21
|
+
* Reads the object list of a device using optimized batch reading.
|
|
22
|
+
* First tries readPropertyMultiple for faster performance, falls back to sequential reading if failed.
|
|
22
23
|
* @param {BacnetClient} client
|
|
23
24
|
* @param {object} device
|
|
24
25
|
* eg:{
|
|
@@ -38,15 +39,97 @@ module.exports = {
|
|
|
38
39
|
readObjectList: async function (client, device) {
|
|
39
40
|
const objectId = { type: baEnum.ObjectType.DEVICE, instance: device.deviceId };
|
|
40
41
|
const propertyId = baEnum.PropertyIdentifier.OBJECT_LIST;
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId);
|
|
44
|
+
if (result.length == 0) {
|
|
45
|
+
result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads a property that returns an array using readPropertyMultiple for better performance.
|
|
53
|
+
* Attempts to read the property in batches to minimize round trips.
|
|
54
|
+
* @param {BacnetClient} client
|
|
55
|
+
* @param {object} device
|
|
56
|
+
* @param {object} objectId
|
|
57
|
+
* @param {number} propertyId
|
|
58
|
+
* @returns array of objects
|
|
59
|
+
* @async
|
|
60
|
+
*/
|
|
61
|
+
readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId) {
|
|
62
|
+
let addressSet = device.ipAddress;
|
|
63
|
+
if (device.macAddress != null && device.network != null) {
|
|
64
|
+
addressSet = {
|
|
65
|
+
ip: device.ipAddress,
|
|
66
|
+
adr: device.macAddress,
|
|
67
|
+
net: device.network
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = [];
|
|
72
|
+
let healthy = true;
|
|
73
|
+
|
|
74
|
+
// helper for single batch request
|
|
75
|
+
async function readBatch(arrayIndex) {
|
|
76
|
+
const reqArr = [{
|
|
77
|
+
objectId: objectId,
|
|
78
|
+
properties: [{
|
|
79
|
+
id: propertyId,
|
|
80
|
+
index: arrayIndex != null ? arrayIndex : baEnum.ASN1_ARRAY_ALL
|
|
81
|
+
}]
|
|
82
|
+
}];
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
client.readPropertyMultiple(
|
|
86
|
+
addressSet,
|
|
87
|
+
reqArr,
|
|
88
|
+
{ maxApdu: device.maxApdu },
|
|
89
|
+
(err, value) => {
|
|
90
|
+
if (err) return reject(err);
|
|
91
|
+
resolve(value);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// try reading all at once first
|
|
98
|
+
try {
|
|
99
|
+
const value = await readBatch(baEnum.ASN1_ARRAY_ALL);
|
|
100
|
+
if (value?.values?.[0]?.values?.[0]?.value) {
|
|
101
|
+
return value.values[0].values[0].value;
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
void err
|
|
105
|
+
healthy = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// try using index read
|
|
109
|
+
if (!healthy) {
|
|
110
|
+
for (let i = 0; ; i++) {
|
|
111
|
+
try {
|
|
112
|
+
const res = await readBatch(i);
|
|
113
|
+
const item = res?.values?.[0]?.values?.[0]?.value?.[0];
|
|
114
|
+
|
|
115
|
+
if (item?.type === 105) break
|
|
116
|
+
|
|
117
|
+
result.push(item);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
void err
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
42
126
|
},
|
|
43
127
|
|
|
44
128
|
/**
|
|
45
129
|
* Smart read multiple properties from a BACnet device with concurrent read feature and various read methods
|
|
46
130
|
* readMethod
|
|
47
131
|
* 0: use readProperty only
|
|
48
|
-
* 1: try readPropertyMultiple
|
|
49
|
-
* 2: try readPropertyMultiple twice with high and conservative query size, fallback to readProperty if failed
|
|
132
|
+
* 1: try readPropertyMultiple, reduce query size if failed, and fallback to readProperty if query size reduced to 1
|
|
50
133
|
* @param {BacnetClient} client
|
|
51
134
|
* @param {object} device
|
|
52
135
|
* eg:{
|
|
@@ -71,13 +154,15 @@ module.exports = {
|
|
|
71
154
|
* maximum concurrent point to read in single read mode
|
|
72
155
|
* @param {number} singleReadFailedRetry
|
|
73
156
|
* retry times for single read failed
|
|
157
|
+
* @param {number} concurrentTaskDelay
|
|
158
|
+
* delay between concurrent tasks
|
|
74
159
|
* @returns array of objects
|
|
75
160
|
* eg: [{type: 12, value: {type: 8, instance: 123}, ...],
|
|
76
161
|
* @async
|
|
77
162
|
*/
|
|
78
163
|
smartReadProperty: async function (
|
|
79
164
|
client, device, reqArr, readMethod = 1, maxConcurrentSinglePointRead = 5,
|
|
80
|
-
singleReadFailedRetry = 5
|
|
165
|
+
singleReadFailedRetry = 5, concurrentTaskDelay = 50
|
|
81
166
|
) {
|
|
82
167
|
/* reqArr example
|
|
83
168
|
[{
|
|
@@ -95,16 +180,15 @@ module.exports = {
|
|
|
95
180
|
if (readMethod > 0) {
|
|
96
181
|
// calculate batch sizes
|
|
97
182
|
if (device.maxApdu == null) // default batch size if null or undefined
|
|
98
|
-
batchSizes.add(20)
|
|
183
|
+
batchSizes.add(20);
|
|
99
184
|
else {
|
|
100
|
-
const perValueBytes = [30
|
|
101
|
-
for (let x = 0; x < perValueBytes.length; x++)
|
|
102
|
-
|
|
185
|
+
const perValueBytes = [30] // typical numeric point is 17 byte
|
|
186
|
+
for (let x = 0; x < perValueBytes.length; x++) {
|
|
187
|
+
let batchSize = Math.trunc(device.maxApdu / perValueBytes[x])
|
|
188
|
+
if (batchSize > 0)
|
|
189
|
+
batchSizes.add(batchSize)
|
|
190
|
+
}
|
|
103
191
|
}
|
|
104
|
-
|
|
105
|
-
// if readMethod === 1
|
|
106
|
-
if (readMethod === 1 && batchSizes.size > 1)
|
|
107
|
-
batchSizes = new Set([...batchSizes].slice(-1));
|
|
108
192
|
}
|
|
109
193
|
|
|
110
194
|
for (const batchSize of batchSizes) {
|
|
@@ -112,6 +196,7 @@ module.exports = {
|
|
|
112
196
|
let first = true
|
|
113
197
|
let reqArrBatch
|
|
114
198
|
let batchCount
|
|
199
|
+
let currBatchSize = batchSize
|
|
115
200
|
|
|
116
201
|
do { // use current batch size until query failed
|
|
117
202
|
// get ready for next batch
|
|
@@ -132,7 +217,7 @@ module.exports = {
|
|
|
132
217
|
if (reqArrIndexNext >= reqArr.length)
|
|
133
218
|
break
|
|
134
219
|
|
|
135
|
-
if (batchCount + reqArr[reqArrIndexNext].properties.length >
|
|
220
|
+
if (batchCount + reqArr[reqArrIndexNext].properties.length > currBatchSize)
|
|
136
221
|
break
|
|
137
222
|
|
|
138
223
|
reqArrBatch.push(reqArr[reqArrIndexNext])
|
|
@@ -143,6 +228,14 @@ module.exports = {
|
|
|
143
228
|
// read batch block
|
|
144
229
|
const value = await module.exports.readPropertyMultple(client, device, reqArrBatch)
|
|
145
230
|
.catch(() => {
|
|
231
|
+
// Reduce batch size by half (minimum 1)
|
|
232
|
+
currBatchSize = Math.max(1, Math.floor(currBatchSize / 2))
|
|
233
|
+
|
|
234
|
+
// If batch size is too small, mark as unhealthy
|
|
235
|
+
if (currBatchSize <= 1) {
|
|
236
|
+
healthy = false
|
|
237
|
+
}
|
|
238
|
+
|
|
146
239
|
reqArrIndexNext = reqArrIndex
|
|
147
240
|
healthy = false
|
|
148
241
|
})
|
|
@@ -172,7 +265,6 @@ module.exports = {
|
|
|
172
265
|
if (!success) {
|
|
173
266
|
let failedCount = 0
|
|
174
267
|
let result_single = []
|
|
175
|
-
|
|
176
268
|
const dummyEventEmitter = new EventEmitter();
|
|
177
269
|
const tasks = reqArr.slice(reqArrIndexNext).flatMap((req, x) =>
|
|
178
270
|
req.properties.map((prop, y) => ({
|
|
@@ -196,7 +288,7 @@ module.exports = {
|
|
|
196
288
|
return value;
|
|
197
289
|
} catch (err) {
|
|
198
290
|
failedCount++;
|
|
199
|
-
if (
|
|
291
|
+
if (failedCount >= singleReadFailedRetry)
|
|
200
292
|
throw err
|
|
201
293
|
}
|
|
202
294
|
}
|
|
@@ -204,7 +296,7 @@ module.exports = {
|
|
|
204
296
|
);
|
|
205
297
|
|
|
206
298
|
try {
|
|
207
|
-
await concurrentTasks(dummyEventEmitter, tasks, maxConcurrentSinglePointRead);
|
|
299
|
+
await concurrentTasks(dummyEventEmitter, tasks, maxConcurrentSinglePointRead, concurrentTaskDelay);
|
|
208
300
|
} catch (err) {
|
|
209
301
|
void err
|
|
210
302
|
}
|
|
@@ -253,9 +345,14 @@ module.exports = {
|
|
|
253
345
|
* @param {EventEmitter} eventEmitter
|
|
254
346
|
* @param {number} maxConcurrentWrite
|
|
255
347
|
* maximum concurrent point to write
|
|
348
|
+
* @param {number} concurrentTaskDelay
|
|
349
|
+
* delay between concurrent tasks
|
|
256
350
|
* @async
|
|
257
351
|
*/
|
|
258
|
-
smartWriteProperty: async function (
|
|
352
|
+
smartWriteProperty: async function (
|
|
353
|
+
client, device, writePoints, eventEmitter, maxConcurrentWrite,
|
|
354
|
+
concurrentTaskDelay = 50
|
|
355
|
+
) {
|
|
259
356
|
const entries = Object.entries(writePoints);
|
|
260
357
|
|
|
261
358
|
// current writePropertyMultiple will throw ERR_TIMEOUT if any of the write fails
|
|
@@ -281,7 +378,7 @@ module.exports = {
|
|
|
281
378
|
}
|
|
282
379
|
}));
|
|
283
380
|
|
|
284
|
-
await concurrentTasks(eventEmitter, tasks, maxConcurrentWrite);
|
|
381
|
+
await concurrentTasks(eventEmitter, tasks, maxConcurrentWrite, concurrentTaskDelay);
|
|
285
382
|
|
|
286
383
|
// write properties multiples example
|
|
287
384
|
// const values = [
|
|
@@ -317,7 +414,7 @@ module.exports = {
|
|
|
317
414
|
* eg: 85
|
|
318
415
|
* @returns object
|
|
319
416
|
* eg: { len: 11, objectId: { type: 19, instance: 1 },
|
|
320
|
-
* property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }
|
|
417
|
+
* property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
|
|
321
418
|
* @async
|
|
322
419
|
*/
|
|
323
420
|
readProperty: async function (client, device, objectId, propertyId) {
|
|
@@ -485,4 +582,4 @@ module.exports = {
|
|
|
485
582
|
});
|
|
486
583
|
});
|
|
487
584
|
},
|
|
488
|
-
}
|
|
585
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require('./_alias.js');
|
|
3
3
|
|
|
4
4
|
const { EVENT_OUTPUT, EVENT_ERROR } = require('@root/common/core/constant.js');
|
|
5
|
+
const { delay } = require('@root/common/core/util.js');
|
|
5
6
|
|
|
6
7
|
// ---------------------------------- type def ----------------------------------
|
|
7
8
|
/**
|
|
@@ -16,10 +17,13 @@ module.exports = {
|
|
|
16
17
|
* @param {EventEmitter} eventEmitter - The event emitter to emit events to.
|
|
17
18
|
* @param {Array} tasks - The list of tasks to execute.
|
|
18
19
|
* @param {number} maxConcurrent - The maximum number of concurrent tasks.
|
|
20
|
+
* @param {number} concurrentTaskDelay - The delay between concurrent tasks.
|
|
19
21
|
* @returns {Promise<Array>} - A promise that resolves with an array of results.
|
|
20
22
|
* @async
|
|
21
23
|
*/
|
|
22
|
-
concurrentTasks: async function (
|
|
24
|
+
concurrentTasks: async function (
|
|
25
|
+
eventEmitter, tasks, maxConcurrent, concurrentTaskDelay = 50
|
|
26
|
+
) {
|
|
23
27
|
const executing = new Set();
|
|
24
28
|
const results = [];
|
|
25
29
|
|
|
@@ -43,8 +47,8 @@ module.exports = {
|
|
|
43
47
|
await Promise.race(executing);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
if (concurrentTaskDelay > 0)
|
|
51
|
+
await delay(concurrentTaskDelay)
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
await Promise.allSettled(executing);
|
|
@@ -31,10 +31,9 @@ module.exports = {
|
|
|
31
31
|
* @param {number} lowLimit - Low limit of the network range.
|
|
32
32
|
* @param {number} highLimit - High limit of the network range.
|
|
33
33
|
* @param {number} whoIsTimeout - Who is request timeout in milliseconds.
|
|
34
|
-
* @param {number} readDeviceNameTimeout - Read device name timeout in milliseconds.
|
|
35
34
|
*/
|
|
36
35
|
constructor(
|
|
37
|
-
client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout,
|
|
36
|
+
client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout,
|
|
38
37
|
name = 'discover device'
|
|
39
38
|
) {
|
|
40
39
|
super();
|
|
@@ -44,7 +43,6 @@ module.exports = {
|
|
|
44
43
|
this.lowLimit = lowLimit
|
|
45
44
|
this.highLimit = highLimit
|
|
46
45
|
this.whoIsTimeout = whoIsTimeout
|
|
47
|
-
this.readDeviceNameTimeout = readDeviceNameTimeout
|
|
48
46
|
this.name = name
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -108,7 +106,6 @@ module.exports = {
|
|
|
108
106
|
* @returns {Promise<void>}
|
|
109
107
|
*/
|
|
110
108
|
async #readDevices() {
|
|
111
|
-
const chunkSize = 100;
|
|
112
109
|
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
113
110
|
|
|
114
111
|
const readDeviceName = (d, addressSet) => {
|
|
@@ -138,26 +135,22 @@ module.exports = {
|
|
|
138
135
|
});
|
|
139
136
|
};
|
|
140
137
|
|
|
141
|
-
for (let i = 0; i < this.discoverList.length; i
|
|
142
|
-
const
|
|
138
|
+
for (let i = 0; i < this.discoverList.length; i++) {
|
|
139
|
+
const d = this.discoverList[i];
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return readDeviceName(d, addressSet)
|
|
151
|
-
.then(deviceInfo => this.discoveredDevices.push(deviceInfo));
|
|
152
|
-
});
|
|
141
|
+
let addressSet = d.address;
|
|
142
|
+
if (d.adr !== undefined && d.net !== undefined) {
|
|
143
|
+
addressSet = { ip: d.address, adr: d.adr, net: d.net };
|
|
144
|
+
}
|
|
153
145
|
|
|
154
|
-
await
|
|
155
|
-
|
|
146
|
+
const deviceInfo = await readDeviceName(d, addressSet);
|
|
147
|
+
this.discoveredDevices.push(deviceInfo);
|
|
156
148
|
|
|
157
|
-
//
|
|
149
|
+
// throttle to avoid bursts
|
|
150
|
+
await delay(75); // tweak between 50–150ms
|
|
158
151
|
this.#updateProgress(
|
|
159
|
-
Math.min(90, Math.round((80 / this.discoverList.length) * (i +
|
|
160
|
-
)
|
|
152
|
+
Math.min(90, Math.round((80 / this.discoverList.length) * (i + 1) + 10))
|
|
153
|
+
);
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
this.#updateProgress(90)
|
|
@@ -53,7 +53,8 @@ module.exports = {
|
|
|
53
53
|
|
|
54
54
|
constructor(
|
|
55
55
|
client, eventEmitter, inputDevices, discoverMode, readMethod, groupExportDeviceCount,
|
|
56
|
-
maxConcurrentDeviceRead, maxConcurrentSinglePointRead,
|
|
56
|
+
maxConcurrentDeviceRead, maxConcurrentSinglePointRead, concurrentTaskDelay = 50,
|
|
57
|
+
name = 'discover point'
|
|
57
58
|
) {
|
|
58
59
|
super();
|
|
59
60
|
this.client = client
|
|
@@ -64,6 +65,7 @@ module.exports = {
|
|
|
64
65
|
this.groupExportDeviceCount = groupExportDeviceCount
|
|
65
66
|
this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
|
|
66
67
|
this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
|
|
68
|
+
this.concurrentTaskDelay = concurrentTaskDelay
|
|
67
69
|
this.name = name
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -194,7 +196,7 @@ module.exports = {
|
|
|
194
196
|
|
|
195
197
|
return await readPoints(
|
|
196
198
|
d, objectListFinal, discoverPointEvent, this.name, this.client, this.readMethod,
|
|
197
|
-
this.maxConcurrentSinglePointRead
|
|
199
|
+
this.maxConcurrentSinglePointRead, this.concurrentTaskDelay
|
|
198
200
|
);
|
|
199
201
|
} catch (error) {
|
|
200
202
|
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, `Error reading ${d.deviceName} points`, error));
|
|
@@ -237,9 +239,12 @@ module.exports = {
|
|
|
237
239
|
|
|
238
240
|
// ---------------------------------- functions ----------------------------------
|
|
239
241
|
const readPoints = async (
|
|
240
|
-
device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead
|
|
242
|
+
device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead,
|
|
243
|
+
concurrentTaskDelay
|
|
241
244
|
) => {
|
|
242
245
|
const points = [];
|
|
246
|
+
|
|
247
|
+
// First query - get basic properties (excluding STATE_TEXT)
|
|
243
248
|
const reqArr = objects.map(obj => ({
|
|
244
249
|
objectId: { type: obj.value.type, instance: obj.value.instance },
|
|
245
250
|
properties: [
|
|
@@ -250,38 +255,80 @@ const readPoints = async (
|
|
|
250
255
|
{ id: baEnum.PropertyIdentifier.INACTIVE_TEXT },
|
|
251
256
|
{ id: baEnum.PropertyIdentifier.ACTIVE_TEXT }
|
|
252
257
|
] : []),
|
|
253
|
-
...(multiStateObjectTypes.includes(obj.value.type) ? [{ id: baEnum.PropertyIdentifier.STATE_TEXT }] : []),
|
|
254
|
-
// ...(scheduleObjectTypes.includes(obj.value.type) ? [
|
|
255
|
-
// { id: baEnum.PropertyIdentifier.WEEKLY_SCHEDULE },
|
|
256
|
-
// { id: baEnum.PropertyIdentifier.EXCEPTION_SCHEDULE },
|
|
257
|
-
// { id: baEnum.PropertyIdentifier.SCHEDULE_DEFAULT }
|
|
258
|
-
// ] : [])
|
|
259
258
|
]
|
|
260
259
|
}));
|
|
261
260
|
|
|
262
261
|
try {
|
|
262
|
+
// First request - get basic properties
|
|
263
263
|
const result = await smartReadProperty(
|
|
264
|
-
client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50
|
|
264
|
+
client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50, concurrentTaskDelay
|
|
265
265
|
);
|
|
266
|
+
|
|
267
|
+
// Process basic properties first
|
|
266
268
|
result.forEach(i => {
|
|
267
|
-
/** i example
|
|
268
|
-
{
|
|
269
|
-
objectId: { type: 1, instance: 0 },
|
|
270
|
-
values: [
|
|
271
|
-
{ id: 85, index: 4294967295, value: [Array] },
|
|
272
|
-
{ id: 77, index: 4294967295, value: [Array] }
|
|
273
|
-
]
|
|
274
|
-
}
|
|
275
|
-
*/
|
|
276
269
|
const point = processPoint(i, device.deviceName);
|
|
277
270
|
points.push(point);
|
|
278
271
|
});
|
|
279
|
-
|
|
272
|
+
|
|
273
|
+
// Second query - get STATE_TEXT for multistate objects only
|
|
274
|
+
const multistateObjects = objects.filter(obj => multiStateObjectTypes.includes(obj.value.type));
|
|
275
|
+
|
|
276
|
+
if (multistateObjects.length > 0) {
|
|
277
|
+
const stateTextReqArr = multistateObjects.map(obj => ({
|
|
278
|
+
objectId: { type: obj.value.type, instance: obj.value.instance },
|
|
279
|
+
properties: [{ id: baEnum.PropertyIdentifier.STATE_TEXT }]
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Force use single read on state text to reduce loss especially long text
|
|
284
|
+
const stateTextResult = await smartReadProperty(
|
|
285
|
+
client, device, stateTextReqArr, 0, 1, 50, concurrentTaskDelay
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Update points with STATE_TEXT
|
|
289
|
+
stateTextResult.forEach(stateTextData => {
|
|
290
|
+
const matchingPoint = points.find(point =>
|
|
291
|
+
point.bacType === stateTextData.objectId.type &&
|
|
292
|
+
point.bacInstance === stateTextData.objectId.instance
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (matchingPoint) {
|
|
296
|
+
updatePointWithStateText(matchingPoint, stateTextData);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
} catch (stateTextError) {
|
|
300
|
+
// Log the error but don't fail the entire operation
|
|
301
|
+
eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
302
|
+
name,
|
|
303
|
+
'Warning: Failed to read STATE_TEXT properties',
|
|
304
|
+
stateTextError
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return points;
|
|
280
310
|
} catch (error) {
|
|
281
311
|
eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
|
|
282
312
|
}
|
|
283
313
|
};
|
|
284
314
|
|
|
315
|
+
const updatePointWithStateText = (point, stateTextData) => {
|
|
316
|
+
const stateTextProp = stateTextData.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT);
|
|
317
|
+
const states = stateTextProp?.value;
|
|
318
|
+
|
|
319
|
+
if (Array.isArray(states)) {
|
|
320
|
+
// Update the facets with state text information
|
|
321
|
+
const stateTextFacet = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
|
|
322
|
+
|
|
323
|
+
// If point already has facets, append; otherwise, set new facets
|
|
324
|
+
if (point.facets) {
|
|
325
|
+
point.facets += `;${stateTextFacet}`;
|
|
326
|
+
} else {
|
|
327
|
+
point.facets = stateTextFacet;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
285
332
|
const processPoint = (i, deviceName) => {
|
|
286
333
|
const point = {
|
|
287
334
|
deviceName,
|
|
@@ -310,12 +357,8 @@ const setPointFacets = (point, i) => {
|
|
|
310
357
|
const inactive = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.INACTIVE_TEXT)?.value?.[0]?.value;
|
|
311
358
|
const active = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.ACTIVE_TEXT)?.value?.[0]?.value;
|
|
312
359
|
point.facets = `falseText:${inactive ? inactive : 'false'};trueText:${active ? active : 'true'}`;
|
|
313
|
-
} else if (multiStateObjectTypes.includes(point.bacType)) {
|
|
314
|
-
const states = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT)?.value;
|
|
315
|
-
if (Array.isArray(states)) {
|
|
316
|
-
point.facets = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
|
|
317
|
-
}
|
|
318
360
|
}
|
|
361
|
+
// STATE_TEXT handling removed from here - now handled separately
|
|
319
362
|
};
|
|
320
363
|
|
|
321
364
|
const setPointValues = (point, i) => {
|
|
@@ -334,7 +377,7 @@ const setPointValues = (point, i) => {
|
|
|
334
377
|
|
|
335
378
|
const processValue = (value, facets) => {
|
|
336
379
|
if (value?.errorClass != null && value?.errorCode != null) return null;
|
|
337
|
-
const match = facets
|
|
380
|
+
const match = facets?.match(/precision:(\d+)/);
|
|
338
381
|
const precision = match ? +match[1] : null;
|
|
339
382
|
|
|
340
383
|
if (typeof value === 'number')
|
|
@@ -342,4 +385,4 @@ const processValue = (value, facets) => {
|
|
|
342
385
|
else
|
|
343
386
|
return value
|
|
344
387
|
|
|
345
|
-
};
|
|
388
|
+
};
|
package/common/job/read_point.js
CHANGED
|
@@ -23,7 +23,7 @@ module.exports = {
|
|
|
23
23
|
|
|
24
24
|
constructor(
|
|
25
25
|
client, eventEmitter, devices, points, readMethod, maxConcurrentDeviceRead = 2,
|
|
26
|
-
maxConcurrentSinglePointRead = 5, name = 'read point'
|
|
26
|
+
maxConcurrentSinglePointRead = 5, concurrentTaskDelay = 50, name = 'read point'
|
|
27
27
|
) {
|
|
28
28
|
super();
|
|
29
29
|
this.client = client
|
|
@@ -33,6 +33,7 @@ module.exports = {
|
|
|
33
33
|
this.readMethod = readMethod
|
|
34
34
|
this.maxConcurrentDeviceRead = maxConcurrentDeviceRead
|
|
35
35
|
this.maxConcurrentSinglePointRead = maxConcurrentSinglePointRead
|
|
36
|
+
this.concurrentTaskDelay = concurrentTaskDelay
|
|
36
37
|
this.name = name
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -219,7 +220,8 @@ module.exports = {
|
|
|
219
220
|
id: k,
|
|
220
221
|
task: async () => {
|
|
221
222
|
return await smartReadProperty(
|
|
222
|
-
this.client, v.device, v.points, this.readMethod,
|
|
223
|
+
this.client, v.device, v.points, this.readMethod,
|
|
224
|
+
this.maxConcurrentSinglePointRead, 5, this.concurrentTaskDelay
|
|
223
225
|
);
|
|
224
226
|
}
|
|
225
227
|
}));
|
|
@@ -25,6 +25,7 @@ module.exports = {
|
|
|
25
25
|
constructor(
|
|
26
26
|
client, eventEmitter, devices, points, writePoints,
|
|
27
27
|
maxConcurrentDeviceWrite = 2, maxConcurrentPointWrite = 1,
|
|
28
|
+
concurrentTaskDelay = 50,
|
|
28
29
|
name = 'write point'
|
|
29
30
|
) {
|
|
30
31
|
super();
|
|
@@ -35,6 +36,7 @@ module.exports = {
|
|
|
35
36
|
this.writePoints = writePoints
|
|
36
37
|
this.maxConcurrentDeviceWrite = maxConcurrentDeviceWrite
|
|
37
38
|
this.maxConcurrentPointWrite = maxConcurrentPointWrite
|
|
39
|
+
this.concurrentTaskDelay = concurrentTaskDelay
|
|
38
40
|
this.name = name
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -277,7 +279,7 @@ module.exports = {
|
|
|
277
279
|
task: async () => {
|
|
278
280
|
return await smartWriteProperty(
|
|
279
281
|
this.client, v.device, v.writePoints, smartWriteEvent,
|
|
280
|
-
this.maxConcurrentPointWrite
|
|
282
|
+
this.maxConcurrentPointWrite, this.concurrentTaskDelay
|
|
281
283
|
);
|
|
282
284
|
}
|
|
283
285
|
}));
|
|
@@ -12,8 +12,10 @@ const encode = (buffer, func, msgLength) => {
|
|
|
12
12
|
exports.encode = encode;
|
|
13
13
|
const decode = (buffer, offset) => {
|
|
14
14
|
let len;
|
|
15
|
+
let linkAddress;
|
|
15
16
|
const func = buffer[1];
|
|
16
17
|
const msgLength = (buffer[2] << 8) | (buffer[3] << 0);
|
|
18
|
+
|
|
17
19
|
if (buffer[0] !== baEnum.BVLL_TYPE_BACNET_IP || buffer.length !== msgLength)
|
|
18
20
|
return;
|
|
19
21
|
switch (func) {
|
|
@@ -25,6 +27,27 @@ const decode = (buffer, offset) => {
|
|
|
25
27
|
break;
|
|
26
28
|
case baEnum.BvlcResultPurpose.FORWARDED_NPDU:
|
|
27
29
|
len = 10;
|
|
30
|
+
// HAL modified. Extract BVLC IP and port from buffer
|
|
31
|
+
if (buffer.length >= 10) {
|
|
32
|
+
const ipParts = [buffer[4], buffer[5], buffer[6], buffer[7]];
|
|
33
|
+
const port = (buffer[8] << 8) | buffer[9];
|
|
34
|
+
|
|
35
|
+
// Validate IP octets (0–255 only)
|
|
36
|
+
const isValidIp = ipParts.every(octet => Number.isInteger(octet) && octet >= 0 && octet <= 255);
|
|
37
|
+
|
|
38
|
+
// Validate port (1–65535; BACnet default is 47808)
|
|
39
|
+
const isValidPort = Number.isInteger(port) && port > 0 && port <= 65535;
|
|
40
|
+
|
|
41
|
+
// Extra IP rules
|
|
42
|
+
const isBroadcast = ipParts.every(octet => octet === 255); // 255.255.255.255
|
|
43
|
+
const isMulticast = ipParts[0] >= 224 && ipParts[0] <= 239; // 224.0.0.0 – 239.255.255.255
|
|
44
|
+
const isZero = ipParts.every(octet => octet === 0); // 0.0.0.0
|
|
45
|
+
|
|
46
|
+
if (isValidIp && isValidPort && !isBroadcast && !isMulticast && !isZero) {
|
|
47
|
+
const ip = ipParts.join(".");
|
|
48
|
+
linkAddress = (port === 47808) ? ip : `${ip}:${port}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
28
51
|
break;
|
|
29
52
|
case baEnum.BvlcResultPurpose.REGISTER_FOREIGN_DEVICE:
|
|
30
53
|
case baEnum.BvlcResultPurpose.READ_FOREIGN_DEVICE_TABLE:
|
|
@@ -41,7 +64,8 @@ const decode = (buffer, offset) => {
|
|
|
41
64
|
return {
|
|
42
65
|
len: len,
|
|
43
66
|
func: func,
|
|
44
|
-
msgLength: msgLength
|
|
67
|
+
msgLength: msgLength,
|
|
68
|
+
linkAddress: linkAddress
|
|
45
69
|
};
|
|
46
70
|
};
|
|
47
71
|
exports.decode = decode;
|
|
@@ -458,6 +458,8 @@ class Client extends events_1.EventEmitter {
|
|
|
458
458
|
this._handlePdu(remoteAddress, apduType, buffer, offset, msgLength, addressObject, destAddress);
|
|
459
459
|
}
|
|
460
460
|
_receiveData(buffer, remoteAddress) {
|
|
461
|
+
let srcAddress = remoteAddress
|
|
462
|
+
|
|
461
463
|
// Check data length
|
|
462
464
|
if (buffer.length < baEnum.BVLC_HEADER_LENGTH)
|
|
463
465
|
return debug('Received invalid data -> Drop package');
|
|
@@ -465,9 +467,14 @@ class Client extends events_1.EventEmitter {
|
|
|
465
467
|
const result = baBvlc.decode(buffer, 0);
|
|
466
468
|
if (!result)
|
|
467
469
|
return debug('Received invalid BVLC header -> Drop package');
|
|
468
|
-
// Check BVLC
|
|
470
|
+
// Check BVLC virtual link controller address if defined
|
|
469
471
|
if (result.func === baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU || result.func === baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU || result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU) {
|
|
470
|
-
|
|
472
|
+
// HAL modified. Use BACnet link address if exists
|
|
473
|
+
if (result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU && result.linkAddress) {
|
|
474
|
+
srcAddress = result.linkAddress
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this._handleNpdu(buffer, result.len, buffer.length - result.len, srcAddress);
|
|
471
478
|
}
|
|
472
479
|
else {
|
|
473
480
|
debug('Received unknown BVLC function -> Drop package');
|
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
lowLimit: { value: 0, required: true, validate: RED.validators.number() },
|
|
10
10
|
highLimit: { value: 4194304, required: true, validate: RED.validators.number() },
|
|
11
11
|
whoIsTimeout: { value: 3000, required: true, validate: RED.validators.number() },
|
|
12
|
-
readDeviceTimeout: { value: 1000, required: true, validate: RED.validators.number() },
|
|
13
12
|
},
|
|
14
13
|
inputs: 1,
|
|
15
14
|
outputs: 1,
|
|
@@ -46,10 +45,6 @@
|
|
|
46
45
|
<label for="node-input-whoIsTimeout"><i class="fa fa-tag"></i> Who Is Timeout (ms)</label>
|
|
47
46
|
<input type="number" id="node-input-whoIsTimeout" placeholder="3000", min="100", max="3600000">
|
|
48
47
|
</div>
|
|
49
|
-
<div class="form-row">
|
|
50
|
-
<label for="node-input-readDeviceTimeout"><i class="fa fa-tag"></i> Read Device Timeout (ms)</label>
|
|
51
|
-
<input type="number" id="node-input-readDeviceTimeout" placeholder="1000", min="100", max="3600000">
|
|
52
|
-
</div>
|
|
53
48
|
</script>
|
|
54
49
|
|
|
55
50
|
<script type="text/html" data-help-name="discover device">
|
|
@@ -28,7 +28,6 @@ module.exports = function (RED) {
|
|
|
28
28
|
this.lowLimit = +config.lowLimit
|
|
29
29
|
this.highLimit = +config.highLimit
|
|
30
30
|
this.whoIsTimeout = +config.whoIsTimeout
|
|
31
|
-
this.readDeviceTimeout = +config.readDeviceTimeout
|
|
32
31
|
|
|
33
32
|
// events
|
|
34
33
|
this.#subscribeListeners();
|
|
@@ -45,7 +44,6 @@ module.exports = function (RED) {
|
|
|
45
44
|
this.lowLimit,
|
|
46
45
|
this.highLimit,
|
|
47
46
|
this.whoIsTimeout,
|
|
48
|
-
this.readDeviceTimeout
|
|
49
47
|
);
|
|
50
48
|
this.task.onStart();
|
|
51
49
|
}
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
<label for="node-input-readMethod"><i class="fa fa-list"></i> Read Method</label>
|
|
42
42
|
<select id="node-input-readMethod">
|
|
43
43
|
<option value="0">Read Single Only</option>
|
|
44
|
-
<option value="1">Read
|
|
45
|
-
<option value="2">Comprehensive Read</option>
|
|
44
|
+
<option value="1">Comprehensive Read</option>
|
|
46
45
|
</select>
|
|
47
46
|
</div>
|
|
48
47
|
<div class="form-row">
|
|
@@ -73,8 +72,7 @@
|
|
|
73
72
|
|
|
74
73
|
<dt>readMethod<span class="property-type">number</span></dt>
|
|
75
74
|
<dd> <b>Read Single Only</b>: use <code>readProperty</code></dd>
|
|
76
|
-
<dd> <b>Read
|
|
77
|
-
<dd> <b>Comprehensive Read</b>: try <code>readPropertyMultiple</code> twice with high and conservative query size, fallback to <code>readProperty</code> if failed</dd>
|
|
75
|
+
<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>
|
|
78
76
|
|
|
79
77
|
<dt>groupExportDeviceCount<span class="property-type">number</span></dt>
|
|
80
78
|
<dd> Emit <code>msg.payload</code> once discovered devices count reaches this limit</dd>
|