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