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.
Files changed (64) hide show
  1. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  2. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  3. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/Changelog +19 -0
  6. package/Dockerfile +3 -3
  7. package/README.md +13 -8
  8. package/app.ts +1 -1
  9. package/config/Config.ts +38 -2
  10. package/config/VersionCheck.ts +27 -12
  11. package/controller/Constants.ts +2 -1
  12. package/controller/Equipment.ts +193 -9
  13. package/controller/Errors.ts +10 -0
  14. package/controller/Lockouts.ts +503 -0
  15. package/controller/State.ts +269 -64
  16. package/controller/boards/AquaLinkBoard.ts +1000 -0
  17. package/controller/boards/BoardFactory.ts +4 -0
  18. package/controller/boards/EasyTouchBoard.ts +468 -144
  19. package/controller/boards/IntelliCenterBoard.ts +466 -307
  20. package/controller/boards/IntelliTouchBoard.ts +37 -5
  21. package/controller/boards/NixieBoard.ts +671 -141
  22. package/controller/boards/SystemBoard.ts +1397 -641
  23. package/controller/comms/Comms.ts +462 -362
  24. package/controller/comms/messages/Messages.ts +174 -30
  25. package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
  26. package/controller/comms/messages/config/CircuitMessage.ts +1 -0
  27. package/controller/comms/messages/config/ExternalMessage.ts +10 -8
  28. package/controller/comms/messages/config/HeaterMessage.ts +141 -29
  29. package/controller/comms/messages/config/OptionsMessage.ts +9 -2
  30. package/controller/comms/messages/config/PumpMessage.ts +53 -35
  31. package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
  32. package/controller/comms/messages/config/ValveMessage.ts +2 -2
  33. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
  34. package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
  35. package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
  36. package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
  37. package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
  38. package/controller/nixie/Nixie.ts +1 -1
  39. package/controller/nixie/bodies/Body.ts +3 -0
  40. package/controller/nixie/chemistry/ChemController.ts +164 -51
  41. package/controller/nixie/chemistry/Chlorinator.ts +137 -88
  42. package/controller/nixie/circuits/Circuit.ts +51 -19
  43. package/controller/nixie/heaters/Heater.ts +241 -31
  44. package/controller/nixie/pumps/Pump.ts +488 -206
  45. package/controller/nixie/schedules/Schedule.ts +91 -35
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +20 -0
  48. package/package.json +21 -21
  49. package/web/Server.ts +94 -49
  50. package/web/bindings/aqualinkD.json +505 -0
  51. package/web/bindings/influxDB.json +71 -1
  52. package/web/bindings/mqtt.json +98 -39
  53. package/web/bindings/mqttAlt.json +59 -1
  54. package/web/interfaces/baseInterface.ts +1 -0
  55. package/web/interfaces/httpInterface.ts +23 -2
  56. package/web/interfaces/influxInterface.ts +45 -10
  57. package/web/interfaces/mqttInterface.ts +114 -54
  58. package/web/services/config/Config.ts +55 -132
  59. package/web/services/state/State.ts +81 -4
  60. package/web/services/state/StateSocket.ts +4 -4
  61. package/web/services/utilities/Utilities.ts +8 -6
  62. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  63. package/config copy.json +0 -300
  64. 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
