nodejs-poolcontroller 8.3.0 → 8.4.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.
Files changed (105) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -63
  7. package/.github/workflows/ghcr-publish.yml +67 -67
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -284
  11. package/Dockerfile +62 -62
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -309
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +0 -0
  23. package/config/VersionCheck.ts +0 -0
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3701
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1944
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1255
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1406
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1193 -1193
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -352
  80. package/docker-compose.yml +31 -31
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -58
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -32
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
@@ -1,1255 +1,1272 @@
1
- /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
- Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as
7
- published by the Free Software Foundation, either version 3 of the
8
- License, or (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- */
18
- import { AutoDetectTypes } from '@serialport/bindings-cpp';
19
- import { EventEmitter } from 'events';
20
- import * as net from 'net';
21
- import { SerialPort, SerialPortMock, SerialPortOpenOptions } from 'serialport';
22
- import { setTimeout } from 'timers';
23
- import { config } from '../../config/Config';
24
- import { logger } from '../../logger/Logger';
25
- import { webApp } from "../../web/Server";
26
- import { utils } from "../Constants";
27
- import { sys } from "../Equipment";
28
- import { InvalidEquipmentDataError, InvalidOperationError, OutboundMessageError } from '../Errors';
29
- import { state } from "../State";
30
- import { Inbound, Message, Outbound, Response } from './messages/Messages';
31
- import { sl } from './ScreenLogic';
32
- const extend = require("extend");
33
- export class Connection {
34
- constructor() { }
35
- public rs485Ports: RS485Port[] = [];
36
- public get mock(): boolean {
37
- let port = this.findPortById(0);
38
- return typeof port !== 'undefined' && port.mock ? true : false;
39
- }
40
- public isPortEnabled(portId: number) {
41
- let port: RS485Port = this.findPortById(portId);
42
- return typeof port === 'undefined' ? false : port.enabled && port.isOpen && !port.closing;
43
- }
44
- public async deleteAuxPort(data: any): Promise<any> {
45
- try {
46
- let portId = parseInt(data.portId, 10);
47
- if (isNaN(portId)) return Promise.reject(new InvalidEquipmentDataError(`A valid port id was not provided to be deleted`, 'RS485Port', data.id));
48
- if (portId === 0) return Promise.reject(new InvalidEquipmentDataError(`You may not delete the primart RS485 Port`, 'RS485Port', data.id));
49
- let port = this.findPortById(portId);
50
- this.removePortById(portId);
51
- let section = `controller.comms` + (portId === 0 ? '' : portId);
52
- let cfg = config.getSection(section, {});
53
- config.removeSection(section);
54
- state.equipment.messages.removeItemByCode(`rs485:${portId}:connection`);
55
- return cfg;
56
- } catch (err) { logger.error(`Error deleting aux port`) }
57
- }
58
- public async setScreenlogicAsync(data: any) {
59
- let ccfg = config.getSection('controller.screenlogic');
60
- if (typeof data.type === 'undefined' || data.type !== 'local' || data.type !== 'remote') return Promise.reject(new InvalidEquipmentDataError(`Invalid Screenlogic type (${data.type}). Allowed values are 'local' or 'remote'`, 'Screenlogic', 'screenlogic'));
61
- if ((data.address as string).slice(8) !== 'Pentair:') return Promise.reject(new InvalidEquipmentDataError(`Invalid address (${data.address}). Must start with 'Pentair:'`, 'Screenlogic', 'screenlogic'));
62
- }
63
-
64
- public async setPortAsync(data: any): Promise<any> {
65
- try {
66
-
67
- let ccfg = config.getSection('controller');
68
- let pConfig;
69
- let portId;
70
- let maxId = -1;
71
- for (let sec in ccfg) {
72
- if (sec.startsWith('comms')) {
73
- let p = ccfg[sec];
74
- maxId = Math.max(p.portId, maxId);
75
- if (p.portId === data.portId) pConfig = p;
76
- }
77
- }
78
- if (typeof pConfig === 'undefined') {
79
- // We are adding a new one.
80
- if (data.portId === -1 || typeof data.portId === 'undefined') portId = maxId + 1;
81
- else portId = data.portId;
82
- }
83
- else portId = pConfig.portId;
84
- if (isNaN(portId) || portId < 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid port id defined ${portId}`, 'RS485Port', data.portId));
85
- let section = `controller.comms` + (portId === 0 ? '' : portId);
86
- // Lets set the config data.
87
- let pdata = config.getSection(section, {
88
- portId: portId,
89
- type: 'local',
90
- rs485Port: "/dev/ttyUSB0",
91
- portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
92
- netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 1000 },
93
- mock: false,
94
- netConnect: false,
95
- netHost: "raspberrypi",
96
- netPort: 9801,
97
- inactivityRetry: 10
98
- });
99
- if (portId === 0) {
100
- pdata.screenlogic = {
101
- connectionType: "local",
102
- systemName: "Pentair: 00-00-00",
103
- password: 1234
104
- }
105
- }
106
-
107
- pdata.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : utils.makeBool(pdata.enabled);
108
- pdata.type = data.type;
109
- pdata.netConnect = data.type === 'network' || data.type === 'netConnect'; // typeof data.netConnect !== 'undefined' ? utils.makeBool(data.netConnect) : utils.makeBool(pdata.netConnect);
110
- pdata.rs485Port = typeof data.rs485Port !== 'undefined' ? data.rs485Port : pdata.rs485Port;
111
- pdata.inactivityRetry = typeof data.inactivityRetry === 'number' ? data.inactivityRetry : pdata.inactivityRetry;
112
- pdata.mock = data.mock; // typeof data.mockPort !== 'undefined' ? utils.makeBool(data.mockPort) : utils.makeBool(pdata.mockPort);
113
- if (pdata.mock) { pdata.rs485Port = 'MOCK_PORT'; }
114
- if (pdata.type === 'netConnect') { // (pdata.netConnect) {
115
- pdata.netHost = typeof data.netHost !== 'undefined' ? data.netHost : pdata.netHost;
116
- pdata.netPort = typeof data.netPort === 'number' ? data.netPort : pdata.netPort;
117
- }
118
- if (typeof data.portSettings !== 'undefined') {
119
- pdata.portSettings = extend(true, { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false }, pdata.portSettings, data.portSettings);
120
- }
121
- if (typeof data.netSettings !== 'undefined') {
122
- pdata.netSettings = extend(true, { keepAlive: false, allowHalfOpen: false, keepAliveInitialDelay: 10000 }, pdata.netSettings, data.netSettings);
123
- }
124
- if (pdata.type === 'screenlogic') {
125
- let password = data.screenlogic.password.toString();
126
- let regx = /Pentair: (?:(?:\d|[A-Z])(?:\d|[A-Z])-){2}(?:\d|[A-Z])(?:\d|[A-Z])/g;
127
- let type = data.screenlogic.connectionType;
128
- let systemName = data.screenlogic.systemName;
129
- if (type !== 'remote' && type !== 'local') return Promise.reject(new InvalidEquipmentDataError(`An invalid type was supplied for Screenlogic ${type}. Must be remote or local.`, 'Screenlogic', data));
130
- if (systemName.match(regx) === null) return Promise.reject(new InvalidEquipmentDataError(`An invalid system name was supplied for Screenlogic ${systemName}}. Must be in the format 'Pentair: xx-xx-xx'.`, 'Screenlogic', data));
131
- if (password.length !== 4) return Promise.reject(new InvalidEquipmentDataError(`An invalid password was supplied for Screenlogic ${password}. (Length must be <= 4)}`, 'Screenlogic', data));
132
- pdata.screenlogic = data.screenlogic;
133
- }
134
- let existing = this.findPortById(portId);
135
- if (typeof existing !== 'undefined')
136
- if (existing.type === 'screenlogic' || sl.enabled) {
137
- await sl.closeAsync();
138
- }
139
- else {
140
- if (!await existing.closeAsync()) {
141
- existing.closing = false; // if closing fails, reset flag so user can try again
142
- return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port (Try to save the port again as it usually works the second time).`, 'setPortAsync'));
143
- }
144
- }
145
- config.setSection(section, pdata);
146
- let cfg = config.getSection(section, {
147
- type: 'local',
148
- rs485Port: "/dev/ttyUSB0",
149
- portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
150
- netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 5 },
151
- mock: false,
152
- netConnect: false,
153
- netHost: "raspberrypi",
154
- netPort: 9801,
155
- inactivityRetry: 10
156
- });
157
- if (portId === 0) {
158
- cfg.screenlogic = {
159
- connectionType: "local",
160
- systemName: "Pentair: 00-00-00",
161
- password: 1234
162
- }
163
- }
164
- existing = this.getPortByCfg(cfg);
165
-
166
- if (typeof existing !== 'undefined') {
167
- if (pdata.type === 'screenlogic') {
168
- await sl.openAsync();
169
- }
170
- else {
171
- existing.reconnects = 0;
172
- //existing.emitPortStats();
173
- if (!await existing.openAsync(cfg)) {
174
- if (cfg.netConnect) return Promise.reject(new InvalidOperationError(`Unable to open Socat Connection to ${pdata.netHost}`, 'setPortAsync'));
175
- return Promise.reject(new InvalidOperationError(`Unable to open RS485 port ${pdata.rs485Port}`, 'setPortAsync'));
176
- }
177
- }
178
- }
179
- return cfg;
180
- } catch (err) { return Promise.reject(err); }
181
- }
182
- public async stopAsync() {
183
- try {
184
- for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
185
- let port = this.rs485Ports[i];
186
- await port.closeAsync();
187
- }
188
- logger.info(`Closed all serial communications connection.`);
189
- } catch (err) { logger.error(`Error closing comms connection: ${err.message} `); }
190
- }
191
- public async initAsync() {
192
- try {
193
- // So now that we are now allowing multiple comm ports we need to initialize each one. We are keeping the comms section from the config.json
194
- // simply because I have no idea what the Docker folks do with this. So the default comms will be the one with an OCP or if there are no aux ports.
195
- let cfg = config.getSection('controller');
196
- for (let section in cfg) {
197
- if (section.startsWith('comms')) {
198
- let c = cfg[section];
199
- if (typeof c.type === 'undefined') {
200
- let type = 'local';
201
- if (c.mock) type = 'mock';
202
- else if (c.netConnect) type = 'network';
203
- config.setSection(`controller.${section}`, c);
204
- console.log(section);
205
- console.log(c);
206
- }
207
- let port = new RS485Port(c);
208
- // Alright now lets do some conversion of the existing data.
209
-
210
- this.rs485Ports.push(port);
211
- await port.openAsync();
212
- }
213
- }
214
- } catch (err) { logger.error(`Error initializing RS485 ports ${err.message}`); }
215
- }
216
- public findPortById(portId?: number): RS485Port { return this.rs485Ports.find(elem => elem.portId === (portId || 0)); }
217
- public async removePortById(portId: number) {
218
- for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
219
- let port = this.rs485Ports[i];
220
- if (port.portId === portId) {
221
- await port.closeAsync();
222
- // Don't remove the primary port. You cannot delete this one.
223
- if (portId !== 0) this.rs485Ports.splice(i, 1);
224
- }
225
- }
226
- }
227
- public
228
- getPortByCfg(cfg: any) {
229
- let port = this.findPortById(cfg.portId || 0);
230
- if (typeof port === 'undefined') {
231
- port = new RS485Port(cfg);
232
- this.rs485Ports.push(port);
233
- }
234
- return port;
235
- }
236
- public async listInstalledPorts(): Promise<any> {
237
- try {
238
- let ports = [];
239
- // So now that we are now allowing multiple comm ports we need to initialize each one. We are keeping the comms section from the config.json
240
- // simply because I have no idea what the Docker folks do with this. So the default comms will be the one with an OCP or if there are no aux ports.
241
- let cfg = config.getSection('controller');
242
- for (let section in cfg) {
243
- if (section.startsWith('comms')) {
244
- let port = config.getSection(`controller.${section}`);
245
- if (port.portId === 0) port.name = 'Primary';
246
- else port.name = `Aux${port.portId}`;
247
- let p = this.findPortById(port.portId);
248
- port.isOpen = typeof p !== 'undefined' ? p.isOpen : false;
249
- ports.push(port);
250
- }
251
- }
252
- return ports;
253
- } catch (err) { logger.error(`Error listing installed RS485 ports ${err.message}`); }
254
-
255
- }
256
- private getBroadcastPorts(currPort: RS485Port) {
257
- // if an ANSLQ25 controller is present, broadcast outbound writes to all other ports that are not mock or dedicated for a pump or chlor
258
- let anslq25port = sys.anslq25.portId;
259
- let duplicateTo: number[] = [];
260
- if (anslq25port >= 0) {
261
- let ports = this.rs485Ports;
262
- for (let i = 0; i < ports.length; i++) {
263
- // if (ports[i].mock) continue;
264
- if (ports[i].portId === currPort.portId) continue;
265
- if (ports[i].portId === anslq25port) continue; // don't resend
266
- if (!ports[i].isOpen) continue;
267
- duplicateTo.push(ports[i].portId);
268
- }
269
- let pumps = sys.pumps.get();
270
- for (let i = 0; i < pumps.length; i++) {
271
- if (pumps[i].portId === currPort.portId ||
272
- pumps[i].portId === anslq25port) {
273
- if (duplicateTo.includes(pumps[i].portId)) duplicateTo.splice(duplicateTo.indexOf(pumps[i].portId, 1));
274
- }
275
- }
276
- let chlors = sys.chlorinators.get();
277
- for (let i = 0; i < chlors.length; i++) {
278
- if (chlors[i].portId === currPort.portId ||
279
- chlors[i].portId === anslq25port) {
280
- if (duplicateTo.includes(chlors[i].portId)) duplicateTo.splice(duplicateTo.indexOf(chlors[i].portId, 1));
281
- }
282
- }
283
- }
284
- // send to the ansql25 port first, where possible
285
- if (currPort.portId !== anslq25port) duplicateTo.unshift(anslq25port);
286
- return duplicateTo;
287
- }
288
- /* public queueInboundToAnslq25(_msg: Inbound) {
289
- // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
290
- if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
291
- if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
292
- let anslq25port = sys.anslq25.portId;
293
- if (anslq25port === _msg.portId) return;
294
- let port = this.findPortById(anslq25port);
295
- let msg = _msg.clone();
296
- msg.portId = port.portId;
297
- msg.isClone = true;
298
- msg.id = Message.nextMessageId;
299
- (msg as Inbound).process();
300
- } */
301
-
302
-
303
- /* public queueInboundToBroadcast(_msg: Outbound) {
304
- // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
305
- if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
306
- if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
307
- let anslq25port = sys.anslq25.portId;
308
- if (anslq25port === _msg.portId) return;
309
- let port = this.findPortById(anslq25port);
310
- let msg = _msg.clone();
311
- msg.portId = port.portId;
312
- msg.isClone = true;
313
- msg.id = Message.nextMessageId;
314
- (msg as Inbound).process();
315
- } */
316
-
317
- /* public queueOutboundToAnslq25(_msg: Outbound) {
318
- // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
319
- if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
320
- if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
321
- let anslq25port = sys.anslq25.portId;
322
- let _ports = this.getBroadcastPorts(this.findPortById(_msg.portId));
323
- let msgs: Outbound[] = [];
324
- for (let i = 0; i < _ports.length; i++) {
325
- let port = this.findPortById(_ports[i]);
326
- if (port.portId === _msg.portId) continue;
327
- let msg = _msg.clone() as Outbound;
328
- msg.isClone = true;
329
- msg.portId = port.portId;
330
- msg.response = _msg.response;
331
- msgs.push(msg);
332
- }
333
- return msgs;
334
- } */
335
- public queueOutboundToBroadcast(_msg: Outbound) {
336
- // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
337
- if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
338
- if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
339
- let anslq25port = sys.anslq25.portId;
340
- let _ports = this.getBroadcastPorts(this.findPortById(_msg.portId));
341
- let msgs: Inbound[] = [];
342
- for (let i = 0; i < _ports.length; i++) {
343
- let port = this.findPortById(_ports[i]);
344
- if (port.portId === _msg.portId) continue;
345
- // // let msg = _msg.clone() as Inbound;
346
- // let msg = Message.convertOutboundToInbound(_msg);
347
- // msg.isClone = true;
348
- // msg.portId = port.portId;
349
-
350
- // msg.process();
351
- setTimeout(() => { port.pushIn(Buffer.from(_msg.toPacket())) }, 100);
352
- logger.silly(`mock inbound write bytes port:${_msg.portId} id:${_msg.id} bytes:${_msg.toShortPacket()}`)
353
- // logger.packet()
354
- // (msg as Inbound).process();
355
- // msgs.push(msg);
356
- }
357
- // return msgs;
358
- }
359
- public queueSendMessage(msg: Outbound) {
360
- let port = this.findPortById(msg.portId);
361
- if (typeof port !== 'undefined') {
362
- port.emitter.emit('messagewrite', msg);
363
- }
364
- else
365
- logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
366
- }
367
-
368
- public async queueSendMessageAsync(msg: Outbound): Promise<boolean> {
369
- return new Promise(async (resolve, reject) => {
370
-
371
-
372
- let port = this.findPortById(msg.portId);
373
-
374
- if (typeof port === 'undefined') {
375
- logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
376
- return;
377
- }
378
- // also send to other broadcast ports
379
- // let msgs = conn.queueOutboundToAnslq25(msg);
380
- let msgs = [];
381
- // conn.queueInboundToBroadcast(msg);
382
- conn.queueOutboundToBroadcast(msg);
383
- /* if (msgs.le
384
- ngth > 0) {
385
- msgs.push(msg);
386
- let promises: Promise<boolean>[] = [];
387
- for (let i = 0; i < msgs.length; i++) {
388
- let p: Promise<boolean> = new Promise((_resolve, _reject) => {
389
- msgs[i].onComplete = (err) => {
390
- if (err) {
391
- console.log(`rejecting ${msg.id} ${msg.portId} ${msg.action}`);
392
- _reject(err);
393
- }
394
- else
395
- {
396
- console.log(`resolving id:${msg.id} portid:${msg.portId} dir:${msg.direction} action:${msg.action}`);
397
- _resolve(true);
398
- }
399
- }
400
- let _port = this.findPortById(msgs[i].portId);
401
- _port.emitter.emit('messagewrite', msgs[i]);
402
- });
403
- promises.push(p);
404
- }
405
- let res = false;
406
- await Promise.allSettled(promises).
407
- then((results) => {
408
-
409
- results.forEach((result) => {
410
- console.log(result.status);
411
- if (result.status === 'fulfilled') {res = true;}
412
- });
413
- });
414
- if (res) resolve(true); else reject(`No packets had responses.`);
415
- }
416
- else { */
417
- msg.onComplete = (err) => {
418
- if (err) {
419
- reject(err);
420
- }
421
- else resolve(true);
422
- }
423
- port.emitter.emit('messagewrite', msg);
424
- // let ports = this.getBroadcastPorts(port);
425
- //}
426
-
427
-
428
-
429
-
430
- })
431
- }
432
-
433
- // public sendMockPacket(msg: Inbound) {
434
- // let port = this.findPortById(msg.portId);
435
- // port.emitter.emit('mockmessagewrite', msg);
436
- // }
437
-
438
- public pauseAll() {
439
- for (let i = 0; i < this.rs485Ports.length; i++) {
440
- let port = this.rs485Ports[i];
441
- port.pause();
442
- }
443
- }
444
- public resumeAll() {
445
- for (let i = 0; i < this.rs485Ports.length; i++) {
446
- let port = this.rs485Ports[i];
447
- port.resume();
448
- }
449
- }
450
- public async getLocalPortsAsync(): Promise<any> {
451
- try {
452
- return await SerialPort.list();
453
- } catch (err) { logger.error(`Error retrieving local ports ${err.message}`); }
454
- }
455
- }
456
- export class Counter {
457
- constructor() {
458
- this.bytesReceived = 0;
459
- this.recSuccess = 0;
460
- this.recFailed = 0;
461
- this.recCollisions = 0;
462
- this.bytesSent = 0;
463
- this.sndAborted = 0;
464
- this.sndRetries = 0;
465
- this.sndSuccess = 0;
466
- this.recFailureRate = 0;
467
- this.sndFailureRate = 0;
468
- this.recRewinds = 0;
469
- }
470
- public bytesReceived: number;
471
- public bytesSent: number;
472
- public recSuccess: number;
473
- public recFailed: number;
474
- public recCollisions: number;
475
- public recFailureRate: number;
476
- public sndSuccess: number;
477
- public sndAborted: number;
478
- public sndRetries: number;
479
- public sndFailureRate: number;
480
- public recRewinds: number;
481
- public updatefailureRate(): void {
482
- this.recFailureRate = (this.recFailed + this.recSuccess) !== 0 ? (this.recFailed / (this.recFailed + this.recSuccess) * 100) : 0;
483
- this.sndFailureRate = (this.sndAborted + this.sndSuccess) !== 0 ? (this.sndAborted / (this.sndAborted + this.sndSuccess) * 100) : 0;
484
- }
485
- public toLog(): string {
486
- return `{ "bytesReceived": ${this.bytesReceived} "success": ${this.recSuccess}, "failed": ${this.recFailed}, "bytesSent": ${this.bytesSent}, "collisions": ${this.recCollisions}, "failureRate": ${this.recFailureRate.toFixed(2)}% }`;
487
- }
488
- }
489
- // The following class allows njsPC to have multiple RS485 buses. Each port has its own buffer and message processor
490
- // so that devices on the bus can be isolated to a particular port. By doing this the communications are such that multiple
491
- // ports can be used to accommodate differing port speeds and fixed port addresses. If an
492
- export class RS485Port {
493
- constructor(cfg: any) {
494
- this._cfg = cfg;
495
-
496
- this.emitter = new EventEmitter();
497
- this._inBuffer = [];
498
- this._outBuffer = [];
499
- this.procTimer = null;
500
- this.emitter.on('messagewrite', (msg) => { this.pushOut(msg); });
501
- this.emitter.on('mockmessagewrite', (msg) => {
502
- let bytes = msg.toPacket();
503
- this.counter.bytesSent += bytes.length;
504
- this.counter.sndSuccess++;
505
- this.emitPortStats();
506
- msg.process();
507
- });
508
-
509
- }
510
- public get name(): string { return this.portId === 0 ? 'Primary' : `Aux${this.portId}` }
511
- public isRTS: boolean = true;
512
- public reconnects: number = 0;
513
- public emitter: EventEmitter;
514
- public get portId() { return typeof this._cfg !== 'undefined' && typeof this._cfg.portId !== 'undefined' ? this._cfg.portId : 0; }
515
- public get type() { return typeof this._cfg.type !== 'undefined' ? this._cfg.type : this._cfg.netConnect ? 'netConnect' : this._cfg.mock ? 'mock' : 'local' };
516
- public isOpen: boolean = false;
517
- public closing: boolean = false;
518
- private _cfg: any;
519
- private _port: SerialPort | SerialPortMock | net.Socket;
520
- public mock: boolean = false;
521
- private isPaused: boolean = false;
522
- private connTimer: NodeJS.Timeout;
523
- //public buffer: SendRecieveBuffer;
524
- public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
525
- public counter: Counter = new Counter();
526
- private procTimer: NodeJS.Timeout;
527
- public writeTimer: NodeJS.Timeout
528
- private _processing: boolean = false;
529
- private _lastTx: number = 0;
530
- private _lastRx: number = 0;
531
- private _inBytes: number[] = [];
532
- private _inBuffer: number[] = [];
533
- private _outBuffer: Outbound[] = [];
534
- private _waitingPacket: Outbound;
535
- private _msg: Inbound;
536
- // Connection management functions
537
- public async openAsync(cfg?: any): Promise<boolean> {
538
- if (this.isOpen) await this.closeAsync();
539
- if (typeof cfg !== 'undefined') this._cfg = cfg;
540
- if (!this._cfg.enabled) {
541
- this.emitPortStats();
542
- state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
543
- return true;
544
- }
545
- if (this._cfg.netConnect && !this._cfg.mock) {
546
- if (typeof this._port !== 'undefined' && this.isOpen) {
547
- // This used to try to reconnect and recreate events even though the socket was already connected. This resulted in
548
- // instances where multiple event processors were present. Node doesn't give us any indication that the socket is
549
- // still viable or if it is closing from either end.
550
- return true;
551
- }
552
- else if (typeof this._port !== 'undefined') {
553
- // We need to kill the existing connection by ending it.
554
- let port = this._port as net.Socket;
555
- await new Promise<boolean>((resolve, _) => {
556
- port.end(() => {
557
- resolve(true);
558
- });
559
- });
560
- port.destroy();
561
- }
562
- let opts = extend(true, { keepAliveInitialDelay: 0 }, this._cfg.netSettings);
563
- // Convert the initial delay to milliseconds.
564
- if (typeof this._cfg.netSettings !== 'undefined' && typeof this._cfg.netSettings.keepAliveInitialDelay === 'number') opts.keepAliveInitialDelay = this._cfg.netSettings.keepAliveInitialDelay * 1000;
565
- let nc: net.Socket = new net.Socket(opts);
566
- nc.once('connect', () => { logger.info(`Net connect (socat) ${this._cfg.portId} connected to: ${this._cfg.netHost}:${this._cfg.netPort}`); }); // Socket is opened but not yet ready.
567
- nc.once('ready', () => {
568
- this.isOpen = true;
569
- this.isRTS = true;
570
- logger.info(`Net connect (socat) ${this._cfg.portId} ready and communicating: ${this._cfg.netHost}:${this._cfg.netPort}`);
571
- nc.on('data', (data) => {
572
- //this.resetConnTimer();
573
- if (data.length > 0 && !this.isPaused) this.pushIn(data);
574
- });
575
- this.emitPortStats();
576
- this.processPackets(); // if any new packets have been added to queue, process them.
577
- state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
578
- });
579
-
580
- nc.once('close', (p) => {
581
- this.isOpen = false;
582
- if (typeof this._port !== 'undefined' && !this._port.destroyed) this._port.destroy();
583
- this._port = undefined;
584
- this.clearOutboundBuffer();
585
- this.emitPortStats();
586
- if (!this.closing) {
587
- // If we are closing manually this event should have been cleared already and should never be called. If this is fired out
588
- // of sequence then we will check the closing flag to ensure we are not forcibly closing the socket.
589
- if (typeof this.connTimer !== 'undefined' && this.connTimer) {
590
- clearTimeout(this.connTimer);
591
- this.connTimer = null;
592
- }
593
- this.connTimer = setTimeout(async () => {
594
- try {
595
- // We are already closed so give some inactivity retry and try again.
596
- await this.openAsync();
597
- } catch (err) { }
598
- }, this._cfg.inactivityRetry * 1000);
599
- }
600
- logger.info(`Net connect (socat) ${this._cfg.portId} closed ${p === true ? 'due to error' : ''}: ${this._cfg.netHost}:${this._cfg.netPort}`);
601
- });
602
- nc.on('end', () => { // Happens when the other end of the socket closes.
603
- this.isOpen = false;
604
- logger.info(`Net connect (socat) ${this.portId} end event was fired`);
605
- });
606
- //nc.on('drain', () => { logger.info(`The drain event was fired.`); });
607
- //nc.on('lookup', (o) => { logger.info(`The lookup event was fired ${o}`); });
608
- // Occurs when there is no activity. This should not reset the connection, the previous implementation did so and
609
- // left the connection in a weird state where the previous connection was processing events and the new connection was
610
- // doing so as well. This isn't an error it is a warning as the RS485 bus will most likely be communicating at all times.
611
- //nc.on('timeout', () => { logger.warn(`Net connect (socat) Connection Idle: ${this._cfg.netHost}:${this._cfg.netPort}`); });
612
- if (this._cfg.inactivityRetry > 0) {
613
- nc.setTimeout(Math.max(this._cfg.inactivityRetry, 10) * 1000, async () => {
614
- logger.warn(`Net connect (socat) connection idle: ${this._cfg.netHost}:${this._cfg.netPort} retrying connection.`);
615
- try {
616
- await this.closeAsync();
617
- await this.openAsync();
618
- } catch (err) { logger.error(`Net connect (socat)$ {this.portId} error retrying connection ${err.message}`); }
619
- });
620
- }
621
-
622
- return await new Promise<boolean>((resolve, _) => {
623
- // We only connect an error once as we will destroy this connection on error then recreate a new socket on failure.
624
- nc.once('error', (err) => {
625
- logger.error(`Net connect (socat) error: ${err.message}`);
626
- //logger.error(`Net connect (socat) Connection: ${err}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
627
- //this.resetConnTimer();
628
- this.isOpen = false;
629
- this.emitPortStats();
630
- this.processPackets(); // if any new packets have been added to queue, process them.
631
-
632
- // if the promise has already been fulfilled, but the error happens later, we don't want to call the promise again.
633
- if (typeof resolve !== 'undefined') { resolve(false); }
634
- if (this._cfg.inactivityRetry > 0) {
635
- logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
636
- if (this.connTimer) clearTimeout(this.connTimer);
637
- this.connTimer = setTimeout(async () => { try { await this.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
638
- }
639
- else logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Never retrying -- No retry time set`);
640
- state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
641
- });
642
- nc.connect(this._cfg.netPort, this._cfg.netHost, () => {
643
- if (typeof this._port !== 'undefined') logger.warn(`Net connect (socat) ${this.portId} recovered from lost connection.`);
644
- logger.info(`Net connect (socat) Connection ${this.portId} connected`);
645
- this._port = nc;
646
- // if just changing existing port, reset key flags
647
- this.isOpen = true;
648
- this.isRTS = true;
649
- this.closing = false;
650
- this._processing = false;
651
- this.emitPortStats();
652
- resolve(true);
653
- resolve = undefined;
654
- });
655
- });
656
- }
657
- else {
658
- if (typeof this._port !== 'undefined' && this.isOpen) {
659
- // This used to try to reconnect even though the serial port was already connected. This resulted in
660
- // instances where an access denied error was emitted. So if the port is open we will simply return.
661
- this.resetConnTimer();
662
- return true;
663
- }
664
- let sp: SerialPort | SerialPortMock = null;
665
- if (this._cfg.mock) {
666
- this.mock = true;
667
- let portPath = 'MOCK_PORT';
668
- SerialPortMock.binding.createPort(portPath)
669
- // SerialPortMock.binding = SerialPortMock;
670
- // SerialPortMock.createPort(portPath, { echo: false, record: true });
671
- let opts: SerialPortOpenOptions<AutoDetectTypes> = { path: portPath, autoOpen: false, baudRate: 9600 };
672
- sp = new SerialPortMock(opts);
673
- }
674
- else if (this._cfg.type === 'screenlogic') {
675
- return await sl.openAsync();
676
- }
677
- else {
678
- this.mock = false;
679
- let opts: SerialPortOpenOptions<AutoDetectTypes> = extend(true, { path: this._cfg.rs485Port }, this._cfg.portSettings);
680
- sp = new SerialPort(opts);
681
- }
682
- return await new Promise<boolean>((resolve, _) => {
683
- // The serial port open method calls the callback just once. Unfortunately that is not the case for
684
- // network serial port connections. There really isn't a way to make it syncronous. The openAsync will truly
685
- // be open if a hardware interface is used and this method returns.
686
- sp.open((err) => {
687
- if (err) {
688
- if (!this.mock) this.resetConnTimer();
689
- this.isOpen = false;
690
- logger.error(`Error opening port ${this.portId}: ${err.message}. ${this._cfg.inactivityRetry > 0 && !this.mock ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; (fwiw, inactivityRetry set to ${this._cfg.inactivityRetry})`}`);
691
- resolve(false);
692
- state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
693
- }
694
- else {
695
- state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
696
- resolve(true);
697
- }
698
- this.emitPortStats();
699
-
700
- });
701
- // The event processors below should not resolve or reject the promise. This is the misnomer with the stupid javascript promise
702
- // structure when dealing with serial ports. The original promise will be either accepted or rejected above with the open method. These
703
- // won't be called until long after the promise is resolved above. Yes we should never reject this promise. The resolution is true
704
- // for a successul connect and false otherwise.
705
- sp.on('open', () => {
706
- if (typeof this._port !== 'undefined') logger.info(`Serial Port ${this.portId}: ${this._cfg.rs485Port} recovered from lost connection.`)
707
- else logger.info(`Serial port: ${sp.path} request to open successful ${sp.baudRate}b ${sp.port.openOptions.dataBits}-${sp.port.openOptions.parity}-${sp.port.openOptions.stopBits}`);
708
- this._port = sp;
709
- this.isOpen = true;
710
- /// if just changing existing port, reset key flags
711
- this.isRTS = true;
712
- this.closing = false;
713
- this._processing = false;
714
- sp.on('data', (data) => {
715
- if (!this.mock && !this.isPaused) this.resetConnTimer();
716
- this.pushIn(data);
717
- });
718
- if (!this.mock) this.resetConnTimer();
719
- this.emitPortStats();
720
- });
721
- sp.on('close', (err) => {
722
- this.isOpen = false;
723
- if (err && err.disconnected) {
724
- logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been disconnected and closed. ${JSON.stringify(err)}`)
725
- }
726
- else {
727
- logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been closed. ${err ? JSON.stringify(err) : ''}`);
728
- }
729
- });
730
- sp.on('error', (err) => {
731
- // an underlying streams error from a SP write may call the error event
732
- // instead/in leiu of the error callback
733
- if (typeof this.writeTimer !== 'undefined') { clearTimeout(this.writeTimer); this.writeTimer = null; }
734
- this.isOpen = false;
735
- if (sp.isOpen) sp.close((err) => { }); // call this with the error callback so that it doesn't emit to the error again.
736
- if (!this.mock) this.resetConnTimer();
737
- logger.error(`Serial Port ${this.portId}: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
738
- this.emitPortStats();
739
-
740
- });
741
- });
742
- }
743
- }
744
- public async closeAsync(): Promise<boolean> {
745
- try {
746
- if (this.closing) return false;
747
- this.closing = true;
748
- if (this.connTimer) clearTimeout(this.connTimer);
749
- if (typeof this._port !== 'undefined' && this.isOpen) {
750
- let success = await new Promise<boolean>(async (resolve, reject) => {
751
- if (this._cfg.netConnect) {
752
- this._port.removeAllListeners();
753
- this._port.once('error', (err) => {
754
- if (err) {
755
- logger.error(`Error closing ${this.portId} ${this._cfg.netHost}: ${this._cfg.netPort} / ${this._cfg.rs485Port}: ${err}`);
756
- resolve(false);
757
- }
758
- else {
759
- // RSG - per the docs the error event will subsequently
760
- // fire the close event. This block should never be called and
761
- // likely isn't needed; error listener should always have an err passed
762
- this._port.removeAllListeners(); // call again since we added 2x .once below.
763
- this._port = undefined;
764
- this.isOpen = false;
765
- logger.info(`Successfully closed (socat) ${this.portId} port ${this._cfg.netHost}:${this._cfg.netPort} / ${this._cfg.rs485Port}`);
766
- resolve(true);
767
- }
768
- });
769
- this._port.once('end', () => {
770
- logger.info(`Net connect (socat) ${this.portId} closing: ${this._cfg.netHost}:${this._cfg.netPort}`);
771
- });
772
- this._port.once('close', (p) => {
773
- this._port.removeAllListeners(); // call again since we added 2x .once above.
774
- this.isOpen = false;
775
- this._port = undefined;
776
- logger.info(`Net connect (socat) ${this.portId} successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
777
- resolve(true);
778
- });
779
- logger.info(`Net connect (socat) ${this.portId} request close: ${this._cfg.netHost}:${this._cfg.netPort}`);
780
- // Unfortunately the end call does not actually work in node. It will simply not return anything so we are going to
781
- // just call destroy and forcibly close it.
782
- let port = this._port as net.Socket;
783
- await new Promise<boolean>((resfin, _) => {
784
- port.end(() => {
785
- logger.info(`Net connect (socat) ${this.portId} sent FIN packet: ${this._cfg.netHost}:${this._cfg.netPort}`);
786
- resfin(true);
787
- });
788
- });
789
-
790
- if (typeof this._port !== 'undefined') {
791
- logger.info(`Net connect (socat) destroy socket: ${this._cfg.netHost}:${this._cfg.netPort}`);
792
- this._port.destroy();
793
- }
794
- }
795
- else if (!(this._port instanceof net.Socket) && typeof this._port.close === 'function') {
796
- this._port.close((err) => {
797
- if (err) {
798
- logger.error(`Error closing ${this.portId} serial port ${this._cfg.rs485Port}: ${err}`);
799
- resolve(false);
800
- }
801
- else {
802
- this._port.removeAllListeners(); // remove any listeners still around
803
- this._port = undefined;
804
- logger.info(`Successfully closed portId ${this.portId} for serial port ${this._cfg.rs485Port}`);
805
- this.isOpen = false;
806
- resolve(true);
807
- }
808
- });
809
- }
810
- else {
811
- resolve(true);
812
- this._port = undefined;
813
- }
814
- });
815
- if (success) { this.closeBuffer(); }
816
- return success;
817
- }
818
- return true;
819
- } catch (err) { logger.error(`Error closing comms connection ${this.portId}: ${err.message}`); return false; }
820
- finally { this.emitPortStats(); }
821
- }
822
- public pause() { this.isPaused = true; this.clearBuffer(); this.drain(function (err) { }); }
823
- // RKS: Resume is executed in a closure. This is because we want the current async process to complete
824
- // before we resume. This way the messages are cleared right before we restart.
825
- public resume() { if (this.isPaused) setTimeout(() => { this.clearBuffer(); this.isPaused = false; }, 0); }
826
- protected resetConnTimer(...args) {
827
- //console.log(`resetting connection timer`);
828
- if (this.connTimer !== null) clearTimeout(this.connTimer);
829
- if (!this._cfg.mock && this._cfg.inactivityRetry > 0 && !this.closing) this.connTimer = setTimeout(async () => {
830
- try {
831
- if (this._cfg.netConnect)
832
- logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
833
- else
834
- logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
835
- //await this.closeAsync();
836
- this.reconnects++;
837
- await this.openAsync();
838
- }
839
- catch (err) { logger.error(`Error resetting RS485 port on inactivity: ${err.message}`); };
840
- }, this._cfg.inactivityRetry * 1000);
841
- }
842
- // Data management functions
843
- public drain(cb: (err?: Error) => void) {
844
- if (typeof this._port === 'undefined') {
845
- logger.debug(`Serial Port ${this.portId}: Cannot perform drain function on port that is not open.`);
846
- cb();
847
- }
848
- if ((this._port instanceof SerialPort || this._port instanceof SerialPortMock) && typeof (this._port.drain) === 'function')
849
- this._port.drain(cb as (err) => void);
850
- else // Call the method immediately as the port doesn't wait to send.
851
- cb();
852
- }
853
- public write(msg: Outbound, cb: (err?: Error) => void) {
854
- let bytes = Buffer.from(msg.toPacket());
855
- let _cb = cb;
856
- if (this._cfg.netConnect) {
857
- // SOCAT drops the connection and destroys the stream. Could be weeks or as little as a day.
858
- if (typeof this._port === 'undefined' || this._port.destroyed !== false) {
859
- this.openAsync().then(() => {
860
- (this._port as net.Socket).write(bytes, 'binary', cb);
861
- });
862
- }
863
- else
864
- (this._port as net.Socket).write(bytes, 'binary', cb);
865
- }
866
- else {
867
- if (this._port instanceof SerialPortMock && this.mock === true) {
868
- msg.processMock();
869
- cb();
870
- }
871
- else {
872
-
873
- this.writeTimer = setTimeout(() => {
874
- // RSG - I ran into a scenario where the underlying stream
875
- // processor was not retuning the CB and comms would
876
- // completely stop. This timeout is a failsafe.
877
- // Further, the underlying stream may throw an event error
878
- // and not call the callback (per node docs) hence the
879
- // public writeTimer.
880
- if (typeof cb === 'function') {
881
- cb = undefined;
882
- _cb(new Error(`Serialport stream has not called the callback in 3s.`));
883
- }
884
- }, 3000);
885
- this._port.write(bytes, (err) => {
886
- if (typeof this.writeTimer !== 'undefined') {
887
- clearTimeout(this.writeTimer);
888
- this.writeTimer = null;
889
- // resolve();
890
- if (typeof cb === 'function') {
891
- cb = undefined;
892
- _cb(err);
893
- }
894
- }
895
- });
896
- }
897
-
898
- }
899
- }
900
- // make public for now; should enable writing directly to mock port at Conn level...
901
- public pushIn(pkt: Buffer) {
902
- this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data);
903
- this._lastRx = Date.now();
904
- if (sys.isReady) setImmediate(() => { this.processPackets(); });
905
- }
906
- private pushOut(msg) {
907
- this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); });
908
- }
909
- private clearBuffer() { this._inBuffer.length = 0; this.clearOutboundBuffer(); }
910
- private closeBuffer() { clearTimeout(this.procTimer); this.clearBuffer(); this._msg = undefined; }
911
- private clearOutboundBuffer() {
912
- // let processing = this._processing; // we are closing the port. don't need to reinstate this status afterwards
913
- clearTimeout(this.procTimer);
914
- this.procTimer = null;
915
- this._processing = true;
916
- this.isRTS = false;
917
- let msg: Outbound = typeof this._waitingPacket !== 'undefined' ? this._waitingPacket : this._outBuffer.shift();
918
- this._waitingPacket = null;
919
- while (typeof msg !== 'undefined' && msg) {
920
- // Fail the message.
921
- msg.failed = true;
922
- if (typeof msg.onAbort === 'function') msg.onAbort();
923
- else logger.warn(`Message cleared from outbound buffer: ${msg.toShortPacket()} `);
924
- let err = new OutboundMessageError(msg, `Message cleared from outbound buffer: ${msg.toShortPacket()} `);
925
- if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
926
- if (msg.requiresResponse) {
927
- // Wait for this current process to complete then bombard all the processes with the callback.
928
- if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') setImmediate(msg.response.callback, msg);
929
- }
930
- this.counter.sndAborted++;
931
- msg = this._outBuffer.shift();
932
- }
933
- //this._processing = false; // processing; - we are closing the port
934
- //this.isRTS = true; // - we are closing the port
935
- }
936
- private processPackets() {
937
- if (this._processing || this.closing) return;
938
- if (this.procTimer) {
939
- clearTimeout(this.procTimer);
940
- this.procTimer = null;
941
- }
942
- this._processing = true;
943
- this.processInboundPackets();
944
- this.processOutboundPackets();
945
- this._processing = false;
946
- }
947
- private processWaitPacket(): boolean {
948
- if (typeof this._waitingPacket !== 'undefined' && this._waitingPacket) {
949
- let timeout = this._waitingPacket.timeout || 1000;
950
- let dt = new Date();
951
- if (this._waitingPacket.timestamp.getTime() + timeout < dt.getTime()) {
952
- logger.silly(`Retrying outbound message after ${(dt.getTime() - this._waitingPacket.timestamp.getTime()) / 1000} secs with ${this._waitingPacket.remainingTries} attempt(s) left. - ${this._waitingPacket.toShortPacket()} `);
953
- this.counter.sndRetries++;
954
- this.writeMessage(this._waitingPacket);
955
- }
956
- return true;
957
- }
958
- return false;
959
- }
960
- protected processOutboundPackets() {
961
- let msg: Outbound;
962
- if (!this.processWaitPacket() && this._outBuffer.length > 0) {
963
- if (this.isOpen || this.closing) {
964
- if (this.isRTS) {
965
- msg = this._outBuffer.shift();
966
- if (typeof msg === 'undefined' || !msg) return;
967
- // If the serial port is busy we don't want to process any outbound. However, this used to
968
- // not process the outbound even when the incoming bytes didn't mean anything. Now we only delay
969
- // the outbound when we actually have a message signatures to process.
970
- this.writeMessage(msg);
971
- }
972
- }
973
- else {
974
- // port is closed, reject message
975
- msg = this._outBuffer.shift();
976
- msg.failed = true;
977
- logger.warn(`Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
978
- // This is a hard fail. We don't have any more tries left and the message didn't
979
- // make it onto the wire.
980
- if (typeof msg.onAbort === 'function') msg.onAbort();
981
- else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
982
- let error = new OutboundMessageError(msg, `Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
983
- if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
984
- this._waitingPacket = null;
985
- this.counter.sndAborted++;
986
- this.counter.updatefailureRate();
987
- this.emitPortStats();
988
- // return; // if port isn't open, do not continue and setTimeout
989
- }
990
- }
991
- // RG: added the last `|| typeof msg !== 'undef'` because virtual chem controller only sends a single packet
992
- // but this condition would be eval'd before the callback of port.write was calls and the outbound packet
993
- // would be sitting idle for eternity.
994
- if (this._outBuffer.length > 0 || typeof this._waitingPacket !== 'undefined' || this._waitingPacket || typeof msg !== 'undefined') {
995
- // Configurable inter-frame delay (default 30ms) overrides fixed 100ms.
996
- const dCfg = (config.getSection('controller').txDelays || {});
997
- const interFrame = Math.max(0, Number(dCfg.interFrameDelayMs || 30));
998
- let self = this;
999
- this.procTimer = setTimeout(() => self.processPackets(), interFrame);
1000
- }
1001
- }
1002
- private writeMessage(msg: Outbound) {
1003
- // Make sure we are not re-entrant while the the port.write is going on.
1004
- // This ends in goofiness as it can send more than one message at a time while it
1005
- // waits for the command buffer to be flushed. NOTE: There is no success message and the callback to
1006
- // write only verifies that the buffer got ahold of it.
1007
- let self = this;
1008
- try {
1009
- if (!this.isRTS || this.closing) return;
1010
- var bytes = msg.toPacket();
1011
- if (this.isOpen) {
1012
- this.isRTS = false; // only set if port is open, otherwise it won't be set back to true
1013
- if (msg.remainingTries <= 0) {
1014
- // It will almost never fall into here. The rare case where
1015
- // we have an RTS semaphore and a waiting response might make it go here.
1016
- msg.failed = true;
1017
- this._waitingPacket = null;
1018
- if (typeof msg.onAbort === 'function') msg.onAbort();
1019
- else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
1020
- let err = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
1021
- if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
1022
- if (msg.requiresResponse) {
1023
- if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') {
1024
- setTimeout(msg.response.callback, 100, msg);
1025
- }
1026
- }
1027
- this.counter.sndAborted++;
1028
- this.isRTS = true;
1029
- return;
1030
- }
1031
- const dCfg = (config.getSection('controller').txDelays || {});
1032
- const idleBeforeTx = Math.max(0, Number(dCfg.idleBeforeTxMs || 0));
1033
- const interByte = Math.max(0, Number(dCfg.interByteDelayMs || 0));
1034
- const now = Date.now();
1035
- const idleElapsed = now - Math.max(this._lastTx, this._lastRx);
1036
- const doWrite = () => {
1037
- this.counter.bytesSent += bytes.length;
1038
- msg.timestamp = new Date();
1039
- logger.packet(msg);
1040
- if (interByte > 0 && bytes.length > 1 && this._port && (this._port instanceof SerialPort || this._port instanceof SerialPortMock)) {
1041
- // Manual inter-byte pacing
1042
- let idx = 0;
1043
- const writeNext = () => {
1044
- if (idx >= bytes.length) {
1045
- this._lastTx = Date.now();
1046
- completeWrite(undefined);
1047
- return;
1048
- }
1049
- const b = Buffer.from([bytes[idx++]]);
1050
- (this._port as any).write(b, (err) => {
1051
- if (err) {
1052
- this._lastTx = Date.now();
1053
- completeWrite(err);
1054
- return;
1055
- }
1056
- if (interByte > 0) setTimeout(writeNext, interByte);
1057
- else setImmediate(writeNext);
1058
- });
1059
- };
1060
- writeNext();
1061
- } else {
1062
- this.write(msg, (err) => {
1063
- this._lastTx = Date.now();
1064
- completeWrite(err);
1065
- });
1066
- }
1067
- };
1068
- const completeWrite = (err?: Error) => {
1069
- clearTimeout(this.writeTimer);
1070
- this.writeTimer = null;
1071
- msg.tries++;
1072
- this.isRTS = true;
1073
- if (err) {
1074
- if (msg.remainingTries > 0) self._waitingPacket = msg;
1075
- else {
1076
- msg.failed = true;
1077
- logger.warn(`Message aborted after ${msg.tries} attempt(s): ${bytes}: ${err} `);
1078
- // this is a hard fail. We don't have any more tries left and the message didn't
1079
- // make it onto the wire.
1080
- let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
1081
- if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
1082
- self._waitingPacket = null;
1083
- self.counter.sndAborted++;
1084
- }
1085
- }
1086
- else {
1087
- logger.verbose(`Wrote packet [Port ${this.portId} id: ${msg.id}] [${bytes}].Retries remaining: ${msg.remainingTries} `);
1088
- // We have all the success we are going to get so if the call succeeded then
1089
- // don't set the waiting packet when we aren't actually waiting for a response.
1090
- if (!msg.requiresResponse) {
1091
- // As far as we know the message made it to OCP.
1092
- self._waitingPacket = null;
1093
- self.counter.sndSuccess++;
1094
- if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
1095
- }
1096
- else if (msg.remainingTries >= 0) self._waitingPacket = msg;
1097
- }
1098
- self.counter.updatefailureRate();
1099
- self.emitPortStats();
1100
- };
1101
- // Honor idle-before-TX if not enough bus quiet time has elapsed
1102
- if (idleBeforeTx > 0 && idleElapsed < idleBeforeTx) {
1103
- const wait = idleBeforeTx - idleElapsed;
1104
- setTimeout(doWrite, wait);
1105
- } else doWrite();
1106
- }
1107
- }
1108
- catch (err) {
1109
- logger.error(`Error sending message: ${err.message}
1110
- for message: ${msg.toShortPacket()}`)
1111
- // the show, err, messages, must go on!
1112
- if (this.isOpen) {
1113
- clearTimeout(this.writeTimer);
1114
- this.writeTimer = null;
1115
- msg.tries++;
1116
- this.isRTS = true;
1117
- msg.failed = true;
1118
- // this is a hard fail. We don't have any more tries left and the message didn't
1119
- // make it onto the wire.
1120
- let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
1121
- if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
1122
- this._waitingPacket = null;
1123
- this.counter.sndAborted++;
1124
-
1125
- }
1126
- }
1127
- }
1128
- private clearResponses(msgIn: Inbound) {
1129
- if (this._outBuffer.length === 0 && typeof (this._waitingPacket) !== 'object' && this._waitingPacket) return;
1130
- var callback;
1131
- let msgOut = this._waitingPacket;
1132
- if (typeof (this._waitingPacket) !== 'undefined' && this._waitingPacket) {
1133
- var resp = msgOut.response;
1134
- if (msgOut.requiresResponse) {
1135
- if (resp instanceof Response && resp.isResponse(msgIn, msgOut)) {
1136
- this._waitingPacket = null;
1137
- if (typeof msgOut.onComplete === 'function') msgOut.onComplete(undefined, msgIn);
1138
- callback = resp.callback;
1139
- resp.message = msgIn;
1140
- this.counter.sndSuccess++;
1141
- if (resp.ack) this.pushOut(resp.ack);
1142
- }
1143
- }
1144
- }
1145
- // Go through and remove all the packets that need to be removed from the queue.
1146
- // RG - when would there be additional packets besides the first in the outbuffer that needs to be removed from a single incoming packet?
1147
- // RKS: This occurs when two of the same message signature is thrown onto the queue. Most often when there is a queue full of configuration requests. The
1148
- // triggers that cause the outbound message may come at the same time that another controller makes a call.
1149
- var i = this._outBuffer.length - 1;
1150
- while (i >= 0) {
1151
- let out = this._outBuffer[i--];
1152
- if (typeof out === 'undefined') continue;
1153
- let resp = out.response;
1154
- // RG - added check for msgOut because the *Touch chlor packet 153 adds an status packet 217
1155
- // but if it is the only packet on the queue the outbound will have been cleared out already.
1156
- if (out.requiresResponse && msgOut !== null) {
1157
- if (resp instanceof Response && resp.isResponse(msgIn, out) && (typeof out.scope === 'undefined' || out.scope === msgOut.scope)) {
1158
- resp.message = msgIn;
1159
- if (typeof (resp.callback) === 'function' && resp.callback) callback = resp.callback;
1160
- this._outBuffer.splice(i, 1);
1161
- }
1162
- }
1163
- }
1164
- // RKS: This callback is important because we are managing queues. The position of this callback
1165
- // occurs after all things related to the message have been processed including removal of subsequent
1166
- // messages from the queue. This is because another panel on the bus may throw additional messages
1167
- // that we also need. This occurs when more than one panel on the bus requests a reconfig at the same time.
1168
- if (typeof (callback) === 'function') { setTimeout(callback, 100, msgOut); }
1169
- }
1170
- public get stats() {
1171
- let status = this.isOpen ? 'open' : this._cfg.enabled ? 'closed' : 'disabled';
1172
- return extend(true, { portId: this.portId, status: status, reconnects: this.reconnects }, this.counter)
1173
- }
1174
- public emitPortStats() {
1175
- webApp.emitToChannel('rs485PortStats', 'rs485Stats', this.stats);
1176
- }
1177
- private processCompletedMessage(msg: Inbound, ndx): number {
1178
- msg.timestamp = new Date();
1179
- msg.portId = this.portId;
1180
- msg.id = Message.nextMessageId;
1181
- //console.log(`msg id ${msg.id} assigned to port${msg.portId} action:${msg.action} ${msg.toShortPacket()}`)
1182
- this.counter.recCollisions += msg.collisions;
1183
- this.counter.recRewinds += msg.rewinds;
1184
- this.emitPortStats();
1185
- if (msg.isValid) {
1186
- this.counter.recSuccess++;
1187
- this.counter.updatefailureRate();
1188
- msg.process();
1189
- //conn.queueInboundToAnslq25(msg);
1190
- this.clearResponses(msg);
1191
- }
1192
- else {
1193
- this.counter.recFailed++;
1194
- this.counter.updatefailureRate();
1195
- console.log('RS485 Stats:' + this.counter.toLog());
1196
- ndx = this.rewindFailedMessage(msg, ndx);
1197
- }
1198
- logger.packet(msg); // RSG - Moving this after msg clearing responses so emit will include responseFor data
1199
- return ndx;
1200
- }
1201
- private rewindFailedMessage(msg: Inbound, ndx: number): number {
1202
- this.counter.recRewinds++;
1203
- // Lets see if we can do a rewind to capture another message from the
1204
- // crap on the bus. This will get us to the innermost message. While the outer message may have failed the inner message should
1205
- // be able to buck up and make it happen.
1206
- this._inBytes = this._inBytes.slice(ndx); // Start by removing all of the bytes related to the original message.
1207
- // Add all of the elements of the message back in reverse.
1208
- this._inBytes.unshift(...msg.term);
1209
- this._inBytes.unshift(...msg.payload);
1210
- this._inBytes.unshift(...msg.header.slice(1)); // Trim off the first byte from the header. This means it won't find 16,2 or start with a 165. The
1211
- // algorithm looks for the header bytes to determine the protocol so the rewind shouldn't include the 16 in 16,2 otherwise it will just keep rewinding.
1212
- this._msg = msg = new Inbound();
1213
- ndx = msg.readPacket(this._inBytes);
1214
- if (msg.isComplete) { ndx = this.processCompletedMessage(msg, ndx); }
1215
- return ndx;
1216
- }
1217
- protected processInboundPackets() {
1218
- this.counter.bytesReceived += this._inBuffer.length;
1219
- this._inBytes.push.apply(this._inBytes, this._inBuffer.splice(0, this._inBuffer.length));
1220
- if (this._inBytes.length >= 1) { // Wait until we have something to process.
1221
- let ndx: number = 0;
1222
- let msg: Inbound = this._msg;
1223
- do {
1224
- if (typeof (msg) === 'undefined' || msg === null || msg.isComplete || !msg.isValid) {
1225
- this._msg = msg = new Inbound();
1226
- ndx = msg.readPacket(this._inBytes);
1227
- }
1228
- else ndx = msg.mergeBytes(this._inBytes);
1229
- if (msg.isComplete) ndx = this.processCompletedMessage(msg, ndx);
1230
- if (ndx > 0) {
1231
- this._inBytes = this._inBytes.slice(ndx);
1232
- ndx = 0;
1233
- }
1234
- else break;
1235
-
1236
- } while (ndx < this._inBytes.length);
1237
- }
1238
- }
1239
- public hasAssignedEquipment() {
1240
- let pumps = sys.pumps.get();
1241
- for (let i = 0; i < pumps.length; i++) {
1242
- if (pumps[i].portId === this.portId) {
1243
- return true;
1244
- }
1245
- }
1246
- let chlors = sys.chlorinators.get();
1247
- for (let i = 0; i < chlors.length; i++) {
1248
- if (chlors[i].portId === this.portId) {
1249
- return true;
1250
- }
1251
- }
1252
- return false;
1253
- }
1254
- }
1255
- export var conn: Connection = new Connection();
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ import { AutoDetectTypes } from '@serialport/bindings-cpp';
19
+ import { EventEmitter } from 'events';
20
+ import * as net from 'net';
21
+ import { SerialPort, SerialPortMock, SerialPortOpenOptions } from 'serialport';
22
+ import { setTimeout } from 'timers';
23
+ import { config } from '../../config/Config';
24
+ import { logger } from '../../logger/Logger';
25
+ import { webApp } from "../../web/Server";
26
+ import { utils } from "../Constants";
27
+ import { sys } from "../Equipment";
28
+ import { InvalidEquipmentDataError, InvalidOperationError, OutboundMessageError } from '../Errors';
29
+ import { state } from "../State";
30
+ import { Inbound, Message, Outbound, Response } from './messages/Messages';
31
+ import { sl } from './ScreenLogic';
32
+ const extend = require("extend");
33
+ export class Connection {
34
+ constructor() { }
35
+ public rs485Ports: RS485Port[] = [];
36
+ public get mock(): boolean {
37
+ let port = this.findPortById(0);
38
+ return typeof port !== 'undefined' && port.mock ? true : false;
39
+ }
40
+ public isPortEnabled(portId: number) {
41
+ let port: RS485Port = this.findPortById(portId);
42
+ return typeof port === 'undefined' ? false : port.enabled && port.isOpen && !port.closing;
43
+ }
44
+ public async deleteAuxPort(data: any): Promise<any> {
45
+ try {
46
+ let portId = parseInt(data.portId, 10);
47
+ if (isNaN(portId)) return Promise.reject(new InvalidEquipmentDataError(`A valid port id was not provided to be deleted`, 'RS485Port', data.id));
48
+ if (portId === 0) return Promise.reject(new InvalidEquipmentDataError(`You may not delete the primart RS485 Port`, 'RS485Port', data.id));
49
+ let port = this.findPortById(portId);
50
+ this.removePortById(portId);
51
+ let section = `controller.comms` + (portId === 0 ? '' : portId);
52
+ let cfg = config.getSection(section, {});
53
+ config.removeSection(section);
54
+ state.equipment.messages.removeItemByCode(`rs485:${portId}:connection`);
55
+ return cfg;
56
+ } catch (err) { logger.error(`Error deleting aux port`) }
57
+ }
58
+ public async setScreenlogicAsync(data: any) {
59
+ let ccfg = config.getSection('controller.screenlogic');
60
+ if (typeof data.type === 'undefined' || data.type !== 'local' || data.type !== 'remote') return Promise.reject(new InvalidEquipmentDataError(`Invalid Screenlogic type (${data.type}). Allowed values are 'local' or 'remote'`, 'Screenlogic', 'screenlogic'));
61
+ if ((data.address as string).slice(8) !== 'Pentair:') return Promise.reject(new InvalidEquipmentDataError(`Invalid address (${data.address}). Must start with 'Pentair:'`, 'Screenlogic', 'screenlogic'));
62
+ }
63
+
64
+ public async setPortAsync(data: any): Promise<any> {
65
+ try {
66
+
67
+ let ccfg = config.getSection('controller');
68
+ let pConfig;
69
+ let portId;
70
+ let maxId = -1;
71
+ for (let sec in ccfg) {
72
+ if (sec.startsWith('comms')) {
73
+ let p = ccfg[sec];
74
+ maxId = Math.max(p.portId, maxId);
75
+ if (p.portId === data.portId) pConfig = p;
76
+ }
77
+ }
78
+ if (typeof pConfig === 'undefined') {
79
+ // We are adding a new one.
80
+ if (data.portId === -1 || typeof data.portId === 'undefined') portId = maxId + 1;
81
+ else portId = data.portId;
82
+ }
83
+ else portId = pConfig.portId;
84
+ if (isNaN(portId) || portId < 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid port id defined ${portId}`, 'RS485Port', data.portId));
85
+ let section = `controller.comms` + (portId === 0 ? '' : portId);
86
+ // Lets set the config data.
87
+ let pdata = config.getSection(section, {
88
+ portId: portId,
89
+ type: 'local',
90
+ rs485Port: "/dev/ttyUSB0",
91
+ portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
92
+ netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 1000 },
93
+ mock: false,
94
+ netConnect: false,
95
+ netHost: "raspberrypi",
96
+ netPort: 9801,
97
+ inactivityRetry: 10
98
+ });
99
+ if (portId === 0) {
100
+ pdata.screenlogic = {
101
+ connectionType: "local",
102
+ systemName: "Pentair: 00-00-00",
103
+ password: 1234
104
+ }
105
+ }
106
+
107
+ pdata.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : utils.makeBool(pdata.enabled);
108
+ pdata.type = data.type;
109
+ pdata.netConnect = data.type === 'network' || data.type === 'netConnect'; // typeof data.netConnect !== 'undefined' ? utils.makeBool(data.netConnect) : utils.makeBool(pdata.netConnect);
110
+ pdata.rs485Port = typeof data.rs485Port !== 'undefined' ? data.rs485Port : pdata.rs485Port;
111
+ pdata.inactivityRetry = typeof data.inactivityRetry === 'number' ? data.inactivityRetry : pdata.inactivityRetry;
112
+ pdata.mock = data.mock; // typeof data.mockPort !== 'undefined' ? utils.makeBool(data.mockPort) : utils.makeBool(pdata.mockPort);
113
+ if (pdata.mock) { pdata.rs485Port = 'MOCK_PORT'; }
114
+ if (pdata.type === 'netConnect') { // (pdata.netConnect) {
115
+ pdata.netHost = typeof data.netHost !== 'undefined' ? data.netHost : pdata.netHost;
116
+ pdata.netPort = typeof data.netPort === 'number' ? data.netPort : pdata.netPort;
117
+ }
118
+ if (typeof data.portSettings !== 'undefined') {
119
+ pdata.portSettings = extend(true, { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false }, pdata.portSettings, data.portSettings);
120
+ }
121
+ if (typeof data.netSettings !== 'undefined') {
122
+ pdata.netSettings = extend(true, { keepAlive: false, allowHalfOpen: false, keepAliveInitialDelay: 10000 }, pdata.netSettings, data.netSettings);
123
+ }
124
+ if (pdata.type === 'screenlogic') {
125
+ let password = data.screenlogic.password.toString();
126
+ let regx = /Pentair: (?:(?:\d|[A-Z])(?:\d|[A-Z])-){2}(?:\d|[A-Z])(?:\d|[A-Z])/g;
127
+ let type = data.screenlogic.connectionType;
128
+ let systemName = data.screenlogic.systemName;
129
+ if (type !== 'remote' && type !== 'local') return Promise.reject(new InvalidEquipmentDataError(`An invalid type was supplied for Screenlogic ${type}. Must be remote or local.`, 'Screenlogic', data));
130
+ if (systemName.match(regx) === null) return Promise.reject(new InvalidEquipmentDataError(`An invalid system name was supplied for Screenlogic ${systemName}}. Must be in the format 'Pentair: xx-xx-xx'.`, 'Screenlogic', data));
131
+ if (password.length !== 4) return Promise.reject(new InvalidEquipmentDataError(`An invalid password was supplied for Screenlogic ${password}. (Length must be <= 4)}`, 'Screenlogic', data));
132
+ pdata.screenlogic = data.screenlogic;
133
+ }
134
+ let existing = this.findPortById(portId);
135
+ if (typeof existing !== 'undefined')
136
+ if (existing.type === 'screenlogic' || sl.enabled) {
137
+ await sl.closeAsync();
138
+ }
139
+ else {
140
+ if (!await existing.closeAsync()) {
141
+ existing.closing = false; // if closing fails, reset flag so user can try again
142
+ return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port (Try to save the port again as it usually works the second time).`, 'setPortAsync'));
143
+ }
144
+ }
145
+ config.setSection(section, pdata);
146
+ let cfg = config.getSection(section, {
147
+ type: 'local',
148
+ rs485Port: "/dev/ttyUSB0",
149
+ portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
150
+ netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 5 },
151
+ mock: false,
152
+ netConnect: false,
153
+ netHost: "raspberrypi",
154
+ netPort: 9801,
155
+ inactivityRetry: 10
156
+ });
157
+ if (portId === 0) {
158
+ cfg.screenlogic = {
159
+ connectionType: "local",
160
+ systemName: "Pentair: 00-00-00",
161
+ password: 1234
162
+ }
163
+ }
164
+ existing = this.getPortByCfg(cfg);
165
+
166
+ if (typeof existing !== 'undefined') {
167
+ if (pdata.type === 'screenlogic') {
168
+ await sl.openAsync();
169
+ }
170
+ else {
171
+ existing.reconnects = 0;
172
+ //existing.emitPortStats();
173
+ if (!await existing.openAsync(cfg)) {
174
+ if (cfg.netConnect) return Promise.reject(new InvalidOperationError(`Unable to open Socat Connection to ${pdata.netHost}`, 'setPortAsync'));
175
+ return Promise.reject(new InvalidOperationError(`Unable to open RS485 port ${pdata.rs485Port}`, 'setPortAsync'));
176
+ }
177
+ }
178
+ }
179
+ return cfg;
180
+ } catch (err) { return Promise.reject(err); }
181
+ }
182
+ public async stopAsync() {
183
+ try {
184
+ for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
185
+ let port = this.rs485Ports[i];
186
+ await port.closeAsync();
187
+ }
188
+ logger.info(`Closed all serial communications connection.`);
189
+ } catch (err) { logger.error(`Error closing comms connection: ${err.message} `); }
190
+ }
191
+ public async initAsync() {
192
+ try {
193
+ // So now that we are now allowing multiple comm ports we need to initialize each one. We are keeping the comms section from the config.json
194
+ // simply because I have no idea what the Docker folks do with this. So the default comms will be the one with an OCP or if there are no aux ports.
195
+ let cfg = config.getSection('controller');
196
+ for (let section in cfg) {
197
+ if (section.startsWith('comms')) {
198
+ let c = cfg[section];
199
+ if (typeof c.type === 'undefined') {
200
+ let type = 'local';
201
+ if (c.mock) type = 'mock';
202
+ else if (c.netConnect) type = 'network';
203
+ config.setSection(`controller.${section}`, c);
204
+ console.log(section);
205
+ console.log(c);
206
+ }
207
+ let port = new RS485Port(c);
208
+ // Alright now lets do some conversion of the existing data.
209
+
210
+ this.rs485Ports.push(port);
211
+ await port.openAsync();
212
+ }
213
+ }
214
+ } catch (err) { logger.error(`Error initializing RS485 ports ${err.message}`); }
215
+ }
216
+ public findPortById(portId?: number): RS485Port { return this.rs485Ports.find(elem => elem.portId === (portId || 0)); }
217
+ public async removePortById(portId: number) {
218
+ for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
219
+ let port = this.rs485Ports[i];
220
+ if (port.portId === portId) {
221
+ await port.closeAsync();
222
+ // Don't remove the primary port. You cannot delete this one.
223
+ if (portId !== 0) this.rs485Ports.splice(i, 1);
224
+ }
225
+ }
226
+ }
227
+ public
228
+ getPortByCfg(cfg: any) {
229
+ let port = this.findPortById(cfg.portId || 0);
230
+ if (typeof port === 'undefined') {
231
+ port = new RS485Port(cfg);
232
+ this.rs485Ports.push(port);
233
+ }
234
+ return port;
235
+ }
236
+ public async listInstalledPorts(): Promise<any> {
237
+ try {
238
+ let ports = [];
239
+ // So now that we are now allowing multiple comm ports we need to initialize each one. We are keeping the comms section from the config.json
240
+ // simply because I have no idea what the Docker folks do with this. So the default comms will be the one with an OCP or if there are no aux ports.
241
+ let cfg = config.getSection('controller');
242
+ for (let section in cfg) {
243
+ if (section.startsWith('comms')) {
244
+ let port = config.getSection(`controller.${section}`);
245
+ if (port.portId === 0) port.name = 'Primary';
246
+ else port.name = `Aux${port.portId}`;
247
+ let p = this.findPortById(port.portId);
248
+ port.isOpen = typeof p !== 'undefined' ? p.isOpen : false;
249
+ ports.push(port);
250
+ }
251
+ }
252
+ return ports;
253
+ } catch (err) { logger.error(`Error listing installed RS485 ports ${err.message}`); }
254
+
255
+ }
256
+ private getBroadcastPorts(currPort: RS485Port) {
257
+ // if an ANSLQ25 controller is present, broadcast outbound writes to all other ports that are not mock or dedicated for a pump or chlor
258
+ let anslq25port = sys.anslq25.portId;
259
+ let duplicateTo: number[] = [];
260
+ if (anslq25port >= 0) {
261
+ let ports = this.rs485Ports;
262
+ for (let i = 0; i < ports.length; i++) {
263
+ // if (ports[i].mock) continue;
264
+ if (ports[i].portId === currPort.portId) continue;
265
+ if (ports[i].portId === anslq25port) continue; // don't resend
266
+ if (!ports[i].isOpen) continue;
267
+ duplicateTo.push(ports[i].portId);
268
+ }
269
+ let pumps = sys.pumps.get();
270
+ for (let i = 0; i < pumps.length; i++) {
271
+ if (pumps[i].portId === currPort.portId ||
272
+ pumps[i].portId === anslq25port) {
273
+ if (duplicateTo.includes(pumps[i].portId)) duplicateTo.splice(duplicateTo.indexOf(pumps[i].portId, 1));
274
+ }
275
+ }
276
+ let chlors = sys.chlorinators.get();
277
+ for (let i = 0; i < chlors.length; i++) {
278
+ if (chlors[i].portId === currPort.portId ||
279
+ chlors[i].portId === anslq25port) {
280
+ if (duplicateTo.includes(chlors[i].portId)) duplicateTo.splice(duplicateTo.indexOf(chlors[i].portId, 1));
281
+ }
282
+ }
283
+ }
284
+ // send to the ansql25 port first, where possible
285
+ if (currPort.portId !== anslq25port) duplicateTo.unshift(anslq25port);
286
+ return duplicateTo;
287
+ }
288
+ /* public queueInboundToAnslq25(_msg: Inbound) {
289
+ // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
290
+ if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
291
+ if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
292
+ let anslq25port = sys.anslq25.portId;
293
+ if (anslq25port === _msg.portId) return;
294
+ let port = this.findPortById(anslq25port);
295
+ let msg = _msg.clone();
296
+ msg.portId = port.portId;
297
+ msg.isClone = true;
298
+ msg.id = Message.nextMessageId;
299
+ (msg as Inbound).process();
300
+ } */
301
+
302
+
303
+ /* public queueInboundToBroadcast(_msg: Outbound) {
304
+ // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
305
+ if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
306
+ if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
307
+ let anslq25port = sys.anslq25.portId;
308
+ if (anslq25port === _msg.portId) return;
309
+ let port = this.findPortById(anslq25port);
310
+ let msg = _msg.clone();
311
+ msg.portId = port.portId;
312
+ msg.isClone = true;
313
+ msg.id = Message.nextMessageId;
314
+ (msg as Inbound).process();
315
+ } */
316
+
317
+ /* public queueOutboundToAnslq25(_msg: Outbound) {
318
+ // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
319
+ if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
320
+ if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
321
+ let anslq25port = sys.anslq25.portId;
322
+ let _ports = this.getBroadcastPorts(this.findPortById(_msg.portId));
323
+ let msgs: Outbound[] = [];
324
+ for (let i = 0; i < _ports.length; i++) {
325
+ let port = this.findPortById(_ports[i]);
326
+ if (port.portId === _msg.portId) continue;
327
+ let msg = _msg.clone() as Outbound;
328
+ msg.isClone = true;
329
+ msg.portId = port.portId;
330
+ msg.response = _msg.response;
331
+ msgs.push(msg);
332
+ }
333
+ return msgs;
334
+ } */
335
+ public queueOutboundToBroadcast(_msg: Outbound) {
336
+ // if we have a valid inbound packet on any port (besides dedicated pump/chlor) then also send to anslq25
337
+ if (!sys.anslq25.isActive || sys.anslq25.portId < 0 || !sys.anslq25.broadcastComms) return;
338
+ if (typeof _msg.isClone !== 'undefined' && _msg.isClone) return;
339
+ let anslq25port = sys.anslq25.portId;
340
+ let _ports = this.getBroadcastPorts(this.findPortById(_msg.portId));
341
+ let msgs: Inbound[] = [];
342
+ for (let i = 0; i < _ports.length; i++) {
343
+ let port = this.findPortById(_ports[i]);
344
+ if (port.portId === _msg.portId) continue;
345
+ // // let msg = _msg.clone() as Inbound;
346
+ // let msg = Message.convertOutboundToInbound(_msg);
347
+ // msg.isClone = true;
348
+ // msg.portId = port.portId;
349
+
350
+ // msg.process();
351
+ setTimeout(() => { port.pushIn(Buffer.from(_msg.toPacket())) }, 100);
352
+ logger.silly(`mock inbound write bytes port:${_msg.portId} id:${_msg.id} bytes:${_msg.toShortPacket()}`)
353
+ // logger.packet()
354
+ // (msg as Inbound).process();
355
+ // msgs.push(msg);
356
+ }
357
+ // return msgs;
358
+ }
359
+ public queueSendMessage(msg: Outbound) {
360
+ let port = this.findPortById(msg.portId);
361
+ if (typeof port !== 'undefined') {
362
+ // In mock mode:
363
+ // - never retry the same outbound packet multiple times
364
+ // - never wait for responses (so API callers get "sent" semantics)
365
+ if (port.mock) {
366
+ msg.retries = 0;
367
+ if (msg.requiresResponse) msg.response = undefined;
368
+ }
369
+ port.emitter.emit('messagewrite', msg);
370
+ }
371
+ else
372
+ logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
373
+ }
374
+
375
+ public async queueSendMessageAsync(msg: Outbound): Promise<boolean> {
376
+ return new Promise(async (resolve, reject) => {
377
+
378
+
379
+ let port = this.findPortById(msg.portId);
380
+
381
+ if (typeof port === 'undefined') {
382
+ logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
383
+ return;
384
+ }
385
+ // In mock mode:
386
+ // - never retry the same outbound packet multiple times
387
+ // - never wait for responses (so API callers get "sent" semantics)
388
+ if (port.mock) {
389
+ msg.retries = 0;
390
+ if (msg.requiresResponse) msg.response = undefined;
391
+ }
392
+ // also send to other broadcast ports
393
+ // let msgs = conn.queueOutboundToAnslq25(msg);
394
+ let msgs = [];
395
+ // conn.queueInboundToBroadcast(msg);
396
+ conn.queueOutboundToBroadcast(msg);
397
+ /* if (msgs.le
398
+ ngth > 0) {
399
+ msgs.push(msg);
400
+ let promises: Promise<boolean>[] = [];
401
+ for (let i = 0; i < msgs.length; i++) {
402
+ let p: Promise<boolean> = new Promise((_resolve, _reject) => {
403
+ msgs[i].onComplete = (err) => {
404
+ if (err) {
405
+ console.log(`rejecting ${msg.id} ${msg.portId} ${msg.action}`);
406
+ _reject(err);
407
+ }
408
+ else
409
+ {
410
+ console.log(`resolving id:${msg.id} portid:${msg.portId} dir:${msg.direction} action:${msg.action}`);
411
+ _resolve(true);
412
+ }
413
+ }
414
+ let _port = this.findPortById(msgs[i].portId);
415
+ _port.emitter.emit('messagewrite', msgs[i]);
416
+ });
417
+ promises.push(p);
418
+ }
419
+ let res = false;
420
+ await Promise.allSettled(promises).
421
+ then((results) => {
422
+
423
+ results.forEach((result) => {
424
+ console.log(result.status);
425
+ if (result.status === 'fulfilled') {res = true;}
426
+ });
427
+ });
428
+ if (res) resolve(true); else reject(`No packets had responses.`);
429
+ }
430
+ else { */
431
+ msg.onComplete = (err) => {
432
+ if (err) {
433
+ reject(err);
434
+ }
435
+ else resolve(true);
436
+ }
437
+ port.emitter.emit('messagewrite', msg);
438
+ // let ports = this.getBroadcastPorts(port);
439
+ //}
440
+
441
+
442
+
443
+
444
+ })
445
+ }
446
+
447
+ // public sendMockPacket(msg: Inbound) {
448
+ // let port = this.findPortById(msg.portId);
449
+ // port.emitter.emit('mockmessagewrite', msg);
450
+ // }
451
+
452
+ public pauseAll() {
453
+ for (let i = 0; i < this.rs485Ports.length; i++) {
454
+ let port = this.rs485Ports[i];
455
+ port.pause();
456
+ }
457
+ }
458
+ public resumeAll() {
459
+ for (let i = 0; i < this.rs485Ports.length; i++) {
460
+ let port = this.rs485Ports[i];
461
+ port.resume();
462
+ }
463
+ }
464
+ public async getLocalPortsAsync(): Promise<any> {
465
+ try {
466
+ return await SerialPort.list();
467
+ } catch (err) { logger.error(`Error retrieving local ports ${err.message}`); }
468
+ }
469
+ }
470
+ export class Counter {
471
+ constructor() {
472
+ this.bytesReceived = 0;
473
+ this.recSuccess = 0;
474
+ this.recFailed = 0;
475
+ this.recCollisions = 0;
476
+ this.bytesSent = 0;
477
+ this.sndAborted = 0;
478
+ this.sndRetries = 0;
479
+ this.sndSuccess = 0;
480
+ this.recFailureRate = 0;
481
+ this.sndFailureRate = 0;
482
+ this.recRewinds = 0;
483
+ }
484
+ public bytesReceived: number;
485
+ public bytesSent: number;
486
+ public recSuccess: number;
487
+ public recFailed: number;
488
+ public recCollisions: number;
489
+ public recFailureRate: number;
490
+ public sndSuccess: number;
491
+ public sndAborted: number;
492
+ public sndRetries: number;
493
+ public sndFailureRate: number;
494
+ public recRewinds: number;
495
+ public updatefailureRate(): void {
496
+ this.recFailureRate = (this.recFailed + this.recSuccess) !== 0 ? (this.recFailed / (this.recFailed + this.recSuccess) * 100) : 0;
497
+ this.sndFailureRate = (this.sndAborted + this.sndSuccess) !== 0 ? (this.sndAborted / (this.sndAborted + this.sndSuccess) * 100) : 0;
498
+ }
499
+ public toLog(): string {
500
+ return `{ "bytesReceived": ${this.bytesReceived} "success": ${this.recSuccess}, "failed": ${this.recFailed}, "bytesSent": ${this.bytesSent}, "collisions": ${this.recCollisions}, "failureRate": ${this.recFailureRate.toFixed(2)}% }`;
501
+ }
502
+ }
503
+ // The following class allows njsPC to have multiple RS485 buses. Each port has its own buffer and message processor
504
+ // so that devices on the bus can be isolated to a particular port. By doing this the communications are such that multiple
505
+ // ports can be used to accommodate differing port speeds and fixed port addresses. If an
506
+ export class RS485Port {
507
+ constructor(cfg: any) {
508
+ this._cfg = cfg;
509
+
510
+ this.emitter = new EventEmitter();
511
+ this._inBuffer = [];
512
+ this._outBuffer = [];
513
+ this.procTimer = null;
514
+ this.emitter.on('messagewrite', (msg) => { this.pushOut(msg); });
515
+ this.emitter.on('mockmessagewrite', (msg) => {
516
+ let bytes = msg.toPacket();
517
+ this.counter.bytesSent += bytes.length;
518
+ this.counter.sndSuccess++;
519
+ this.emitPortStats();
520
+ msg.process();
521
+ });
522
+
523
+ }
524
+ public get name(): string { return this.portId === 0 ? 'Primary' : `Aux${this.portId}` }
525
+ public isRTS: boolean = true;
526
+ public reconnects: number = 0;
527
+ public emitter: EventEmitter;
528
+ public get portId() { return typeof this._cfg !== 'undefined' && typeof this._cfg.portId !== 'undefined' ? this._cfg.portId : 0; }
529
+ public get type() { return typeof this._cfg.type !== 'undefined' ? this._cfg.type : this._cfg.netConnect ? 'netConnect' : this._cfg.mock ? 'mock' : 'local' };
530
+ public isOpen: boolean = false;
531
+ public closing: boolean = false;
532
+ private _cfg: any;
533
+ private _port: SerialPort | SerialPortMock | net.Socket;
534
+ public mock: boolean = false;
535
+ private isPaused: boolean = false;
536
+ private connTimer: NodeJS.Timeout;
537
+ //public buffer: SendRecieveBuffer;
538
+ public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
539
+ public counter: Counter = new Counter();
540
+ private procTimer: NodeJS.Timeout;
541
+ public writeTimer: NodeJS.Timeout
542
+ private _processing: boolean = false;
543
+ private _lastTx: number = 0;
544
+ private _lastRx: number = 0;
545
+ private _inBytes: number[] = [];
546
+ private _inBuffer: number[] = [];
547
+ private _outBuffer: Outbound[] = [];
548
+ private _waitingPacket: Outbound;
549
+ private _msg: Inbound;
550
+ // Connection management functions
551
+ public async openAsync(cfg?: any): Promise<boolean> {
552
+ if (this.isOpen) await this.closeAsync();
553
+ if (typeof cfg !== 'undefined') this._cfg = cfg;
554
+ if (!this._cfg.enabled) {
555
+ this.emitPortStats();
556
+ state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
557
+ return true;
558
+ }
559
+ if (this._cfg.netConnect && !this._cfg.mock) {
560
+ if (typeof this._port !== 'undefined' && this.isOpen) {
561
+ // This used to try to reconnect and recreate events even though the socket was already connected. This resulted in
562
+ // instances where multiple event processors were present. Node doesn't give us any indication that the socket is
563
+ // still viable or if it is closing from either end.
564
+ return true;
565
+ }
566
+ else if (typeof this._port !== 'undefined') {
567
+ // We need to kill the existing connection by ending it.
568
+ let port = this._port as net.Socket;
569
+ await new Promise<boolean>((resolve, _) => {
570
+ port.end(() => {
571
+ resolve(true);
572
+ });
573
+ });
574
+ port.destroy();
575
+ }
576
+ let opts = extend(true, { keepAliveInitialDelay: 0 }, this._cfg.netSettings);
577
+ // Convert the initial delay to milliseconds.
578
+ if (typeof this._cfg.netSettings !== 'undefined' && typeof this._cfg.netSettings.keepAliveInitialDelay === 'number') opts.keepAliveInitialDelay = this._cfg.netSettings.keepAliveInitialDelay * 1000;
579
+ let nc: net.Socket = new net.Socket(opts);
580
+ nc.once('connect', () => { logger.info(`Net connect (socat) ${this._cfg.portId} connected to: ${this._cfg.netHost}:${this._cfg.netPort}`); }); // Socket is opened but not yet ready.
581
+ nc.once('ready', () => {
582
+ this.isOpen = true;
583
+ this.isRTS = true;
584
+ logger.info(`Net connect (socat) ${this._cfg.portId} ready and communicating: ${this._cfg.netHost}:${this._cfg.netPort}`);
585
+ nc.on('data', (data) => {
586
+ //this.resetConnTimer();
587
+ if (data.length > 0 && !this.isPaused) this.pushIn(data);
588
+ });
589
+ this.emitPortStats();
590
+ this.processPackets(); // if any new packets have been added to queue, process them.
591
+ state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
592
+ });
593
+
594
+ nc.once('close', (p) => {
595
+ this.isOpen = false;
596
+ if (typeof this._port !== 'undefined' && !this._port.destroyed) this._port.destroy();
597
+ this._port = undefined;
598
+ this.clearOutboundBuffer();
599
+ this.emitPortStats();
600
+ if (!this.closing) {
601
+ // If we are closing manually this event should have been cleared already and should never be called. If this is fired out
602
+ // of sequence then we will check the closing flag to ensure we are not forcibly closing the socket.
603
+ if (typeof this.connTimer !== 'undefined' && this.connTimer) {
604
+ clearTimeout(this.connTimer);
605
+ this.connTimer = null;
606
+ }
607
+ this.connTimer = setTimeout(async () => {
608
+ try {
609
+ // We are already closed so give some inactivity retry and try again.
610
+ await this.openAsync();
611
+ } catch (err) { }
612
+ }, this._cfg.inactivityRetry * 1000);
613
+ }
614
+ logger.info(`Net connect (socat) ${this._cfg.portId} closed ${p === true ? 'due to error' : ''}: ${this._cfg.netHost}:${this._cfg.netPort}`);
615
+ });
616
+ nc.on('end', () => { // Happens when the other end of the socket closes.
617
+ this.isOpen = false;
618
+ logger.info(`Net connect (socat) ${this.portId} end event was fired`);
619
+ });
620
+ //nc.on('drain', () => { logger.info(`The drain event was fired.`); });
621
+ //nc.on('lookup', (o) => { logger.info(`The lookup event was fired ${o}`); });
622
+ // Occurs when there is no activity. This should not reset the connection, the previous implementation did so and
623
+ // left the connection in a weird state where the previous connection was processing events and the new connection was
624
+ // doing so as well. This isn't an error it is a warning as the RS485 bus will most likely be communicating at all times.
625
+ //nc.on('timeout', () => { logger.warn(`Net connect (socat) Connection Idle: ${this._cfg.netHost}:${this._cfg.netPort}`); });
626
+ if (this._cfg.inactivityRetry > 0) {
627
+ nc.setTimeout(Math.max(this._cfg.inactivityRetry, 10) * 1000, async () => {
628
+ logger.warn(`Net connect (socat) connection idle: ${this._cfg.netHost}:${this._cfg.netPort} retrying connection.`);
629
+ try {
630
+ await this.closeAsync();
631
+ await this.openAsync();
632
+ } catch (err) { logger.error(`Net connect (socat)$ {this.portId} error retrying connection ${err.message}`); }
633
+ });
634
+ }
635
+
636
+ return await new Promise<boolean>((resolve, _) => {
637
+ // We only connect an error once as we will destroy this connection on error then recreate a new socket on failure.
638
+ nc.once('error', (err) => {
639
+ logger.error(`Net connect (socat) error: ${err.message}`);
640
+ //logger.error(`Net connect (socat) Connection: ${err}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
641
+ //this.resetConnTimer();
642
+ this.isOpen = false;
643
+ this.emitPortStats();
644
+ this.processPackets(); // if any new packets have been added to queue, process them.
645
+
646
+ // if the promise has already been fulfilled, but the error happens later, we don't want to call the promise again.
647
+ if (typeof resolve !== 'undefined') { resolve(false); }
648
+ if (this._cfg.inactivityRetry > 0) {
649
+ logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
650
+ if (this.connTimer) clearTimeout(this.connTimer);
651
+ this.connTimer = setTimeout(async () => { try { await this.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
652
+ }
653
+ else logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Never retrying -- No retry time set`);
654
+ state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
655
+ });
656
+ nc.connect(this._cfg.netPort, this._cfg.netHost, () => {
657
+ if (typeof this._port !== 'undefined') logger.warn(`Net connect (socat) ${this.portId} recovered from lost connection.`);
658
+ logger.info(`Net connect (socat) Connection ${this.portId} connected`);
659
+ this._port = nc;
660
+ // if just changing existing port, reset key flags
661
+ this.isOpen = true;
662
+ this.isRTS = true;
663
+ this.closing = false;
664
+ this._processing = false;
665
+ this.emitPortStats();
666
+ resolve(true);
667
+ resolve = undefined;
668
+ });
669
+ });
670
+ }
671
+ else {
672
+ if (typeof this._port !== 'undefined' && this.isOpen) {
673
+ // This used to try to reconnect even though the serial port was already connected. This resulted in
674
+ // instances where an access denied error was emitted. So if the port is open we will simply return.
675
+ this.resetConnTimer();
676
+ return true;
677
+ }
678
+ let sp: SerialPort | SerialPortMock = null;
679
+ if (this._cfg.mock) {
680
+ this.mock = true;
681
+ let portPath = 'MOCK_PORT';
682
+ SerialPortMock.binding.createPort(portPath)
683
+ // SerialPortMock.binding = SerialPortMock;
684
+ // SerialPortMock.createPort(portPath, { echo: false, record: true });
685
+ let opts: SerialPortOpenOptions<AutoDetectTypes> = { path: portPath, autoOpen: false, baudRate: 9600 };
686
+ sp = new SerialPortMock(opts);
687
+ }
688
+ else if (this._cfg.type === 'screenlogic') {
689
+ return await sl.openAsync();
690
+ }
691
+ else {
692
+ this.mock = false;
693
+ let opts: SerialPortOpenOptions<AutoDetectTypes> = extend(true, { path: this._cfg.rs485Port }, this._cfg.portSettings);
694
+ sp = new SerialPort(opts);
695
+ }
696
+ return await new Promise<boolean>((resolve, _) => {
697
+ // The serial port open method calls the callback just once. Unfortunately that is not the case for
698
+ // network serial port connections. There really isn't a way to make it syncronous. The openAsync will truly
699
+ // be open if a hardware interface is used and this method returns.
700
+ sp.open((err) => {
701
+ if (err) {
702
+ if (!this.mock) this.resetConnTimer();
703
+ this.isOpen = false;
704
+ logger.error(`Error opening port ${this.portId}: ${err.message}. ${this._cfg.inactivityRetry > 0 && !this.mock ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; (fwiw, inactivityRetry set to ${this._cfg.inactivityRetry})`}`);
705
+ resolve(false);
706
+ state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
707
+ }
708
+ else {
709
+ state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
710
+ resolve(true);
711
+ }
712
+ this.emitPortStats();
713
+
714
+ });
715
+ // The event processors below should not resolve or reject the promise. This is the misnomer with the stupid javascript promise
716
+ // structure when dealing with serial ports. The original promise will be either accepted or rejected above with the open method. These
717
+ // won't be called until long after the promise is resolved above. Yes we should never reject this promise. The resolution is true
718
+ // for a successul connect and false otherwise.
719
+ sp.on('open', () => {
720
+ if (typeof this._port !== 'undefined') logger.info(`Serial Port ${this.portId}: ${this._cfg.rs485Port} recovered from lost connection.`)
721
+ else logger.info(`Serial port: ${sp.path} request to open successful ${sp.baudRate}b ${sp.port.openOptions.dataBits}-${sp.port.openOptions.parity}-${sp.port.openOptions.stopBits}`);
722
+ this._port = sp;
723
+ this.isOpen = true;
724
+ /// if just changing existing port, reset key flags
725
+ this.isRTS = true;
726
+ this.closing = false;
727
+ this._processing = false;
728
+ sp.on('data', (data) => {
729
+ if (!this.mock && !this.isPaused) this.resetConnTimer();
730
+ this.pushIn(data);
731
+ });
732
+ if (!this.mock) this.resetConnTimer();
733
+ this.emitPortStats();
734
+ });
735
+ sp.on('close', (err) => {
736
+ this.isOpen = false;
737
+ if (err && err.disconnected) {
738
+ logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been disconnected and closed. ${JSON.stringify(err)}`)
739
+ }
740
+ else {
741
+ logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been closed. ${err ? JSON.stringify(err) : ''}`);
742
+ }
743
+ });
744
+ sp.on('error', (err) => {
745
+ // an underlying streams error from a SP write may call the error event
746
+ // instead/in leiu of the error callback
747
+ if (typeof this.writeTimer !== 'undefined') { clearTimeout(this.writeTimer); this.writeTimer = null; }
748
+ this.isOpen = false;
749
+ if (sp.isOpen) sp.close((err) => { }); // call this with the error callback so that it doesn't emit to the error again.
750
+ if (!this.mock) this.resetConnTimer();
751
+ logger.error(`Serial Port ${this.portId}: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
752
+ this.emitPortStats();
753
+
754
+ });
755
+ });
756
+ }
757
+ }
758
+ public async closeAsync(): Promise<boolean> {
759
+ try {
760
+ if (this.closing) return false;
761
+ this.closing = true;
762
+ if (this.connTimer) clearTimeout(this.connTimer);
763
+ if (typeof this._port !== 'undefined' && this.isOpen) {
764
+ let success = await new Promise<boolean>(async (resolve, reject) => {
765
+ if (this._cfg.netConnect) {
766
+ this._port.removeAllListeners();
767
+ this._port.once('error', (err) => {
768
+ if (err) {
769
+ logger.error(`Error closing ${this.portId} ${this._cfg.netHost}: ${this._cfg.netPort} / ${this._cfg.rs485Port}: ${err}`);
770
+ resolve(false);
771
+ }
772
+ else {
773
+ // RSG - per the docs the error event will subsequently
774
+ // fire the close event. This block should never be called and
775
+ // likely isn't needed; error listener should always have an err passed
776
+ this._port.removeAllListeners(); // call again since we added 2x .once below.
777
+ this._port = undefined;
778
+ this.isOpen = false;
779
+ logger.info(`Successfully closed (socat) ${this.portId} port ${this._cfg.netHost}:${this._cfg.netPort} / ${this._cfg.rs485Port}`);
780
+ resolve(true);
781
+ }
782
+ });
783
+ this._port.once('end', () => {
784
+ logger.info(`Net connect (socat) ${this.portId} closing: ${this._cfg.netHost}:${this._cfg.netPort}`);
785
+ });
786
+ this._port.once('close', (p) => {
787
+ this._port.removeAllListeners(); // call again since we added 2x .once above.
788
+ this.isOpen = false;
789
+ this._port = undefined;
790
+ logger.info(`Net connect (socat) ${this.portId} successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
791
+ resolve(true);
792
+ });
793
+ logger.info(`Net connect (socat) ${this.portId} request close: ${this._cfg.netHost}:${this._cfg.netPort}`);
794
+ // Unfortunately the end call does not actually work in node. It will simply not return anything so we are going to
795
+ // just call destroy and forcibly close it.
796
+ let port = this._port as net.Socket;
797
+ await new Promise<boolean>((resfin, _) => {
798
+ port.end(() => {
799
+ logger.info(`Net connect (socat) ${this.portId} sent FIN packet: ${this._cfg.netHost}:${this._cfg.netPort}`);
800
+ resfin(true);
801
+ });
802
+ });
803
+
804
+ if (typeof this._port !== 'undefined') {
805
+ logger.info(`Net connect (socat) destroy socket: ${this._cfg.netHost}:${this._cfg.netPort}`);
806
+ this._port.destroy();
807
+ }
808
+ }
809
+ else if (!(this._port instanceof net.Socket) && typeof this._port.close === 'function') {
810
+ this._port.close((err) => {
811
+ if (err) {
812
+ logger.error(`Error closing ${this.portId} serial port ${this._cfg.rs485Port}: ${err}`);
813
+ resolve(false);
814
+ }
815
+ else {
816
+ this._port.removeAllListeners(); // remove any listeners still around
817
+ this._port = undefined;
818
+ logger.info(`Successfully closed portId ${this.portId} for serial port ${this._cfg.rs485Port}`);
819
+ this.isOpen = false;
820
+ resolve(true);
821
+ }
822
+ });
823
+ }
824
+ else {
825
+ resolve(true);
826
+ this._port = undefined;
827
+ }
828
+ });
829
+ if (success) { this.closeBuffer(); }
830
+ return success;
831
+ }
832
+ return true;
833
+ } catch (err) { logger.error(`Error closing comms connection ${this.portId}: ${err.message}`); return false; }
834
+ finally { this.emitPortStats(); }
835
+ }
836
+ public pause() { this.isPaused = true; this.clearBuffer(); this.drain(function (err) { }); }
837
+ // RKS: Resume is executed in a closure. This is because we want the current async process to complete
838
+ // before we resume. This way the messages are cleared right before we restart.
839
+ public resume() { if (this.isPaused) setTimeout(() => { this.clearBuffer(); this.isPaused = false; }, 0); }
840
+ protected resetConnTimer(...args) {
841
+ //console.log(`resetting connection timer`);
842
+ if (this.connTimer !== null) clearTimeout(this.connTimer);
843
+ if (!this._cfg.mock && this._cfg.inactivityRetry > 0 && !this.closing) this.connTimer = setTimeout(async () => {
844
+ try {
845
+ if (this._cfg.netConnect)
846
+ logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
847
+ else
848
+ logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
849
+ //await this.closeAsync();
850
+ this.reconnects++;
851
+ await this.openAsync();
852
+ }
853
+ catch (err) { logger.error(`Error resetting RS485 port on inactivity: ${err.message}`); };
854
+ }, this._cfg.inactivityRetry * 1000);
855
+ }
856
+ // Data management functions
857
+ public drain(cb: (err?: Error) => void) {
858
+ if (typeof this._port === 'undefined') {
859
+ logger.debug(`Serial Port ${this.portId}: Cannot perform drain function on port that is not open.`);
860
+ cb();
861
+ }
862
+ if ((this._port instanceof SerialPort || this._port instanceof SerialPortMock) && typeof (this._port.drain) === 'function')
863
+ this._port.drain(cb as (err) => void);
864
+ else // Call the method immediately as the port doesn't wait to send.
865
+ cb();
866
+ }
867
+ public write(msg: Outbound, cb: (err?: Error) => void) {
868
+ let bytes = Buffer.from(msg.toPacket());
869
+ let _cb = cb;
870
+ if (this._cfg.netConnect) {
871
+ // SOCAT drops the connection and destroys the stream. Could be weeks or as little as a day.
872
+ if (typeof this._port === 'undefined' || this._port.destroyed !== false) {
873
+ this.openAsync().then(() => {
874
+ (this._port as net.Socket).write(bytes, 'binary', cb);
875
+ });
876
+ }
877
+ else
878
+ (this._port as net.Socket).write(bytes, 'binary', cb);
879
+ }
880
+ else {
881
+ // For mock ports, we still want to exercise the real outbound send pipeline:
882
+ // - log + emit to dashpanel via logger.packet(msg) in writeMessage()
883
+ // - write the exact bytes that would have been sent
884
+ // Do NOT loop outbound messages back into inbound processing here; mock/replay injects inbound separately.
885
+ this.writeTimer = setTimeout(() => {
886
+ // RSG - I ran into a scenario where the underlying stream
887
+ // processor was not retuning the CB and comms would
888
+ // completely stop. This timeout is a failsafe.
889
+ // Further, the underlying stream may throw an event error
890
+ // and not call the callback (per node docs) hence the
891
+ // public writeTimer.
892
+ if (typeof cb === 'function') {
893
+ cb = undefined;
894
+ _cb(new Error(`Serialport stream has not called the callback in 3s.`));
895
+ }
896
+ }, 3000);
897
+ (this._port as any).write(bytes, (err) => {
898
+ if (typeof this.writeTimer !== 'undefined') {
899
+ clearTimeout(this.writeTimer);
900
+ this.writeTimer = null;
901
+ if (typeof cb === 'function') {
902
+ cb = undefined;
903
+ _cb(err);
904
+ }
905
+ }
906
+ });
907
+
908
+ }
909
+ }
910
+ // make public for now; should enable writing directly to mock port at Conn level...
911
+ public pushIn(pkt: Buffer) {
912
+ this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data);
913
+ this._lastRx = Date.now();
914
+ if (sys.isReady) setImmediate(() => { this.processPackets(); });
915
+ }
916
+ private pushOut(msg) {
917
+ this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); });
918
+ }
919
+ private clearBuffer() { this._inBuffer.length = 0; this.clearOutboundBuffer(); }
920
+ private closeBuffer() { clearTimeout(this.procTimer); this.clearBuffer(); this._msg = undefined; }
921
+ private clearOutboundBuffer() {
922
+ // let processing = this._processing; // we are closing the port. don't need to reinstate this status afterwards
923
+ clearTimeout(this.procTimer);
924
+ this.procTimer = null;
925
+ this._processing = true;
926
+ this.isRTS = false;
927
+ let msg: Outbound = typeof this._waitingPacket !== 'undefined' ? this._waitingPacket : this._outBuffer.shift();
928
+ this._waitingPacket = null;
929
+ while (typeof msg !== 'undefined' && msg) {
930
+ // Fail the message.
931
+ msg.failed = true;
932
+ if (typeof msg.onAbort === 'function') msg.onAbort();
933
+ else logger.warn(`Message cleared from outbound buffer: ${msg.toShortPacket()} `);
934
+ let err = new OutboundMessageError(msg, `Message cleared from outbound buffer: ${msg.toShortPacket()} `);
935
+ if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
936
+ if (msg.requiresResponse) {
937
+ // Wait for this current process to complete then bombard all the processes with the callback.
938
+ if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') setImmediate(msg.response.callback, msg);
939
+ }
940
+ this.counter.sndAborted++;
941
+ msg = this._outBuffer.shift();
942
+ }
943
+ //this._processing = false; // processing; - we are closing the port
944
+ //this.isRTS = true; // - we are closing the port
945
+ }
946
+ private processPackets() {
947
+ if (this._processing || this.closing) return;
948
+ if (this.procTimer) {
949
+ clearTimeout(this.procTimer);
950
+ this.procTimer = null;
951
+ }
952
+ this._processing = true;
953
+ this.processInboundPackets();
954
+ this.processOutboundPackets();
955
+ this._processing = false;
956
+ }
957
+ private processWaitPacket(): boolean {
958
+ if (typeof this._waitingPacket !== 'undefined' && this._waitingPacket) {
959
+ let timeout = this._waitingPacket.timeout || 1000;
960
+ let dt = new Date();
961
+ if (this._waitingPacket.timestamp.getTime() + timeout < dt.getTime()) {
962
+ if (this._waitingPacket.remainingTries > 0) {
963
+ logger.silly(`Retrying outbound message after ${(dt.getTime() - this._waitingPacket.timestamp.getTime()) / 1000} secs with ${this._waitingPacket.remainingTries} attempt(s) left. - ${this._waitingPacket.toShortPacket()} `);
964
+ this.counter.sndRetries++;
965
+ this.writeMessage(this._waitingPacket);
966
+ }
967
+ else {
968
+ // No retries remaining; fail the message (writeMessage will abort without writing).
969
+ logger.silly(`Outbound message timed out after ${(dt.getTime() - this._waitingPacket.timestamp.getTime()) / 1000} secs with no retries remaining. - ${this._waitingPacket.toShortPacket()} `);
970
+ this.writeMessage(this._waitingPacket);
971
+ }
972
+ }
973
+ return true;
974
+ }
975
+ return false;
976
+ }
977
+ protected processOutboundPackets() {
978
+ let msg: Outbound;
979
+ if (!this.processWaitPacket() && this._outBuffer.length > 0) {
980
+ if (this.isOpen || this.closing) {
981
+ if (this.isRTS) {
982
+ msg = this._outBuffer.shift();
983
+ if (typeof msg === 'undefined' || !msg) return;
984
+ // If the serial port is busy we don't want to process any outbound. However, this used to
985
+ // not process the outbound even when the incoming bytes didn't mean anything. Now we only delay
986
+ // the outbound when we actually have a message signatures to process.
987
+ this.writeMessage(msg);
988
+ }
989
+ }
990
+ else {
991
+ // port is closed, reject message
992
+ msg = this._outBuffer.shift();
993
+ msg.failed = true;
994
+ logger.warn(`Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
995
+ // This is a hard fail. We don't have any more tries left and the message didn't
996
+ // make it onto the wire.
997
+ if (typeof msg.onAbort === 'function') msg.onAbort();
998
+ else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
999
+ let error = new OutboundMessageError(msg, `Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
1000
+ if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
1001
+ this._waitingPacket = null;
1002
+ this.counter.sndAborted++;
1003
+ this.counter.updatefailureRate();
1004
+ this.emitPortStats();
1005
+ // return; // if port isn't open, do not continue and setTimeout
1006
+ }
1007
+ }
1008
+ // RG: added the last `|| typeof msg !== 'undef'` because virtual chem controller only sends a single packet
1009
+ // but this condition would be eval'd before the callback of port.write was calls and the outbound packet
1010
+ // would be sitting idle for eternity.
1011
+ if (this._outBuffer.length > 0 || typeof this._waitingPacket !== 'undefined' || this._waitingPacket || typeof msg !== 'undefined') {
1012
+ // Configurable inter-frame delay (default 30ms) overrides fixed 100ms.
1013
+ const dCfg = (config.getSection('controller').txDelays || {});
1014
+ const interFrame = Math.max(0, Number(dCfg.interFrameDelayMs || 30));
1015
+ let self = this;
1016
+ this.procTimer = setTimeout(() => self.processPackets(), interFrame);
1017
+ }
1018
+ }
1019
+ private writeMessage(msg: Outbound) {
1020
+ // Make sure we are not re-entrant while the the port.write is going on.
1021
+ // This ends in goofiness as it can send more than one message at a time while it
1022
+ // waits for the command buffer to be flushed. NOTE: There is no success message and the callback to
1023
+ // write only verifies that the buffer got ahold of it.
1024
+ let self = this;
1025
+ try {
1026
+ if (!this.isRTS || this.closing) return;
1027
+ var bytes = msg.toPacket();
1028
+ if (this.isOpen) {
1029
+ this.isRTS = false; // only set if port is open, otherwise it won't be set back to true
1030
+ if (msg.remainingTries <= 0) {
1031
+ // It will almost never fall into here. The rare case where
1032
+ // we have an RTS semaphore and a waiting response might make it go here.
1033
+ msg.failed = true;
1034
+ this._waitingPacket = null;
1035
+ if (typeof msg.onAbort === 'function') msg.onAbort();
1036
+ else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
1037
+ let err = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
1038
+ if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
1039
+ if (msg.requiresResponse) {
1040
+ if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') {
1041
+ setTimeout(msg.response.callback, 100, msg);
1042
+ }
1043
+ }
1044
+ this.counter.sndAborted++;
1045
+ this.isRTS = true;
1046
+ return;
1047
+ }
1048
+ const dCfg = (config.getSection('controller').txDelays || {});
1049
+ const idleBeforeTx = Math.max(0, Number(dCfg.idleBeforeTxMs || 0));
1050
+ const interByte = Math.max(0, Number(dCfg.interByteDelayMs || 0));
1051
+ const now = Date.now();
1052
+ const idleElapsed = now - Math.max(this._lastTx, this._lastRx);
1053
+ const doWrite = () => {
1054
+ this.counter.bytesSent += bytes.length;
1055
+ msg.timestamp = new Date();
1056
+ logger.packet(msg);
1057
+ if (interByte > 0 && bytes.length > 1 && this._port && (this._port instanceof SerialPort || this._port instanceof SerialPortMock)) {
1058
+ // Manual inter-byte pacing
1059
+ let idx = 0;
1060
+ const writeNext = () => {
1061
+ if (idx >= bytes.length) {
1062
+ this._lastTx = Date.now();
1063
+ completeWrite(undefined);
1064
+ return;
1065
+ }
1066
+ const b = Buffer.from([bytes[idx++]]);
1067
+ (this._port as any).write(b, (err) => {
1068
+ if (err) {
1069
+ this._lastTx = Date.now();
1070
+ completeWrite(err);
1071
+ return;
1072
+ }
1073
+ if (interByte > 0) setTimeout(writeNext, interByte);
1074
+ else setImmediate(writeNext);
1075
+ });
1076
+ };
1077
+ writeNext();
1078
+ } else {
1079
+ this.write(msg, (err) => {
1080
+ this._lastTx = Date.now();
1081
+ completeWrite(err);
1082
+ });
1083
+ }
1084
+ };
1085
+ const completeWrite = (err?: Error) => {
1086
+ clearTimeout(this.writeTimer);
1087
+ this.writeTimer = null;
1088
+ msg.tries++;
1089
+ this.isRTS = true;
1090
+ if (err) {
1091
+ if (msg.remainingTries > 0) self._waitingPacket = msg;
1092
+ else {
1093
+ msg.failed = true;
1094
+ logger.warn(`Message aborted after ${msg.tries} attempt(s): ${bytes}: ${err} `);
1095
+ // this is a hard fail. We don't have any more tries left and the message didn't
1096
+ // make it onto the wire.
1097
+ let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
1098
+ if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
1099
+ self._waitingPacket = null;
1100
+ self.counter.sndAborted++;
1101
+ }
1102
+ }
1103
+ else {
1104
+ logger.verbose(`Wrote packet [Port ${this.portId} id: ${msg.id}] [${bytes}].Retries remaining: ${msg.remainingTries} `);
1105
+ // We have all the success we are going to get so if the call succeeded then
1106
+ // don't set the waiting packet when we aren't actually waiting for a response.
1107
+ if (!msg.requiresResponse) {
1108
+ // As far as we know the message made it to OCP.
1109
+ self._waitingPacket = null;
1110
+ self.counter.sndSuccess++;
1111
+ if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
1112
+ }
1113
+ else if (msg.remainingTries >= 0) self._waitingPacket = msg;
1114
+ }
1115
+ self.counter.updatefailureRate();
1116
+ self.emitPortStats();
1117
+ };
1118
+ // Honor idle-before-TX if not enough bus quiet time has elapsed
1119
+ if (idleBeforeTx > 0 && idleElapsed < idleBeforeTx) {
1120
+ const wait = idleBeforeTx - idleElapsed;
1121
+ setTimeout(doWrite, wait);
1122
+ } else doWrite();
1123
+ }
1124
+ }
1125
+ catch (err) {
1126
+ logger.error(`Error sending message: ${err.message}
1127
+ for message: ${msg.toShortPacket()}`)
1128
+ // the show, err, messages, must go on!
1129
+ if (this.isOpen) {
1130
+ clearTimeout(this.writeTimer);
1131
+ this.writeTimer = null;
1132
+ msg.tries++;
1133
+ this.isRTS = true;
1134
+ msg.failed = true;
1135
+ // this is a hard fail. We don't have any more tries left and the message didn't
1136
+ // make it onto the wire.
1137
+ let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
1138
+ if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
1139
+ this._waitingPacket = null;
1140
+ this.counter.sndAborted++;
1141
+
1142
+ }
1143
+ }
1144
+ }
1145
+ private clearResponses(msgIn: Inbound) {
1146
+ if (this._outBuffer.length === 0 && typeof (this._waitingPacket) !== 'object' && this._waitingPacket) return;
1147
+ var callback;
1148
+ let msgOut = this._waitingPacket;
1149
+ if (typeof (this._waitingPacket) !== 'undefined' && this._waitingPacket) {
1150
+ var resp = msgOut.response;
1151
+ if (msgOut.requiresResponse) {
1152
+ if (resp instanceof Response && resp.isResponse(msgIn, msgOut)) {
1153
+ this._waitingPacket = null;
1154
+ if (typeof msgOut.onComplete === 'function') msgOut.onComplete(undefined, msgIn);
1155
+ callback = resp.callback;
1156
+ resp.message = msgIn;
1157
+ this.counter.sndSuccess++;
1158
+ if (resp.ack) this.pushOut(resp.ack);
1159
+ }
1160
+ }
1161
+ }
1162
+ // Go through and remove all the packets that need to be removed from the queue.
1163
+ // RG - when would there be additional packets besides the first in the outbuffer that needs to be removed from a single incoming packet?
1164
+ // RKS: This occurs when two of the same message signature is thrown onto the queue. Most often when there is a queue full of configuration requests. The
1165
+ // triggers that cause the outbound message may come at the same time that another controller makes a call.
1166
+ var i = this._outBuffer.length - 1;
1167
+ while (i >= 0) {
1168
+ let out = this._outBuffer[i--];
1169
+ if (typeof out === 'undefined') continue;
1170
+ let resp = out.response;
1171
+ // RG - added check for msgOut because the *Touch chlor packet 153 adds an status packet 217
1172
+ // but if it is the only packet on the queue the outbound will have been cleared out already.
1173
+ if (out.requiresResponse && msgOut !== null) {
1174
+ if (resp instanceof Response && resp.isResponse(msgIn, out) && (typeof out.scope === 'undefined' || out.scope === msgOut.scope)) {
1175
+ resp.message = msgIn;
1176
+ if (typeof (resp.callback) === 'function' && resp.callback) callback = resp.callback;
1177
+ this._outBuffer.splice(i, 1);
1178
+ }
1179
+ }
1180
+ }
1181
+ // RKS: This callback is important because we are managing queues. The position of this callback
1182
+ // occurs after all things related to the message have been processed including removal of subsequent
1183
+ // messages from the queue. This is because another panel on the bus may throw additional messages
1184
+ // that we also need. This occurs when more than one panel on the bus requests a reconfig at the same time.
1185
+ if (typeof (callback) === 'function') { setTimeout(callback, 100, msgOut); }
1186
+ }
1187
+ public get stats() {
1188
+ let status = this.isOpen ? 'open' : this._cfg.enabled ? 'closed' : 'disabled';
1189
+ return extend(true, { portId: this.portId, status: status, reconnects: this.reconnects }, this.counter)
1190
+ }
1191
+ public emitPortStats() {
1192
+ webApp.emitToChannel('rs485PortStats', 'rs485Stats', this.stats);
1193
+ }
1194
+ private processCompletedMessage(msg: Inbound, ndx): number {
1195
+ msg.timestamp = new Date();
1196
+ msg.portId = this.portId;
1197
+ msg.id = Message.nextMessageId;
1198
+ //console.log(`msg id ${msg.id} assigned to port${msg.portId} action:${msg.action} ${msg.toShortPacket()}`)
1199
+ this.counter.recCollisions += msg.collisions;
1200
+ this.counter.recRewinds += msg.rewinds;
1201
+ this.emitPortStats();
1202
+ if (msg.isValid) {
1203
+ this.counter.recSuccess++;
1204
+ this.counter.updatefailureRate();
1205
+ msg.process();
1206
+ //conn.queueInboundToAnslq25(msg);
1207
+ this.clearResponses(msg);
1208
+ }
1209
+ else {
1210
+ this.counter.recFailed++;
1211
+ this.counter.updatefailureRate();
1212
+ console.log('RS485 Stats:' + this.counter.toLog());
1213
+ ndx = this.rewindFailedMessage(msg, ndx);
1214
+ }
1215
+ logger.packet(msg); // RSG - Moving this after msg clearing responses so emit will include responseFor data
1216
+ return ndx;
1217
+ }
1218
+ private rewindFailedMessage(msg: Inbound, ndx: number): number {
1219
+ this.counter.recRewinds++;
1220
+ // Lets see if we can do a rewind to capture another message from the
1221
+ // crap on the bus. This will get us to the innermost message. While the outer message may have failed the inner message should
1222
+ // be able to buck up and make it happen.
1223
+ this._inBytes = this._inBytes.slice(ndx); // Start by removing all of the bytes related to the original message.
1224
+ // Add all of the elements of the message back in reverse.
1225
+ this._inBytes.unshift(...msg.term);
1226
+ this._inBytes.unshift(...msg.payload);
1227
+ this._inBytes.unshift(...msg.header.slice(1)); // Trim off the first byte from the header. This means it won't find 16,2 or start with a 165. The
1228
+ // algorithm looks for the header bytes to determine the protocol so the rewind shouldn't include the 16 in 16,2 otherwise it will just keep rewinding.
1229
+ this._msg = msg = new Inbound();
1230
+ ndx = msg.readPacket(this._inBytes);
1231
+ if (msg.isComplete) { ndx = this.processCompletedMessage(msg, ndx); }
1232
+ return ndx;
1233
+ }
1234
+ protected processInboundPackets() {
1235
+ this.counter.bytesReceived += this._inBuffer.length;
1236
+ this._inBytes.push.apply(this._inBytes, this._inBuffer.splice(0, this._inBuffer.length));
1237
+ if (this._inBytes.length >= 1) { // Wait until we have something to process.
1238
+ let ndx: number = 0;
1239
+ let msg: Inbound = this._msg;
1240
+ do {
1241
+ if (typeof (msg) === 'undefined' || msg === null || msg.isComplete || !msg.isValid) {
1242
+ this._msg = msg = new Inbound();
1243
+ ndx = msg.readPacket(this._inBytes);
1244
+ }
1245
+ else ndx = msg.mergeBytes(this._inBytes);
1246
+ if (msg.isComplete) ndx = this.processCompletedMessage(msg, ndx);
1247
+ if (ndx > 0) {
1248
+ this._inBytes = this._inBytes.slice(ndx);
1249
+ ndx = 0;
1250
+ }
1251
+ else break;
1252
+
1253
+ } while (ndx < this._inBytes.length);
1254
+ }
1255
+ }
1256
+ public hasAssignedEquipment() {
1257
+ let pumps = sys.pumps.get();
1258
+ for (let i = 0; i < pumps.length; i++) {
1259
+ if (pumps[i].portId === this.portId) {
1260
+ return true;
1261
+ }
1262
+ }
1263
+ let chlors = sys.chlorinators.get();
1264
+ for (let i = 0; i < chlors.length; i++) {
1265
+ if (chlors[i].portId === this.portId) {
1266
+ return true;
1267
+ }
1268
+ }
1269
+ return false;
1270
+ }
1271
+ }
1272
+ export var conn: Connection = new Connection();