nodejs-poolcontroller 7.5.1 → 7.7.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/.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/Changelog +19 -0
- package/Dockerfile +3 -3
- package/README.md +13 -8
- package/app.ts +1 -1
- package/config/Config.ts +38 -2
- package/config/VersionCheck.ts +27 -12
- package/controller/Constants.ts +2 -1
- package/controller/Equipment.ts +193 -9
- package/controller/Errors.ts +10 -0
- package/controller/Lockouts.ts +503 -0
- package/controller/State.ts +269 -64
- package/controller/boards/AquaLinkBoard.ts +1000 -0
- package/controller/boards/BoardFactory.ts +4 -0
- package/controller/boards/EasyTouchBoard.ts +468 -144
- package/controller/boards/IntelliCenterBoard.ts +466 -307
- package/controller/boards/IntelliTouchBoard.ts +37 -5
- package/controller/boards/NixieBoard.ts +671 -141
- package/controller/boards/SystemBoard.ts +1397 -641
- package/controller/comms/Comms.ts +462 -362
- package/controller/comms/messages/Messages.ts +174 -30
- package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
- package/controller/comms/messages/config/CircuitMessage.ts +1 -0
- package/controller/comms/messages/config/ExternalMessage.ts +10 -8
- package/controller/comms/messages/config/HeaterMessage.ts +141 -29
- package/controller/comms/messages/config/OptionsMessage.ts +9 -2
- package/controller/comms/messages/config/PumpMessage.ts +53 -35
- package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
- package/controller/comms/messages/config/ValveMessage.ts +2 -2
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
- package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
- package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
- package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
- package/controller/nixie/Nixie.ts +1 -1
- package/controller/nixie/bodies/Body.ts +3 -0
- package/controller/nixie/chemistry/ChemController.ts +164 -51
- package/controller/nixie/chemistry/Chlorinator.ts +137 -88
- package/controller/nixie/circuits/Circuit.ts +51 -19
- package/controller/nixie/heaters/Heater.ts +241 -31
- package/controller/nixie/pumps/Pump.ts +488 -206
- package/controller/nixie/schedules/Schedule.ts +91 -35
- package/controller/nixie/valves/Valve.ts +1 -1
- package/defaultConfig.json +20 -0
- package/package.json +21 -21
- package/web/Server.ts +94 -49
- package/web/bindings/aqualinkD.json +505 -0
- package/web/bindings/influxDB.json +71 -1
- package/web/bindings/mqtt.json +98 -39
- package/web/bindings/mqttAlt.json +59 -1
- package/web/interfaces/baseInterface.ts +1 -0
- package/web/interfaces/httpInterface.ts +23 -2
- package/web/interfaces/influxInterface.ts +45 -10
- package/web/interfaces/mqttInterface.ts +114 -54
- package/web/services/config/Config.ts +55 -132
- package/web/services/state/State.ts +81 -4
- package/web/services/state/StateSocket.ts +4 -4
- package/web/services/utilities/Utilities.ts +8 -6
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
- package/config copy.json +0 -300
- package/issue_template.md +0 -52
|
@@ -22,39 +22,67 @@ import { logger } from '../../logger/Logger';
|
|
|
22
22
|
import * as net from 'net';
|
|
23
23
|
import { setTimeout, setInterval } from 'timers';
|
|
24
24
|
import { Message, Outbound, Inbound, Response } from './messages/Messages';
|
|
25
|
-
import { InvalidOperationError, MessageError, OutboundMessageError } from '../Errors';
|
|
25
|
+
import { InvalidEquipmentDataError, InvalidOperationError, MessageError, OutboundMessageError } from '../Errors';
|
|
26
26
|
import { utils } from "../Constants";
|
|
27
|
+
import { sys } from "../Equipment";
|
|
27
28
|
import { webApp } from "../../web/Server";
|
|
28
29
|
const extend = require("extend");
|
|
29
30
|
export class Connection {
|
|
30
|
-
constructor() {
|
|
31
|
-
|
|
31
|
+
constructor() {}
|
|
32
|
+
public rs485Ports: RS485Port[] = [];
|
|
33
|
+
public get mockPort(): boolean {
|
|
34
|
+
let port = this.findPortById(0);
|
|
35
|
+
return typeof port !== 'undefined' && port.mockPort ? true : false;
|
|
32
36
|
}
|
|
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);
|
|
37
|
+
public isPortEnabled(portId: number) {
|
|
38
|
+
let port: RS485Port = this.findPortById(portId);
|
|
39
|
+
return typeof port === 'undefined' ? false : port.enabled;
|
|
50
40
|
}
|
|
51
|
-
public
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
public async deleteAuxPort(data: any): Promise<any> {
|
|
42
|
+
try {
|
|
43
|
+
let portId = parseInt(data.portId, 10);
|
|
44
|
+
if (isNaN(portId)) return Promise.reject(new InvalidEquipmentDataError(`A valid port id was not provided to be deleted`, 'RS485Port', data.id));
|
|
45
|
+
if (portId === 0) return Promise.reject(new InvalidEquipmentDataError(`You may not delete the primart RS485 Port`, 'RS485Port', data.id));
|
|
46
|
+
let port = this.findPortById(portId);
|
|
47
|
+
this.removePortById(portId);
|
|
48
|
+
let section = `controller.comms` + (portId === 0 ? '' : portId);
|
|
49
|
+
let cfg = config.getSection(section, {});
|
|
50
|
+
config.removeSection(section);
|
|
51
|
+
return cfg;
|
|
52
|
+
} catch (err) { logger.error(`Error deleting aux port`) }
|
|
53
|
+
}
|
|
54
|
+
public async setPortAsync(data: any): Promise<any> {
|
|
55
55
|
try {
|
|
56
|
+
let ccfg = config.getSection('controller');
|
|
57
|
+
let pConfig;
|
|
58
|
+
let portId;
|
|
59
|
+
let maxId = -1;
|
|
60
|
+
for (let sec in ccfg) {
|
|
61
|
+
if (sec.startsWith('comms')) {
|
|
62
|
+
let p = ccfg[sec];
|
|
63
|
+
maxId = Math.max(p.portId, maxId);
|
|
64
|
+
if (p.portId === data.portId) pConfig = p;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (typeof pConfig === 'undefined') {
|
|
68
|
+
// We are adding a new one.
|
|
69
|
+
if (data.portId === -1 || typeof data.portId === 'undefined') portId = maxId + 1;
|
|
70
|
+
else portId = data.portId;
|
|
71
|
+
}
|
|
72
|
+
else portId = pConfig.portId;
|
|
73
|
+
if (isNaN(portId) || portId < 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid port id defined ${portId}`, 'RS485Port', data.portId));
|
|
74
|
+
let section = `controller.comms` + (portId === 0 ? '' : portId);
|
|
56
75
|
// Lets set the config data.
|
|
57
|
-
let pdata = config.getSection(
|
|
76
|
+
let pdata = config.getSection(section, {
|
|
77
|
+
portId: portId,
|
|
78
|
+
rs485Port: "/dev/ttyUSB0",
|
|
79
|
+
portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
|
|
80
|
+
mockPort: false,
|
|
81
|
+
netConnect: false,
|
|
82
|
+
netHost: "raspberrypi",
|
|
83
|
+
netPort: 9801,
|
|
84
|
+
inactivityRetry: 10
|
|
85
|
+
});
|
|
58
86
|
pdata.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : utils.makeBool(pdata.enabled);
|
|
59
87
|
pdata.netConnect = typeof data.netConnect !== 'undefined' ? utils.makeBool(data.netConnect) : utils.makeBool(pdata.netConnect);
|
|
60
88
|
pdata.rs485Port = typeof data.rs485Port !== 'undefined' ? data.rs485Port : pdata.rs485Port;
|
|
@@ -63,11 +91,17 @@ export class Connection {
|
|
|
63
91
|
pdata.netHost = typeof data.netHost !== 'undefined' ? data.netHost : pdata.netHost;
|
|
64
92
|
pdata.netPort = typeof data.netPort === 'number' ? data.netPort : pdata.netPort;
|
|
65
93
|
}
|
|
66
|
-
if (
|
|
67
|
-
|
|
94
|
+
if (typeof data.portSettings !== 'undefined') {
|
|
95
|
+
pdata.portSettings = extend(true, { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false }, pdata.portSettings, data.portSettings);
|
|
96
|
+
}
|
|
97
|
+
let existing = this.findPortById(portId);
|
|
98
|
+
if (typeof existing !== 'undefined') {
|
|
99
|
+
if (!await existing.closeAsync()) {
|
|
100
|
+
return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port`, 'setPortAsync'));
|
|
101
|
+
}
|
|
68
102
|
}
|
|
69
|
-
config.setSection(
|
|
70
|
-
|
|
103
|
+
config.setSection(section, pdata);
|
|
104
|
+
let cfg = config.getSection(section, {
|
|
71
105
|
rs485Port: "/dev/ttyUSB0",
|
|
72
106
|
portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
|
|
73
107
|
mockPort: false,
|
|
@@ -76,46 +110,224 @@ export class Connection {
|
|
|
76
110
|
netPort: 9801,
|
|
77
111
|
inactivityRetry: 10
|
|
78
112
|
});
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
existing = this.getPortById(cfg);
|
|
114
|
+
if (typeof existing !== 'undefined') {
|
|
115
|
+
existing.reconnects = 0;
|
|
116
|
+
if (!await existing.openAsync(cfg)) {
|
|
117
|
+
return Promise.reject(new InvalidOperationError(`Unable to open RS485 port ${pdata.rs485Port}`, 'setPortAsync'));
|
|
118
|
+
}
|
|
81
119
|
}
|
|
82
|
-
return
|
|
120
|
+
return cfg;
|
|
83
121
|
} catch (err) { return Promise.reject(err); }
|
|
84
122
|
}
|
|
85
|
-
public async
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
public async stopAsync() {
|
|
124
|
+
try {
|
|
125
|
+
for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
|
|
126
|
+
let port = this.rs485Ports[i];
|
|
127
|
+
await port.closeAsync();
|
|
128
|
+
}
|
|
129
|
+
logger.info(`Closed all serial communications connection.`);
|
|
130
|
+
} catch (err) { logger.error(`Error closing comms connection: ${err.message} `); }
|
|
131
|
+
}
|
|
132
|
+
public async initAsync() {
|
|
133
|
+
try {
|
|
134
|
+
// 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
|
|
135
|
+
// 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.
|
|
136
|
+
let cfg = config.getSection('controller');
|
|
137
|
+
for (let section in cfg) {
|
|
138
|
+
if (section.startsWith('comms')) {
|
|
139
|
+
let port = new RS485Port(cfg[section]);
|
|
140
|
+
this.rs485Ports.push(port);
|
|
141
|
+
await port.openAsync();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) { logger.error(`Error initializing RS485 ports ${err.message}`); }
|
|
145
|
+
}
|
|
146
|
+
public findPortById(portId?: number): RS485Port { return this.rs485Ports.find(elem => elem.portId === (portId || 0)); }
|
|
147
|
+
public async removePortById(portId: number) {
|
|
148
|
+
for (let i = this.rs485Ports.length - 1; i >= 0; i--) {
|
|
149
|
+
let port = this.rs485Ports[i];
|
|
150
|
+
if (port.portId === portId) {
|
|
151
|
+
await port.closeAsync();
|
|
152
|
+
// Don't remove the primary port. You cannot delete this one.
|
|
153
|
+
if(portId !== 0) this.rs485Ports.splice(i, 1);
|
|
154
|
+
}
|
|
90
155
|
}
|
|
156
|
+
}
|
|
157
|
+
public getPortById(cfg: any) {
|
|
158
|
+
let port = this.findPortById(cfg.portId || 0);
|
|
159
|
+
if (typeof port === 'undefined') {
|
|
160
|
+
port = new RS485Port(cfg);
|
|
161
|
+
this.rs485Ports.push(port);
|
|
162
|
+
}
|
|
163
|
+
return port;
|
|
164
|
+
}
|
|
165
|
+
public async listInstalledPorts(): Promise<any> {
|
|
166
|
+
try {
|
|
167
|
+
let ports = [];
|
|
168
|
+
// 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
|
|
169
|
+
// 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.
|
|
170
|
+
let cfg = config.getSection('controller');
|
|
171
|
+
for (let section in cfg) {
|
|
172
|
+
if (section.startsWith('comms')) {
|
|
173
|
+
let port = config.getSection(`controller.${section}`);
|
|
174
|
+
if (port.portId === 0) port.name = 'Primary';
|
|
175
|
+
else port.name = `Aux${port.portId}`;
|
|
176
|
+
let p = this.findPortById(port.portId);
|
|
177
|
+
port.isOpen = typeof p !== 'undefined' ? p.isOpen : false;
|
|
178
|
+
ports.push(port);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return ports;
|
|
182
|
+
} catch (err) { logger.error(`Error listing installed RS485 ports ${err.message}`); }
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
public queueSendMessage(msg: Outbound) {
|
|
186
|
+
let port = this.findPortById(msg.portId);
|
|
187
|
+
if (typeof port !== 'undefined')
|
|
188
|
+
port.emitter.emit('messagewrite', msg);
|
|
189
|
+
else
|
|
190
|
+
logger.error(`queueSendMessage: Message was targeted for undefined port ${msg.portId || 0}`);
|
|
191
|
+
}
|
|
192
|
+
public pauseAll() {
|
|
193
|
+
for (let i = 0; i < this.rs485Ports.length; i++) {
|
|
194
|
+
let port = this.rs485Ports[i];
|
|
195
|
+
port.pause();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
public resumeAll() {
|
|
199
|
+
for (let i = 0; i < this.rs485Ports.length; i++) {
|
|
200
|
+
let port = this.rs485Ports[i];
|
|
201
|
+
port.resume();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
public async getLocalPortsAsync(): Promise<any> {
|
|
205
|
+
try {
|
|
206
|
+
return await SerialPort.list();
|
|
207
|
+
} catch (err) { logger.error(`Error retrieving local ports ${err.message}`); }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export class Counter {
|
|
211
|
+
constructor() {
|
|
212
|
+
this.bytesReceived = 0;
|
|
213
|
+
this.recSuccess = 0;
|
|
214
|
+
this.recFailed = 0;
|
|
215
|
+
this.recCollisions = 0;
|
|
216
|
+
this.bytesSent = 0;
|
|
217
|
+
this.sndAborted = 0;
|
|
218
|
+
this.sndRetries = 0;
|
|
219
|
+
this.sndSuccess = 0;
|
|
220
|
+
this.recFailureRate = 0;
|
|
221
|
+
this.sndFailureRate = 0;
|
|
222
|
+
this.recRewinds = 0;
|
|
223
|
+
}
|
|
224
|
+
public bytesReceived: number;
|
|
225
|
+
public bytesSent: number;
|
|
226
|
+
public recSuccess: number;
|
|
227
|
+
public recFailed: number;
|
|
228
|
+
public recCollisions: number;
|
|
229
|
+
public recFailureRate: number;
|
|
230
|
+
public sndSuccess: number;
|
|
231
|
+
public sndAborted: number;
|
|
232
|
+
public sndRetries: number;
|
|
233
|
+
public sndFailureRate: number;
|
|
234
|
+
public recRewinds: number;
|
|
235
|
+
public updatefailureRate(): void {
|
|
236
|
+
this.recFailureRate = (this.recFailed + this.recSuccess) !== 0 ? (this.recFailed / (this.recFailed + this.recSuccess) * 100) : 0;
|
|
237
|
+
this.sndFailureRate = (this.sndAborted + this.sndSuccess) !== 0 ? (this.sndAborted / (this.sndAborted + this.sndSuccess) * 100) : 0;
|
|
238
|
+
}
|
|
239
|
+
public toLog(): string {
|
|
240
|
+
return `{ "bytesReceived": ${this.bytesReceived} "success": ${this.recSuccess}, "failed": ${this.recFailed}, "bytesSent": ${this.bytesSent}, "collisions": ${this.recCollisions}, "failureRate": ${this.recFailureRate.toFixed(2)}% }`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// The following class allows njsPC to have multiple RS485 buses. Each port has its own buffer and message processor
|
|
244
|
+
// so that devices on the bus can be isolated to a particular port. By doing this the communications are such that multiple
|
|
245
|
+
// ports can be used to accommodate differing port speeds and fixed port addresses. If an
|
|
246
|
+
export class RS485Port {
|
|
247
|
+
constructor(cfg: any) {
|
|
248
|
+
this._cfg = cfg;
|
|
249
|
+
|
|
250
|
+
this.emitter = new EventEmitter();
|
|
251
|
+
this._inBuffer = [];
|
|
252
|
+
this._outBuffer = [];
|
|
253
|
+
this.procTimer = null;
|
|
254
|
+
this.emitter.on('messagewrite', (msg) => { this.pushOut(msg); });
|
|
255
|
+
}
|
|
256
|
+
public isRTS: boolean = true;
|
|
257
|
+
public reconnects:number = 0;
|
|
258
|
+
public emitter: EventEmitter;
|
|
259
|
+
public get portId() { return typeof this._cfg !== 'undefined' && typeof this._cfg.portId !== 'undefined' ? this._cfg.portId : 0; }
|
|
260
|
+
public isOpen: boolean = false;
|
|
261
|
+
private _closing: boolean = false;
|
|
262
|
+
private _cfg: any;
|
|
263
|
+
private _port: any;
|
|
264
|
+
public mockPort: boolean = false;
|
|
265
|
+
private isPaused: boolean = false;
|
|
266
|
+
private connTimer: NodeJS.Timeout;
|
|
267
|
+
//public buffer: SendRecieveBuffer;
|
|
268
|
+
public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
|
|
269
|
+
public counter: Counter = new Counter();
|
|
270
|
+
private procTimer: NodeJS.Timeout;
|
|
271
|
+
private _processing: boolean = false;
|
|
272
|
+
private _inBytes: number[] = [];
|
|
273
|
+
private _inBuffer: number[] = [];
|
|
274
|
+
private _outBuffer: Outbound[] = [];
|
|
275
|
+
private _waitingPacket: Outbound;
|
|
276
|
+
private _msg: Inbound;
|
|
277
|
+
// Connection management functions
|
|
278
|
+
public async openAsync(cfg?: any): Promise<boolean> {
|
|
279
|
+
if (this.isOpen) await this.closeAsync();
|
|
280
|
+
if (typeof cfg !== 'undefined') this._cfg = cfg;
|
|
281
|
+
if (!this._cfg.enabled) return true;
|
|
91
282
|
if (this._cfg.netConnect && !this._cfg.mockPort) {
|
|
92
|
-
|
|
283
|
+
let sock: net.Socket = this._port as net.Socket;
|
|
284
|
+
if (typeof this._port !== 'undefined' && this.isOpen) {
|
|
93
285
|
// This used to try to reconnect and recreate events even though the socket was already connected. This resulted in
|
|
94
|
-
// instances where multiple event processors were present.
|
|
95
|
-
|
|
286
|
+
// instances where multiple event processors were present. Node doesn't give us any indication that the socket is
|
|
287
|
+
// still viable or if it is closing from either end.
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
else if (typeof this._port !== 'undefined') {
|
|
291
|
+
// We need to kill the existing connection by ending it.
|
|
292
|
+
this._port.end();
|
|
96
293
|
}
|
|
97
294
|
let nc: net.Socket = new net.Socket();
|
|
98
|
-
nc.
|
|
99
|
-
nc.
|
|
295
|
+
nc.once('connect', () => { logger.info(`Net connect (socat) ${this._cfg.portId} connected to: ${this._cfg.netHost}:${this._cfg.netPort}`); }); // Socket is opened but not yet ready.
|
|
296
|
+
nc.once('ready', () => {
|
|
100
297
|
this.isOpen = true;
|
|
101
298
|
this.isRTS = true;
|
|
102
|
-
logger.info(`Net connect (socat) ready and communicating: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
299
|
+
logger.info(`Net connect (socat) ${this._cfg.portId} ready and communicating: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
103
300
|
nc.on('data', (data) => {
|
|
104
301
|
//this.resetConnTimer();
|
|
105
|
-
if (data.length > 0 && !this.isPaused) this.
|
|
302
|
+
if (data.length > 0 && !this.isPaused) this.pushIn(data);
|
|
106
303
|
});
|
|
304
|
+
this.emitPortStats();
|
|
107
305
|
});
|
|
108
|
-
nc.
|
|
306
|
+
nc.once('close', (p) => {
|
|
109
307
|
this.isOpen = false;
|
|
110
|
-
if (typeof this._port !== 'undefined') this._port.destroy();
|
|
308
|
+
if (typeof this._port !== 'undefined' && !this._port.destroyed) this._port.destroy();
|
|
111
309
|
this._port = undefined;
|
|
112
|
-
this.
|
|
113
|
-
|
|
310
|
+
this.clearOutboundBuffer();
|
|
311
|
+
this.emitPortStats();
|
|
312
|
+
if (!this._closing) {
|
|
313
|
+
// If we are closing manually this event should have been cleared already and should never be called. If this is fired out
|
|
314
|
+
// of sequence then we will check the closing flag to ensure we are not forcibly closing the socket.
|
|
315
|
+
if (typeof this.connTimer !== 'undefined' && this.connTimer) {
|
|
316
|
+
clearTimeout(this.connTimer);
|
|
317
|
+
this.connTimer = null;
|
|
318
|
+
}
|
|
319
|
+
this.connTimer = setTimeout(async () => {
|
|
320
|
+
try {
|
|
321
|
+
// We are already closed so give some inactivity retry and try again.
|
|
322
|
+
await this.openAsync();
|
|
323
|
+
} catch (err) { }
|
|
324
|
+
}, this._cfg.inactivityRetry * 1000);
|
|
325
|
+
}
|
|
326
|
+
logger.info(`Net connect (socat) ${this._cfg.portId} closed ${p === true ? 'due to error' : ''}: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
114
327
|
});
|
|
115
328
|
nc.on('end', () => { // Happens when the other end of the socket closes.
|
|
116
329
|
this.isOpen = false;
|
|
117
|
-
|
|
118
|
-
logger.info(`Net connect (socat) end event was fired`);
|
|
330
|
+
logger.info(`Net connect (socat) ${this.portId} end event was fired`);
|
|
119
331
|
});
|
|
120
332
|
//nc.on('drain', () => { logger.info(`The drain event was fired.`); });
|
|
121
333
|
//nc.on('lookup', (o) => { logger.info(`The lookup event was fired ${o}`); });
|
|
@@ -123,13 +335,15 @@ export class Connection {
|
|
|
123
335
|
// left the connection in a weird state where the previous connection was processing events and the new connection was
|
|
124
336
|
// 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.
|
|
125
337
|
//nc.on('timeout', () => { logger.warn(`Net connect (socat) Connection Idle: ${this._cfg.netHost}:${this._cfg.netPort}`); });
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
338
|
+
if (this._cfg.inactivityRetry > 0) {
|
|
339
|
+
nc.setTimeout(Math.max(this._cfg.inactivityRetry, 10) * 1000, async () => {
|
|
340
|
+
logger.warn(`Net connect (socat) connection idle: ${this._cfg.netHost}:${this._cfg.netPort} retrying connection.`);
|
|
341
|
+
try {
|
|
342
|
+
await this.closeAsync();
|
|
343
|
+
await this.openAsync();
|
|
344
|
+
} catch (err) { logger.error(`Net connect (socat)$ {this.portId} error retrying connection ${err.message}`); }
|
|
345
|
+
});
|
|
346
|
+
}
|
|
133
347
|
|
|
134
348
|
return await new Promise<boolean>((resolve, _) => {
|
|
135
349
|
// We only connect an error once as we will destroy this connection on error then recreate a new socket on failure.
|
|
@@ -137,19 +351,21 @@ export class Connection {
|
|
|
137
351
|
//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}`}`);
|
|
138
352
|
//this.resetConnTimer();
|
|
139
353
|
this.isOpen = false;
|
|
354
|
+
this.emitPortStats();
|
|
140
355
|
// if the promise has already been fulfilled, but the error happens later, we don't want to call the promise again.
|
|
141
356
|
if (typeof resolve !== 'undefined') { resolve(false); }
|
|
142
357
|
if (this._cfg.inactivityRetry > 0) {
|
|
143
|
-
logger.error(`Net connect (socat) connection error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
|
|
144
|
-
setTimeout(async () => { try { await
|
|
358
|
+
logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Retry in ${this._cfg.inactivityRetry} seconds`);
|
|
359
|
+
setTimeout(async () => { try { await this.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
|
|
145
360
|
}
|
|
146
|
-
else logger.error(`Net connect (socat) connection error: ${err}. Never retrying -- No retry time set`);
|
|
361
|
+
else logger.error(`Net connect (socat) connection ${this.portId} error: ${err}. Never retrying -- No retry time set`);
|
|
147
362
|
});
|
|
148
|
-
nc.connect(
|
|
149
|
-
if (typeof this._port !== 'undefined') logger.warn(
|
|
150
|
-
logger.info(`Net connect (socat) Connection connected`);
|
|
363
|
+
nc.connect(this._cfg.netPort, this._cfg.netHost, () => {
|
|
364
|
+
if (typeof this._port !== 'undefined') logger.warn(`Net connect (socat) ${this.portId} recovered from lost connection.`);
|
|
365
|
+
logger.info(`Net connect (socat) Connection ${this.portId} connected`);
|
|
151
366
|
this._port = nc;
|
|
152
367
|
this.isOpen = true;
|
|
368
|
+
this.emitPortStats();
|
|
153
369
|
resolve(true);
|
|
154
370
|
resolve = undefined;
|
|
155
371
|
});
|
|
@@ -158,9 +374,9 @@ export class Connection {
|
|
|
158
374
|
else {
|
|
159
375
|
if (typeof this._port !== 'undefined' && this._port.isOpen) {
|
|
160
376
|
// This used to try to reconnect even though the serial port was already connected. This resulted in
|
|
161
|
-
// instances where an access denied error was emitted.
|
|
377
|
+
// instances where an access denied error was emitted. So if the port is open we will simply return.
|
|
162
378
|
this.resetConnTimer();
|
|
163
|
-
return
|
|
379
|
+
return true;
|
|
164
380
|
}
|
|
165
381
|
let sp: SerialPort = null;
|
|
166
382
|
if (this._cfg.mockPort) {
|
|
@@ -172,9 +388,9 @@ export class Connection {
|
|
|
172
388
|
}
|
|
173
389
|
else {
|
|
174
390
|
this.mockPort = false;
|
|
175
|
-
sp = new SerialPort(
|
|
391
|
+
sp = new SerialPort(this._cfg.rs485Port, this._cfg.portSettings);
|
|
176
392
|
}
|
|
177
|
-
return new Promise<boolean>((resolve, _) => {
|
|
393
|
+
return await new Promise<boolean>((resolve, _) => {
|
|
178
394
|
// The serial port open method calls the callback just once. Unfortunately that is not the case for
|
|
179
395
|
// network serial port connections. There really isn't a way to make it syncronous. The openAsync will truly
|
|
180
396
|
// be open if a hardware interface is used and this method returns.
|
|
@@ -182,7 +398,7 @@ export class Connection {
|
|
|
182
398
|
if (err) {
|
|
183
399
|
this.resetConnTimer();
|
|
184
400
|
this.isOpen = false;
|
|
185
|
-
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}`}`);
|
|
401
|
+
logger.error(`Error opening port ${this.portId}: ${err.message}. ${this._cfg.inactivityRetry > 0 ? `Retry in ${this._cfg.inactivityRetry} seconds` : `Never retrying; inactivityRetry set to ${this._cfg.inactivityRetry}`}`);
|
|
186
402
|
resolve(false);
|
|
187
403
|
}
|
|
188
404
|
else resolve(true);
|
|
@@ -192,28 +408,31 @@ export class Connection {
|
|
|
192
408
|
// won't be called until long after the promise is resolved above. Yes we should never reject this promise. The resolution is true
|
|
193
409
|
// for a successul connect and false otherwise.
|
|
194
410
|
sp.on('open', () => {
|
|
195
|
-
if (typeof
|
|
196
|
-
else logger.info(`Serial port: ${this._cfg.rs485Port} request to open successful`);
|
|
411
|
+
if (typeof this._port !== 'undefined') logger.info(`Serial Port ${this.portId}: ${this._cfg.rs485Port} recovered from lost connection.`)
|
|
412
|
+
else logger.info(`Serial port: ${this._cfg.rs485Port} request to open successful ${this._cfg.portSettings.baudRate}b ${this._cfg.portSettings.dataBits}-${this._cfg.portSettings.parity}-${this._cfg.portSettings.stopBits}`);
|
|
197
413
|
this._port = sp;
|
|
198
414
|
this.isOpen = true;
|
|
199
|
-
sp.on('data', (data) => { if (!this.mockPort && !this.isPaused) this.
|
|
415
|
+
sp.on('data', (data) => { if (!this.mockPort && !this.isPaused) this.resetConnTimer(); this.pushIn(data); });
|
|
200
416
|
this.resetConnTimer();
|
|
417
|
+
this.emitPortStats();
|
|
201
418
|
});
|
|
202
419
|
sp.on('close', (err) => {
|
|
203
420
|
this.isOpen = false;
|
|
204
|
-
logger.info(`Serial Port has been closed: ${err ? JSON.stringify(err) : ''}`);
|
|
421
|
+
logger.info(`Serial Port ${this.portId} has been closed ${this.portId}: ${err ? JSON.stringify(err) : ''}`);
|
|
205
422
|
});
|
|
206
423
|
sp.on('error', (err) => {
|
|
207
424
|
this.isOpen = false;
|
|
208
425
|
if (sp.isOpen) sp.close((err) => { }); // call this with the error callback so that it doesn't emit to the error again.
|
|
209
426
|
this.resetConnTimer();
|
|
210
|
-
logger.error(`Serial Port: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
|
|
427
|
+
logger.error(`Serial Port ${this.portId}: An error occurred : ${this._cfg.rs485Port}: ${JSON.stringify(err)}`);
|
|
428
|
+
this.emitPortStats();
|
|
211
429
|
});
|
|
212
430
|
});
|
|
213
431
|
}
|
|
214
432
|
}
|
|
215
433
|
public async closeAsync(): Promise<boolean> {
|
|
216
434
|
try {
|
|
435
|
+
if (this._closing) return false;
|
|
217
436
|
this._closing = true;
|
|
218
437
|
if (this.connTimer) clearTimeout(this.connTimer);
|
|
219
438
|
if (typeof this._port !== 'undefined' && this.isOpen) {
|
|
@@ -222,33 +441,39 @@ export class Connection {
|
|
|
222
441
|
this._port.removeAllListeners();
|
|
223
442
|
this._port.once('error', (err) => {
|
|
224
443
|
if (err) {
|
|
225
|
-
logger.error(`Error closing ${this._cfg.netHost}
|
|
444
|
+
logger.error(`Error closing ${this.portId} ${ this._cfg.netHost }: ${ this._cfg.netPort } / ${ this._cfg.rs485Port }: ${ err }`);
|
|
226
445
|
resolve(false);
|
|
227
446
|
}
|
|
228
447
|
else {
|
|
229
|
-
|
|
448
|
+
this._port = undefined;
|
|
230
449
|
this.isOpen = false;
|
|
231
|
-
logger.info(`Successfully closed (socat) port ${this._cfg.netHost}:${this._cfg.netPort}
|
|
450
|
+
logger.info(`Successfully closed (socat) ${this.portId} port ${this._cfg.netHost}:${this._cfg.netPort} / ${this._cfg.rs485Port}`);
|
|
232
451
|
resolve(true);
|
|
233
452
|
}
|
|
234
453
|
});
|
|
454
|
+
this._port.once('end', () => {
|
|
455
|
+
logger.info(`Net connect (socat) ${this.portId} closing: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
456
|
+
});
|
|
235
457
|
this._port.once('close', (p) => {
|
|
236
458
|
this.isOpen = false;
|
|
237
459
|
this._port = undefined;
|
|
238
|
-
logger.info(`Net connect (socat) successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
460
|
+
logger.info(`Net connect (socat) ${this.portId} successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
239
461
|
resolve(true);
|
|
240
462
|
});
|
|
463
|
+
logger.info(`Net connect (socat) ${this.portId} request close: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
464
|
+
// Unfortunately the end call does not actually work in node. It will simply not return anything so we are going to
|
|
465
|
+
// just call destroy and forcibly close it.
|
|
241
466
|
this._port.destroy();
|
|
242
467
|
}
|
|
243
|
-
else if (typeof
|
|
244
|
-
|
|
468
|
+
else if (typeof this._port.close === 'function') {
|
|
469
|
+
this._port.close((err) => {
|
|
245
470
|
if (err) {
|
|
246
|
-
logger.error(`Error closing ${this._cfg.rs485Port}: ${err}`);
|
|
471
|
+
logger.error(`Error closing ${this.portId} serial port ${this._cfg.rs485Port}: ${err}`);
|
|
247
472
|
resolve(false);
|
|
248
473
|
}
|
|
249
474
|
else {
|
|
250
|
-
|
|
251
|
-
logger.info(`Successfully closed
|
|
475
|
+
this._port = undefined;
|
|
476
|
+
logger.info(`Successfully closed ${this.portId} serial port ${this._cfg.rs485Port}`);
|
|
252
477
|
resolve(true);
|
|
253
478
|
this.isOpen = false;
|
|
254
479
|
}
|
|
@@ -256,270 +481,167 @@ export class Connection {
|
|
|
256
481
|
}
|
|
257
482
|
else {
|
|
258
483
|
resolve(true);
|
|
259
|
-
|
|
484
|
+
this._port = undefined;
|
|
260
485
|
}
|
|
261
486
|
});
|
|
262
|
-
if (success) {
|
|
263
|
-
if (typeof conn.buffer !== 'undefined') conn.buffer.close();
|
|
264
|
-
}
|
|
265
|
-
|
|
487
|
+
if (success) { this.closeBuffer(); }
|
|
266
488
|
return success;
|
|
267
489
|
}
|
|
268
490
|
return true;
|
|
269
|
-
} catch (err) { logger.error(`Error closing comms connection: ${err.message}`); return Promise.resolve(false); }
|
|
491
|
+
} catch (err) { logger.error(`Error closing comms connection ${this.portId}: ${err.message}`); return Promise.resolve(false); }
|
|
492
|
+
finally { this._closing = false; this.emitPortStats(); }
|
|
270
493
|
}
|
|
271
|
-
public
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
logger.info(`Successfully closed (socat) port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}`);
|
|
288
|
-
resolve(true);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
this._port.once('close', (p) => {
|
|
292
|
-
this.isOpen = false;
|
|
293
|
-
this._port = undefined;
|
|
294
|
-
logger.info(`Net connect (socat) successfully closed: ${this._cfg.netHost}:${this._cfg.netPort}`);
|
|
295
|
-
resolve(true);
|
|
296
|
-
});
|
|
297
|
-
this._port.destroy();
|
|
298
|
-
}
|
|
299
|
-
else if (typeof conn._port.close === 'function') {
|
|
300
|
-
conn._port.close((err) => {
|
|
301
|
-
if (err) {
|
|
302
|
-
logger.error(`Error closing ${this._cfg.rs485Port}: ${err}`);
|
|
303
|
-
resolve(false);
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
conn._port = undefined;
|
|
307
|
-
logger.info(`Successfully closed seral port ${this._cfg.rs485Port}`);
|
|
308
|
-
resolve(true);
|
|
309
|
-
this.isOpen = false;
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
resolve(true);
|
|
315
|
-
conn._port = undefined;
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
if (success) {
|
|
319
|
-
if (typeof conn.buffer !== 'undefined') conn.buffer.close();
|
|
320
|
-
}
|
|
321
|
-
return success;
|
|
494
|
+
public pause() { this.isPaused = true; this.clearBuffer(); this.drain(function (err) { }); }
|
|
495
|
+
// RKS: Resume is executed in a closure. This is because we want the current async process to complete
|
|
496
|
+
// before we resume. This way the messages are cleared right before we restart.
|
|
497
|
+
public resume() { if (this.isPaused) setTimeout(() => { this.clearBuffer(); this.isPaused = false; }, 0); }
|
|
498
|
+
protected resetConnTimer(...args) {
|
|
499
|
+
//console.log(`resetting connection timer`);
|
|
500
|
+
if (this.connTimer !== null) clearTimeout(this.connTimer);
|
|
501
|
+
if (!this._cfg.mockPort && this._cfg.inactivityRetry > 0 && !this._closing) this.connTimer = setTimeout(async () => {
|
|
502
|
+
try {
|
|
503
|
+
if (this._cfg.netConnect)
|
|
504
|
+
logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
|
|
505
|
+
else
|
|
506
|
+
logger.warn(`Inactivity timeout for ${this.portId} serial port ${this._cfg.rs485Port} after ${this._cfg.inactivityRetry} seconds`);
|
|
507
|
+
//await this.closeAsync();
|
|
508
|
+
this.reconnects++;
|
|
509
|
+
await this.openAsync();
|
|
322
510
|
}
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
finally { this._closing = false; }
|
|
511
|
+
catch (err) { logger.error(`Error resetting RS485 port on inactivity: ${err.message}`); };
|
|
512
|
+
}, this._cfg.inactivityRetry * 1000);
|
|
326
513
|
}
|
|
514
|
+
// Data management functions
|
|
327
515
|
public drain(cb: Function) {
|
|
328
|
-
if (typeof
|
|
329
|
-
|
|
516
|
+
if (typeof this._port === 'undefined') {
|
|
517
|
+
logger.debug(`Serial Port ${this.portId}: Cannot perform drain function on port that is not open.`);
|
|
518
|
+
cb();
|
|
519
|
+
}
|
|
520
|
+
if (typeof (this._port.drain) === 'function')
|
|
521
|
+
this._port.drain(cb);
|
|
330
522
|
else // Call the method immediately as the port doesn't wait to send.
|
|
331
523
|
cb();
|
|
332
524
|
}
|
|
333
525
|
public write(bytes: Buffer, cb: Function) {
|
|
334
|
-
if (
|
|
526
|
+
if (this._cfg.netConnect) {
|
|
335
527
|
// SOCAT drops the connection and destroys the stream. Could be weeks or as little as a day.
|
|
336
|
-
if (typeof
|
|
337
|
-
|
|
338
|
-
|
|
528
|
+
if (typeof this._port === 'undefined' || this._port.destroyed !== false) {
|
|
529
|
+
this.openAsync().then(() => {
|
|
530
|
+
this._port.write(bytes, 'binary', cb);
|
|
339
531
|
});
|
|
340
532
|
}
|
|
341
533
|
else
|
|
342
|
-
|
|
534
|
+
this._port.write(bytes, 'binary', cb);
|
|
343
535
|
}
|
|
344
536
|
else
|
|
345
|
-
|
|
537
|
+
this._port.write(bytes, cb);
|
|
346
538
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
netPort: 9801,
|
|
373
|
-
inactivityRetry: 10
|
|
374
|
-
}));
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
public reloadConfig(cfg) {
|
|
378
|
-
let c = extend({
|
|
379
|
-
rs485Port: "/dev/ttyUSB0",
|
|
380
|
-
portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
|
|
381
|
-
mockPort: false,
|
|
382
|
-
netConnect: false,
|
|
383
|
-
netHost: "raspberrypi",
|
|
384
|
-
netPort: 9801,
|
|
385
|
-
inactivityRetry: 10
|
|
386
|
-
}, cfg);
|
|
387
|
-
if (JSON.stringify(c) !== JSON.stringify(this._cfg)) {
|
|
388
|
-
this.closeAsync();
|
|
389
|
-
this._cfg = c;
|
|
390
|
-
if (this._cfg.enabled) this.openAsync();
|
|
539
|
+
private pushIn(pkt) { this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data); if(sys.isReady) setImmediate(() => { this.processPackets(); }); }
|
|
540
|
+
private pushOut(msg) { this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); }); }
|
|
541
|
+
private clearBuffer() { this._inBuffer.length = 0; this.clearOutboundBuffer(); }
|
|
542
|
+
private closeBuffer() { clearTimeout(this.procTimer); this.clearBuffer(); this._msg = undefined; }
|
|
543
|
+
private clearOutboundBuffer() {
|
|
544
|
+
let processing = this._processing;
|
|
545
|
+
clearTimeout(this.procTimer);
|
|
546
|
+
this.procTimer = null;
|
|
547
|
+
this._processing = true;
|
|
548
|
+
this.isRTS = false;
|
|
549
|
+
let msg: Outbound = typeof this._waitingPacket !== 'undefined' ? this._waitingPacket : this._outBuffer.shift();
|
|
550
|
+
this._waitingPacket = null;
|
|
551
|
+
while (typeof msg !== 'undefined' && msg) {
|
|
552
|
+
// Fail the message.
|
|
553
|
+
msg.failed = true;
|
|
554
|
+
if (typeof msg.onAbort === 'function') msg.onAbort();
|
|
555
|
+
else logger.warn(`Message cleared from outbound buffer: ${msg.toShortPacket()} `);
|
|
556
|
+
let err = new OutboundMessageError(msg, `Message cleared from outbound buffer: ${msg.toShortPacket()} `);
|
|
557
|
+
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
558
|
+
if (msg.requiresResponse) {
|
|
559
|
+
// Wait for this current process to complete then bombard all the processes with the callback.
|
|
560
|
+
if (msg.response instanceof Response && typeof (msg.response.callback) === 'function') setImmediate(msg.response.callback, msg);
|
|
561
|
+
}
|
|
562
|
+
this.counter.sndAborted++;
|
|
563
|
+
msg = this._outBuffer.shift();
|
|
391
564
|
}
|
|
565
|
+
this._processing = processing;
|
|
566
|
+
this.isRTS = true;
|
|
392
567
|
}
|
|
393
|
-
public queueSendMessage(msg: Outbound) { conn.emitter.emit('messagewrite', msg); }
|
|
394
|
-
public pause() { conn.isPaused = true; conn.buffer.clear(); conn.drain(function (err) { }); }
|
|
395
|
-
// RKS: Resume is executed in a closure. This is because we want the current async process to complete
|
|
396
|
-
// before we resume. This way the messages are cleared right before we restart.
|
|
397
|
-
public resume() { if (this.isPaused) setTimeout(function () { conn.buffer.clear(); conn.isPaused = false; }, 0); }
|
|
398
|
-
// RKS: This appears to not be used.
|
|
399
|
-
//public queueReceiveMessage(pkt: Inbound) {
|
|
400
|
-
// logger.info(`Receiving ${ pkt.action } `);
|
|
401
|
-
// conn.buffer.pushIn(pkt);
|
|
402
|
-
//}
|
|
403
|
-
}
|
|
404
|
-
export class SendRecieveBuffer {
|
|
405
|
-
constructor() {
|
|
406
|
-
this._inBuffer = [];
|
|
407
|
-
this._outBuffer = [];
|
|
408
|
-
this.procTimer = null;//setInterval(this.processPackets, 175);
|
|
409
|
-
}
|
|
410
|
-
public counter: Counter = new Counter();
|
|
411
|
-
private procTimer: NodeJS.Timeout;
|
|
412
|
-
private _processing: boolean = false;
|
|
413
|
-
private _inBytes: number[] = [];
|
|
414
|
-
private _inBuffer: number[] = [];
|
|
415
|
-
private _outBuffer: Outbound[] = [];
|
|
416
|
-
private _waitingPacket: Outbound;
|
|
417
|
-
private _msg: Inbound;
|
|
418
|
-
public pushIn(pkt) {
|
|
419
|
-
let self = this;
|
|
420
|
-
conn.buffer._inBuffer.push.apply(conn.buffer._inBuffer, pkt.toJSON().data); setTimeout(() => { self.processPackets(); }, 0);
|
|
421
|
-
}
|
|
422
|
-
public pushOut(msg) { conn.buffer._outBuffer.push(msg); setTimeout(() => { this.processPackets(); }, 0); }
|
|
423
|
-
public clear() { conn.buffer._inBuffer.length = 0; conn.buffer._outBuffer.length = 0; }
|
|
424
|
-
public close() { clearTimeout(conn.buffer.procTimer); conn.buffer.clear(); this._msg = undefined; }
|
|
425
|
-
public clearOutbound() { conn.buffer._outBuffer.length = 0; conn.buffer._waitingPacket = undefined; }
|
|
426
|
-
/********************************************************************
|
|
427
|
-
* RKS: 06-06-20
|
|
428
|
-
* This used to process every 175ms. While the processing was light
|
|
429
|
-
* when there was nothing to process this should have always been
|
|
430
|
-
* event based so the processing timer has been reworked.
|
|
431
|
-
*
|
|
432
|
-
* Now this method gets called only during the following conditions.
|
|
433
|
-
* 1. A packetread event comes from the serial port and has data
|
|
434
|
-
* 2. A message is placed onto the outbound queue
|
|
435
|
-
* 3. The outbound queue has messages that are waiting to send. In
|
|
436
|
-
* this instance this method is called every 200ms until the queue
|
|
437
|
-
* is empty. If one of the above conditions are met then this method
|
|
438
|
-
* will be triggered earlier.
|
|
439
|
-
*
|
|
440
|
-
****************************************************************** */
|
|
441
568
|
private processPackets() {
|
|
442
|
-
if (
|
|
443
|
-
if (
|
|
444
|
-
clearTimeout(
|
|
445
|
-
|
|
569
|
+
if (this._processing) return;
|
|
570
|
+
if (this.procTimer) {
|
|
571
|
+
clearTimeout(this.procTimer);
|
|
572
|
+
this.procTimer = null;
|
|
446
573
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
574
|
+
this._processing = true;
|
|
575
|
+
this.processInboundPackets();
|
|
576
|
+
this.processOutboundPackets();
|
|
577
|
+
this._processing = false;
|
|
451
578
|
}
|
|
452
579
|
private processWaitPacket(): boolean {
|
|
453
|
-
if (typeof
|
|
454
|
-
let timeout =
|
|
580
|
+
if (typeof this._waitingPacket !== 'undefined' && this._waitingPacket) {
|
|
581
|
+
let timeout = this._waitingPacket.timeout || 1000;
|
|
455
582
|
let dt = new Date();
|
|
456
|
-
if (
|
|
457
|
-
logger.silly(`Retrying outbound message after ${(dt.getTime() -
|
|
458
|
-
|
|
459
|
-
|
|
583
|
+
if (this._waitingPacket.timestamp.getTime() + timeout < dt.getTime()) {
|
|
584
|
+
logger.silly(`Retrying outbound message after ${(dt.getTime() - this._waitingPacket.timestamp.getTime()) / 1000} secs with ${this._waitingPacket.remainingTries} attempt(s) left. - ${this._waitingPacket.toShortPacket()} `);
|
|
585
|
+
this.counter.sndRetries++;
|
|
586
|
+
this.writeMessage(this._waitingPacket);
|
|
460
587
|
}
|
|
461
588
|
return true;
|
|
462
589
|
}
|
|
463
590
|
return false;
|
|
464
591
|
}
|
|
465
|
-
protected
|
|
592
|
+
protected processOutboundPackets() {
|
|
466
593
|
let msg: Outbound;
|
|
467
|
-
if (!
|
|
468
|
-
if (
|
|
469
|
-
if (
|
|
470
|
-
msg =
|
|
594
|
+
if (!this.processWaitPacket() && this._outBuffer.length > 0) {
|
|
595
|
+
if (this.isOpen) {
|
|
596
|
+
if (this.isRTS) {
|
|
597
|
+
msg = this._outBuffer.shift();
|
|
471
598
|
if (typeof msg === 'undefined' || !msg) return;
|
|
472
599
|
// If the serial port is busy we don't want to process any outbound. However, this used to
|
|
473
600
|
// not process the outbound even when the incoming bytes didn't mean anything. Now we only delay
|
|
474
601
|
// the outbound when we actually have a message signatures to process.
|
|
475
|
-
|
|
602
|
+
this.writeMessage(msg);
|
|
476
603
|
}
|
|
477
604
|
}
|
|
478
605
|
else {
|
|
479
606
|
// port is closed, reject message
|
|
480
|
-
msg =
|
|
607
|
+
msg = this._outBuffer.shift();
|
|
481
608
|
msg.failed = true;
|
|
482
|
-
logger.warn(`Comms port is not open.Message aborted: ${msg.toShortPacket()} `);
|
|
609
|
+
logger.warn(`Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
|
|
483
610
|
// This is a hard fail. We don't have any more tries left and the message didn't
|
|
484
611
|
// make it onto the wire.
|
|
485
|
-
|
|
612
|
+
if (typeof msg.onAbort === 'function') msg.onAbort();
|
|
613
|
+
else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
|
|
614
|
+
let error = new OutboundMessageError(msg, `Comms port ${msg.portId} is not open. Message aborted: ${msg.toShortPacket()} `);
|
|
486
615
|
if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
|
|
487
|
-
|
|
616
|
+
this._waitingPacket = null;
|
|
617
|
+
this.counter.sndAborted++;
|
|
618
|
+
this.counter.updatefailureRate();
|
|
619
|
+
this.emitPortStats();
|
|
488
620
|
}
|
|
489
621
|
}
|
|
490
622
|
// RG: added the last `|| typeof msg !== 'undef'` because virtual chem controller only sends a single packet
|
|
491
|
-
// but this condition would be eval'd before the callback of
|
|
623
|
+
// but this condition would be eval'd before the callback of port.write was calls and the outbound packet
|
|
492
624
|
// would be sitting idle for eternity.
|
|
493
|
-
if (
|
|
625
|
+
if (this._outBuffer.length > 0 || typeof this._waitingPacket !== 'undefined' || this._waitingPacket || typeof msg !== 'undefined') {
|
|
494
626
|
// Come back later as we still have items to send.
|
|
495
627
|
let self = this;
|
|
496
|
-
|
|
628
|
+
this.procTimer = setTimeout(() => self.processPackets(), 100);
|
|
497
629
|
}
|
|
498
630
|
}
|
|
499
|
-
/*
|
|
500
|
-
* 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
|
|
501
|
-
* 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
|
|
502
|
-
* 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
|
|
503
|
-
* 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
|
|
504
|
-
* port is unaware of our protocol.
|
|
505
|
-
*
|
|
506
|
-
* 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.
|
|
507
|
-
*/
|
|
508
|
-
|
|
509
631
|
private writeMessage(msg: Outbound) {
|
|
510
632
|
// Make sure we are not re-entrant while the the port.write is going on.
|
|
511
633
|
// This ends in goofiness as it can send more than one message at a time while it
|
|
512
634
|
// waits for the command buffer to be flushed. NOTE: There is no success message and the callback to
|
|
513
635
|
// write only verifies that the buffer got ahold of it.
|
|
514
|
-
if (!
|
|
515
|
-
|
|
636
|
+
if (!this.isRTS || this.mockPort) return;
|
|
637
|
+
this.isRTS = false;
|
|
516
638
|
var bytes = msg.toPacket();
|
|
517
|
-
if (
|
|
639
|
+
if (this.isOpen) {
|
|
518
640
|
if (msg.remainingTries <= 0) {
|
|
519
641
|
// It will almost never fall into here. The rare case where
|
|
520
642
|
// we have an RTS semaphore and a waiting response might make it go here.
|
|
521
643
|
msg.failed = true;
|
|
522
|
-
|
|
644
|
+
this._waitingPacket = null;
|
|
523
645
|
if (typeof msg.onAbort === 'function') msg.onAbort();
|
|
524
646
|
else logger.warn(`Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
|
|
525
647
|
let err = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${msg.toShortPacket()} `);
|
|
@@ -529,20 +651,20 @@ export class SendRecieveBuffer {
|
|
|
529
651
|
setTimeout(msg.response.callback, 100, msg);
|
|
530
652
|
}
|
|
531
653
|
}
|
|
532
|
-
|
|
533
|
-
|
|
654
|
+
this.counter.sndAborted++;
|
|
655
|
+
this.isRTS = true;
|
|
534
656
|
return;
|
|
535
657
|
}
|
|
536
|
-
|
|
658
|
+
this.counter.bytesSent += bytes.length;
|
|
537
659
|
msg.timestamp = new Date();
|
|
538
660
|
logger.packet(msg);
|
|
539
|
-
|
|
661
|
+
this.write(Buffer.from(bytes), (err) => {
|
|
540
662
|
msg.tries++;
|
|
541
|
-
|
|
663
|
+
this.isRTS = true;
|
|
542
664
|
if (err) {
|
|
543
665
|
logger.error('Error writing packet %s', err);
|
|
544
666
|
// We had an error so we need to set the waiting packet if there are retries
|
|
545
|
-
if (msg.remainingTries > 0)
|
|
667
|
+
if (msg.remainingTries > 0) this._waitingPacket = msg;
|
|
546
668
|
else {
|
|
547
669
|
msg.failed = true;
|
|
548
670
|
logger.warn(`Message aborted after ${msg.tries} attempt(s): ${bytes}: ${err} `);
|
|
@@ -550,43 +672,44 @@ export class SendRecieveBuffer {
|
|
|
550
672
|
// make it onto the wire.
|
|
551
673
|
let error = new OutboundMessageError(msg, `Message aborted after ${msg.tries} attempt(s): ${err} `);
|
|
552
674
|
if (typeof msg.onComplete === 'function') msg.onComplete(error, undefined);
|
|
553
|
-
|
|
554
|
-
|
|
675
|
+
this._waitingPacket = null;
|
|
676
|
+
this.counter.sndAborted++;
|
|
555
677
|
}
|
|
556
678
|
}
|
|
557
679
|
else {
|
|
558
680
|
logger.verbose(`Wrote packet[${bytes}].Retries remaining: ${msg.remainingTries} `);
|
|
559
681
|
// We have all the success we are going to get so if the call succeeded then
|
|
560
682
|
// don't set the waiting packet when we aren't actually waiting for a response.
|
|
561
|
-
conn.buffer.counter.sndSuccess++;
|
|
562
683
|
if (!msg.requiresResponse) {
|
|
563
684
|
// As far as we know the message made it to OCP.
|
|
564
|
-
|
|
685
|
+
this._waitingPacket = null;
|
|
686
|
+
this.counter.sndSuccess++;
|
|
565
687
|
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
566
688
|
|
|
567
689
|
}
|
|
568
690
|
else if (msg.remainingTries >= 0) {
|
|
569
|
-
|
|
691
|
+
this._waitingPacket = msg;
|
|
570
692
|
}
|
|
571
693
|
}
|
|
572
|
-
|
|
573
|
-
|
|
694
|
+
this.counter.updatefailureRate();
|
|
695
|
+
this.emitPortStats();
|
|
574
696
|
});
|
|
575
697
|
}
|
|
576
698
|
}
|
|
577
699
|
private clearResponses(msgIn: Inbound) {
|
|
578
|
-
if (
|
|
700
|
+
if (this._outBuffer.length === 0 && typeof (this._waitingPacket) !== 'object' && this._waitingPacket) return;
|
|
579
701
|
var callback;
|
|
580
|
-
let msgOut =
|
|
581
|
-
if (typeof (
|
|
702
|
+
let msgOut = this._waitingPacket;
|
|
703
|
+
if (typeof (this._waitingPacket) !== 'undefined' && this._waitingPacket) {
|
|
582
704
|
var resp = msgOut.response;
|
|
583
705
|
if (msgOut.requiresResponse) {
|
|
584
706
|
if (resp instanceof Response && resp.isResponse(msgIn, msgOut)) {
|
|
585
|
-
|
|
707
|
+
this._waitingPacket = null;
|
|
586
708
|
if (typeof msgOut.onComplete === 'function') msgOut.onComplete(undefined, msgIn);
|
|
587
709
|
callback = resp.callback;
|
|
588
710
|
resp.message = msgIn;
|
|
589
|
-
|
|
711
|
+
this.counter.sndSuccess++;
|
|
712
|
+
if (resp.ack) this.pushOut(resp.ack);
|
|
590
713
|
}
|
|
591
714
|
}
|
|
592
715
|
}
|
|
@@ -594,9 +717,9 @@ export class SendRecieveBuffer {
|
|
|
594
717
|
// RG - when would there be additional packets besides the first in the outbuffer that needs to be removed from a single incoming packet?
|
|
595
718
|
// 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
|
|
596
719
|
// triggers that cause the outbound message may come at the same time that another controller makes a call.
|
|
597
|
-
var i =
|
|
720
|
+
var i = this._outBuffer.length - 1;
|
|
598
721
|
while (i >= 0) {
|
|
599
|
-
let out =
|
|
722
|
+
let out = this._outBuffer[i--];
|
|
600
723
|
if (typeof out === 'undefined') continue;
|
|
601
724
|
let resp = out.response;
|
|
602
725
|
// RG - added check for msgOut because the *Touch chlor packet 153 adds an status packet 217
|
|
@@ -605,7 +728,7 @@ export class SendRecieveBuffer {
|
|
|
605
728
|
if (resp instanceof Response && resp.isResponse(msgIn, out) && (typeof out.scope === 'undefined' || out.scope === msgOut.scope)) {
|
|
606
729
|
resp.message = msgIn;
|
|
607
730
|
if (typeof (resp.callback) === 'function' && resp.callback) callback = resp.callback;
|
|
608
|
-
|
|
731
|
+
this._outBuffer.splice(i, 1);
|
|
609
732
|
}
|
|
610
733
|
}
|
|
611
734
|
}
|
|
@@ -615,95 +738,72 @@ export class SendRecieveBuffer {
|
|
|
615
738
|
// that we also need. This occurs when more than one panel on the bus requests a reconfig at the same time.
|
|
616
739
|
if (typeof (callback) === 'function') { setTimeout(callback, 100, msgOut); }
|
|
617
740
|
}
|
|
741
|
+
public get stats() {
|
|
742
|
+
let status = this.isOpen ? 'open' : this._cfg.enabled ? 'closed' : 'disabled';
|
|
743
|
+
return extend(true, { portId: this.portId, status: status, reconnects: this.reconnects }, this.counter)
|
|
744
|
+
}
|
|
745
|
+
private emitPortStats() {
|
|
746
|
+
webApp.emitToChannel('rs485PortStats', 'rs485Stats', this.stats);
|
|
747
|
+
}
|
|
618
748
|
private processCompletedMessage(msg: Inbound, ndx): number {
|
|
619
749
|
msg.timestamp = new Date();
|
|
750
|
+
msg.portId = this.portId;
|
|
620
751
|
msg.id = Message.nextMessageId;
|
|
621
|
-
|
|
752
|
+
this.counter.recCollisions += msg.collisions;
|
|
753
|
+
this.counter.recRewinds += msg.rewinds;
|
|
622
754
|
logger.packet(msg);
|
|
623
|
-
|
|
755
|
+
this.emitPortStats();
|
|
624
756
|
if (msg.isValid) {
|
|
625
|
-
|
|
626
|
-
|
|
757
|
+
this.counter.recSuccess++;
|
|
758
|
+
this.counter.updatefailureRate();
|
|
627
759
|
msg.process();
|
|
628
|
-
|
|
760
|
+
this.clearResponses(msg);
|
|
629
761
|
}
|
|
630
762
|
else {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
console.log('RS485 Stats:' +
|
|
763
|
+
this.counter.recFailed++;
|
|
764
|
+
this.counter.updatefailureRate();
|
|
765
|
+
console.log('RS485 Stats:' + this.counter.toLog());
|
|
634
766
|
ndx = this.rewindFailedMessage(msg, ndx);
|
|
635
767
|
}
|
|
636
768
|
return ndx;
|
|
637
769
|
}
|
|
638
770
|
private rewindFailedMessage(msg: Inbound, ndx: number): number {
|
|
771
|
+
this.counter.recRewinds++;
|
|
639
772
|
// Lets see if we can do a rewind to capture another message from the
|
|
640
773
|
// crap on the bus. This will get us to the innermost message. While the outer message may have failed the inner message should
|
|
641
774
|
// be able to buck up and make it happen.
|
|
642
|
-
|
|
775
|
+
this._inBytes = this._inBytes.slice(ndx); // Start by removing all of the bytes related to the original message.
|
|
643
776
|
// Add all of the elements of the message back in reverse.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
777
|
+
this._inBytes.unshift(...msg.term);
|
|
778
|
+
this._inBytes.unshift(...msg.payload);
|
|
779
|
+
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
|
|
647
780
|
// 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.
|
|
648
|
-
|
|
649
|
-
ndx = msg.readPacket(
|
|
781
|
+
this._msg = msg = new Inbound();
|
|
782
|
+
ndx = msg.readPacket(this._inBytes);
|
|
650
783
|
if (msg.isComplete) { ndx = this.processCompletedMessage(msg, ndx); }
|
|
651
784
|
return ndx;
|
|
652
785
|
}
|
|
653
|
-
protected
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (
|
|
786
|
+
protected processInboundPackets() {
|
|
787
|
+
this.counter.bytesReceived += this._inBuffer.length;
|
|
788
|
+
this._inBytes.push.apply(this._inBytes, this._inBuffer.splice(0, this._inBuffer.length));
|
|
789
|
+
if (this._inBytes.length >= 1) { // Wait until we have something to process.
|
|
657
790
|
let ndx: number = 0;
|
|
658
|
-
let msg: Inbound =
|
|
791
|
+
let msg: Inbound = this._msg;
|
|
659
792
|
do {
|
|
660
793
|
if (typeof (msg) === 'undefined' || msg === null || msg.isComplete || !msg.isValid) {
|
|
661
|
-
|
|
662
|
-
ndx = msg.readPacket(
|
|
794
|
+
this._msg = msg = new Inbound();
|
|
795
|
+
ndx = msg.readPacket(this._inBytes);
|
|
663
796
|
}
|
|
664
|
-
else ndx = msg.mergeBytes(
|
|
797
|
+
else ndx = msg.mergeBytes(this._inBytes);
|
|
665
798
|
if (msg.isComplete) ndx = this.processCompletedMessage(msg, ndx);
|
|
666
799
|
if (ndx > 0) {
|
|
667
|
-
|
|
800
|
+
this._inBytes = this._inBytes.slice(ndx);
|
|
668
801
|
ndx = 0;
|
|
669
802
|
}
|
|
670
803
|
else break;
|
|
671
804
|
|
|
672
|
-
} while (ndx <
|
|
805
|
+
} while (ndx < this._inBytes.length);
|
|
673
806
|
}
|
|
674
807
|
}
|
|
675
808
|
}
|
|
676
|
-
export
|
|
677
|
-
constructor() {
|
|
678
|
-
this.bytesReceived = 0;
|
|
679
|
-
this.recSuccess = 0;
|
|
680
|
-
this.recFailed = 0;
|
|
681
|
-
this.recCollisions = 0;
|
|
682
|
-
this.bytesSent = 0;
|
|
683
|
-
this.sndAborted = 0;
|
|
684
|
-
this.sndRetries = 0;
|
|
685
|
-
this.sndSuccess = 0;
|
|
686
|
-
this.recFailureRate = 0;
|
|
687
|
-
this.sndFailureRate = 0;
|
|
688
|
-
}
|
|
689
|
-
public bytesReceived: number;
|
|
690
|
-
public bytesSent: number;
|
|
691
|
-
public recSuccess: number;
|
|
692
|
-
public recFailed: number;
|
|
693
|
-
public recCollisions: number;
|
|
694
|
-
public recFailureRate: number;
|
|
695
|
-
public sndSuccess: number;
|
|
696
|
-
public sndAborted: number;
|
|
697
|
-
public sndRetries: number;
|
|
698
|
-
public sndFailureRate: number;
|
|
699
|
-
public updatefailureRate(): void {
|
|
700
|
-
conn.buffer.counter.recFailureRate = (this.recFailed + this.recSuccess) !== 0 ? (this.recFailed / (this.recFailed + this.recSuccess) * 100) : 0;
|
|
701
|
-
conn.buffer.counter.sndFailureRate = (this.sndAborted + this.sndSuccess) !== 0 ? (this.sndAborted / (this.sndAborted + this.sndSuccess) * 100) : 0;
|
|
702
|
-
//conn.buffer.counter.recFailureRate = `${(conn.buffer.counter.recFailed / (conn.buffer.counter.recFailed + conn.buffer.counter.recSuccess) * 100).toFixed(2)}% `;
|
|
703
|
-
//conn.buffer.counter.sndFailureRate = `${(conn.buffer.counter.sndAborted / (conn.buffer.counter.sndAborted + conn.buffer.counter.sndSuccess) * 100).toFixed(2)}% `;
|
|
704
|
-
}
|
|
705
|
-
public toLog(): string {
|
|
706
|
-
return `{ "bytesReceived": ${this.bytesReceived} "success": ${this.recSuccess}, "failed": ${this.recFailed}, "bytesSent": ${this.bytesSent}, "collisions": ${this.recCollisions}, "failureRate": ${this.recFailureRate.toFixed(2)}% }`;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
export var conn: Connection = new Connection();
|
|
809
|
+
export var conn: Connection = new Connection();
|