nodejs-poolcontroller 7.7.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +26 -35
- package/Changelog +22 -0
- package/README.md +7 -3
- package/anslq25/MessagesMock.ts +218 -0
- package/anslq25/boards/MockBoardFactory.ts +50 -0
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
- package/anslq25/boards/MockSystemBoard.ts +217 -0
- package/anslq25/chemistry/MockChlorinator.ts +75 -0
- package/anslq25/pumps/MockPump.ts +84 -0
- package/app.ts +10 -14
- package/config/Config.ts +13 -9
- package/config/VersionCheck.ts +6 -2
- package/controller/Constants.ts +58 -25
- package/controller/Equipment.ts +224 -41
- package/controller/Errors.ts +2 -1
- package/controller/Lockouts.ts +34 -2
- package/controller/State.ts +491 -48
- package/controller/boards/AquaLinkBoard.ts +6 -3
- package/controller/boards/BoardFactory.ts +5 -1
- package/controller/boards/EasyTouchBoard.ts +1971 -1751
- package/controller/boards/IntelliCenterBoard.ts +1311 -1688
- package/controller/boards/IntelliComBoard.ts +7 -1
- package/controller/boards/IntelliTouchBoard.ts +153 -42
- package/controller/boards/NixieBoard.ts +209 -66
- package/controller/boards/SunTouchBoard.ts +393 -0
- package/controller/boards/SystemBoard.ts +1862 -1543
- package/controller/comms/Comms.ts +539 -138
- package/controller/comms/ScreenLogic.ts +1663 -0
- package/controller/comms/messages/Messages.ts +242 -60
- package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
- package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
- package/controller/comms/messages/config/CircuitMessage.ts +81 -13
- package/controller/comms/messages/config/ConfigMessage.ts +3 -1
- package/controller/comms/messages/config/CoverMessage.ts +2 -1
- package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
- package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
- package/controller/comms/messages/config/ExternalMessage.ts +33 -3
- package/controller/comms/messages/config/FeatureMessage.ts +2 -1
- package/controller/comms/messages/config/GeneralMessage.ts +2 -1
- package/controller/comms/messages/config/HeaterMessage.ts +3 -1
- package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
- package/controller/comms/messages/config/OptionsMessage.ts +12 -6
- package/controller/comms/messages/config/PumpMessage.ts +9 -12
- package/controller/comms/messages/config/RemoteMessage.ts +80 -13
- package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
- package/controller/comms/messages/config/SecurityMessage.ts +2 -1
- package/controller/comms/messages/config/ValveMessage.ts +43 -26
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
- package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
- package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
- package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
- package/controller/comms/messages/status/VersionMessage.ts +2 -1
- package/controller/nixie/Nixie.ts +15 -4
- package/controller/nixie/NixieEquipment.ts +1 -0
- package/controller/nixie/chemistry/ChemController.ts +300 -129
- package/controller/nixie/chemistry/ChemDoser.ts +806 -0
- package/controller/nixie/chemistry/Chlorinator.ts +133 -129
- package/controller/nixie/circuits/Circuit.ts +171 -30
- package/controller/nixie/heaters/Heater.ts +337 -173
- package/controller/nixie/pumps/Pump.ts +264 -236
- package/controller/nixie/schedules/Schedule.ts +9 -3
- package/defaultConfig.json +45 -5
- package/logger/Logger.ts +38 -9
- package/package.json +13 -9
- package/web/Server.ts +235 -122
- package/web/bindings/aqualinkD.json +114 -59
- package/web/bindings/homeassistant.json +437 -0
- package/web/bindings/influxDB.json +15 -0
- package/web/bindings/mqtt.json +28 -9
- package/web/bindings/mqttAlt.json +15 -0
- package/web/interfaces/baseInterface.ts +58 -7
- package/web/interfaces/httpInterface.ts +5 -2
- package/web/interfaces/influxInterface.ts +9 -2
- package/web/interfaces/mqttInterface.ts +234 -74
- package/web/interfaces/ruleInterface.ts +87 -0
- package/web/services/config/Config.ts +140 -33
- package/web/services/config/ConfigSocket.ts +2 -1
- package/web/services/state/State.ts +144 -3
- package/web/services/state/StateSocket.ts +65 -14
- package/web/services/utilities/Utilities.ts +189 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* nodejs-poolController. An application to control pool equipment.
|
|
2
|
-
Copyright (C) 2016, 2017, 2018, 2019, 2020
|
|
2
|
+
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
|
|
3
|
+
Russell Goldin, tagyoureit. russ.goldin@gmail.com
|
|
3
4
|
|
|
4
5
|
This program is free software: you can redistribute it and/or modify
|
|
5
6
|
it under the terms of the GNU Affero General Public License as
|
|
@@ -14,29 +15,31 @@ GNU Affero General Public License for more details.
|
|
|
14
15
|
You should have received a copy of the GNU Affero General Public License
|
|
15
16
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
17
|
*/
|
|
18
|
+
import { AutoDetectTypes } from '@serialport/bindings-cpp';
|
|
17
19
|
import { EventEmitter } from 'events';
|
|
18
|
-
import * as
|
|
19
|
-
import
|
|
20
|
+
import * as net from 'net';
|
|
21
|
+
import { SerialPort, SerialPortMock, SerialPortOpenOptions } from 'serialport';
|
|
22
|
+
import { setTimeout } from 'timers';
|
|
20
23
|
import { config } from '../../config/Config';
|
|
21
24
|
import { logger } from '../../logger/Logger';
|
|
22
|
-
import
|
|
23
|
-
import { setTimeout, setInterval } from 'timers';
|
|
24
|
-
import { Message, Outbound, Inbound, Response } from './messages/Messages';
|
|
25
|
-
import { InvalidEquipmentDataError, InvalidOperationError, MessageError, OutboundMessageError } from '../Errors';
|
|
25
|
+
import { webApp } from "../../web/Server";
|
|
26
26
|
import { utils } from "../Constants";
|
|
27
27
|
import { sys } from "../Equipment";
|
|
28
|
-
import {
|
|
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';
|
|
29
32
|
const extend = require("extend");
|
|
30
33
|
export class Connection {
|
|
31
|
-
constructor() {}
|
|
34
|
+
constructor() { }
|
|
32
35
|
public rs485Ports: RS485Port[] = [];
|
|
33
|
-
public get
|
|
36
|
+
public get mock(): boolean {
|
|
34
37
|
let port = this.findPortById(0);
|
|
35
|
-
return typeof port !== 'undefined' && port.
|
|
38
|
+
return typeof port !== 'undefined' && port.mock ? true : false;
|
|
36
39
|
}
|
|
37
40
|
public isPortEnabled(portId: number) {
|
|
38
41
|
let port: RS485Port = this.findPortById(portId);
|
|
39
|
-
return typeof port === 'undefined' ? false : port.enabled;
|
|
42
|
+
return typeof port === 'undefined' ? false : port.enabled && port.isOpen && !port.closing;
|
|
40
43
|
}
|
|
41
44
|
public async deleteAuxPort(data: any): Promise<any> {
|
|
42
45
|
try {
|
|
@@ -48,11 +51,19 @@ export class Connection {
|
|
|
48
51
|
let section = `controller.comms` + (portId === 0 ? '' : portId);
|
|
49
52
|
let cfg = config.getSection(section, {});
|
|
50
53
|
config.removeSection(section);
|
|
54
|
+
state.equipment.messages.removeItemByCode(`rs485:${portId}:connection`);
|
|
51
55
|
return cfg;
|
|
52
56
|
} catch (err) { logger.error(`Error deleting aux port`) }
|
|
53
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
|
+
|
|
54
64
|
public async setPortAsync(data: any): Promise<any> {
|
|
55
65
|
try {
|
|
66
|
+
|
|
56
67
|
let ccfg = config.getSection('controller');
|
|
57
68
|
let pConfig;
|
|
58
69
|
let portId;
|
|
@@ -75,46 +86,94 @@ export class Connection {
|
|
|
75
86
|
// Lets set the config data.
|
|
76
87
|
let pdata = config.getSection(section, {
|
|
77
88
|
portId: portId,
|
|
89
|
+
type: 'local',
|
|
78
90
|
rs485Port: "/dev/ttyUSB0",
|
|
79
91
|
portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
|
|
80
|
-
|
|
92
|
+
netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 1000 },
|
|
93
|
+
mock: false,
|
|
81
94
|
netConnect: false,
|
|
82
95
|
netHost: "raspberrypi",
|
|
83
96
|
netPort: 9801,
|
|
84
97
|
inactivityRetry: 10
|
|
85
98
|
});
|
|
99
|
+
if (portId === 0) {
|
|
100
|
+
pdata.screenlogic = {
|
|
101
|
+
connectionType: "local",
|
|
102
|
+
systemName: "Pentair: 00-00-00",
|
|
103
|
+
password: 1234
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
pdata.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : utils.makeBool(pdata.enabled);
|
|
87
|
-
pdata.
|
|
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);
|
|
88
110
|
pdata.rs485Port = typeof data.rs485Port !== 'undefined' ? data.rs485Port : pdata.rs485Port;
|
|
89
111
|
pdata.inactivityRetry = typeof data.inactivityRetry === 'number' ? data.inactivityRetry : pdata.inactivityRetry;
|
|
90
|
-
|
|
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) {
|
|
91
115
|
pdata.netHost = typeof data.netHost !== 'undefined' ? data.netHost : pdata.netHost;
|
|
92
116
|
pdata.netPort = typeof data.netPort === 'number' ? data.netPort : pdata.netPort;
|
|
93
117
|
}
|
|
94
118
|
if (typeof data.portSettings !== 'undefined') {
|
|
95
119
|
pdata.portSettings = extend(true, { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false }, pdata.portSettings, data.portSettings);
|
|
96
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
|
+
}
|
|
97
134
|
let existing = this.findPortById(portId);
|
|
98
|
-
if (typeof existing !== 'undefined')
|
|
99
|
-
if (
|
|
100
|
-
|
|
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`, 'setPortAsync'));
|
|
143
|
+
}
|
|
101
144
|
}
|
|
102
|
-
}
|
|
103
145
|
config.setSection(section, pdata);
|
|
104
146
|
let cfg = config.getSection(section, {
|
|
147
|
+
type: 'local',
|
|
105
148
|
rs485Port: "/dev/ttyUSB0",
|
|
106
149
|
portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
|
|
107
|
-
|
|
150
|
+
netSettings: { allowHalfOpen: false, keepAlive: false, keepAliveInitialDelay: 5 },
|
|
151
|
+
mock: false,
|
|
108
152
|
netConnect: false,
|
|
109
153
|
netHost: "raspberrypi",
|
|
110
154
|
netPort: 9801,
|
|
111
155
|
inactivityRetry: 10
|
|
112
156
|
});
|
|
113
|
-
|
|
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
|
+
|
|
114
166
|
if (typeof existing !== 'undefined') {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
177
|
}
|
|
119
178
|
}
|
|
120
179
|
return cfg;
|
|
@@ -136,7 +195,18 @@ export class Connection {
|
|
|
136
195
|
let cfg = config.getSection('controller');
|
|
137
196
|
for (let section in cfg) {
|
|
138
197
|
if (section.startsWith('comms')) {
|
|
139
|
-
let
|
|
198
|
+
let c = cfg[section];
|
|
199
|
+
if (typeof c.type === 'undefined') {
|
|
200
|
+
let type = 'local';
|
|
201
|
+
if (c.mockPort) 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
|
+
|
|
140
210
|
this.rs485Ports.push(port);
|
|
141
211
|
await port.openAsync();
|
|
142
212
|
}
|
|
@@ -150,11 +220,12 @@ export class Connection {
|
|
|
150
220
|
if (port.portId === portId) {
|
|
151
221
|
await port.closeAsync();
|
|
152
222
|
// Don't remove the primary port. You cannot delete this one.
|
|
153
|
-
if(portId !== 0) this.rs485Ports.splice(i, 1);
|
|
223
|
+
if (portId !== 0) this.rs485Ports.splice(i, 1);
|
|
154
224
|
}
|
|
155
225
|
}
|
|
156
226
|
}
|
|
157
|
-
public
|
|
227
|
+
public
|
|
228
|
+
getPortByCfg(cfg: any) {
|
|
158
229
|
let port = this.findPortById(cfg.portId || 0);
|
|
159
230
|
if (typeof port === 'undefined') {
|
|
160
231
|
port = new RS485Port(cfg);
|
|
@@ -182,13 +253,187 @@ export class Connection {
|
|
|
182
253
|
} catch (err) { logger.error(`Error listing installed RS485 ports ${err.message}`); }
|
|
183
254
|
|
|
184
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].mockPort) 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
|
+
}
|
|
185
359
|
public queueSendMessage(msg: Outbound) {
|
|
186
360
|
let port = this.findPortById(msg.portId);
|
|
187
|
-
if (typeof port !== 'undefined')
|
|
361
|
+
if (typeof port !== 'undefined') {
|
|
188
362
|
port.emitter.emit('messagewrite', msg);
|
|
363
|
+
}
|
|
189
364
|
else
|
|
190
365
|
logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
|
|
191
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.length > 0) {
|
|
384
|
+
msgs.push(msg);
|
|
385
|
+
let promises: Promise<boolean>[] = [];
|
|
386
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
387
|
+
let p: Promise<boolean> = new Promise((_resolve, _reject) => {
|
|
388
|
+
msgs[i].onComplete = (err) => {
|
|
389
|
+
if (err) {
|
|
390
|
+
console.log(`rejecting ${msg.id} ${msg.portId} ${msg.action}`);
|
|
391
|
+
_reject(err);
|
|
392
|
+
}
|
|
393
|
+
else
|
|
394
|
+
{
|
|
395
|
+
console.log(`resolving id:${msg.id} portid:${msg.portId} dir:${msg.direction} action:${msg.action}`);
|
|
396
|
+
_resolve(true);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
let _port = this.findPortById(msgs[i].portId);
|
|
400
|
+
_port.emitter.emit('messagewrite', msgs[i]);
|
|
401
|
+
});
|
|
402
|
+
promises.push(p);
|
|
403
|
+
}
|
|
404
|
+
let res = false;
|
|
405
|
+
await Promise.allSettled(promises).
|
|
406
|
+
then((results) => {
|
|
407
|
+
|
|
408
|
+
results.forEach((result) => {
|
|
409
|
+
console.log(result.status);
|
|
410
|
+
if (result.status === 'fulfilled') {res = true;}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
if (res) resolve(true); else reject(`No packets had responses.`);
|
|
414
|
+
}
|
|
415
|
+
else { */
|
|
416
|
+
msg.onComplete = (err) => {
|
|
417
|
+
if (err) {
|
|
418
|
+
reject(err);
|
|
419
|
+
}
|
|
420
|
+
else resolve(true);
|
|
421
|
+
}
|
|
422
|
+
port.emitter.emit('messagewrite', msg);
|
|
423
|
+
// let ports = this.getBroadcastPorts(port);
|
|
424
|
+
//}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// public sendMockPacket(msg: Inbound) {
|
|
433
|
+
// let port = this.findPortById(msg.portId);
|
|
434
|
+
// port.emitter.emit('mockmessagewrite', msg);
|
|
435
|
+
// }
|
|
436
|
+
|
|
192
437
|
public pauseAll() {
|
|
193
438
|
for (let i = 0; i < this.rs485Ports.length; i++) {
|
|
194
439
|
let port = this.rs485Ports[i];
|
|
@@ -246,28 +491,39 @@ export class Counter {
|
|
|
246
491
|
export class RS485Port {
|
|
247
492
|
constructor(cfg: any) {
|
|
248
493
|
this._cfg = cfg;
|
|
249
|
-
|
|
494
|
+
|
|
250
495
|
this.emitter = new EventEmitter();
|
|
251
496
|
this._inBuffer = [];
|
|
252
497
|
this._outBuffer = [];
|
|
253
498
|
this.procTimer = null;
|
|
254
499
|
this.emitter.on('messagewrite', (msg) => { this.pushOut(msg); });
|
|
500
|
+
this.emitter.on('mockmessagewrite', (msg) => {
|
|
501
|
+
let bytes = msg.toPacket();
|
|
502
|
+
this.counter.bytesSent += bytes.length;
|
|
503
|
+
this.counter.sndSuccess++;
|
|
504
|
+
this.emitPortStats();
|
|
505
|
+
msg.process();
|
|
506
|
+
});
|
|
507
|
+
|
|
255
508
|
}
|
|
509
|
+
public get name(): string { return this.portId === 0 ? 'Primary' : `Aux${this.portId}` }
|
|
256
510
|
public isRTS: boolean = true;
|
|
257
|
-
public reconnects:number = 0;
|
|
511
|
+
public reconnects: number = 0;
|
|
258
512
|
public emitter: EventEmitter;
|
|
259
513
|
public get portId() { return typeof this._cfg !== 'undefined' && typeof this._cfg.portId !== 'undefined' ? this._cfg.portId : 0; }
|
|
514
|
+
public get type() { return typeof this._cfg.type !== 'undefined' ? this._cfg.type : this._cfg.netConnect ? 'netConnect' : this._cfg.mockPort || this._cfg.mock ? 'mock' : 'local' };
|
|
260
515
|
public isOpen: boolean = false;
|
|
261
|
-
|
|
516
|
+
public closing: boolean = false;
|
|
262
517
|
private _cfg: any;
|
|
263
|
-
private _port:
|
|
264
|
-
public
|
|
518
|
+
private _port: SerialPort | SerialPortMock | net.Socket;
|
|
519
|
+
public mock: boolean = false;
|
|
265
520
|
private isPaused: boolean = false;
|
|
266
521
|
private connTimer: NodeJS.Timeout;
|
|
267
522
|
//public buffer: SendRecieveBuffer;
|
|
268
523
|
public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
|
|
269
524
|
public counter: Counter = new Counter();
|
|
270
525
|
private procTimer: NodeJS.Timeout;
|
|
526
|
+
public writeTimer: NodeJS.Timeout
|
|
271
527
|
private _processing: boolean = false;
|
|
272
528
|
private _inBytes: number[] = [];
|
|
273
529
|
private _inBuffer: number[] = [];
|
|
@@ -278,9 +534,12 @@ export class RS485Port {
|
|
|
278
534
|
public async openAsync(cfg?: any): Promise<boolean> {
|
|
279
535
|
if (this.isOpen) await this.closeAsync();
|
|
280
536
|
if (typeof cfg !== 'undefined') this._cfg = cfg;
|
|
281
|
-
if (!this._cfg.enabled)
|
|
282
|
-
|
|
283
|
-
|
|
537
|
+
if (!this._cfg.enabled) {
|
|
538
|
+
this.emitPortStats();
|
|
539
|
+
state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (this._cfg.netConnect && !this._cfg.mock) {
|
|
284
543
|
if (typeof this._port !== 'undefined' && this.isOpen) {
|
|
285
544
|
// This used to try to reconnect and recreate events even though the socket was already connected. This resulted in
|
|
286
545
|
// instances where multiple event processors were present. Node doesn't give us any indication that the socket is
|
|
@@ -289,9 +548,18 @@ export class RS485Port {
|
|
|
289
548
|
}
|
|
290
549
|
else if (typeof this._port !== 'undefined') {
|
|
291
550
|
// We need to kill the existing connection by ending it.
|
|
292
|
-
this._port.
|
|
551
|
+
let port = this._port as net.Socket;
|
|
552
|
+
await new Promise<boolean>((resolve, _) => {
|
|
553
|
+
port.end(() => {
|
|
554
|
+
resolve(true);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
port.destroy();
|
|
293
558
|
}
|
|
294
|
-
let
|
|
559
|
+
let opts = extend(true, { keepAliveInitialDelay: 0 }, this._cfg.netSettings);
|
|
560
|
+
// Convert the initial delay to milliseconds.
|
|
561
|
+
if (typeof this._cfg.netSettings !== 'undefined' && typeof this._cfg.netSettings.keepAliveInitialDelay === 'number') opts.keepAliveInitialDelay = this._cfg.netSettings.keepAliveInitialDelay * 1000;
|
|
562
|
+
let nc: net.Socket = new net.Socket(opts);
|
|
295
563
|
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.
|
|
296
564
|
nc.once('ready', () => {
|
|
297
565
|
this.isOpen = true;
|
|
@@ -302,14 +570,17 @@ export class RS485Port {
|
|
|
302
570
|
if (data.length > 0 && !this.isPaused) this.pushIn(data);
|
|
303
571
|
});
|
|
304
572
|
this.emitPortStats();
|
|
573
|
+
this.processPackets(); // if any new packets have been added to queue, process them.
|
|
574
|
+
state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
|
|
305
575
|
});
|
|
576
|
+
|
|
306
577
|
nc.once('close', (p) => {
|
|
307
578
|
this.isOpen = false;
|
|
308
579
|
if (typeof this._port !== 'undefined' && !this._port.destroyed) this._port.destroy();
|
|
309
580
|
this._port = undefined;
|
|
310
581
|
this.clearOutboundBuffer();
|
|
311
582
|
this.emitPortStats();
|
|
312
|
-
if (!this.
|
|
583
|
+
if (!this.closing) {
|
|
313
584
|
// If we are closing manually this event should have been cleared already and should never be called. If this is fired out
|
|
314
585
|
// of sequence then we will check the closing flag to ensure we are not forcibly closing the socket.
|
|
315
586
|
if (typeof this.connTimer !== 'undefined' && this.connTimer) {
|
|
@@ -348,23 +619,32 @@ export class RS485Port {
|
|
|
348
619
|
return await new Promise<boolean>((resolve, _) => {
|
|
349
620
|
// We only connect an error once as we will destroy this connection on error then recreate a new socket on failure.
|
|
350
621
|
nc.once('error', (err) => {
|
|
622
|
+
logger.error(`Net connect (socat) error: ${err.message}`);
|
|
351
623
|
//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}`}`);
|
|
352
624
|
//this.resetConnTimer();
|
|
353
625
|
this.isOpen = false;
|
|
354
626
|
this.emitPortStats();
|
|
627
|
+
this.processPackets(); // if any new packets have been added to queue, process them.
|
|
628
|
+
|
|
355
629
|
// if the promise has already been fulfilled, but the error happens later, we don't want to call the promise again.
|
|
356
630
|
if (typeof resolve !== 'undefined') { resolve(false); }
|
|
357
631
|
if (this._cfg.inactivityRetry > 0) {
|
|
358
632
|
logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
|
|
359
|
-
|
|
633
|
+
if (this.connTimer) clearTimeout(this.connTimer);
|
|
634
|
+
this.connTimer = setTimeout(async () => { try { await this.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
|
|
360
635
|
}
|
|
361
636
|
else logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Never retrying -- No retry time set`);
|
|
637
|
+
state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
|
|
362
638
|
});
|
|
363
639
|
nc.connect(this._cfg.netPort, this._cfg.netHost, () => {
|
|
364
640
|
if (typeof this._port !== 'undefined') logger.warn(`Net connect (socat) ${this.portId} recovered from lost connection.`);
|
|
365
641
|
logger.info(`Net connect (socat) Connection ${this.portId} connected`);
|
|
366
642
|
this._port = nc;
|
|
643
|
+
// if just changing existing port, reset key flags
|
|
367
644
|
this.isOpen = true;
|
|
645
|
+
this.isRTS = true;
|
|
646
|
+
this.closing = false;
|
|
647
|
+
this._processing = false;
|
|
368
648
|
this.emitPortStats();
|
|
369
649
|
resolve(true);
|
|
370
650
|
resolve = undefined;
|
|
@@ -372,23 +652,26 @@ export class RS485Port {
|
|
|
372
652
|
});
|
|
373
653
|
}
|
|
374
654
|
else {
|
|
375
|
-
if (typeof this._port !== 'undefined' && this.
|
|
655
|
+
if (typeof this._port !== 'undefined' && this.isOpen) {
|
|
376
656
|
// This used to try to reconnect even though the serial port was already connected. This resulted in
|
|
377
657
|
// instances where an access denied error was emitted. So if the port is open we will simply return.
|
|
378
658
|
this.resetConnTimer();
|
|
379
659
|
return true;
|
|
380
660
|
}
|
|
381
|
-
let sp: SerialPort = null;
|
|
382
|
-
if (this._cfg.
|
|
383
|
-
this.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
661
|
+
let sp: SerialPort | SerialPortMock = null;
|
|
662
|
+
if (this._cfg.mock) {
|
|
663
|
+
this.mock = true;
|
|
664
|
+
let portPath = 'MOCK_PORT';
|
|
665
|
+
SerialPortMock.binding.createPort(portPath)
|
|
666
|
+
// SerialPortMock.binding = SerialPortMock;
|
|
667
|
+
// SerialPortMock.createPort(portPath, { echo: false, record: true });
|
|
668
|
+
let opts: SerialPortOpenOptions<AutoDetectTypes> = { path: portPath, autoOpen: false, baudRate: 9600 };
|
|
669
|
+
sp = new SerialPortMock(opts);
|
|
388
670
|
}
|
|
389
671
|
else {
|
|
390
|
-
this.
|
|
391
|
-
|
|
672
|
+
this.mock = false;
|
|
673
|
+
let opts: SerialPortOpenOptions<AutoDetectTypes> = extend(true, { path: this._cfg.rs485Port }, this._cfg.portSettings);
|
|
674
|
+
sp = new SerialPort(opts);
|
|
392
675
|
}
|
|
393
676
|
return await new Promise<boolean>((resolve, _) => {
|
|
394
677
|
// The serial port open method calls the callback just once. Unfortunately that is not the case for
|
|
@@ -398,10 +681,16 @@ export class RS485Port {
|
|
|
398
681
|
if (err) {
|
|
399
682
|
this.resetConnTimer();
|
|
400
683
|
this.isOpen = false;
|
|
401
|
-
logger.error(`Error opening port ${this.portId}: ${err.message}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
|
|
684
|
+
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})`}`);
|
|
402
685
|
resolve(false);
|
|
686
|
+
state.equipment.messages.setMessageByCode(`rs485:${this.portId}:connection`, 'error', `${this.name} RS485 port disconnected`);
|
|
403
687
|
}
|
|
404
|
-
else
|
|
688
|
+
else {
|
|
689
|
+
state.equipment.messages.removeItemByCode(`rs485:${this.portId}:connection`);
|
|
690
|
+
resolve(true);
|
|
691
|
+
}
|
|
692
|
+
this.emitPortStats();
|
|
693
|
+
|
|
405
694
|
});
|
|
406
695
|
// The event processors below should not resolve or reject the promise. This is the misnomer with the stupid javascript promise
|
|
407
696
|
// structure when dealing with serial ports. The original promise will be either accepted or rejected above with the open method. These
|
|
@@ -409,42 +698,62 @@ export class RS485Port {
|
|
|
409
698
|
// for a successul connect and false otherwise.
|
|
410
699
|
sp.on('open', () => {
|
|
411
700
|
if (typeof this._port !== 'undefined') logger.info(`Serial Port ${this.portId}: ${this._cfg.rs485Port} recovered from lost connection.`)
|
|
412
|
-
else logger.info(`Serial port: ${
|
|
701
|
+
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}`);
|
|
413
702
|
this._port = sp;
|
|
414
703
|
this.isOpen = true;
|
|
415
|
-
|
|
704
|
+
/// if just changing existing port, reset key flags
|
|
705
|
+
this.isRTS = true;
|
|
706
|
+
this.closing = false;
|
|
707
|
+
this._processing = false;
|
|
708
|
+
sp.on('data', (data) => {
|
|
709
|
+
if (!this.mock && !this.isPaused) this.resetConnTimer();
|
|
710
|
+
this.pushIn(data);
|
|
711
|
+
});
|
|
416
712
|
this.resetConnTimer();
|
|
417
713
|
this.emitPortStats();
|
|
418
714
|
});
|
|
419
715
|
sp.on('close', (err) => {
|
|
420
716
|
this.isOpen = false;
|
|
421
|
-
|
|
717
|
+
if (err && err.disconnected) {
|
|
718
|
+
logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been disconnected and closed. ${JSON.stringify(err)}`)
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
logger.info(`Serial Port ${this.portId} - ${this._cfg.rs485Port} has been closed. ${err ? JSON.stringify(err) : ''}`);
|
|
722
|
+
}
|
|
422
723
|
});
|
|
423
724
|
sp.on('error', (err) => {
|
|
725
|
+
// an underlying streams error from a SP write may call the error event
|
|
726
|
+
// instead/in leiu of the error callback
|
|
727
|
+
if (typeof this.writeTimer !== 'undefined') { clearTimeout(this.writeTimer); this.writeTimer = null; }
|
|
424
728
|
this.isOpen = false;
|
|
425
729
|
if (sp.isOpen) sp.close((err) => { }); // call this with the error callback so that it doesn't emit to the error again.
|
|
426
730
|
this.resetConnTimer();
|
|
427
731
|
logger.error(`Serial Port ${this.portId}: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
|
|
428
732
|
this.emitPortStats();
|
|
733
|
+
|
|
429
734
|
});
|
|
430
735
|
});
|
|
431
736
|
}
|
|
432
737
|
}
|
|
433
738
|
public async closeAsync(): Promise<boolean> {
|
|
434
739
|
try {
|
|
435
|
-
if (this.
|
|
436
|
-
this.
|
|
740
|
+
if (this.closing) return false;
|
|
741
|
+
this.closing = true;
|
|
437
742
|
if (this.connTimer) clearTimeout(this.connTimer);
|
|
438
743
|
if (typeof this._port !== 'undefined' && this.isOpen) {
|
|
439
|
-
let success = await new Promise<boolean>((resolve, reject) => {
|
|
744
|
+
let success = await new Promise<boolean>(async (resolve, reject) => {
|
|
440
745
|
if (this._cfg.netConnect) {
|
|
441
746
|
this._port.removeAllListeners();
|
|
442
747
|
this._port.once('error', (err) => {
|
|
443
748
|
if (err) {
|
|
444
|
-
logger.error(`Error closing ${this.portId} ${
|
|
749
|
+
logger.error(`Error closing ${this.portId} ${this._cfg.netHost}: ${this._cfg.netPort} / ${this._cfg.rs485Port}: ${err}`);
|
|
445
750
|
resolve(false);
|
|
446
751
|
}
|
|
447
752
|
else {
|
|
753
|
+
// RSG - per the docs the error event will subsequently
|
|
754
|
+
// fire the close event. This block should never be called and
|
|
755
|
+
// likely isn't needed; error listener should always have an err passed
|
|
756
|
+
this._port.removeAllListeners(); // call again since we added 2x .once below.
|
|
448
757
|
this._port = undefined;
|
|
449
758
|
this.isOpen = false;
|
|
450
759
|
logger.info(`Successfully closed (socat) ${this.portId} port ${this._cfg.netHost}:${this._cfg.netPort} / ${this._cfg.rs485Port}`);
|
|
@@ -455,6 +764,7 @@ export class RS485Port {
|
|
|
455
764
|
logger.info(`Net connect (socat) ${this.portId} closing: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
456
765
|
});
|
|
457
766
|
this._port.once('close', (p) => {
|
|
767
|
+
this._port.removeAllListeners(); // call again since we added 2x .once above.
|
|
458
768
|
this.isOpen = false;
|
|
459
769
|
this._port = undefined;
|
|
460
770
|
logger.info(`Net connect (socat) ${this.portId} successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
@@ -463,19 +773,31 @@ export class RS485Port {
|
|
|
463
773
|
logger.info(`Net connect (socat) ${this.portId} request close: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
464
774
|
// Unfortunately the end call does not actually work in node. It will simply not return anything so we are going to
|
|
465
775
|
// just call destroy and forcibly close it.
|
|
466
|
-
this._port.
|
|
776
|
+
let port = this._port as net.Socket;
|
|
777
|
+
await new Promise<boolean>((resfin, _) => {
|
|
778
|
+
port.end(() => {
|
|
779
|
+
logger.info(`Net connect (socat) ${this.portId} sent FIN packet: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
780
|
+
resfin(true);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
if (typeof this._port !== 'undefined') {
|
|
785
|
+
logger.info(`Net connect (socat) destroy socket: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
786
|
+
this._port.destroy();
|
|
787
|
+
}
|
|
467
788
|
}
|
|
468
|
-
else if (typeof this._port.close === 'function') {
|
|
789
|
+
else if (!(this._port instanceof net.Socket) && typeof this._port.close === 'function') {
|
|
469
790
|
this._port.close((err) => {
|
|
470
791
|
if (err) {
|
|
471
792
|
logger.error(`Error closing ${this.portId} serial port ${this._cfg.rs485Port}: ${err}`);
|
|
472
793
|
resolve(false);
|
|
473
794
|
}
|
|
474
795
|
else {
|
|
796
|
+
this._port.removeAllListeners(); // remove any listeners still around
|
|
475
797
|
this._port = undefined;
|
|
476
|
-
logger.info(`Successfully closed ${this.portId} serial port ${this._cfg.rs485Port}`);
|
|
477
|
-
resolve(true);
|
|
798
|
+
logger.info(`Successfully closed portId ${this.portId} for serial port ${this._cfg.rs485Port}`);
|
|
478
799
|
this.isOpen = false;
|
|
800
|
+
resolve(true);
|
|
479
801
|
}
|
|
480
802
|
});
|
|
481
803
|
}
|
|
@@ -488,8 +810,8 @@ export class RS485Port {
|
|
|
488
810
|
return success;
|
|
489
811
|
}
|
|
490
812
|
return true;
|
|
491
|
-
} catch (err) { logger.error(`Error closing comms connection ${this.portId}: ${err.message}`); return
|
|
492
|
-
finally { this.
|
|
813
|
+
} catch (err) { logger.error(`Error closing comms connection ${this.portId}: ${err.message}`); return false; }
|
|
814
|
+
finally { this.emitPortStats(); }
|
|
493
815
|
}
|
|
494
816
|
public pause() { this.isPaused = true; this.clearBuffer(); this.drain(function (err) { }); }
|
|
495
817
|
// RKS: Resume is executed in a closure. This is because we want the current async process to complete
|
|
@@ -498,7 +820,7 @@ export class RS485Port {
|
|
|
498
820
|
protected resetConnTimer(...args) {
|
|
499
821
|
//console.log(`resetting connection timer`);
|
|
500
822
|
if (this.connTimer !== null) clearTimeout(this.connTimer);
|
|
501
|
-
if (!this._cfg.mockPort && this._cfg.inactivityRetry > 0 && !this.
|
|
823
|
+
if (!this._cfg.mockPort && this._cfg.inactivityRetry > 0 && !this.closing) this.connTimer = setTimeout(async () => {
|
|
502
824
|
try {
|
|
503
825
|
if (this._cfg.netConnect)
|
|
504
826
|
logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
|
|
@@ -512,36 +834,74 @@ export class RS485Port {
|
|
|
512
834
|
}, this._cfg.inactivityRetry * 1000);
|
|
513
835
|
}
|
|
514
836
|
// Data management functions
|
|
515
|
-
public drain(cb:
|
|
837
|
+
public drain(cb: (err?: Error) => void) {
|
|
516
838
|
if (typeof this._port === 'undefined') {
|
|
517
839
|
logger.debug(`Serial Port ${this.portId}: Cannot perform drain function on port that is not open.`);
|
|
518
840
|
cb();
|
|
519
841
|
}
|
|
520
|
-
if (typeof (this._port.drain) === 'function')
|
|
521
|
-
this._port.drain(cb);
|
|
842
|
+
if ((this._port instanceof SerialPort || this._port instanceof SerialPortMock) && typeof (this._port.drain) === 'function')
|
|
843
|
+
this._port.drain(cb as (err) => void);
|
|
522
844
|
else // Call the method immediately as the port doesn't wait to send.
|
|
523
845
|
cb();
|
|
524
846
|
}
|
|
525
|
-
public write(
|
|
847
|
+
public write(msg: Outbound, cb: (err?: Error) => void) {
|
|
848
|
+
let bytes = Buffer.from(msg.toPacket());
|
|
849
|
+
let _cb = cb;
|
|
526
850
|
if (this._cfg.netConnect) {
|
|
527
851
|
// SOCAT drops the connection and destroys the stream. Could be weeks or as little as a day.
|
|
528
852
|
if (typeof this._port === 'undefined' || this._port.destroyed !== false) {
|
|
529
853
|
this.openAsync().then(() => {
|
|
530
|
-
this._port.write(bytes, 'binary', cb);
|
|
854
|
+
(this._port as net.Socket).write(bytes, 'binary', cb);
|
|
531
855
|
});
|
|
532
856
|
}
|
|
533
857
|
else
|
|
534
|
-
this._port.write(bytes, 'binary', cb);
|
|
858
|
+
(this._port as net.Socket).write(bytes, 'binary', cb);
|
|
535
859
|
}
|
|
536
|
-
else
|
|
537
|
-
this._port.
|
|
860
|
+
else {
|
|
861
|
+
if (this._port instanceof SerialPortMock && this.mock === true) {
|
|
862
|
+
msg.processMock();
|
|
863
|
+
cb();
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
|
|
867
|
+
this.writeTimer = setTimeout(() => {
|
|
868
|
+
// RSG - I ran into a scenario where the underlying stream
|
|
869
|
+
// processor was not retuning the CB and comms would
|
|
870
|
+
// completely stop. This timeout is a failsafe.
|
|
871
|
+
// Further, the underlying stream may throw an event error
|
|
872
|
+
// and not call the callback (per node docs) hence the
|
|
873
|
+
// public writeTimer.
|
|
874
|
+
if (typeof cb === 'function') {
|
|
875
|
+
cb = undefined;
|
|
876
|
+
_cb(new Error(`Serialport stream has not called the callback in 3s.`));
|
|
877
|
+
}
|
|
878
|
+
}, 3000);
|
|
879
|
+
this._port.write(bytes, (err) => {
|
|
880
|
+
if (typeof this.writeTimer !== 'undefined') {
|
|
881
|
+
clearTimeout(this.writeTimer);
|
|
882
|
+
this.writeTimer = null;
|
|
883
|
+
// resolve();
|
|
884
|
+
if (typeof cb === 'function') {
|
|
885
|
+
cb = undefined;
|
|
886
|
+
_cb(err);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// make public for now; should enable writing directly to mock port at Conn level...
|
|
895
|
+
public pushIn(pkt: Buffer) {
|
|
896
|
+
this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data); if (sys.isReady) setImmediate(() => { this.processPackets(); });
|
|
897
|
+
}
|
|
898
|
+
private pushOut(msg) {
|
|
899
|
+
this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); });
|
|
538
900
|
}
|
|
539
|
-
private pushIn(pkt) { this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data); if(sys.isReady) setImmediate(() => { this.processPackets(); }); }
|
|
540
|
-
private pushOut(msg) { this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); }); }
|
|
541
901
|
private clearBuffer() { this._inBuffer.length = 0; this.clearOutboundBuffer(); }
|
|
542
902
|
private closeBuffer() { clearTimeout(this.procTimer); this.clearBuffer(); this._msg = undefined; }
|
|
543
903
|
private clearOutboundBuffer() {
|
|
544
|
-
let processing = this._processing;
|
|
904
|
+
// let processing = this._processing; // we are closing the port. don't need to reinstate this status afterwards
|
|
545
905
|
clearTimeout(this.procTimer);
|
|
546
906
|
this.procTimer = null;
|
|
547
907
|
this._processing = true;
|
|
@@ -562,11 +922,11 @@ export class RS485Port {
|
|
|
562
922
|
this.counter.sndAborted++;
|
|
563
923
|
msg = this._outBuffer.shift();
|
|
564
924
|
}
|
|
565
|
-
this._processing = processing;
|
|
566
|
-
this.isRTS = true;
|
|
925
|
+
//this._processing = false; // processing; - we are closing the port
|
|
926
|
+
//this.isRTS = true; // - we are closing the port
|
|
567
927
|
}
|
|
568
928
|
private processPackets() {
|
|
569
|
-
if (this._processing) return;
|
|
929
|
+
if (this._processing || this.closing) return;
|
|
570
930
|
if (this.procTimer) {
|
|
571
931
|
clearTimeout(this.procTimer);
|
|
572
932
|
this.procTimer = null;
|
|
@@ -592,7 +952,7 @@ export class RS485Port {
|
|
|
592
952
|
protected processOutboundPackets() {
|
|
593
953
|
let msg: Outbound;
|
|
594
954
|
if (!this.processWaitPacket() && this._outBuffer.length > 0) {
|
|
595
|
-
if (this.isOpen) {
|
|
955
|
+
if (this.isOpen || this.closing) {
|
|
596
956
|
if (this.isRTS) {
|
|
597
957
|
msg = this._outBuffer.shift();
|
|
598
958
|
if (typeof msg === 'undefined' || !msg) return;
|
|
@@ -617,6 +977,7 @@ export class RS485Port {
|
|
|
617
977
|
this.counter.sndAborted++;
|
|
618
978
|
this.counter.updatefailureRate();
|
|
619
979
|
this.emitPortStats();
|
|
980
|
+
// return; // if port isn't open, do not continue and setTimeout
|
|
620
981
|
}
|
|
621
982
|
}
|
|
622
983
|
// RG: added the last `|| typeof msg !== 'undef'` because virtual chem controller only sends a single packet
|
|
@@ -633,67 +994,90 @@ export class RS485Port {
|
|
|
633
994
|
// This ends in goofiness as it can send more than one message at a time while it
|
|
634
995
|
// waits for the command buffer to be flushed. NOTE: There is no success message and the callback to
|
|
635
996
|
// write only verifies that the buffer got ahold of it.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (
|
|
651
|
-
|
|
997
|
+
let self = this;
|
|
998
|
+
try {
|
|
999
|
+
if (!this.isRTS || this.closing) return;
|
|
1000
|
+
var bytes = msg.toPacket();
|
|
1001
|
+
if (this.isOpen) {
|
|
1002
|
+
this.isRTS = false; // only set if port is open, otherwise it won't be set back to true
|
|
1003
|
+
if (msg.remainingTries <= 0) {
|
|
1004
|
+
// It will almost never fall into here. The rare case where
|
|
1005
|
+
// we have an RTS semaphore and a waiting response might make it go here.
|
|
1006
|
+
msg.failed = true;
|
|
1007
|
+
this._waitingPacket = null;
|
|
1008
|
+
if (typeof msg.onAbort === 'function') msg.onAbort();
|
|
1009
|
+
else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
|
|
1010
|
+
let err = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
|
|
1011
|
+
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
1012
|
+
if (msg.requiresResponse) {
|
|
1013
|
+
if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') {
|
|
1014
|
+
setTimeout(msg.response.callback, 100, msg);
|
|
1015
|
+
}
|
|
652
1016
|
}
|
|
1017
|
+
this.counter.sndAborted++;
|
|
1018
|
+
this.isRTS = true;
|
|
1019
|
+
return;
|
|
653
1020
|
}
|
|
654
|
-
this.counter.
|
|
655
|
-
|
|
656
|
-
|
|
1021
|
+
this.counter.bytesSent += bytes.length;
|
|
1022
|
+
msg.timestamp = new Date();
|
|
1023
|
+
logger.packet(msg);
|
|
1024
|
+
this.write(msg, (err) => {
|
|
1025
|
+
clearTimeout(this.writeTimer);
|
|
1026
|
+
this.writeTimer = null;
|
|
1027
|
+
msg.tries++;
|
|
1028
|
+
this.isRTS = true;
|
|
1029
|
+
if (err) {
|
|
1030
|
+
if (msg.remainingTries > 0) self._waitingPacket = msg;
|
|
1031
|
+
else {
|
|
1032
|
+
msg.failed = true;
|
|
1033
|
+
logger.warn(`Message aborted after ${msg.tries} attempt(s): ${bytes}: ${err} `);
|
|
1034
|
+
// this is a hard fail. We don't have any more tries left and the message didn't
|
|
1035
|
+
// make it onto the wire.
|
|
1036
|
+
let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
|
|
1037
|
+
if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
|
|
1038
|
+
self._waitingPacket = null;
|
|
1039
|
+
self.counter.sndAborted++;
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
logger.verbose(`Wrote packet [Port ${this.portId} id: ${msg.id}] [${bytes}].Retries remaining: ${msg.remainingTries} `);
|
|
1045
|
+
// We have all the success we are going to get so if the call succeeded then
|
|
1046
|
+
// don't set the waiting packet when we aren't actually waiting for a response.
|
|
1047
|
+
if (!msg.requiresResponse) {
|
|
1048
|
+
// As far as we know the message made it to OCP.
|
|
1049
|
+
self._waitingPacket = null;
|
|
1050
|
+
self.counter.sndSuccess++;
|
|
1051
|
+
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
1052
|
+
|
|
1053
|
+
}
|
|
1054
|
+
else if (msg.remainingTries >= 0) {
|
|
1055
|
+
self._waitingPacket = msg;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
self.counter.updatefailureRate();
|
|
1059
|
+
self.emitPortStats();
|
|
1060
|
+
});
|
|
657
1061
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
logger.
|
|
661
|
-
|
|
1062
|
+
}
|
|
1063
|
+
catch (err) {
|
|
1064
|
+
logger.error(`Error sending message: ${err.message}
|
|
1065
|
+
for message: ${msg.toShortPacket()}`)
|
|
1066
|
+
// the show, err, messages, must go on!
|
|
1067
|
+
if (this.isOpen) {
|
|
1068
|
+
clearTimeout(this.writeTimer);
|
|
1069
|
+
this.writeTimer = null;
|
|
662
1070
|
msg.tries++;
|
|
663
1071
|
this.isRTS = true;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// This is a hard fail. We don't have any more tries left and the message didn't
|
|
672
|
-
// make it onto the wire.
|
|
673
|
-
let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
|
|
674
|
-
if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
|
|
675
|
-
this._waitingPacket = null;
|
|
676
|
-
this.counter.sndAborted++;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
else {
|
|
680
|
-
logger.verbose(`Wrote packet[${bytes}].Retries remaining: ${msg.remainingTries} `);
|
|
681
|
-
// We have all the success we are going to get so if the call succeeded then
|
|
682
|
-
// don't set the waiting packet when we aren't actually waiting for a response.
|
|
683
|
-
if (!msg.requiresResponse) {
|
|
684
|
-
// As far as we know the message made it to OCP.
|
|
685
|
-
this._waitingPacket = null;
|
|
686
|
-
this.counter.sndSuccess++;
|
|
687
|
-
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
1072
|
+
msg.failed = true;
|
|
1073
|
+
// this is a hard fail. We don't have any more tries left and the message didn't
|
|
1074
|
+
// make it onto the wire.
|
|
1075
|
+
let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
|
|
1076
|
+
if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
|
|
1077
|
+
this._waitingPacket = null;
|
|
1078
|
+
this.counter.sndAborted++;
|
|
688
1079
|
|
|
689
|
-
|
|
690
|
-
else if (msg.remainingTries >= 0) {
|
|
691
|
-
this._waitingPacket = msg;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
this.counter.updatefailureRate();
|
|
695
|
-
this.emitPortStats();
|
|
696
|
-
});
|
|
1080
|
+
}
|
|
697
1081
|
}
|
|
698
1082
|
}
|
|
699
1083
|
private clearResponses(msgIn: Inbound) {
|
|
@@ -742,21 +1126,22 @@ export class RS485Port {
|
|
|
742
1126
|
let status = this.isOpen ? 'open' : this._cfg.enabled ? 'closed' : 'disabled';
|
|
743
1127
|
return extend(true, { portId: this.portId, status: status, reconnects: this.reconnects }, this.counter)
|
|
744
1128
|
}
|
|
745
|
-
|
|
1129
|
+
public emitPortStats() {
|
|
746
1130
|
webApp.emitToChannel('rs485PortStats', 'rs485Stats', this.stats);
|
|
747
1131
|
}
|
|
748
1132
|
private processCompletedMessage(msg: Inbound, ndx): number {
|
|
749
1133
|
msg.timestamp = new Date();
|
|
750
1134
|
msg.portId = this.portId;
|
|
751
1135
|
msg.id = Message.nextMessageId;
|
|
1136
|
+
//console.log(`msg id ${msg.id} assigned to port${msg.portId} action:${msg.action} ${msg.toShortPacket()}`)
|
|
752
1137
|
this.counter.recCollisions += msg.collisions;
|
|
753
1138
|
this.counter.recRewinds += msg.rewinds;
|
|
754
|
-
logger.packet(msg);
|
|
755
1139
|
this.emitPortStats();
|
|
756
1140
|
if (msg.isValid) {
|
|
757
1141
|
this.counter.recSuccess++;
|
|
758
1142
|
this.counter.updatefailureRate();
|
|
759
1143
|
msg.process();
|
|
1144
|
+
//conn.queueInboundToAnslq25(msg);
|
|
760
1145
|
this.clearResponses(msg);
|
|
761
1146
|
}
|
|
762
1147
|
else {
|
|
@@ -765,6 +1150,7 @@ export class RS485Port {
|
|
|
765
1150
|
console.log('RS485 Stats:' + this.counter.toLog());
|
|
766
1151
|
ndx = this.rewindFailedMessage(msg, ndx);
|
|
767
1152
|
}
|
|
1153
|
+
logger.packet(msg); // RSG - Moving this after msg clearing responses so emit will include responseFor data
|
|
768
1154
|
return ndx;
|
|
769
1155
|
}
|
|
770
1156
|
private rewindFailedMessage(msg: Inbound, ndx: number): number {
|
|
@@ -805,5 +1191,20 @@ export class RS485Port {
|
|
|
805
1191
|
} while (ndx < this._inBytes.length);
|
|
806
1192
|
}
|
|
807
1193
|
}
|
|
1194
|
+
public hasAssignedEquipment() {
|
|
1195
|
+
let pumps = sys.pumps.get();
|
|
1196
|
+
for (let i = 0; i < pumps.length; i++) {
|
|
1197
|
+
if (pumps[i].portId === this.portId) {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
let chlors = sys.chlorinators.get();
|
|
1202
|
+
for (let i = 0; i < chlors.length; i++) {
|
|
1203
|
+
if (chlors[i].portId === this.portId) {
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
808
1209
|
}
|
|
809
1210
|
export var conn: Connection = new Connection();
|