@halsystems/red-bacnet 1.0.22 → 1.1.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 +15 -0
- package/common/bacnet.js +103 -14
- package/common/core/concurrent.js +2 -2
- package/common/job/discover_device.js +13 -20
- package/common/job/discover_point.js +66 -17
- package/common/job/read_point.js +0 -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,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0]
|
|
4
|
+
### Changed
|
|
5
|
+
- Implemented auto resize batch size when querying using readPropertyMultiple to improve performance
|
|
6
|
+
- Added readPropertyMultiple to read object list to reduce query speed
|
|
7
|
+
- Changed strategy when discovering device name, implemented delay instead of burst to reduce traffic congestion and yield better result
|
|
8
|
+
- Force use readProperty to read multistate object to improve result
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Failure to discover device name for Forwarded NPDU devices
|
|
12
|
+
- Missed bacnet objects when using discover point
|
|
13
|
+
|
|
14
|
+
## [1.0.22]
|
|
15
|
+
### Fixed
|
|
16
|
+
- Removed console.log in Read Point
|
|
17
|
+
|
|
3
18
|
## [1.0.22]
|
|
4
19
|
### Fixed
|
|
5
20
|
- Read Point supports string reading
|
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,98 @@ 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
|
+
console.log(item)
|
|
119
|
+
} catch (err) {
|
|
120
|
+
void err
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
42
127
|
},
|
|
43
128
|
|
|
44
129
|
/**
|
|
45
130
|
* Smart read multiple properties from a BACnet device with concurrent read feature and various read methods
|
|
46
131
|
* readMethod
|
|
47
132
|
* 0: use readProperty only
|
|
48
|
-
* 1: try readPropertyMultiple
|
|
49
|
-
* 2: try readPropertyMultiple twice with high and conservative query size, fallback to readProperty if failed
|
|
133
|
+
* 1: try readPropertyMultiple, reduce query size if failed, and fallback to readProperty if query size reduced to 1
|
|
50
134
|
* @param {BacnetClient} client
|
|
51
135
|
* @param {object} device
|
|
52
136
|
* eg:{
|
|
@@ -95,16 +179,12 @@ module.exports = {
|
|
|
95
179
|
if (readMethod > 0) {
|
|
96
180
|
// calculate batch sizes
|
|
97
181
|
if (device.maxApdu == null) // default batch size if null or undefined
|
|
98
|
-
batchSizes.add(20)
|
|
182
|
+
batchSizes.add(20);
|
|
99
183
|
else {
|
|
100
|
-
const perValueBytes = [30
|
|
184
|
+
const perValueBytes = [30] // typical numeric point is 17 byte
|
|
101
185
|
for (let x = 0; x < perValueBytes.length; x++)
|
|
102
186
|
batchSizes.add(Math.trunc(device.maxApdu / perValueBytes[x]))
|
|
103
187
|
}
|
|
104
|
-
|
|
105
|
-
// if readMethod === 1
|
|
106
|
-
if (readMethod === 1 && batchSizes.size > 1)
|
|
107
|
-
batchSizes = new Set([...batchSizes].slice(-1));
|
|
108
188
|
}
|
|
109
189
|
|
|
110
190
|
for (const batchSize of batchSizes) {
|
|
@@ -112,6 +192,7 @@ module.exports = {
|
|
|
112
192
|
let first = true
|
|
113
193
|
let reqArrBatch
|
|
114
194
|
let batchCount
|
|
195
|
+
let currBatchSize = batchSize
|
|
115
196
|
|
|
116
197
|
do { // use current batch size until query failed
|
|
117
198
|
// get ready for next batch
|
|
@@ -132,7 +213,7 @@ module.exports = {
|
|
|
132
213
|
if (reqArrIndexNext >= reqArr.length)
|
|
133
214
|
break
|
|
134
215
|
|
|
135
|
-
if (batchCount + reqArr[reqArrIndexNext].properties.length >
|
|
216
|
+
if (batchCount + reqArr[reqArrIndexNext].properties.length > currBatchSize)
|
|
136
217
|
break
|
|
137
218
|
|
|
138
219
|
reqArrBatch.push(reqArr[reqArrIndexNext])
|
|
@@ -143,6 +224,14 @@ module.exports = {
|
|
|
143
224
|
// read batch block
|
|
144
225
|
const value = await module.exports.readPropertyMultple(client, device, reqArrBatch)
|
|
145
226
|
.catch(() => {
|
|
227
|
+
// Reduce batch size by half (minimum 1)
|
|
228
|
+
currBatchSize = Math.max(1, Math.floor(currBatchSize / 2))
|
|
229
|
+
|
|
230
|
+
// If batch size is too small, mark as unhealthy
|
|
231
|
+
if (currBatchSize <= 1) {
|
|
232
|
+
healthy = false
|
|
233
|
+
}
|
|
234
|
+
|
|
146
235
|
reqArrIndexNext = reqArrIndex
|
|
147
236
|
healthy = false
|
|
148
237
|
})
|
|
@@ -196,7 +285,7 @@ module.exports = {
|
|
|
196
285
|
return value;
|
|
197
286
|
} catch (err) {
|
|
198
287
|
failedCount++;
|
|
199
|
-
if (readMethod <
|
|
288
|
+
if (readMethod < 1 && failedCount >= singleReadFailedRetry)
|
|
200
289
|
throw err
|
|
201
290
|
}
|
|
202
291
|
}
|
|
@@ -317,7 +406,7 @@ module.exports = {
|
|
|
317
406
|
* eg: 85
|
|
318
407
|
* @returns object
|
|
319
408
|
* eg: { len: 11, objectId: { type: 19, instance: 1 },
|
|
320
|
-
* property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }
|
|
409
|
+
* property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
|
|
321
410
|
* @async
|
|
322
411
|
*/
|
|
323
412
|
readProperty: async function (client, device, objectId, propertyId) {
|
|
@@ -485,4 +574,4 @@ module.exports = {
|
|
|
485
574
|
});
|
|
486
575
|
});
|
|
487
576
|
},
|
|
488
|
-
}
|
|
577
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -43,8 +44,7 @@ module.exports = {
|
|
|
43
44
|
await Promise.race(executing);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
// if (typeof delay === 'function') await delay(1);
|
|
47
|
+
await delay(50)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
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)
|
|
@@ -35,10 +35,14 @@ const multiStateObjectTypes = [
|
|
|
35
35
|
baEnum.ObjectType.MULTI_STATE_OUTPUT,
|
|
36
36
|
baEnum.ObjectType.MULTI_STATE_VALUE
|
|
37
37
|
]
|
|
38
|
+
// const scheduleObjectTypes = [
|
|
39
|
+
// baEnum.ObjectType.SCHEDULE,
|
|
40
|
+
// ];
|
|
38
41
|
const supportedObjectTypes = [
|
|
39
42
|
...analogObjectTypes,
|
|
40
43
|
...binaryObjectTypes,
|
|
41
|
-
...multiStateObjectTypes
|
|
44
|
+
...multiStateObjectTypes,
|
|
45
|
+
// ...scheduleObjectTypes
|
|
42
46
|
]
|
|
43
47
|
|
|
44
48
|
// ---------------------------------- export ----------------------------------
|
|
@@ -236,6 +240,8 @@ const readPoints = async (
|
|
|
236
240
|
device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead
|
|
237
241
|
) => {
|
|
238
242
|
const points = [];
|
|
243
|
+
|
|
244
|
+
// First query - get basic properties (excluding STATE_TEXT)
|
|
239
245
|
const reqArr = objects.map(obj => ({
|
|
240
246
|
objectId: { type: obj.value.type, instance: obj.value.instance },
|
|
241
247
|
properties: [
|
|
@@ -246,33 +252,80 @@ const readPoints = async (
|
|
|
246
252
|
{ id: baEnum.PropertyIdentifier.INACTIVE_TEXT },
|
|
247
253
|
{ id: baEnum.PropertyIdentifier.ACTIVE_TEXT }
|
|
248
254
|
] : []),
|
|
249
|
-
...(multiStateObjectTypes.includes(obj.value.type) ? [{ id: baEnum.PropertyIdentifier.STATE_TEXT }] : [])
|
|
250
255
|
]
|
|
251
256
|
}));
|
|
252
257
|
|
|
253
258
|
try {
|
|
259
|
+
// First request - get basic properties
|
|
254
260
|
const result = await smartReadProperty(
|
|
255
261
|
client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50
|
|
256
262
|
);
|
|
263
|
+
|
|
264
|
+
// Process basic properties first
|
|
257
265
|
result.forEach(i => {
|
|
258
|
-
/** i example
|
|
259
|
-
{
|
|
260
|
-
objectId: { type: 1, instance: 0 },
|
|
261
|
-
values: [
|
|
262
|
-
{ id: 85, index: 4294967295, value: [Array] },
|
|
263
|
-
{ id: 77, index: 4294967295, value: [Array] }
|
|
264
|
-
]
|
|
265
|
-
}
|
|
266
|
-
*/
|
|
267
266
|
const point = processPoint(i, device.deviceName);
|
|
268
267
|
points.push(point);
|
|
269
268
|
});
|
|
270
|
-
|
|
269
|
+
|
|
270
|
+
// Second query - get STATE_TEXT for multistate objects only
|
|
271
|
+
const multistateObjects = objects.filter(obj => multiStateObjectTypes.includes(obj.value.type));
|
|
272
|
+
|
|
273
|
+
if (multistateObjects.length > 0) {
|
|
274
|
+
const stateTextReqArr = multistateObjects.map(obj => ({
|
|
275
|
+
objectId: { type: obj.value.type, instance: obj.value.instance },
|
|
276
|
+
properties: [{ id: baEnum.PropertyIdentifier.STATE_TEXT }]
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// Force use single read on state text to reduce loss especially long text
|
|
281
|
+
const stateTextResult = await smartReadProperty(
|
|
282
|
+
client, device, stateTextReqArr, 0, 1, 50
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Update points with STATE_TEXT
|
|
286
|
+
stateTextResult.forEach(stateTextData => {
|
|
287
|
+
const matchingPoint = points.find(point =>
|
|
288
|
+
point.bacType === stateTextData.objectId.type &&
|
|
289
|
+
point.bacInstance === stateTextData.objectId.instance
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (matchingPoint) {
|
|
293
|
+
updatePointWithStateText(matchingPoint, stateTextData);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
} catch (stateTextError) {
|
|
297
|
+
// Log the error but don't fail the entire operation
|
|
298
|
+
eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
299
|
+
name,
|
|
300
|
+
'Warning: Failed to read STATE_TEXT properties',
|
|
301
|
+
stateTextError
|
|
302
|
+
));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return points;
|
|
271
307
|
} catch (error) {
|
|
272
308
|
eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
|
|
273
309
|
}
|
|
274
310
|
};
|
|
275
311
|
|
|
312
|
+
const updatePointWithStateText = (point, stateTextData) => {
|
|
313
|
+
const stateTextProp = stateTextData.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT);
|
|
314
|
+
const states = stateTextProp?.value;
|
|
315
|
+
|
|
316
|
+
if (Array.isArray(states)) {
|
|
317
|
+
// Update the facets with state text information
|
|
318
|
+
const stateTextFacet = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
|
|
319
|
+
|
|
320
|
+
// If point already has facets, append; otherwise, set new facets
|
|
321
|
+
if (point.facets) {
|
|
322
|
+
point.facets += `;${stateTextFacet}`;
|
|
323
|
+
} else {
|
|
324
|
+
point.facets = stateTextFacet;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
276
329
|
const processPoint = (i, deviceName) => {
|
|
277
330
|
const point = {
|
|
278
331
|
deviceName,
|
|
@@ -301,12 +354,8 @@ const setPointFacets = (point, i) => {
|
|
|
301
354
|
const inactive = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.INACTIVE_TEXT)?.value?.[0]?.value;
|
|
302
355
|
const active = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.ACTIVE_TEXT)?.value?.[0]?.value;
|
|
303
356
|
point.facets = `falseText:${inactive ? inactive : 'false'};trueText:${active ? active : 'true'}`;
|
|
304
|
-
} else if (multiStateObjectTypes.includes(point.bacType)) {
|
|
305
|
-
const states = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT)?.value;
|
|
306
|
-
if (Array.isArray(states)) {
|
|
307
|
-
point.facets = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
|
|
308
|
-
}
|
|
309
357
|
}
|
|
358
|
+
// STATE_TEXT handling removed from here - now handled separately
|
|
310
359
|
};
|
|
311
360
|
|
|
312
361
|
const setPointValues = (point, i) => {
|
package/common/job/read_point.js
CHANGED
|
@@ -247,7 +247,6 @@ module.exports = {
|
|
|
247
247
|
|
|
248
248
|
// format point
|
|
249
249
|
const facetObj = facetsStrToObj(p.facets);
|
|
250
|
-
console.log(value)
|
|
251
250
|
if (facetObj.trueText != null || facetObj.falseText != null) { // boolean
|
|
252
251
|
fvalue = fvalue ? facetObj.trueText : facetObj.falseText
|
|
253
252
|
} else if (facetObj.range != null) { // enum
|
|
@@ -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>
|