- this.emitter = new EventEmitter();
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 isOpen: boolean = false;
34
- private _closing: boolean = false;
35
- private _cfg: any;
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 isRTS: boolean = true;
52
- public emitter: EventEmitter;
53
- public get enabled(): boolean { return typeof this._cfg !== 'undefined' && this._cfg.enabled; }
54
- public async setPortAsync(data: any) : Promise<any> {
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('controller.comms', {});
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 (!await this.closeAsync()) {
67
- return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port`, 'setPortAsync'));
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('controller.comms', pdata);
70
- this._cfg = config.getSection('controller.comms', {
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
- if (!await this.openAsync()) {
80
- return Promise.reject(new InvalidOperationError(`Unable to open RS485 port ${pdata.rs485Port}`, 'setPortAsync'));
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 this._cfg;
120
+ return cfg;
83
121
  } catch (err) { return Promise.reject(err); }
84
122
  }
85
- public async openAsync(): Promise<boolean> {
86
- if (typeof this.buffer === 'undefined') {
87
- this.buffer = new SendRecieveBuffer();
88
- this.emitter.on('packetread', (pkt) => { this.buffer.pushIn(pkt); });
89
- this.emitter.on('messagewrite', (msg) => { this.buffer.pushOut(msg); });
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
- if (typeof this._port !== 'undefined' && this._port.isOpen) {
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
- return Promise.resolve(true);
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.on('connect', () => { logger.info(`Net connect (socat) connected to: ${this._cfg.netHost}:${this._cfg.netPort}`); }); // Socket is opened but not yet ready.
99
- nc.on('ready', () => {
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.emitter.emit('packetread', data);
302
+ if (data.length > 0 && !this.isPaused) this.pushIn(data);
106
303
  });
304
+ this.emitPortStats();
107
305
  });
108
- nc.on('close', (p) => {
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.buffer.clearOutbound();
113
- logger.info(`Net connect (socat) closed ${p === true ? 'due to error' : ''}: ${this._cfg.netHost}:${this._cfg.netPort}`);
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
- //this.resetConnTimer();
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
- nc.setTimeout(Math.max(this._cfg.inactivityRetry, 10) * 1000, async () => {
127
- logger.warn(`Net connect (socat) connection idle: ${this._cfg.netHost}:${this._cfg.netPort} retrying connection.`);
128
- try {
129
- await conn.endAsync();
130
- await conn.openAsync();
131
- } catch (err) { logger.error(`Net connect (socat) error retrying connection ${err.message}`); }
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 conn.openAsync(); } catch (err) { } }, this._cfg.inactivityRetry * 1000);
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(conn._cfg.netPort, conn._cfg.netHost, () => {
149
- if (typeof this._port !== 'undefined') logger.warn('Net connect (socat) recovered from lost connection.');
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 Promise.resolve(true);
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(conn._cfg.rs485Port, conn._cfg.portSettings);
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 conn._port !== 'undefined') logger.info(`Serial Port: ${this._cfg.rs485Port} recovered from lost connection.`)
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.emitter.emit('packetread', data); this.resetConnTimer(); });
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}:${this._cfg.netPort}/${this._cfg.rs485Port}: ${err}`);
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
- conn._port = undefined;
448
+ this._port = undefined;
230
449
  this.isOpen = false;
231
- logger.info(`Successfully closed (socat) port ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}`);
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 conn._port.close === 'function') {
244
- conn._port.close((err) => {
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
- conn._port = undefined;
251
- logger.info(`Successfully closed seral port ${this._cfg.rs485Port}`);
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
- conn._port = undefined;
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 async endAsync(): Promise<boolean> {
272
- try {
273
- this._closing = true;
274
- if (this.connTimer) clearTimeout(this.connTimer);
275
- if (typeof this._port !== 'undefined' && this.isOpen) {
276
- let success = await new Promise<boolean>((resolve, reject) => {
277
- if (this._cfg.netConnect) {
278
- this._port.removeAllListeners();
279
- this._port.once('error', (err) => {
280
- if (err) {
281
- logger.error(`Error closing ${this._cfg.netHost}:${this._cfg.netPort}/${this._cfg.rs485Port}: ${err}`);
282
- resolve(false);
283
- }
284
- else {
285
- conn._port = undefined;
286
- this.isOpen = false;
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
- return true;
324
- } catch (err) { logger.error(`Error closing comms connection: ${err.message}`); return Promise.resolve(false); }
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 (conn._port.drain) === 'function')
329
- conn._port.drain(cb);
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 (conn._cfg.netConnect) {
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 conn._port === 'undefined' || conn._port.destroyed !== false) {
337
- conn.openAsync().then(() => {
338
- conn._port.write(bytes, 'binary', cb);
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
- conn._port.write(bytes, 'binary', cb);
534
+ this._port.write(bytes, 'binary', cb);
343
535
  }
344
536
  else
345
- conn._port.write(bytes, cb);
537
+ this._port.write(bytes, cb);
346
538
  }
347
- public async stopAsync() {
348
- try {
349
- await conn.closeAsync();
350
- logger.info(`Closed serial communications connection.`);
351
- } catch (err) { logger.error(`Error closing comms connection: ${err.message} `); }
352
- }
353
- public init() {
354
- conn._cfg = config.getSection('controller.comms', {
355
- rs485Port: "/dev/ttyUSB0",
356
- portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
357
- mockPort: false,
358
- netConnect: false,
359
- netHost: "raspberrypi",
360
- netPort: 9801,
361
- inactivityRetry: 10
362
- });
363
- 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}`); });
364
- config.emitter.on('reloaded', () => {
365
- console.log('Config reloaded');
366
- this.reloadConfig(config.getSection('controller.comms', {
367
- rs485Port: "/dev/ttyUSB0",
368
- portSettings: { baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1, flowControl: false, autoOpen: false, lock: false },
369
- mockPort: false,
370
- netConnect: false,
371
- netHost: "raspberrypi",
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 (conn.buffer._processing) return;
443
- if (conn.buffer.procTimer) {
444
- clearTimeout(conn.buffer.procTimer);
445
- conn.buffer.procTimer = null;
569
+ if (this._processing) return;
570
+ if (this.procTimer) {
571
+ clearTimeout(this.procTimer);
572
+ this.procTimer = null;
446
573
  }
447
- conn.buffer._processing = true;
448
- conn.buffer.processInbound();
449
- conn.buffer.processOutbound();
450
- conn.buffer._processing = false;
574
+ this._processing = true;
575
+ this.processInboundPackets();
576
+ this.processOutboundPackets();
577
+ this._processing = false;
451
578
  }
452
579
  private processWaitPacket(): boolean {
453
- if (typeof conn.buffer._waitingPacket !== 'undefined' && conn.buffer._waitingPacket) {
454
- let timeout = conn.buffer._waitingPacket.timeout || 1000;
580
+ if (typeof this._waitingPacket !== 'undefined' && this._waitingPacket) {
581
+ let timeout = this._waitingPacket.timeout || 1000;
455
582
  let dt = new Date();
456
- if (conn.buffer._waitingPacket.timestamp.getTime() + timeout < dt.getTime()) {
457
- logger.silly(`Retrying outbound message after ${(dt.getTime() - conn.buffer._waitingPacket.timestamp.getTime()) / 1000} secs with ${conn.buffer._waitingPacket.remainingTries} attempt(s) left. - ${conn.buffer._waitingPacket.toShortPacket()} `);
458
- conn.buffer.counter.sndRetries++;
459
- conn.buffer.writeMessage(conn.buffer._waitingPacket);
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 processOutbound() {
592
+ protected processOutboundPackets() {
466
593
  let msg: Outbound;
467
- if (!conn.buffer.processWaitPacket() && conn.buffer._outBuffer.length > 0) {
468
- if (conn.isOpen) {
469
- if (conn.isRTS) {
470
- msg = conn.buffer._outBuffer.shift();
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
- conn.buffer.writeMessage(msg);
602
+ this.writeMessage(msg);
476
603
  }
477
604
  }
478
605
  else {
479
606
  // port is closed, reject message
480
- msg = conn.buffer._outBuffer.shift();
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
- let error = new OutboundMessageError(msg, `Comms port is not open.Message aborted: ${msg.toShortPacket()} `);
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
- conn.buffer._waitingPacket = null;
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 conn.write was calls and the outbound packet
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 (conn.buffer._outBuffer.length > 0 || typeof conn.buffer._waitingPacket !== 'undefined' || conn.buffer._waitingPacket || typeof msg !== 'undefined') {
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
- conn.buffer.procTimer = setTimeout(() => self.processPackets(), 100);
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 (!conn.isRTS || conn.mockPort) return;
515
- conn.isRTS = false;
636
+ if (!this.isRTS || this.mockPort) return;
637
+ this.isRTS = false;
516
638
  var bytes = msg.toPacket();
517
- if (conn.isOpen) {
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
- conn.buffer._waitingPacket = null;
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
- conn.buffer.counter.sndAborted++;
533
- conn.isRTS = true;
654
+ this.counter.sndAborted++;
655
+ this.isRTS = true;
534
656
  return;
535
657
  }
536
- conn.buffer.counter.bytesSent += bytes.length;
658
+ this.counter.bytesSent += bytes.length;
537
659
  msg.timestamp = new Date();
538
660
  logger.packet(msg);
539
- conn.write(Buffer.from(bytes), function (err) {
661
+ this.write(Buffer.from(bytes), (err) => {
540
662
  msg.tries++;
541
- conn.isRTS = true;
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) conn.buffer._waitingPacket = msg;
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
- conn.buffer._waitingPacket = null;
554
- conn.buffer.counter.sndAborted++;
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
- conn.buffer._waitingPacket = null;
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
- conn.buffer._waitingPacket = msg;
691
+ this._waitingPacket = msg;
570
692
  }
571
693
  }
572
- conn.buffer.counter.updatefailureRate();
573
- webApp.emitToChannel('rs485PortStats', 'rs485Stats', conn.buffer.counter);
694
+ this.counter.updatefailureRate();
695
+ this.emitPortStats();
574
696
  });
575
697
  }
576
698
  }
577
699
  private clearResponses(msgIn: Inbound) {
578
- if (conn.buffer._outBuffer.length === 0 && typeof (conn.buffer._waitingPacket) !== 'object' && conn.buffer._waitingPacket) return;
700
+ if (this._outBuffer.length === 0 && typeof (this._waitingPacket) !== 'object' && this._waitingPacket) return;
579
701
  var callback;
580
- let msgOut = conn.buffer._waitingPacket;
581
- if (typeof (conn.buffer._waitingPacket) !== 'undefined' && conn.buffer._waitingPacket) {
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
- conn.buffer._waitingPacket = null;
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
- if (resp.ack) conn.queueSendMessage(resp.ack);
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 = conn.buffer._outBuffer.length - 1;
720
+ var i = this._outBuffer.length - 1;
598
721
  while (i >= 0) {
599
- let out = conn.buffer._outBuffer[i--];
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
- conn.buffer._outBuffer.splice(i, 1);
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
- conn.buffer.counter.recCollisions += msg.collisions;
752
+ this.counter.recCollisions += msg.collisions;
753
+ this.counter.recRewinds += msg.rewinds;
622
754
  logger.packet(msg);
623
- webApp.emitToChannel('rs485PortStats', 'rs485Stats', conn.buffer.counter);
755
+ this.emitPortStats();
624
756
  if (msg.isValid) {
625
- conn.buffer.counter.recSuccess++;
626
- conn.buffer.counter.updatefailureRate();
757
+ this.counter.recSuccess++;
758
+ this.counter.updatefailureRate();
627
759
  msg.process();
628
- conn.buffer.clearResponses(msg);
760
+ this.clearResponses(msg);
629
761
  }
630
762
  else {
631
- conn.buffer.counter.recFailed++;
632
- conn.buffer.counter.updatefailureRate();
633
- console.log('RS485 Stats:' + conn.buffer.counter.toLog());
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
- conn.buffer._inBytes = conn.buffer._inBytes.slice(ndx); // Start by removing all of the bytes related to the original message.
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
- conn.buffer._inBytes.unshift(...msg.term);
645
- conn.buffer._inBytes.unshift(...msg.payload);
646
- conn.buffer._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
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
- conn.buffer._msg = msg = new Inbound();
649
- ndx = msg.readPacket(conn.buffer._inBytes);
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 processInbound() {
654
- conn.buffer.counter.bytesReceived += conn.buffer._inBuffer.length;
655
- conn.buffer._inBytes.push.apply(conn.buffer._inBytes, conn.buffer._inBuffer.splice(0, conn.buffer._inBuffer.length));
656
- if (conn.buffer._inBytes.length >= 1) { // Wait until we have something to process.
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 = conn.buffer._msg;
791
+ let msg: Inbound = this._msg;
659
792
  do {
660
793
  if (typeof (msg) === 'undefined' || msg === null || msg.isComplete || !msg.isValid) {
661
- conn.buffer._msg = msg = new Inbound();
662
- ndx = msg.readPacket(conn.buffer._inBytes);
794
+ this._msg = msg = new Inbound();
795
+ ndx = msg.readPacket(this._inBytes);
663
796
  }
664
- else ndx = msg.mergeBytes(conn.buffer._inBytes);
797
+ else ndx = msg.mergeBytes(this._inBytes);
665
798
  if (msg.isComplete) ndx = this.processCompletedMessage(msg, ndx);
666
799
  if (ndx > 0) {
667
- conn.buffer._inBytes = conn.buffer._inBytes.slice(ndx);
800
+ this._inBytes = this._inBytes.slice(ndx);
668
801
  ndx = 0;
669
802
  }
670
803
  else break;
671
804
 
672
- } while (ndx < conn.buffer._inBytes.length);
805
+ } while (ndx < this._inBytes.length);
673
806
  }
674
807
  }
675
808
  }
676
- export class Counter {
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();