@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 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
@@ -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
  }
@@ -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, ERR_ONSTART_NOT_IMPLEMENTED,
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.#iAmHandler.bind(this))
54
+ this.client.on(EVENT_I_AM, this.#boundIAmHandler)
53
55
  }
54
56
  async onStop() {
55
- this.client.removeListener(EVENT_I_AM, this.#iAmHandler.bind(this))
57
+ this.client.removeListener(EVENT_I_AM, this.#boundIAmHandler)
56
58
  }
57
59
 
58
60
  async execute() {
59
- // discover device job will be reuse
60
- this.knownDevices.clear()
61
+ this.knownDevices.clear();
61
62
  this.discoverList = [];
62
63
  this.discoveredDevices = [];
63
64
 
64
- this.#updateProgress(0)
65
-
66
- return new Promise((resolve, reject) => {
67
- try {
68
- this.client.whoIs({ net: this.network, lowLimit: this.lowLimit, highLimit: this.highLimit })
69
-
70
- setTimeout(() => {
71
- this.#updateProgress(10)
72
-
73
- this.#readDevices().then(() => {
74
- let validatedDevices = [];
75
- for (let i = 0; i < this.discoveredDevices.length; i++) {
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
- this.msg.payload = validatedDevices;
92
- this.eventEmitter.emit(EVENT_OUTPUT, this.msg);
93
- this.#updateProgress(100)
94
- resolve();
95
- }).catch(error => {
96
- this.#updateProgress(100)
97
- reject(error);
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
- return new Promise((resolve) => {
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, ERR_INVALID_DATA_TYPE,
84
- {
85
- devices: this?.inputDevices,
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
- let deviceNameSet = new Set();
96
- for (let i = 0; i < this.inputDevices.length; i++) {
97
- const device = this.inputDevices[i];
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
- // ensure no duplicate device name
114
- if (deviceNameSet.has(result.deviceName)) {
115
- this.eventEmitter.emit(EVENT_ERROR, errMsg(
116
- this.name, ERR_IGNORE_DUPLICATED_DEVICE_NAME, result
117
- ))
118
- continue
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
- // read all device points
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() {
@@ -72,7 +72,7 @@ module.exports = {
72
72
  getQueue() {
73
73
  if (!globalQueue) {
74
74
  stopping = false;
75
- globalQueue = new CoalescedJobQueue(100, true, 5);
75
+ globalQueue = new CoalescedJobQueue(50, true, 5);
76
76
  }
77
77
 
78
78
  if (!globalQueue.isRunning && !stopping) {
@@ -39,28 +39,18 @@ module.exports = {
39
39
  }
40
40
 
41
41
  async execute() {
42
- this.#updateProgress(0)
43
-
44
- return new Promise((resolve) => {
45
- try {
46
- if (!this.#validateData()) {
47
- this.#updateProgress(100)
48
- resolve()
49
- return
50
- }
51
-
52
- this.#updateProgress(10)
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
- return new Promise((resolve) => {
49
- try {
50
- if (!this.#validateData()) {
51
- this.#updateProgress(100)
52
- resolve()
53
- return
54
- }
55
-
56
- this.#updateProgress(10)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "2.2.0",
3
+ "version": "2.2.2-alpha.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -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, EVENT_CLOSE
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
- this.task.network = (msg.network === undefined) ? this.network : msg.network;
74
- this.task.lowLimit = (msg.lowLimit === undefined) ? this.lowLimit : msg.lowLimit;
75
- this.task.highLimit = (msg.highLimit === undefined) ? this.highLimit : msg.highLimit;
76
- this.task.msg = msg;
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: this.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);