nodejs-poolcontroller 8.1.2 → 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.
- package/.eslintrc.json +36 -36
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
- package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
- package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/copilot-instructions.md +63 -0
- package/.github/workflows/ghcr-publish.yml +67 -0
- package/AGENTS.md +597 -0
- package/CONTRIBUTING.md +74 -74
- package/Changelog +292 -257
- package/Dockerfile +62 -19
- package/Gruntfile.js +40 -40
- package/LICENSE +661 -661
- package/README.md +318 -191
- package/anslq25/MessagesMock.ts +221 -221
- package/anslq25/boards/MockBoardFactory.ts +49 -49
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
- package/anslq25/boards/MockSystemBoard.ts +216 -216
- package/anslq25/chemistry/MockChlorinator.ts +98 -98
- package/anslq25/pumps/MockPump.ts +83 -83
- package/app.ts +115 -115
- package/config/Config.ts +57 -7
- package/config/VersionCheck.ts +63 -35
- package/controller/Constants.ts +809 -805
- package/controller/Equipment.ts +2688 -2664
- package/controller/Errors.ts +181 -181
- package/controller/Lockouts.ts +549 -549
- package/controller/State.ts +3738 -3690
- package/controller/boards/AquaLinkBoard.ts +1003 -1003
- package/controller/boards/BoardFactory.ts +53 -53
- package/controller/boards/EasyTouchBoard.ts +3202 -3202
- package/controller/boards/IntelliCenterBoard.ts +4393 -3899
- package/controller/boards/IntelliComBoard.ts +69 -69
- package/controller/boards/IntelliTouchBoard.ts +382 -382
- package/controller/boards/NixieBoard.ts +1944 -1929
- package/controller/boards/SunTouchBoard.ts +400 -400
- package/controller/boards/SystemBoard.ts +5268 -5268
- package/controller/comms/Comms.ts +1272 -1214
- package/controller/comms/ScreenLogic.ts +1665 -1665
- package/controller/comms/messages/Messages.ts +1433 -1243
- package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
- package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
- package/controller/comms/messages/config/CircuitMessage.ts +0 -0
- package/controller/comms/messages/config/ConfigMessage.ts +6 -0
- package/controller/comms/messages/config/CoverMessage.ts +0 -0
- package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
- package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
- package/controller/comms/messages/config/ExternalMessage.ts +96 -10
- package/controller/comms/messages/config/FeatureMessage.ts +0 -0
- package/controller/comms/messages/config/GeneralMessage.ts +0 -0
- package/controller/comms/messages/config/HeaterMessage.ts +0 -0
- package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
- package/controller/comms/messages/config/OptionsMessage.ts +194 -174
- package/controller/comms/messages/config/PumpMessage.ts +0 -0
- package/controller/comms/messages/config/RemoteMessage.ts +0 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
- package/controller/comms/messages/config/SecurityMessage.ts +0 -0
- package/controller/comms/messages/config/ValveMessage.ts +0 -0
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
- package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
- package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
- package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
- package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
- package/controller/comms/messages/status/VersionMessage.ts +103 -41
- package/controller/nixie/Nixie.ts +173 -173
- package/controller/nixie/NixieEquipment.ts +104 -104
- package/controller/nixie/bodies/Body.ts +120 -120
- package/controller/nixie/bodies/Filter.ts +135 -135
- package/controller/nixie/chemistry/ChemController.ts +2724 -2724
- package/controller/nixie/chemistry/ChemDoser.ts +806 -806
- package/controller/nixie/chemistry/Chlorinator.ts +367 -367
- package/controller/nixie/circuits/Circuit.ts +478 -478
- package/controller/nixie/heaters/Heater.ts +834 -834
- package/controller/nixie/pumps/Pump.ts +1194 -996
- package/controller/nixie/schedules/Schedule.ts +401 -401
- package/controller/nixie/valves/Valve.ts +170 -170
- package/defaultConfig.json +352 -347
- package/docker-compose.yml +32 -0
- package/logger/DataLogger.ts +448 -448
- package/logger/Logger.ts +448 -436
- package/package.json +58 -60
- package/sendSocket.js +32 -32
- package/tsconfig.json +25 -25
- package/types/express-multer.d.ts +32 -0
- package/web/Server.ts +1937 -1927
- package/web/bindings/aqualinkD.json +559 -559
- package/web/bindings/influxDB.json +1066 -1066
- package/web/bindings/mqtt.json +721 -721
- package/web/bindings/mqttAlt.json +746 -746
- package/web/bindings/rulesManager.json +54 -54
- package/web/bindings/smartThings-Hubitat.json +31 -31
- package/web/bindings/valveRelays.json +20 -20
- package/web/bindings/vera.json +25 -25
- package/web/interfaces/baseInterface.ts +188 -188
- package/web/interfaces/httpInterface.ts +148 -148
- package/web/interfaces/influxInterface.ts +283 -283
- package/web/interfaces/mqttInterface.ts +695 -695
- package/web/interfaces/ruleInterface.ts +101 -87
- package/web/services/config/Config.ts +1063 -1053
- package/web/services/config/ConfigSocket.ts +0 -0
- package/web/services/state/State.ts +0 -0
- package/web/services/state/StateSocket.ts +0 -0
- package/web/services/utilities/Utilities.ts +233 -233
- package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
|
@@ -1,1214 +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
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
public
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
public get
|
|
525
|
-
public
|
|
526
|
-
|
|
527
|
-
public
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
private
|
|
533
|
-
private
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.
|
|
583
|
-
this.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
this.
|
|
644
|
-
// if
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
this.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
sp.on('
|
|
720
|
-
this.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
this.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
this.
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
this._port.once('
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
this.
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
this._port
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (this.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
else
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
this.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
this.
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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();
|