@halsystems/red-bacnet 2.2.0 → 2.2.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/common/core/constant.js +0 -2
- package/common/job/core.js +4 -6
- package/common/job/discover_device.js +32 -43
- package/common/job/discover_point.js +46 -59
- package/common/job/global_queue_manager.js +1 -1
- package/common/job/read_point.js +14 -23
- package/common/job/write_point.js +11 -21
- package/package.json +1 -1
- package/red-bacnet/discover_device.js +11 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.2-alpha.0]
|
|
4
|
+
### Added
|
|
5
|
+
- `CoalescedJobQueue` added `onStart` and `onStop` hooks
|
|
6
|
+
- auto sync to `red-bacnet-public` repo
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- `discover device` fixed `#IAmHandler` memory leak issue
|
|
10
|
+
- `CICD` Fixed broken pipeline due to package registry caching / propagation issue
|
|
11
|
+
|
|
12
|
+
## [2.2.1]
|
|
13
|
+
### Fixed
|
|
14
|
+
- Fixed unhandled promise rejections in BACnet `jobs` that could hang the job queue or crash Node-RED; errors are now reported instead.
|
|
15
|
+
|
|
3
16
|
## [2.2.0]
|
|
4
17
|
### Changed
|
|
5
18
|
- Matched v2.2.0-alpha.2 release
|
package/common/core/constant.js
CHANGED
|
@@ -31,7 +31,5 @@ module.exports = {
|
|
|
31
31
|
|
|
32
32
|
// job error
|
|
33
33
|
ERR_CONSTRUCT_ABSTRACT_JOB: 'Cannot construct abstract Job class',
|
|
34
|
-
ERR_ONSTART_NOT_IMPLEMENTED: 'OnStart method is not implemented.',
|
|
35
|
-
ERR_ONSTOP_NOT_IMPLEMENTED: 'OnStop method is not implemented.',
|
|
36
34
|
ERR_EXECUTE_NOT_IMPLEMENTED: 'Execute method is not implemented.',
|
|
37
35
|
}
|
package/common/job/core.js
CHANGED
|
@@ -3,8 +3,7 @@ require('./_alias.js');
|
|
|
3
3
|
|
|
4
4
|
const { print } = require('@root/common/core/util.js');
|
|
5
5
|
const {
|
|
6
|
-
ERR_CONSTRUCT_ABSTRACT_JOB,
|
|
7
|
-
ERR_ONSTOP_NOT_IMPLEMENTED, ERR_EXECUTE_NOT_IMPLEMENTED
|
|
6
|
+
ERR_CONSTRUCT_ABSTRACT_JOB, ERR_EXECUTE_NOT_IMPLEMENTED
|
|
8
7
|
} = require('@root/common/core/constant.js')
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -51,10 +50,8 @@ class BaseJob {
|
|
|
51
50
|
*
|
|
52
51
|
* @async
|
|
53
52
|
* @returns {Promise<void>}
|
|
54
|
-
* @throws {Error} If not implemented by subclass.
|
|
55
53
|
*/
|
|
56
54
|
async onStart() {
|
|
57
|
-
throw new Error(ERR_ONSTART_NOT_IMPLEMENTED);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
@@ -79,10 +76,8 @@ class BaseJob {
|
|
|
79
76
|
*
|
|
80
77
|
* @async
|
|
81
78
|
* @returns {Promise<void>}
|
|
82
|
-
* @throws {Error} If not implemented by subclass.
|
|
83
79
|
*/
|
|
84
80
|
async onStop() {
|
|
85
|
-
throw new Error(ERR_ONSTOP_NOT_IMPLEMENTED);
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
|
|
@@ -160,9 +155,12 @@ class CoalescedJobQueue {
|
|
|
160
155
|
|
|
161
156
|
const job = this.queue.splice(jobIndex, 1)[0];
|
|
162
157
|
try {
|
|
158
|
+
await job.task.onStart();
|
|
163
159
|
await job.task.execute();
|
|
164
160
|
} catch (error) {
|
|
165
161
|
console.error(`Error executing job ${job.id}:`, error);
|
|
162
|
+
} finally {
|
|
163
|
+
await job.task.onStop();
|
|
166
164
|
}
|
|
167
165
|
}
|
|
168
166
|
await new Promise(resolve => setTimeout(resolve, this.delay));
|
|
@@ -22,6 +22,7 @@ module.exports = {
|
|
|
22
22
|
knownDevices = new Set();
|
|
23
23
|
discoverList = [];
|
|
24
24
|
discoveredDevices = [];
|
|
25
|
+
#boundIAmHandler;
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Construct a DiscoverDeviceJob.
|
|
@@ -46,62 +47,50 @@ module.exports = {
|
|
|
46
47
|
this.highLimit = highLimit
|
|
47
48
|
this.whoIsTimeout = whoIsTimeout
|
|
48
49
|
this.name = name
|
|
50
|
+
this.#boundIAmHandler = this.#iAmHandler.bind(this)
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
async onStart() {
|
|
52
|
-
this.client.on(EVENT_I_AM, this.#
|
|
54
|
+
this.client.on(EVENT_I_AM, this.#boundIAmHandler)
|
|
53
55
|
}
|
|
54
56
|
async onStop() {
|
|
55
|
-
this.client.removeListener(EVENT_I_AM, this.#
|
|
57
|
+
this.client.removeListener(EVENT_I_AM, this.#boundIAmHandler)
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
async execute() {
|
|
59
|
-
|
|
60
|
-
this.knownDevices.clear()
|
|
61
|
+
this.knownDevices.clear();
|
|
61
62
|
this.discoverList = [];
|
|
62
63
|
this.discoveredDevices = [];
|
|
63
64
|
|
|
64
|
-
this.#updateProgress(0)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const device = this.discoveredDevices[i];
|
|
77
|
-
const { error, value: result } = bacnetDeviceSchema.validate(device);
|
|
78
|
-
|
|
79
|
-
if (error) {
|
|
80
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
81
|
-
this.name, `BACnet device schema validation error (${device.deviceId})`, error
|
|
82
|
-
))
|
|
83
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
84
|
-
this.name, `${ERR_SCHEMA_VALIDATION} (${device.deviceId})`, error
|
|
85
|
-
))
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
validatedDevices.push(result);
|
|
89
|
-
}
|
|
65
|
+
this.#updateProgress(0);
|
|
66
|
+
try {
|
|
67
|
+
this.client.whoIs({ net: this.network, lowLimit: this.lowLimit, highLimit: this.highLimit });
|
|
68
|
+
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, this.whoIsTimeout));
|
|
70
|
+
|
|
71
|
+
this.#updateProgress(10);
|
|
72
|
+
await this.#readDevices();
|
|
73
|
+
|
|
74
|
+
const validatedDevices = [];
|
|
75
|
+
for (const device of this.discoveredDevices) {
|
|
76
|
+
const { error, value: result } = bacnetDeviceSchema.validate(device);
|
|
90
77
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
}, this.whoIsTimeout);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)))
|
|
102
|
-
resolve();
|
|
78
|
+
if (error) {
|
|
79
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
80
|
+
this.name, `${ERR_SCHEMA_VALIDATION} (${device.deviceId})`, error
|
|
81
|
+
));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
validatedDevices.push(result);
|
|
103
85
|
}
|
|
104
|
-
|
|
86
|
+
|
|
87
|
+
this.msg.payload = validatedDevices;
|
|
88
|
+
this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)));
|
|
91
|
+
} finally {
|
|
92
|
+
this.#updateProgress(100);
|
|
93
|
+
}
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
/**
|
|
@@ -73,73 +73,60 @@ module.exports = {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async execute() {
|
|
76
|
-
this.#updateProgress('discovering')
|
|
76
|
+
this.#updateProgress('discovering');
|
|
77
|
+
try {
|
|
78
|
+
// validate devices
|
|
79
|
+
if (!Array.isArray(this.inputDevices)) {
|
|
80
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
81
|
+
this.name, ERR_INVALID_DATA_TYPE,
|
|
82
|
+
{
|
|
83
|
+
devices: this?.inputDevices,
|
|
84
|
+
expected: 'array',
|
|
85
|
+
example: EG_BACNET_DEVICES
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const deviceNameSet = new Set();
|
|
92
|
+
for (const device of this.inputDevices) {
|
|
93
|
+
const { error, value: result } = bacnetDeviceSchema.validate(
|
|
94
|
+
device,
|
|
95
|
+
{ stripUnknown: true }
|
|
96
|
+
);
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
// validate devices
|
|
81
|
-
if (!Array.isArray(this.inputDevices)) {
|
|
98
|
+
if (error) {
|
|
82
99
|
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
83
|
-
this.name,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expected: 'array',
|
|
87
|
-
example: EG_BACNET_DEVICES
|
|
88
|
-
})
|
|
89
|
-
)
|
|
90
|
-
this.#updateProgress('completed')
|
|
91
|
-
resolve();
|
|
92
|
-
return
|
|
100
|
+
this.name, ERR_SCHEMA_VALIDATION, error
|
|
101
|
+
));
|
|
102
|
+
continue;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const { error, value: result } = bacnetDeviceSchema.validate(
|
|
99
|
-
device,
|
|
100
|
-
{ stripUnknown: true }
|
|
101
|
-
);
|
|
102
|
-
// if device id is null, set to 4194303
|
|
103
|
-
if (result.deviceId == null)
|
|
104
|
-
result.deviceId = 4194303;
|
|
105
|
-
|
|
106
|
-
if (error) {
|
|
107
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
108
|
-
this.name, ERR_SCHEMA_VALIDATION, error
|
|
109
|
-
))
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
105
|
+
// if device id is null, set to 4194303
|
|
106
|
+
if (result.deviceId == null)
|
|
107
|
+
result.deviceId = 4194303;
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
deviceNameSet.add(result.deviceName);
|
|
121
|
-
|
|
122
|
-
// add device to list
|
|
123
|
-
this.devices.push(result);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// If no valid devices, complete immediately
|
|
127
|
-
if (this.devices.length === 0) {
|
|
128
|
-
this.#updateProgress('completed')
|
|
129
|
-
resolve();
|
|
130
|
-
return
|
|
109
|
+
// ensure no duplicate device name
|
|
110
|
+
if (deviceNameSet.has(result.deviceName)) {
|
|
111
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(
|
|
112
|
+
this.name, ERR_IGNORE_DUPLICATED_DEVICE_NAME, result
|
|
113
|
+
));
|
|
114
|
+
continue;
|
|
131
115
|
}
|
|
116
|
+
deviceNameSet.add(result.deviceName);
|
|
132
117
|
|
|
133
|
-
|
|
134
|
-
this.#readAllDevicePoints().then(() => {
|
|
135
|
-
this.#updateProgress('completed')
|
|
136
|
-
resolve();
|
|
137
|
-
});
|
|
138
|
-
} catch (err) {
|
|
139
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)))
|
|
140
|
-
resolve();
|
|
118
|
+
this.devices.push(result);
|
|
141
119
|
}
|
|
142
|
-
|
|
120
|
+
|
|
121
|
+
// If no valid devices, nothing to do
|
|
122
|
+
if (this.devices.length === 0) return;
|
|
123
|
+
|
|
124
|
+
await this.#readAllDevicePoints();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)));
|
|
127
|
+
} finally {
|
|
128
|
+
this.#updateProgress('completed');
|
|
129
|
+
}
|
|
143
130
|
}
|
|
144
131
|
|
|
145
132
|
async #readAllDevicePoints() {
|
package/common/job/read_point.js
CHANGED
|
@@ -39,28 +39,18 @@ module.exports = {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async execute() {
|
|
42
|
-
this.#updateProgress(0)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.#readPoints().then(() => {
|
|
55
|
-
this.#exportPoints();
|
|
56
|
-
this.#updateProgress(100)
|
|
57
|
-
resolve();
|
|
58
|
-
});
|
|
59
|
-
} catch (err) {
|
|
60
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)))
|
|
61
|
-
resolve();
|
|
62
|
-
}
|
|
63
|
-
})
|
|
42
|
+
this.#updateProgress(0);
|
|
43
|
+
try {
|
|
44
|
+
if (!this.#validateData()) return;
|
|
45
|
+
|
|
46
|
+
this.#updateProgress(10);
|
|
47
|
+
await this.#readPoints();
|
|
48
|
+
this.#exportPoints();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)));
|
|
51
|
+
} finally {
|
|
52
|
+
this.#updateProgress(100);
|
|
53
|
+
}
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
#validateData() {
|
|
@@ -284,8 +274,9 @@ module.exports = {
|
|
|
284
274
|
}
|
|
285
275
|
|
|
286
276
|
// add to result
|
|
277
|
+
const num = typeof value === 'number' ? value : null;
|
|
287
278
|
this.pointsList[`${p.deviceName}${this.devicePointSeparator}${p.pointName}`] = {
|
|
288
|
-
value: facetObj.precision != null ? +value.toFixed(facetObj.precision) : value,
|
|
279
|
+
value: (num != null && facetObj.precision != null) ? +value.toFixed(facetObj.precision) : value,
|
|
289
280
|
fvalue: fvalue,
|
|
290
281
|
}
|
|
291
282
|
if (err)
|
|
@@ -43,27 +43,17 @@ module.exports = {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async execute() {
|
|
46
|
-
this.#updateProgress(0)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.#writePoints().then(() => {
|
|
59
|
-
this.#updateProgress(100)
|
|
60
|
-
resolve();
|
|
61
|
-
});
|
|
62
|
-
} catch (err) {
|
|
63
|
-
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)))
|
|
64
|
-
resolve();
|
|
65
|
-
}
|
|
66
|
-
})
|
|
46
|
+
this.#updateProgress(0);
|
|
47
|
+
try {
|
|
48
|
+
if (!this.#validateData()) return;
|
|
49
|
+
|
|
50
|
+
this.#updateProgress(10);
|
|
51
|
+
await this.#writePoints();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
this.eventEmitter.emit(EVENT_ERROR, errMsg(this.name, ERR_GENERIC, getErrMsg(err)));
|
|
54
|
+
} finally {
|
|
55
|
+
this.#updateProgress(100);
|
|
56
|
+
}
|
|
67
57
|
}
|
|
68
58
|
|
|
69
59
|
#validateData() {
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ const { DiscoverDeviceJob } = require('@root/common/job/discover_device.js')
|
|
|
9
9
|
const { getQueue } = require('@root/common/job/global_queue_manager.js')
|
|
10
10
|
const { generateUniqueJobId } = require('@root/common/func.js')
|
|
11
11
|
const {
|
|
12
|
-
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT
|
|
12
|
+
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT
|
|
13
13
|
} = require('@root/common/core/constant.js')
|
|
14
14
|
|
|
15
15
|
|
|
@@ -37,18 +37,6 @@ module.exports = function (RED) {
|
|
|
37
37
|
|
|
38
38
|
// configure job queue - use global queue
|
|
39
39
|
this.job = getQueue();
|
|
40
|
-
|
|
41
|
-
// init task
|
|
42
|
-
this.task = new DiscoverDeviceJob(
|
|
43
|
-
this.client,
|
|
44
|
-
this.#eventEmitter,
|
|
45
|
-
null,
|
|
46
|
-
this.network,
|
|
47
|
-
this.lowLimit,
|
|
48
|
-
this.highLimit,
|
|
49
|
-
this.whoIsTimeout,
|
|
50
|
-
);
|
|
51
|
-
this.task.onStart();
|
|
52
40
|
}
|
|
53
41
|
|
|
54
42
|
#subscribeListeners() {
|
|
@@ -57,11 +45,6 @@ module.exports = function (RED) {
|
|
|
57
45
|
*/
|
|
58
46
|
// @ts-ignore
|
|
59
47
|
this.on(EVENT_INPUT, async function (msg) {
|
|
60
|
-
/*
|
|
61
|
-
BACnet iAm handler is causing memory leak due to unable to detach after use
|
|
62
|
-
as a result, workaround is introduce before strategy to reuse discoverDeviceJob instead
|
|
63
|
-
of creating new one every time onInput is triggered
|
|
64
|
-
*/
|
|
65
48
|
const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : this.jobId;
|
|
66
49
|
|
|
67
50
|
if (this.job.queue.map(item => item.id).includes(jobId)) {
|
|
@@ -70,14 +53,19 @@ module.exports = function (RED) {
|
|
|
70
53
|
return;
|
|
71
54
|
}
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
const task = new DiscoverDeviceJob(
|
|
57
|
+
this.client,
|
|
58
|
+
this.#eventEmitter,
|
|
59
|
+
msg,
|
|
60
|
+
this.network,
|
|
61
|
+
this.lowLimit,
|
|
62
|
+
this.highLimit,
|
|
63
|
+
this.whoIsTimeout,
|
|
64
|
+
);
|
|
77
65
|
|
|
78
66
|
this.job.addJob({
|
|
79
67
|
id: jobId,
|
|
80
|
-
task:
|
|
68
|
+
task: task,
|
|
81
69
|
priority: Number.isFinite(msg.priority) ? msg.priority : 5,
|
|
82
70
|
});
|
|
83
71
|
|
|
@@ -85,11 +73,6 @@ module.exports = function (RED) {
|
|
|
85
73
|
this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
|
|
86
74
|
});
|
|
87
75
|
|
|
88
|
-
// @ts-ignore
|
|
89
|
-
this.on(EVENT_CLOSE, () => {
|
|
90
|
-
this.task.onStop();
|
|
91
|
-
})
|
|
92
|
-
|
|
93
76
|
this.#eventEmitter.on(EVENT_OUTPUT, (msg) => {
|
|
94
77
|
// @ts-ignore
|
|
95
78
|
this.send(msg);
|