nodejs-poolcontroller 8.1.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
@@ -1,1243 +1,1433 @@
1
- /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
- Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as
7
- published by the Free Software Foundation, either version 3 of the
8
- License, or (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- */
18
- import { ConfigMessage } from "./config/ConfigMessage";
19
- import { PumpMessage } from "./config/PumpMessage";
20
- import { VersionMessage } from "./status/VersionMessage";
21
- import { PumpStateMessage } from "./status/PumpStateMessage";
22
- import { EquipmentStateMessage } from "./status/EquipmentStateMessage";
23
- import { HeaterStateMessage } from "./status/HeaterStateMessage";
24
- import { ChlorinatorStateMessage } from "./status/ChlorinatorStateMessage";
25
- import { ChlorinatorMessage } from "./config/ChlorinatorMessage";
26
- import { ExternalMessage } from "./config/ExternalMessage";
27
- import { Timestamp, ControllerType } from "../../Constants";
28
- import { CircuitMessage } from "./config/CircuitMessage";
29
- import { config } from '../../../config/Config';
30
- import { sys } from '../../Equipment';
31
- import { logger } from "../../../logger/Logger";
32
- import { CustomNameMessage } from "./config/CustomNameMessage";
33
- import { ScheduleMessage } from "./config/ScheduleMessage";
34
- import { RemoteMessage } from "./config/RemoteMessage";
35
- import { OptionsMessage } from "./config/OptionsMessage";
36
- import { EquipmentMessage } from "./config/EquipmentMessage";
37
- import { ValveMessage } from "./config/ValveMessage";
38
- import { state } from "../../State";
39
- import { HeaterMessage } from "./config/HeaterMessage";
40
- import { CircuitGroupMessage } from "./config/CircuitGroupMessage";
41
- import { IntellichemMessage } from "./config/IntellichemMessage";
42
- import { TouchScheduleCommands } from "controller/boards/EasyTouchBoard";
43
- import { IntelliValveStateMessage } from "./status/IntelliValveStateMessage";
44
- import { IntelliChemStateMessage } from "./status/IntelliChemStateMessage";
45
- import { OutboundMessageError } from "../../Errors";
46
- import { conn } from "../Comms"
47
- import extend = require("extend");
48
- import { MessagesMock } from "../../../anslq25/MessagesMock";
49
-
50
- export enum Direction {
51
- In = 'in',
52
- Out = 'out'
53
- }
54
- export enum Protocol {
55
- Unknown = 'unknown',
56
- Broadcast = 'broadcast',
57
- Pump = 'pump',
58
- Chlorinator = 'chlorinator',
59
- IntelliChem = 'intellichem',
60
- IntelliValve = 'intellivalve',
61
- Heater = 'heater',
62
- AquaLink = 'aqualink',
63
- Hayward = 'hayward',
64
- Unidentified = 'unidentified'
65
- }
66
- export class Message {
67
- constructor() { }
68
-
69
- // Internal Storage
70
- protected _complete: boolean = false;
71
- public static headerSubByte: number = 33;
72
- public static pluginAddress: number = config.getSection('controller', { address: 33 }).address;
73
- private _id: number = -1;
74
- // Fields
75
- private static _messageId: number = 0;
76
- public static get nextMessageId(): number {
77
- let i = this._messageId < 80000 ? ++this._messageId : this._messageId = 0;
78
- //logger.debug(`Assigning message id ${i}`)
79
- return i; }
80
- public portId = 0; // This will be the target or source port for the message. If this is from or to an Aux RS485 port the value will be > 0.
81
- public timestamp: Date = new Date();
82
- public direction: Direction = Direction.In;
83
- public protocol: Protocol = Protocol.Unknown;
84
- public padding: number[] = [];
85
- public preamble: number[] = [];
86
- public header: number[] = [];
87
- public payload: number[] = [];
88
- public term: number[] = [];
89
- public packetCount: number = 0;
90
- public get id(): number { return this._id; }
91
- public set id(val: number) { this._id = val; }
92
- public isValid: boolean = true;
93
- public scope: string;
94
- public isClone: boolean;
95
- // Properties
96
- public get isComplete(): boolean { return this._complete; }
97
- public get sub(): number { return this.header.length > 1 ? this.header[1] : -1; }
98
- public get dest(): number {
99
- if (this.header.length > 2) {
100
- if (this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink) {
101
- return this.header.length > 2 ? (this.header[2] >= 80 ? this.header[2] : 0) : -1;
102
- }
103
- else if (this.protocol === Protocol.Hayward) {
104
- // src act dest
105
- //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
106
- return this.header.length > 4 ? this.header[2] : -1;
107
- }
108
- else return this.header.length > 2 ? this.header[2] : -1;
109
- }
110
- else return -1;
111
- }
112
- public get source(): number {
113
- if (this.protocol === Protocol.Chlorinator) {
114
- return this.header.length > 2 ? (this.header[2] >= 80 ? 0 : this.header[2]) : -1;
115
- // have to assume incoming packets with header[2] >= 80 (sent to a chlorinator)
116
- // are from controller (0);
117
- // likewise, if the destination is 0 (controller) we
118
- // have to assume it was sent from the 1st chlorinator (1)
119
- // until we learn otherwise.
120
- }
121
- else if (this.protocol === Protocol.AquaLink) {
122
- // Once we decode the devices we will be able to tell where it came from based upon the commands.
123
- return 0;
124
- }
125
- else if (this.protocol === Protocol.Hayward) {
126
- // src act dest
127
- //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
128
- //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
129
- return this.header.length > 4 ? this.header[4] : -1;
130
- }
131
- if (this.header.length > 3) return this.header[3];
132
- else return -1;
133
- }
134
- public get action(): number {
135
- // The action byte is actually the 4th byte in the header the destination address is the 5th byte.
136
- if (this.protocol === Protocol.Chlorinator ||
137
- this.protocol === Protocol.AquaLink) return this.header.length > 3 ? this.header[3] : -1;
138
- else if (this.protocol === Protocol.Hayward) {
139
- // src act dest
140
- //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
141
- //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
142
- return this.header.length > 3 ? this.header[3] || this.header[2] : -1;
143
- }
144
- if (this.header.length > 4) return this.header[4];
145
- else return -1;
146
- }
147
- public get datalen(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink || this.protocol === Protocol.Hayward ? this.payload.length : this.header.length > 5 ? this.header[5] : -1; }
148
- public get chkHi(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? 0 : this.term.length > 0 ? this.term[0] : -1; }
149
- public get chkLo(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? this.term[0] : this.term[1]; }
150
- public get checksum(): number {
151
- var sum = 0;
152
- for (let i = 0; i < this.header.length; i++) sum += this.header[i];
153
- for (let i = 0; i < this.payload.length; i++) sum += this.payload[i];
154
- return sum;
155
- }
156
-
157
- // Methods
158
- public toPacket(): number[] {
159
- const pkt = [];
160
- pkt.push(...this.padding);
161
- pkt.push(...this.preamble);
162
- pkt.push(...this.header);
163
- pkt.push(...this.payload);
164
- pkt.push(...this.term);
165
- return pkt;
166
- }
167
- public toShortPacket(): number[] {
168
- const pkt = [];
169
- pkt.push(...this.header);
170
- pkt.push(...this.payload);
171
- pkt.push(...this.term);
172
- return pkt;
173
- }
174
- public toLog(): string {
175
- return `{"port":${this.portId},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)}, ${JSON.stringify(this.header)}, ${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts":"${Timestamp.toISOLocal(this.timestamp)}"}`;
176
- }
177
- public static convertOutboundToInbound(out: Outbound): Inbound {
178
- let inbound = new Inbound();
179
- inbound.portId = out.portId;
180
- // inbound.id = Message.nextMessageId;
181
- inbound.protocol = out.protocol;
182
- inbound.scope = out.scope;
183
- inbound.preamble = out.preamble;
184
- inbound.padding = out.padding;
185
- inbound.header = out.header;
186
- inbound.payload = [...out.payload];
187
- inbound.term = out.term;
188
- inbound.portId = out.portId;
189
- return inbound;
190
- }
191
- public static convertInboundToOutbound(inbound: Inbound): Outbound {
192
- let out = new Outbound(
193
- inbound.protocol,
194
- inbound.source,
195
- inbound.dest,
196
- inbound.action,
197
- inbound.payload,
198
- );
199
- out.scope = inbound.scope;
200
- out.preamble = inbound.preamble;
201
- out.padding = inbound.padding;
202
- out.header = inbound.header;
203
- out.term = inbound.term;
204
- out.portId = inbound.portId;
205
- return out;
206
- }
207
- public clone(): Inbound | Outbound {
208
- let msg;
209
- if (this instanceof Inbound) {
210
- msg = new Inbound();
211
- msg.id = Message.nextMessageId;
212
- msg.scope = this.scope;
213
- msg.preamble = this.preamble;
214
- msg.padding = this.padding;
215
- msg.payload = [...this.payload];
216
- msg.header = this.header;
217
- msg.term = this.term;
218
- msg.portId = this.portId;
219
- }
220
- else {
221
- msg = new Outbound(
222
- this.protocol, this.source, this.dest, this.action, [...this.payload],
223
- );
224
- msg.portId = this.portId;
225
- msg.scope = this.scope;
226
- }
227
- return msg;
228
- }
229
- }
230
- export class Inbound extends Message {
231
- // /usr/bin/socat TCP-LISTEN:9801,fork,reuseaddr FILE:/dev/ttyUSB0,b9600,raw
232
- // /usr/bin/socat TCP-LISTEN:9801,fork,reuseaddr FILE:/dev/ttyUSB0,b9600,cs8,cstopb=1,parenb=0,raw
233
- // /usr/bin / socat TCP - LISTEN: 9801,fork,reuseaddr FILE:/dev/ttyUSB0, b9600, cs8, cstopb = 1, parenb = 0, raw
234
- constructor() {
235
- super();
236
- this.direction = Direction.In;
237
- }
238
- // Factory
239
- public static replay(obj?: any) {
240
- let inbound = new Inbound();
241
- inbound.readHeader(obj.header, 0);
242
- inbound.readPayload(obj.payload, 0);
243
- inbound.readChecksum(obj.term, 0);
244
- inbound.process();
245
- }
246
- public responseFor: number[] = [];
247
- public isProcessed: boolean = false;
248
- public collisions: number = 0;
249
- public rewinds: number = 0;
250
- // Private methods
251
- private isValidChecksum(): boolean {
252
- if (this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink) return this.checksum % 256 === this.chkLo;
253
- return (this.chkHi * 256) + this.chkLo === this.checksum;
254
- }
255
- public toLog() {
256
- if (this.responseFor.length > 0)
257
- return `{"port":${this.portId || 0},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","for":${JSON.stringify(this.responseFor)},"pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
258
- return `{"port":${this.portId || 0},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
259
- }
260
- private testChlorHeader(bytes: number[], ndx: number): boolean {
261
- // if packets have 16,2 (eg status=16,2,29) in them and they come as partial packets, they would have
262
- // prev been detected as chlor packets;
263
- // valid chlor packets should have 16,2,0 or 16,2,[80-96];
264
- // this should reduce the number of false chlor packets
265
- // For any of these 16,2 type headers we need at least 5 bytes to determine the routing.
266
- //63,15,16,2,29,9,36,0,0,0,0,0,16,0,32,0,0,2,0,75,75,32,241,80,85,24,241,16,16,48,245,69,45,100,186,16,2,80,17,0,115,16,3
267
- if (bytes.length > ndx + 4) {
268
- if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
269
- let dst = bytes[ndx + 2];
270
- let act = bytes[ndx + 3];
271
- // For now the dst byte will always be 0 or 80.
272
- if (![0, 16, 80, 81, 82, 83].includes(dst)) {
273
- //logger.info(`Sensed chlorinator header but the dst byte is ${dst}`);
274
- return false;
275
- }
276
- else if (dst === 0 && [1, 18, 3].includes(act))
277
- return true;
278
- else if (![0, 17, 19, 20, 21, 22].includes(act)) {
279
- //logger.info(`Sensed out chlorinator header but the dst byte is ${dst} ${act} ${JSON.stringify(bytes)}`);
280
- return false;
281
- }
282
- return true;
283
- }
284
- }
285
- return false;
286
- }
287
- private testAquaLinkHeader(bytes: number[], ndx: number): boolean {
288
- if (bytes.length > ndx + 4 && sys.controllerType === 'aqualink') {
289
- if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
290
- return true;
291
- }
292
- }
293
- return false;
294
- }
295
- private testHaywardHeader(bytes: number[], ndx: number): boolean {
296
- //0x10, 0x02, 0x0C, 0x01, 0x00, 0x2D, 0x00, 0x4C, 0x10, 0x03 -- Command to pump
297
- //[16,2,12,1,0]
298
- //0x10, 0x02, 0x0C, 0x01, 0x00, 0x2D, 0x00, 0x4C, 0x10, 0x03 -- Command to Filter Pump
299
- //[16,2,12,1,0]
300
- //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
301
- //[16,2,12,1,2]
302
- // src act dest
303
- //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
304
- //[16,2,0,12,0] --> Response
305
- //[16,2,0,12,0]
306
- if (bytes.length > ndx + 4) {
307
- if (sys.controllerType === 'aqualink') return false;
308
- if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
309
- let dst = bytes[ndx + 3];
310
- let src = bytes[ndx + 2];
311
- if (dst === 12 || src === 12) return true;
312
- }
313
- }
314
- return false;
315
- }
316
- private testBroadcastHeader(bytes: number[], ndx: number): boolean {
317
- // We are looking for [255,0,255,165]
318
- if (bytes.length > ndx + 3) {
319
- if (bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] === 165) return true;
320
- return false;
321
- }
322
- //return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] === 165;
323
- return false;
324
- }
325
- private testUnidentifiedHeader(bytes: number[], ndx: number): boolean {
326
- if (bytes.length > ndx + 3) {
327
- if (bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] !== 165) return true;
328
- return false;
329
- }
330
- //return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] !== 165;
331
- return false;
332
- }
333
- private testChlorTerm(bytes: number[], ndx: number): boolean { return ndx + 2 < bytes.length && bytes[ndx + 1] === 16 && bytes[ndx + 2] === 3; }
334
- private testAquaLinkTerm(bytes: number[], ndx: number): boolean { return ndx + 2 < bytes.length && bytes[ndx + 1] === 16 && bytes[ndx + 2] === 3; }
335
- private testHaywardTerm(bytes: number[], ndx: number): boolean { return ndx + 3 < bytes.length && bytes[ndx + 2] === 16 && bytes[ndx + 3] === 3; }
336
- private pushBytes(target: number[], bytes: number[], ndx: number, length: number): number {
337
- let end = ndx + length;
338
- while (ndx < bytes.length && ndx < end)
339
- target.push(bytes[ndx++]);
340
- return ndx;
341
- }
342
- // Methods
343
- public rewind(bytes: number[], ndx: number): number {
344
- let buff = [];
345
- //buff.push(...this.padding);
346
- //buff.push(...this.preamble);
347
- buff.push(...this.header);
348
- buff.push(...this.payload);
349
- buff.push(...this.term);
350
- // Add in the remaining bytes.
351
- if (ndx < bytes.length - 1) buff.push(...bytes.slice(ndx, bytes.length - 1));
352
- this.padding.push(...this.preamble);
353
- this.preamble.length = 0;
354
- this.header.length = 0;
355
- this.payload.length = 0;
356
- this.term.length = 0;
357
- buff.shift();
358
- this.protocol = Protocol.Unknown;
359
- this._complete = false;
360
- this.isValid = true;
361
-
362
- this.collisions++;
363
- this.rewinds++;
364
- logger.info(`rewinding message collision ${this.collisions} ${ndx} ${bytes.length} ${JSON.stringify(buff)}`);
365
- this.readPacket(buff);
366
- return ndx;
367
- //return this.padding.length + this.preamble.length;
368
- }
369
- public readPacket(bytes: number[]): number {
370
- //logger.info(`BYTES: ${JSON.stringify(bytes)}`);
371
- var ndx = this.readHeader(bytes, 0);
372
- if (this.isValid && this.header.length > 0) ndx = this.readPayload(bytes, ndx);
373
- if (this.isValid && this.header.length > 0) ndx = this.readChecksum(bytes, ndx);
374
- if (this.isComplete && !this.isValid) return this.rewind(bytes, ndx);
375
- return ndx;
376
- }
377
- public mergeBytes(bytes) {
378
- var ndx = 0;
379
- if (this.header.length === 0) ndx = this.readHeader(bytes, ndx);
380
- if (this.isValid && this.header.length > 0) ndx = this.readPayload(bytes, ndx);
381
- if (this.isValid && this.header.length > 0) ndx = this.readChecksum(bytes, ndx);
382
- //if (this.isComplete && !this.isValid) return this.rewind(bytes, ndx);
383
- return ndx;
384
- }
385
- public readHeader(bytes: number[], ndx: number): number {
386
- // start over to include the padding bytes.
387
- //if (this.protocol !== Protocol.Unknown) {
388
- // logger.warn(`${this.protocol} resulted in an empty message header ${JSON.stringify(this.header)}`);
389
- //}
390
- let ndxStart = ndx;
391
- // RKS: 05-30-22 -- OMG we have not been dealing with short headers. As a result it was restarting
392
- // the header process even after it had identified it.
393
- if (this.protocol === Protocol.Unknown) {
394
- while (ndx < bytes.length) {
395
- if (this.testBroadcastHeader(bytes, ndx)) {
396
- this.protocol = Protocol.Broadcast;
397
- break;
398
- }
399
- if (this.testUnidentifiedHeader(bytes, ndx)) {
400
- this.protocol = Protocol.Unidentified;
401
- break;
402
- }
403
- if (this.testChlorHeader(bytes, ndx)) {
404
- this.protocol = Protocol.Chlorinator;
405
- break;
406
- }
407
- if (this.testAquaLinkHeader(bytes, ndx)) {
408
- this.protocol = Protocol.AquaLink;
409
- break;
410
- }
411
- if (this.testHaywardHeader(bytes, ndx)) {
412
- this.protocol = Protocol.Hayward;
413
- break;
414
- }
415
- this.padding.push(bytes[ndx++]);
416
- }
417
- }
418
- // When the code above finds a protocol, ndx will be at the start of that
419
- // header. If it is not identified then it will rewind to the initial
420
- // start position until we get more bytes. This is the default case below.
421
- let ndxHeader = ndx;
422
- switch (this.protocol) {
423
- case Protocol.Pump:
424
- case Protocol.IntelliChem:
425
- case Protocol.IntelliValve:
426
- case Protocol.Broadcast:
427
- case Protocol.Heater:
428
- case Protocol.Unidentified:
429
- ndx = this.pushBytes(this.preamble, bytes, ndx, 3);
430
- ndx = this.pushBytes(this.header, bytes, ndx, 6);
431
- if (this.header.length < 6) {
432
- // We actually don't have a complete header yet so just return.
433
- // we will pick it up next go around.
434
- // logger.debug(`We have an incoming message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
435
- //logger.info(`We don't have a complete header ${JSON.stringify(this.header)}`);
436
- this.preamble = [];
437
- this.header = [];
438
- return ndxHeader;
439
- }
440
- if (this.source >= 96 && this.source <= 111) this.protocol = Protocol.Pump;
441
- else if (this.dest >= 96 && this.dest <= 111) this.protocol = Protocol.Pump;
442
- else if (this.source >= 112 && this.source <= 127) this.protocol = Protocol.Heater;
443
- else if (this.dest >= 112 && this.dest <= 127) this.protocol = Protocol.Heater;
444
- else if (this.dest >= 144 && this.dest <= 158) this.protocol = Protocol.IntelliChem;
445
- else if (this.source >= 144 && this.source <= 158) this.protocol = Protocol.IntelliChem;
446
- else if (this.source == 12 || this.dest == 12) this.protocol = Protocol.IntelliValve;
447
- if (this.datalen > 75) {
448
- //this.isValid = false;
449
- logger.debug(`Broadcast length ${this.datalen} exceeded 75 bytes for ${this.protocol} message. Message rewound ${this.header}`);
450
- this.padding.push(...this.preamble);
451
- this.padding.push(...this.header.slice(0, 1));
452
- this.preamble = [];
453
- this.header = [];
454
- this.collisions++;
455
- this.rewinds++;
456
- return ndxHeader + 1;
457
- }
458
- break;
459
- case Protocol.Chlorinator:
460
- // RKS: 06-06-20 We occasionally get messages where the 16, 2 is interrupted. The message below
461
- // has an IntelliValve broadcast embedded within as well as a chlorinator status request. So
462
- // in the instance below we have two messages being tossed because something on the bus interrupted
463
- // the chlorinator. The first 240 byte does not belong to the chlorinator nor does it belong to
464
- // the IntelliValve
465
- //[][16, 2, 240][255, 0, 255, 165, 1, 16, 12, 82, 8, 0, 128, 216, 128, 57, 64, 25, 166, 4, 44, 16, 2, 80, 17, 0][115, 16, 3]
466
- //[][16, 2, 80, 17][0][115, 16, 3]
467
- ndx = this.pushBytes(this.header, bytes, ndx, 4);
468
- if (this.header.length < 4) {
469
- // We actually don't have a complete header yet so just return.
470
- // we will pick it up next go around.
471
- logger.debug(`We have an incoming chlorinator message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
472
- this.preamble = [];
473
- this.header = [];
474
- return ndxHeader;
475
- }
476
- break;
477
- case Protocol.Hayward:
478
- ndx = this.pushBytes(this.header, bytes, ndx, 5);
479
- if (this.header.length < 4) {
480
- // We actually don't have a complete header yet so just return.
481
- // we will pick it up next go around.
482
- logger.debug(`We have an incoming Hayward message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
483
- this.preamble = [];
484
- this.header = [];
485
- return ndxHeader;
486
- }
487
- break;
488
- case Protocol.AquaLink:
489
- ndx = this.pushBytes(this.header, bytes, ndx, 5);
490
- if (this.header.length < 5) {
491
- // We actually don't have a complete header yet so just return.
492
- // we will pick it up next go around.
493
- logger.debug(`We have an incoming AquaLink message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
494
- this.preamble = [];
495
- this.header = [];
496
- return ndxHeader;
497
- }
498
- break;
499
- default:
500
- // We didn't get a message signature. don't do anything with it.
501
- ndx = ndxStart;
502
- if (bytes.length > 24) {
503
- // The length of the incoming bytes have exceeded 24 bytes. This is very likely
504
- // flat out garbage on the serial port. Strip off all but the last 5 preamble + signature bytes and move on. Heck we aren't even
505
- // going to keep them.
506
- // 255, 255, 255, 0, 255
507
- ndx = bytes.length - 5;
508
- let arr = bytes.slice(0, ndx);
509
- // Remove all but the last 4 bytes. This will result in nothing anyway.
510
- logger.verbose(`[Port ${this.portId}] Tossed Inbound Bytes ${arr} due to an unrecoverable collision.`);
511
- }
512
- this.padding = [];
513
- break;
514
- }
515
- return ndx;
516
- }
517
- public readPayload(bytes: number[], ndx: number): number {
518
- //if (!this.isValid) return bytes.length;
519
- if (!this.isValid) return ndx;
520
- switch (this.protocol) {
521
- case Protocol.Broadcast:
522
- case Protocol.Pump:
523
- case Protocol.IntelliChem:
524
- case Protocol.IntelliValve:
525
- case Protocol.Heater:
526
- case Protocol.Unidentified:
527
- if (this.datalen - this.payload.length <= 0) {
528
- let buff = bytes.slice(ndx - 1);
529
- //logger.info(`We don't need any more payload ${this.datalen - this.payload.length} ${ndx} ${JSON.stringify(buff)};`);
530
- return ndx; // We don't need any more payload.
531
- }
532
- ndx = this.pushBytes(this.payload, bytes, ndx, this.datalen - this.payload.length);
533
- break;
534
- case Protocol.Chlorinator:
535
- // We need to deal with chlorinator packets where the terminator is actually split meaning only the first byte or
536
- // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
537
- while (ndx + 3 <= bytes.length && !this.testChlorTerm(bytes, ndx)) {
538
- this.payload.push(bytes[ndx++]);
539
- if (this.payload.length > 25) {
540
- this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
541
- logger.debug(`Chlorinator message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
542
- break;
543
- }
544
- }
545
- break;
546
- case Protocol.AquaLink:
547
- // We need to deal with AquaLink packets where the terminator is actually split meaning only the first byte or
548
- // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
549
- while (ndx + 3 <= bytes.length && !this.testAquaLinkTerm(bytes, ndx)) {
550
- this.payload.push(bytes[ndx++]);
551
- if (this.payload.length > 25) {
552
- this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
553
- logger.debug(`AquaLink message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
554
- break;
555
- }
556
- }
557
- break;
558
- case Protocol.Hayward:
559
- // We need to deal with AquaLink packets where the terminator is actually split meaning only the first byte or
560
- // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
561
- while (ndx + 4 <= bytes.length && !this.testHaywardTerm(bytes, ndx)) {
562
- this.payload.push(bytes[ndx++]);
563
- if (this.payload.length > 25) {
564
- this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
565
- logger.debug(`Hayward message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
566
- break;
567
- }
568
- }
569
- break;
570
-
571
- }
572
- return ndx;
573
- }
574
- public readChecksum(bytes: number[], ndx: number): number {
575
- if (!this.isValid) return bytes.length;
576
- if (ndx >= bytes.length) return ndx;
577
- switch (this.protocol) {
578
- case Protocol.Broadcast:
579
- case Protocol.Pump:
580
- case Protocol.IntelliValve:
581
- case Protocol.IntelliChem:
582
- case Protocol.Heater:
583
- case Protocol.Unidentified:
584
- // If we don't have enough bytes to make the terminator then continue on and
585
- // hope we get them on the next go around.
586
- if (this.payload.length >= this.datalen && ndx + 2 <= bytes.length) {
587
- this._complete = true;
588
- ndx = this.pushBytes(this.term, bytes, ndx, 2);
589
- this.isValid = this.isValidChecksum();
590
- }
591
- break;
592
- case Protocol.Chlorinator:
593
- if (ndx + 3 <= bytes.length && this.testChlorTerm(bytes, ndx)) {
594
- this._complete = true;
595
- ndx = this.pushBytes(this.term, bytes, ndx, 3);
596
- this.isValid = this.isValidChecksum();
597
- }
598
- break;
599
- case Protocol.AquaLink:
600
- if (ndx + 3 <= bytes.length && this.testAquaLinkTerm(bytes, ndx)) {
601
- this._complete = true;
602
- ndx = this.pushBytes(this.term, bytes, ndx, 3);
603
- this.isValid = this.isValidChecksum();
604
- }
605
- break;
606
- case Protocol.Hayward:
607
- if (ndx + 4 <= bytes.length && this.testHaywardTerm(bytes, ndx)) {
608
- this._complete = true;
609
- ndx = this.pushBytes(this.term, bytes, ndx, 4);
610
- this.isValid = this.isValidChecksum();
611
- }
612
- break;
613
-
614
- }
615
- return ndx;
616
- }
617
- public extractPayloadString(start: number, length: number) {
618
- var s = '';
619
- for (var i = start; i < this.payload.length && i < start + length; i++) {
620
- if (this.payload[i] <= 0) break;
621
- s += String.fromCharCode(this.payload[i]);
622
- }
623
- return s;
624
- }
625
- // return Little Endian Int
626
- public extractPayloadInt(ndx: number, def?: number) {
627
- return ndx + 1 < this.payload.length ? (this.payload[ndx + 1] * 256) + this.payload[ndx] : def;
628
-
629
- }
630
- // return Big Endian Int
631
- public extractPayloadIntBE(ndx: number, endian = 'le', def?: number) {
632
- return ndx + 1 < this.payload.length ? (this.payload[ndx] * 256) + this.payload[ndx + 1] : def;
633
- }
634
- public extractPayloadByte(ndx: number, def?: number) {
635
- return ndx < this.payload.length ? this.payload[ndx] : def;
636
- }
637
- private processBroadcast(): void {
638
- if (this.action !== 2 && !state.isInitialized) {
639
- // RKS: This is a placeholder for now so that messages aren't processed until we
640
- // are certain who is on the other end of the wire. Once the system config is normalized
641
- // we won't need this check here anymore.
642
- return;
643
- }
644
- switch (sys.controllerType) {
645
- // RKS: 10-10-20 - We have a message somewhere that is ending up in a process for one of the other controllers. This
646
- // makes sure we are processing every message and alerting when a message is not being processed.
647
- case ControllerType.IntelliCenter:
648
- switch (this.action) {
649
- case 1: // ACK
650
- this.isProcessed = true;
651
- break;
652
- case 2:
653
- case 204:
654
- EquipmentStateMessage.process(this);
655
- break;
656
- case 30:
657
- ConfigMessage.process(this);
658
- break;
659
- case 147: // Not sure whether this is only for *Touch. If it is not then it probably should have been caught by the protocol.
660
- IntelliChemStateMessage.process(this);
661
- break;
662
- case 164:
663
- VersionMessage.process(this);
664
- break;
665
- case 168:
666
- ExternalMessage.processIntelliCenter(this);
667
- break;
668
- case 222: // A panel is asking for action 30s
669
- case 228: // A panel is asking for the current version
670
- this.isProcessed = true;
671
- break;
672
- default:
673
- logger.info(`An unprocessed message was received ${this.toPacket()}`)
674
- break;
675
-
676
- }
677
- if (!this.isProcessed) logger.info(`The message was not processed ${this.action} - ${this.toPacket()}`);
678
- break;
679
- default:
680
- switch (this.action) {
681
- case 1: // Ack
682
- break;
683
- case 2: // Shared IntelliCenter/IntelliTouch
684
- case 5:
685
- case 8:
686
- case 96: // intellibrite lights
687
- EquipmentStateMessage.process(this);
688
- break;
689
- // IntelliTouch
690
- case 10:
691
- CustomNameMessage.process(this);
692
- break;
693
- case 11:
694
- CircuitMessage.processTouch(this);
695
- break;
696
- case 25:
697
- ChlorinatorMessage.processTouch(this);
698
- break;
699
- case 153:
700
- ExternalMessage.processTouchChlorinator(this);
701
- break;
702
- case 17:
703
- case 145:
704
- ScheduleMessage.process(this);
705
- break;
706
- case 18:
707
- IntellichemMessage.process(this);
708
- break;
709
- case 24:
710
- case 27:
711
- case 152:
712
- case 155:
713
- PumpMessage.process(this);
714
- break;
715
- case 30:
716
- switch (sys.controllerType) {
717
- case ControllerType.Unknown:
718
- break;
719
- case ControllerType.SunTouch:
720
- ScheduleMessage.processSunTouch(this);
721
- break;
722
- default:
723
- OptionsMessage.process(this);
724
- break;
725
- }
726
- break;
727
- case 22:
728
- case 32:
729
- case 33:
730
- RemoteMessage.process(this);
731
- break;
732
- case 29:
733
- case 35:
734
- ValveMessage.process(this);
735
- break;
736
- case 39:
737
- case 167:
738
- CircuitMessage.processTouch(this);
739
- break;
740
- case 40:
741
- case 168:
742
- OptionsMessage.process(this);
743
- break;
744
- case 41:
745
- CircuitGroupMessage.process(this);
746
- break;
747
- case 197:
748
- EquipmentStateMessage.process(this); // Date/Time request
749
- break;
750
- case 252:
751
- EquipmentMessage.process(this);
752
- break;
753
- case 9:
754
- case 16:
755
- case 34:
756
- case 137:
757
- case 144:
758
- case 162:
759
- HeaterMessage.process(this);
760
- break;
761
- case 114:
762
- case 115:
763
- HeaterStateMessage.process(this);
764
- break
765
- case 147:
766
- IntellichemMessage.process(this);
767
- break;
768
- case 136:
769
- ExternalMessage.processTouchSetHeatMode(this);
770
- break;
771
- default:
772
- if (this.action === 109 && this.payload[1] === 3) break;
773
- if (this.source === 17 && this.payload[0] === 109) break;
774
- logger.debug(`Packet not processed: ${this.toPacket()}`);
775
- break;
776
- }
777
- break;
778
- }
779
- }
780
- public process() {
781
- let port = conn.findPortById(this.portId);
782
- if (this.portId === sys.anslq25.portId) {
783
- return MessagesMock.process(this);
784
- }
785
- if (port.mock && port.hasAssignedEquipment()){
786
- return MessagesMock.process(this);
787
- }
788
- switch (this.protocol) {
789
- case Protocol.Broadcast:
790
- this.processBroadcast();
791
- break;
792
- case Protocol.IntelliValve:
793
- IntelliValveStateMessage.process(this);
794
- break;
795
- case Protocol.IntelliChem:
796
- IntelliChemStateMessage.process(this);
797
- break;
798
- case Protocol.Pump:
799
- if ((this.source >= 96 && this.source <= 111) || (this.dest >= 96 && this.dest <= 111))
800
- PumpStateMessage.process(this);
801
- else
802
- this.processBroadcast();
803
- break;
804
- case Protocol.Heater:
805
- HeaterStateMessage.process(this);
806
- break;
807
- case Protocol.Chlorinator:
808
- ChlorinatorStateMessage.process(this);
809
- break;
810
- case Protocol.Hayward:
811
- PumpStateMessage.processHayward(this);
812
- break;
813
- default:
814
- logger.debug(`Unprocessed Message ${this.toPacket()}`)
815
- break;
816
- }
817
- }
818
- }
819
- class OutboundCommon extends Message {
820
- public set sub(val: number) { if (this.protocol !== Protocol.Chlorinator && this.protocol !== Protocol.AquaLink) this.header[1] = val; }
821
- public get sub() { return super.sub; }
822
- public set dest(val: number) {
823
- if (this.protocol === Protocol.Chlorinator) this.header[2] = val;
824
- else if (this.protocol === Protocol.Hayward) this.header[4] = val;
825
- else this.header[2] = val;
826
- }
827
- public get dest() { return super.dest; }
828
- public set source(val: number) {
829
- switch (this.protocol) {
830
- case Protocol.Chlorinator:
831
- break;
832
- case Protocol.Hayward:
833
- this.header[3] = val;
834
- break;
835
- default:
836
- this.header[3] = val;
837
- break;
838
- }
839
- //if (this.protocol === Protocol.Hayward) this.header[2] = val;
840
- //else if (this.protocol !== Protocol.Chlorinator) this.header[3] = val;
841
- }
842
- public get source() { return super.source; }
843
- public set action(val: number) {
844
- switch (this.protocol) {
845
- case Protocol.Chlorinator:
846
- this.header[3] = val;
847
- break;
848
- case Protocol.Hayward:
849
- this.header[2] = val;
850
- break;
851
- default:
852
- this.header[4] = val;
853
- break;
854
- }
855
- }
856
- public get action() { return super.action; }
857
- public set datalen(val: number) { if (this.protocol !== Protocol.Chlorinator && this.protocol !== Protocol.Hayward) this.header[5] = val; }
858
- public get datalen() { return super.datalen; }
859
- public set chkHi(val: number) { if (this.protocol !== Protocol.Chlorinator) this.term[0] = val; }
860
- public get chkHi() { return super.chkHi; }
861
- public set chkLo(val: number) { if (this.protocol !== Protocol.Chlorinator) this.term[1] = val; else this.term[0] = val; }
862
- public get chkLo() { return super.chkLo; }
863
- // Methods
864
- public calcChecksum() {
865
- this.datalen = this.payload.length;
866
- let sum: number = this.checksum;
867
- switch (this.protocol) {
868
- case Protocol.Pump:
869
- case Protocol.Broadcast:
870
- case Protocol.IntelliValve:
871
- case Protocol.Unidentified:
872
- case Protocol.IntelliChem:
873
- case Protocol.Heater:
874
- case Protocol.Hayward:
875
- this.chkHi = Math.floor(sum / 256);
876
- this.chkLo = (sum - (this.chkHi * 256));
877
- break;
878
- case Protocol.AquaLink:
879
- case Protocol.Chlorinator:
880
- this.term[0] = sum % 256;
881
- break;
882
- }
883
- }
884
- }
885
- export class Outbound extends OutboundCommon {
886
- constructor(proto: Protocol, source: number, dest: number, action: number, payload: number[], retries?: number, response?: Response | boolean, scope?: string) {
887
- super();
888
- this.id = Message.nextMessageId;
889
- this.protocol = proto;
890
- this.direction = Direction.Out;
891
- this.retries = retries || 0;
892
- this.preamble.length = 0;
893
- this.header.length = 0;
894
- this.term.length = 0;
895
- this.payload.length = 0;
896
- if (proto === Protocol.Chlorinator || proto === Protocol.AquaLink) {
897
- this.header.push.apply(this.header, [16, 2, 0, 0]);
898
- this.term.push.apply(this.term, [0, 16, 3]);
899
- }
900
- else if (proto === Protocol.Broadcast) {
901
- this.preamble.push.apply(this.preamble, [255, 0, 255]);
902
- this.header.push.apply(this.header, [165, Message.headerSubByte, 15, Message.pluginAddress, 0, 0]);
903
- this.term.push.apply(this.term, [0, 0]);
904
- }
905
- else if (proto === Protocol.Pump || proto === Protocol.IntelliValve || proto === Protocol.IntelliChem || proto === Protocol.Heater) {
906
- this.preamble.push.apply(this.preamble, [255, 0, 255]);
907
- this.header.push.apply(this.header, [165, 0, 15, Message.pluginAddress, 0, 0]);
908
- this.term.push.apply(this.term, [0, 0]);
909
- }
910
- else if (proto === Protocol.Hayward) {
911
- this.header.push.apply(this.header, [16, 2, 0, 0, 0]);
912
- this.term.push.apply(this.term, [0, 0, 16, 3]);
913
- }
914
- this.scope = scope;
915
- this.source = source;
916
- this.dest = dest;
917
- this.action = action;
918
- this.payload.push.apply(this.payload, payload);
919
- this.calcChecksum();
920
- if (typeof response === "boolean" && response)
921
- this.response = Response.create({ protocol: this.protocol, response: true });
922
- else
923
- this.response = response as Response;
924
- }
925
- // Factory
926
- public static create(obj?: any) {
927
- let o = extend({
928
- protocol: Protocol.Broadcast,
929
- source: sys.board.commandSourceAddress || Message.pluginAddress,
930
- dest: sys.board.commandDestAddress || 16,
931
- action: 0,
932
- payload: [],
933
- retries: 0,
934
- response: false,
935
- }, obj, true);
936
- let out = new Outbound(o.protocol, o.source, o.dest, o.action, o.payload, o.retries, o.response, o.scope);
937
- //let out = new Outbound(obj.protocol || Protocol.Broadcast,
938
- // obj.source || sys.board.commandSourceAddress || Message.pluginAddress, obj.dest || sys.board.commandDestAddress || 16, obj.action || 0, obj.payload || [], obj.retries || 0, obj.response || false, obj.scope || undefined);
939
- out.portId = obj.portId || 0;
940
- out.onComplete = obj.onComplete;
941
- out.onAbort = obj.onAbort;
942
- out.timeout = obj.timeout;
943
- for (let i = 0; i < out.header.length; i++) {
944
- if (out.header[i] >= 0 && out.header[i] <= 255 && out.header[i] !== null && typeof out.header[i] !== 'undefined') continue;
945
- throw new OutboundMessageError(out, `Invalid header detected: ${out.toShortPacket()}`);
946
- }
947
- for (let i = 0; i < out.payload.length; i++) {
948
- if (out.payload[i] >= 0 && out.payload[i] <= 255 && out.payload[i] !== null && typeof out.payload[i] !== 'undefined') continue;
949
- throw new OutboundMessageError(out, `Invalid payload detected: ${out.toShortPacket()}`);
950
- }
951
- return out;
952
- }
953
- public static createMessage(action: number, payload: number[], retries?: number, response?: Response | boolean): Outbound {
954
- return new Outbound(Protocol.Broadcast, sys.board.commandSourceAddress || Message.pluginAddress, sys.board.commandDestAddress || 16, action, payload, retries, response);
955
- }
956
- public async sendAsync() {
957
- return conn.queueSendMessageAsync(this);
958
- }
959
- // Fields
960
- public retries: number = 0;
961
- public tries: number = 0;
962
- public timeout: number = 1000;
963
- public response: Response;
964
- public failed: boolean = false;
965
- public onComplete: (error: Error, msg: Inbound) => void;
966
- public onAbort: () => void;
967
- // Properties
968
- public get requiresResponse(): boolean {
969
- if (typeof this.response === 'undefined' || (typeof this.response === 'boolean' && !this.response)) return false;
970
- if (this.response instanceof Response || typeof this.response === 'function') { return true; }
971
- return false;
972
- }
973
- public get remainingTries(): number { return this.retries - this.tries + 1; } // Always allow 1 try.
974
- public setPayloadByte(ndx: number, value: number, def?: number) {
975
- if (typeof value === 'undefined' || isNaN(value)) value = def;
976
- if (ndx < this.payload.length) this.payload[ndx] = value;
977
- return this;
978
- }
979
- public appendPayloadByte(value: number, def?: number) {
980
- if (typeof value === 'undefined' || isNaN(value)) value = def;
981
- this.payload.push(value);
982
- return this;
983
- }
984
- public appendPayloadBytes(value: number, len: number) {
985
- for (let i = 0; i < len; i++) this.payload.push(value);
986
- return this;
987
- }
988
- public setPayloadBytes(value: number, len: number) {
989
- for (let i = 0; i < len; i++) {
990
- if (i < this.payload.length) this.payload[i] = value;
991
- }
992
- return this;
993
- }
994
- public insertPayloadBytes(ndx: number, value: number, len: number) {
995
- let buf = [];
996
- for (let i = 0; i < len; i++) {
997
- buf.push(value);
998
- }
999
- this.payload.splice(ndx, 0, ...buf);
1000
- return this;
1001
- }
1002
- public setPayloadInt(ndx: number, value: number, def?: number) {
1003
- if (typeof value === 'undefined' || isNaN(value)) value = def;
1004
- let b1 = Math.floor(value / 256);
1005
- let b0 = value - (b1 * 256);
1006
- if (ndx < this.payload.length) this.payload[ndx] = b0;
1007
- if (ndx + 1 < this.payload.length) this.payload[ndx + 1] = b1;
1008
- return this;
1009
- }
1010
- public appendPayloadInt(value: number, def?: number) {
1011
- if (typeof value === 'undefined' || isNaN(value)) value = def;
1012
- let b1 = Math.floor(value / 256);
1013
- let b0 = value - (b1 * 256);
1014
- this.payload.push(b0);
1015
- this.payload.push(b1);
1016
- return this;
1017
- }
1018
- public insertPayloadInt(ndx: number, value: number, def?: number) {
1019
- if (typeof value === 'undefined' || isNaN(value)) value = def;
1020
- let b1 = Math.floor(value / 256);
1021
- let b0 = (value - b1) * 256;
1022
- this.payload.splice(ndx, 0, b0, b1);
1023
- return this;
1024
- }
1025
- public setPayloadString(s: string, len?: number, def?: string) {
1026
- if (typeof s === 'undefined') s = def;
1027
- for (var i = 0; i < s.length; i++) {
1028
- if (i < this.payload.length) this.payload[i] = s.charCodeAt(i);
1029
- }
1030
- if (typeof (len) !== 'undefined') {
1031
- for (var j = i; j < len; j++)
1032
- if (i < this.payload.length) this.payload[i] = 0;
1033
- }
1034
- return this;
1035
- }
1036
- public appendPayloadString(s: string, len?: number, def?: string) {
1037
- if (typeof s === 'undefined') s = def;
1038
- for (var i = 0; i < s.length; i++) {
1039
- if (typeof (len) !== 'undefined' && i >= len) break;
1040
- this.payload.push(s.charCodeAt(i));
1041
- }
1042
- if (typeof (len) !== 'undefined') {
1043
- for (var j = i; j < len; j++) this.payload.push(0);
1044
- }
1045
- return this;
1046
- }
1047
- public insertPayloadString(start: number, s: string, len?: number, def?: string) {
1048
- if (typeof s === 'undefined') s = def;
1049
- let l = typeof len === 'undefined' ? s.length : len;
1050
- let buf = [];
1051
- for (let i = 0; i < l; i++) {
1052
- if (i < s.length) buf.push(s.charCodeAt(i));
1053
- else buf.push(0);
1054
- }
1055
- this.payload.splice(start, l, ...buf);
1056
- return this;
1057
- }
1058
- public toPacket(): number[] {
1059
- var pkt = [];
1060
- this.calcChecksum();
1061
- pkt.push.apply(pkt, this.padding);
1062
- pkt.push.apply(pkt, this.preamble);
1063
- pkt.push.apply(pkt, this.header);
1064
- pkt.push.apply(pkt, this.payload);
1065
- pkt.push.apply(pkt, this.term);
1066
- return pkt;
1067
- }
1068
- public processMock(){
1069
- // When the port is a mock port, we are no longer sending an
1070
- // outbound message but converting it to an inbound and
1071
- // skipping the actual send/receive part of the comms.
1072
- let inbound = Message.convertOutboundToInbound(this);
1073
- let port = conn.findPortById(this.portId);
1074
- if (port.hasAssignedEquipment() || this.portId === sys.anslq25.portId){
1075
- MessagesMock.process(inbound);
1076
- }
1077
- else {
1078
- inbound.process();
1079
- }
1080
-
1081
- }
1082
- }
1083
- export class Ack extends Outbound {
1084
- constructor(byte: number) {
1085
- super(Protocol.Broadcast, Message.pluginAddress, 15, 1, [byte]);
1086
- }
1087
- }
1088
- export class Response extends OutboundCommon {
1089
- /*
1090
- RG 6-2021: This class is now purely for identifying inbound messages and it is a property of the Outbound message.
1091
- This can be created by passing response: Response.create({}) or response: boolean to the Outbound message.
1092
- Response used to accept a function but that is deprecated.
1093
- Response also no longer needs to be passed msgOut because that is the parent object/message and can be
1094
- accessed via the internal symbol parent.
1095
- */
1096
- public message: Inbound;
1097
- // rsg moved accessors here because we won't have a full header; just set/check the individual byte.
1098
- public set action(val: number) { (this.protocol !== Protocol.Chlorinator) ? this.header[4] = val : this.header[3] = val; }
1099
- public get action(): number {
1100
- if (this.protocol === Protocol.Chlorinator) return this.header[3];
1101
- else if (typeof this.header[4] !== 'undefined') return this.header[4]
1102
- else return -1;
1103
- }
1104
- constructor(proto: Protocol, source: number, dest: number, action?: number, payload?: number[], ack?: number, callback?: (err, msg?: Outbound) => void) {
1105
- super();
1106
- this.protocol = proto;
1107
- this.direction = Direction.In;
1108
- this.source = source;
1109
- this.dest = dest;
1110
- this.action = action;
1111
- if (typeof payload !== 'undefined' && payload.length > 0) this.payload.push(...payload);
1112
- if (typeof ack !== 'undefined' && ack !== null) this.ack = new Ack(ack);
1113
- this.callback = callback;
1114
- }
1115
- public static create(obj?: any) {
1116
- let res = new Response(obj.protocol || Protocol.Broadcast,
1117
- obj.source || Message.pluginAddress, obj.dest || 16, obj.action || 0, obj.payload || [], obj.ack, obj.callback);
1118
- res.responseBool = obj.response;
1119
- if (typeof obj.action !== 'undefined') res.responseBool = true;
1120
- return res;
1121
- }
1122
- // Fields
1123
- public ack: Ack;
1124
- public callback: (err, msg?: Outbound) => void;
1125
- public responseBool: boolean; // if `response: true|false` is passed to the Outbound message we will store that input here
1126
-
1127
- // Methods
1128
- public isResponse(msgIn: Inbound, msgOut?: Outbound): boolean {
1129
- let bresp = false;;
1130
- try {
1131
- if (typeof this.responseBool === 'boolean' && this.responseBool) bresp = this.evalResponse(msgIn, msgOut);
1132
- else return bresp;
1133
- if (bresp === true && typeof msgOut !== 'undefined') {
1134
- msgIn.responseFor.push(msgOut.id);
1135
- logger.silly(`Message in ${msgIn.id} is a response for message out ${msgOut.id}`);
1136
- }
1137
- return bresp;
1138
- }
1139
- catch (err) { }
1140
- }
1141
-
1142
- public evalResponse(msgIn: Inbound, msgOut?: Outbound): boolean {
1143
- // this holds the logic to determine if an inbound message is a response.
1144
- // Aka is this Response object
1145
- // a response to the parent message of Outbound class.
1146
- if (typeof msgOut === 'undefined') return false;
1147
- if (msgIn.protocol !== msgOut.protocol) { return false; }
1148
- if (typeof msgIn === 'undefined') { return false; } // getting here on msg send failure
1149
-
1150
- // if these properties were set on the Response (this) object via creation,
1151
- // then use the passed in values. Otherwise, use the msgIn/msgOut matching rules
1152
- if (this.action > 0 && this.payload.length > 0) {
1153
- if (this.action === msgIn.action) {
1154
- for (let i = 0; i < msgIn.payload.length; i++) {
1155
- if (i > this.payload.length - 1)
1156
- return false;
1157
- if (this.payload[i] !== msgIn.payload[i]) return false;
1158
- return true;
1159
- }
1160
- }
1161
- }
1162
- else if (this.action > 0) {
1163
- if (this.action === msgIn.action) return true;
1164
- else return false;
1165
- }
1166
- else if (msgOut.protocol === Protocol.Pump) {
1167
- switch (msgIn.action) {
1168
- case 7:
1169
- // Scenario 1. Request for pump status.
1170
- // Msg In: [165,0,16, 96, 7,15], [4,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0,17,31], [1,95]
1171
- // Msg Out: [165,0,96, 16, 7, 0],[1,28]
1172
- if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; }
1173
- if (msgIn.action === 7 && msgOut.action === 7) { return true; }
1174
- return false;
1175
- default:
1176
- //Scenario 2, pump messages are mimics of each other but the dest/src are swapped
1177
- if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; }
1178
- // sub-case
1179
- // Msg In: [165,0,16, 96, 1, 2], [3,32],[1,59]
1180
- // Msg Out: [165,0,96,16, 1,4],[3,39, 3,32], [1,103]
1181
- if (msgIn.payload[0] === msgOut.payload[2] && msgIn.payload[1] === msgOut.payload[3]) { return true; }
1182
- // else mimics
1183
- if (JSON.stringify(msgIn.payload) === JSON.stringify(msgOut.payload)) { return true; }
1184
- return false;
1185
- }
1186
- }
1187
- else if (msgIn.protocol === Protocol.Chlorinator) {
1188
- switch (msgIn.action) {
1189
- case 1:
1190
- return msgOut.action === 0 ? true : false;
1191
- case 3:
1192
- return msgOut.action === 20 ? true : false;
1193
- case 18:
1194
- case 21:
1195
- case 22:
1196
- return msgOut.action === 17 ? true : false;
1197
- default:
1198
- return false;
1199
- }
1200
- }
1201
- else if (msgIn.protocol === Protocol.IntelliChem) {
1202
- switch (msgIn.action) {
1203
- case 1: // ack
1204
- if (msgIn.source === msgOut.dest && msgIn.payload[0] === msgOut.action) return true;
1205
- break;
1206
- default:
1207
- // in: 18; out 210 fits parent & 0x63 pattern
1208
- if (msgIn.action === (msgOut.action & 63) && msgIn.source === msgOut.dest) return true;
1209
- return false;
1210
- }
1211
- }
1212
- else if (sys.controllerType !== ControllerType.IntelliCenter) {
1213
- switch (msgIn.action) {
1214
- // these responses have multiple items so match the 1st payload byte
1215
- case 1: // ack
1216
- if (msgIn.payload[0] === msgOut.action) return true;
1217
- break;
1218
- case 10:
1219
- case 11:
1220
- case 17:
1221
- if (msgIn.action === (msgOut.action & 63) && msgIn.payload[0] === msgOut.payload[0]) return true;
1222
- break;
1223
- case 252:
1224
- if (msgOut.action === 253) return true;
1225
- break;
1226
- default:
1227
- if (msgIn.action === (msgOut.action & 63)) return true;
1228
- }
1229
- return false;
1230
- }
1231
- else if (sys.controllerType === ControllerType.IntelliCenter) {
1232
- // intellicenter packets
1233
- if (this.dest >= 0 && msgIn.dest !== this.dest) return false;
1234
- for (let i = 0; i < this.payload.length; i++) {
1235
- if (i > msgIn.payload.length - 1)
1236
- return false;
1237
- //console.log({ msg: 'Checking response', p1: msgIn.payload[i], pd: this.payload[i] });
1238
- if (msgIn.payload[i] !== this.payload[i]) return false;
1239
- }
1240
- return true;
1241
- }
1242
- }
1243
- }
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ import { ConfigMessage } from "./config/ConfigMessage";
19
+ import { PumpMessage } from "./config/PumpMessage";
20
+ import { VersionMessage } from "./status/VersionMessage";
21
+ import { PumpStateMessage } from "./status/PumpStateMessage";
22
+ import { EquipmentStateMessage } from "./status/EquipmentStateMessage";
23
+ import { HeaterStateMessage } from "./status/HeaterStateMessage";
24
+ import { ChlorinatorStateMessage } from "./status/ChlorinatorStateMessage";
25
+ import { ChlorinatorMessage } from "./config/ChlorinatorMessage";
26
+ import { ExternalMessage } from "./config/ExternalMessage";
27
+ import { Timestamp, ControllerType } from "../../Constants";
28
+ import { CircuitMessage } from "./config/CircuitMessage";
29
+ import { config } from '../../../config/Config';
30
+ import { sys } from '../../Equipment';
31
+ import { logger } from "../../../logger/Logger";
32
+ import { CustomNameMessage } from "./config/CustomNameMessage";
33
+ import { ScheduleMessage } from "./config/ScheduleMessage";
34
+ import { RemoteMessage } from "./config/RemoteMessage";
35
+ import { OptionsMessage } from "./config/OptionsMessage";
36
+ import { EquipmentMessage } from "./config/EquipmentMessage";
37
+ import { ValveMessage } from "./config/ValveMessage";
38
+ import { state } from "../../State";
39
+ import { HeaterMessage } from "./config/HeaterMessage";
40
+ import { CircuitGroupMessage } from "./config/CircuitGroupMessage";
41
+ import { IntellichemMessage } from "./config/IntellichemMessage";
42
+ import { TouchScheduleCommands } from "controller/boards/EasyTouchBoard";
43
+ import { IntelliValveStateMessage } from "./status/IntelliValveStateMessage";
44
+ import { IntelliChemStateMessage } from "./status/IntelliChemStateMessage";
45
+ import { RegalModbusStateMessage } from "./status/RegalModbusStateMessage";
46
+ import { OutboundMessageError } from "../../Errors";
47
+ import { conn } from "../Comms"
48
+ import extend = require("extend");
49
+ import { MessagesMock } from "../../../anslq25/MessagesMock";
50
+
51
+ export enum Direction {
52
+ In = 'in',
53
+ Out = 'out'
54
+ }
55
+ export enum Protocol {
56
+ Unknown = 'unknown',
57
+ Broadcast = 'broadcast',
58
+ Pump = 'pump',
59
+ Chlorinator = 'chlorinator',
60
+ IntelliChem = 'intellichem',
61
+ IntelliValve = 'intellivalve',
62
+ Heater = 'heater',
63
+ AquaLink = 'aqualink',
64
+ Hayward = 'hayward',
65
+ Unidentified = 'unidentified',
66
+ RegalModbus = 'regalmodbus'
67
+ }
68
+ export class Message {
69
+ constructor() { }
70
+
71
+ // Internal Storage
72
+ protected _complete: boolean = false;
73
+ public static headerSubByte: number = 33;
74
+ public static pluginAddress: number = config.getSection('controller', { address: 33 }).address;
75
+ private _id: number = -1;
76
+ // Fields
77
+ private static _messageId: number = 0;
78
+ public static get nextMessageId(): number {
79
+ let i = this._messageId < 80000 ? ++this._messageId : this._messageId = 0;
80
+ //logger.debug(`Assigning message id ${i}`)
81
+ return i; }
82
+ public portId = 0; // This will be the target or source port for the message. If this is from or to an Aux RS485 port the value will be > 0.
83
+ public timestamp: Date = new Date();
84
+ public direction: Direction = Direction.In;
85
+ public protocol: Protocol = Protocol.Unknown;
86
+ public padding: number[] = [];
87
+ public preamble: number[] = [];
88
+ public header: number[] = [];
89
+ public payload: number[] = [];
90
+ public term: number[] = [];
91
+ public packetCount: number = 0;
92
+ public get id(): number { return this._id; }
93
+ public set id(val: number) { this._id = val; }
94
+ public isValid: boolean = true;
95
+ public scope: string;
96
+ public isClone: boolean;
97
+ // Properties
98
+ public get isComplete(): boolean { return this._complete; }
99
+ public get sub(): number { return this.header.length > 1 ? this.header[1] : -1; }
100
+ public get dest(): number {
101
+ if (this.header.length > 2) {
102
+ if (this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink) {
103
+ return this.header.length > 2 ? (this.header[2] >= 80 ? this.header[2] : 0) : -1;
104
+ }
105
+ else if (this.protocol === Protocol.Hayward) {
106
+ // src act dest
107
+ //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
108
+ return this.header.length > 4 ? this.header[2] : -1;
109
+ }
110
+ else if (this.protocol === Protocol.RegalModbus) {
111
+ return this.header.length > 0 ? this.header[0] : -1;
112
+ }
113
+ else return this.header.length > 2 ? this.header[2] : -1;
114
+ }
115
+ else return -1;
116
+ }
117
+ public get source(): number {
118
+ if (this.protocol === Protocol.Chlorinator) {
119
+ return this.header.length > 2 ? (this.header[2] >= 80 ? 0 : this.header[2]) : -1;
120
+ // have to assume incoming packets with header[2] >= 80 (sent to a chlorinator)
121
+ // are from controller (0);
122
+ // likewise, if the destination is 0 (controller) we
123
+ // have to assume it was sent from the 1st chlorinator (1)
124
+ // until we learn otherwise.
125
+ }
126
+ else if (this.protocol === Protocol.AquaLink) {
127
+ // Once we decode the devices we will be able to tell where it came from based upon the commands.
128
+ return 0;
129
+ }
130
+ else if (this.protocol === Protocol.Hayward) {
131
+ // src act dest
132
+ //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
133
+ //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
134
+ return this.header.length > 4 ? this.header[4] : -1;
135
+ }
136
+ else if (this.protocol === Protocol.RegalModbus) {
137
+ // No source address in RegalModbus.
138
+ return -1;
139
+ }
140
+ if (this.header.length > 3) return this.header[3];
141
+ else return -1;
142
+ }
143
+ public get action(): number {
144
+ // The action byte is actually the 4th byte in the header the destination address is the 5th byte.
145
+ if (this.protocol === Protocol.Chlorinator ||
146
+ this.protocol === Protocol.AquaLink) return this.header.length > 3 ? this.header[3] : -1;
147
+ else if (this.protocol === Protocol.Hayward) {
148
+ // src act dest
149
+ //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
150
+ //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
151
+ return this.header.length > 3 ? this.header[3] || this.header[2] : -1;
152
+ }
153
+ else if (this.protocol === Protocol.RegalModbus) {
154
+ return this.header.length > 1 ? this.header[1]: -1;
155
+ }
156
+ else if (this.header.length > 4) return this.header[4];
157
+ else return -1;
158
+ if (this.header.length > 4) return this.header[4];
159
+ else return -1;
160
+ }
161
+ public get datalen(): number {
162
+ if (
163
+ this.protocol === Protocol.Chlorinator ||
164
+ this.protocol === Protocol.AquaLink ||
165
+ this.protocol === Protocol.Hayward
166
+ ) {
167
+ return this.payload.length;
168
+ }
169
+ else if (this.protocol === Protocol.RegalModbus) {
170
+ let action = this.action;
171
+ let ack = this.header[2];
172
+ switch (action) {
173
+ case 0x41: // Go
174
+ case 0x42: // Stop
175
+ return 0;
176
+ case 0x43: // Status
177
+ switch (ack) {
178
+ case 0x10:
179
+ return 1;
180
+ case 0x20:
181
+ return 0
182
+ }
183
+ case 0x44: // Set demand
184
+ return 3;
185
+ case 0x45: // Read sensor
186
+ switch (ack) {
187
+ case 0x10:
188
+ return 4;
189
+ case 0x20:
190
+ return 2;
191
+ }
192
+ case 0x46: // Read identification
193
+ console.log("RegalModbus: Read identification not implemented yet.");
194
+ break;
195
+ case 0x64: // Configuration read/write
196
+ console.log("RegalModbus: Configuration read/write not implemented yet.");
197
+ break;
198
+ case 0x65: // Store configuration
199
+ return 0;
200
+ }
201
+ }
202
+ return this.header.length > 5 ? this.header[5] : -1;
203
+ }
204
+ public get chkHi(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? 0 : this.term.length > 0 ? this.term[0] : -1; }
205
+ public get chkLo(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? this.term[0] : this.term[1]; }
206
+ public get checksum(): number {
207
+ var sum = 0;
208
+ for (let i = 0; i < this.header.length; i++) sum += this.header[i];
209
+ for (let i = 0; i < this.payload.length; i++) sum += this.payload[i];
210
+ return sum;
211
+ }
212
+
213
+ // Methods
214
+ public toPacket(): number[] {
215
+ const pkt = [];
216
+ pkt.push(...this.padding);
217
+ pkt.push(...this.preamble);
218
+ pkt.push(...this.header);
219
+ pkt.push(...this.payload);
220
+ pkt.push(...this.term);
221
+ return pkt;
222
+ }
223
+ public toShortPacket(): number[] {
224
+ const pkt = [];
225
+ pkt.push(...this.header);
226
+ pkt.push(...this.payload);
227
+ pkt.push(...this.term);
228
+ return pkt;
229
+ }
230
+ public toLog(): string {
231
+ return `{"port":${this.portId},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)}, ${JSON.stringify(this.header)}, ${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts":"${Timestamp.toISOLocal(this.timestamp)}"}`;
232
+ }
233
+ public static convertOutboundToInbound(out: Outbound): Inbound {
234
+ let inbound = new Inbound();
235
+ inbound.portId = out.portId;
236
+ // inbound.id = Message.nextMessageId;
237
+ inbound.protocol = out.protocol;
238
+ inbound.scope = out.scope;
239
+ inbound.preamble = out.preamble;
240
+ inbound.padding = out.padding;
241
+ inbound.header = out.header;
242
+ inbound.payload = [...out.payload];
243
+ inbound.term = out.term;
244
+ inbound.portId = out.portId;
245
+ return inbound;
246
+ }
247
+ public static convertInboundToOutbound(inbound: Inbound): Outbound {
248
+ let out = new Outbound(
249
+ inbound.protocol,
250
+ inbound.source,
251
+ inbound.dest,
252
+ inbound.action,
253
+ inbound.payload,
254
+ );
255
+ out.scope = inbound.scope;
256
+ out.preamble = inbound.preamble;
257
+ out.padding = inbound.padding;
258
+ out.header = inbound.header;
259
+ out.term = inbound.term;
260
+ out.portId = inbound.portId;
261
+ return out;
262
+ }
263
+ public clone(): Inbound | Outbound {
264
+ let msg;
265
+ if (this instanceof Inbound) {
266
+ msg = new Inbound();
267
+ msg.id = Message.nextMessageId;
268
+ msg.scope = this.scope;
269
+ msg.preamble = this.preamble;
270
+ msg.padding = this.padding;
271
+ msg.payload = [...this.payload];
272
+ msg.header = this.header;
273
+ msg.term = this.term;
274
+ msg.portId = this.portId;
275
+ }
276
+ else {
277
+ msg = new Outbound(
278
+ this.protocol, this.source, this.dest, this.action, [...this.payload],
279
+ );
280
+ msg.portId = this.portId;
281
+ msg.scope = this.scope;
282
+ }
283
+ return msg;
284
+ }
285
+ }
286
+ export class Inbound extends Message {
287
+ // /usr/bin/socat TCP-LISTEN:9801,fork,reuseaddr FILE:/dev/ttyUSB0,b9600,raw
288
+ // /usr/bin/socat TCP-LISTEN:9801,fork,reuseaddr FILE:/dev/ttyUSB0,b9600,cs8,cstopb=1,parenb=0,raw
289
+ // /usr/bin / socat TCP - LISTEN: 9801,fork,reuseaddr FILE:/dev/ttyUSB0, b9600, cs8, cstopb = 1, parenb = 0, raw
290
+ constructor() {
291
+ super();
292
+ this.direction = Direction.In;
293
+ }
294
+ // Factory
295
+ public static replay(obj?: any) {
296
+ let inbound = new Inbound();
297
+ inbound.readHeader(obj.header, 0);
298
+ inbound.readPayload(obj.payload, 0);
299
+ inbound.readChecksum(obj.term, 0);
300
+ inbound.process();
301
+ }
302
+ public responseFor: number[] = [];
303
+ public isProcessed: boolean = false;
304
+ public collisions: number = 0;
305
+ public rewinds: number = 0;
306
+ // Private methods
307
+ private isValidChecksum(): boolean {
308
+ switch (this.protocol) {
309
+ case Protocol.Chlorinator:
310
+ case Protocol.AquaLink:
311
+ return this.checksum % 256 === this.chkLo;
312
+ case Protocol.RegalModbus: {
313
+ const data = this.header.concat(this.payload);
314
+ const crcComputed = computeCRC16(data);
315
+ const crcReceived = (this.chkLo << 8) | this.chkHi;
316
+ return crcComputed === crcReceived;
317
+ }
318
+ default:
319
+ return (this.chkHi * 256) + this.chkLo === this.checksum;
320
+ }
321
+ }
322
+ public toLog() {
323
+ if (this.responseFor.length > 0)
324
+ return `{"port":${this.portId || 0},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","for":${JSON.stringify(this.responseFor)},"pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
325
+ return `{"port":${this.portId || 0},"id":${this.id},"valid":${this.isValid},"dir":"${this.direction}","proto":"${this.protocol}","pkt":[${JSON.stringify(this.padding)},${JSON.stringify(this.preamble)},${JSON.stringify(this.header)},${JSON.stringify(this.payload)},${JSON.stringify(this.term)}],"ts": "${Timestamp.toISOLocal(this.timestamp)}"}`;
326
+ }
327
+ private testChlorHeader(bytes: number[], ndx: number): boolean {
328
+ // if packets have 16,2 (eg status=16,2,29) in them and they come as partial packets, they would have
329
+ // prev been detected as chlor packets;
330
+ // valid chlor packets should have 16,2,0 or 16,2,[80-96];
331
+ // this should reduce the number of false chlor packets
332
+ // For any of these 16,2 type headers we need at least 5 bytes to determine the routing.
333
+ //63,15,16,2,29,9,36,0,0,0,0,0,16,0,32,0,0,2,0,75,75,32,241,80,85,24,241,16,16,48,245,69,45,100,186,16,2,80,17,0,115,16,3
334
+ if (bytes.length > ndx + 4) {
335
+ if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
336
+ let dst = bytes[ndx + 2];
337
+ let act = bytes[ndx + 3];
338
+ // For now the dst byte will always be 0 or 80.
339
+ if (![0, 16, 80, 81, 82, 83].includes(dst)) {
340
+ //logger.info(`Sensed chlorinator header but the dst byte is ${dst}`);
341
+ return false;
342
+ }
343
+ else if (dst === 0 && [1, 18, 3].includes(act))
344
+ return true;
345
+ else if (![0, 17, 19, 20, 21, 22].includes(act)) {
346
+ //logger.info(`Sensed out chlorinator header but the dst byte is ${dst} ${act} ${JSON.stringify(bytes)}`);
347
+ return false;
348
+ }
349
+ return true;
350
+ }
351
+ }
352
+ return false;
353
+ }
354
+ private testRegalModbusHeader(bytes: number[], ndx: number): boolean {
355
+ // RegalModbus protocol: header, function, ack, payload, crcLo, crcHi
356
+ if (bytes.length > ndx + 3 && sys.controllerType === 'nixie') {
357
+ // address must be in the range 0x15 to 0xF7
358
+ // function code must be in the range 0x00 to 0x7F
359
+ // ack must be in 0x10, 0x20, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x09, 0x0A
360
+ let addr = bytes[ndx];
361
+ let func = bytes[ndx + 1];
362
+ let ack = bytes[ndx + 2];
363
+ let acceptableAcks = [0x10, 0x20, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x09, 0x0A];
364
+
365
+ // logger.debug('Testing RegalModbus header', bytes, addr, func, ack, acceptableAcks.includes(ack));
366
+ // logger.debug(`Current bytes: ${JSON.stringify(bytes)}`);
367
+
368
+ if (addr >= 0x15 && addr <= 0xF7 && func >= 0x00 && func <= 0x7F && acceptableAcks.includes(ack)) {
369
+ return true;
370
+ }
371
+ }
372
+ return false;
373
+ }
374
+ private testAquaLinkHeader(bytes: number[], ndx: number): boolean {
375
+ if (bytes.length > ndx + 4 && sys.controllerType === 'aqualink') {
376
+ if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
377
+ return true;
378
+ }
379
+ }
380
+ return false;
381
+ }
382
+ private testHaywardHeader(bytes: number[], ndx: number): boolean {
383
+ //0x10, 0x02, 0x0C, 0x01, 0x00, 0x2D, 0x00, 0x4C, 0x10, 0x03 -- Command to pump
384
+ //[16,2,12,1,0]
385
+ //0x10, 0x02, 0x0C, 0x01, 0x00, 0x2D, 0x00, 0x4C, 0x10, 0x03 -- Command to Filter Pump
386
+ //[16,2,12,1,0]
387
+ //0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
388
+ //[16,2,12,1,2]
389
+ // src act dest
390
+ //0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
391
+ //[16,2,0,12,0] --> Response
392
+ //[16,2,0,12,0]
393
+ if (bytes.length > ndx + 4) {
394
+ if (sys.controllerType === 'aqualink') return false;
395
+ if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
396
+ let dst = bytes[ndx + 3];
397
+ let src = bytes[ndx + 2];
398
+ if (dst === 12 || src === 12) return true;
399
+ }
400
+ }
401
+ return false;
402
+ }
403
+ private testBroadcastHeader(bytes: number[], ndx: number): boolean {
404
+ // We are looking for [255,0,255,165]
405
+ if (bytes.length > ndx + 3) {
406
+ if (bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] === 165) return true;
407
+ return false;
408
+ }
409
+ //return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] === 165;
410
+ return false;
411
+ }
412
+ private testUnidentifiedHeader(bytes: number[], ndx: number): boolean {
413
+ if (bytes.length > ndx + 3) {
414
+ if (bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] !== 165) return true;
415
+ return false;
416
+ }
417
+ //return ndx < bytes.length - 3 && bytes[ndx] === 255 && bytes[ndx + 1] === 0 && bytes[ndx + 2] === 255 && bytes[ndx + 3] !== 165;
418
+ return false;
419
+ }
420
+ private testChlorTerm(bytes: number[], ndx: number): boolean { return ndx + 2 < bytes.length && bytes[ndx + 1] === 16 && bytes[ndx + 2] === 3; }
421
+ private testAquaLinkTerm(bytes: number[], ndx: number): boolean { return ndx + 2 < bytes.length && bytes[ndx + 1] === 16 && bytes[ndx + 2] === 3; }
422
+ private testHaywardTerm(bytes: number[], ndx: number): boolean { return ndx + 3 < bytes.length && bytes[ndx + 2] === 16 && bytes[ndx + 3] === 3; }
423
+ private pushBytes(target: number[], bytes: number[], ndx: number, length: number): number {
424
+ let end = ndx + length;
425
+ while (ndx < bytes.length && ndx < end)
426
+ target.push(bytes[ndx++]);
427
+ return ndx;
428
+ }
429
+ // Methods
430
+ public rewind(bytes: number[], ndx: number): number {
431
+ let buff = [];
432
+ //buff.push(...this.padding);
433
+ //buff.push(...this.preamble);
434
+ buff.push(...this.header);
435
+ buff.push(...this.payload);
436
+ buff.push(...this.term);
437
+ // Add in the remaining bytes.
438
+ if (ndx < bytes.length - 1) buff.push(...bytes.slice(ndx, bytes.length - 1));
439
+ this.padding.push(...this.preamble);
440
+ this.preamble.length = 0;
441
+ this.header.length = 0;
442
+ this.payload.length = 0;
443
+ this.term.length = 0;
444
+ buff.shift();
445
+ this.protocol = Protocol.Unknown;
446
+ this._complete = false;
447
+ this.isValid = true;
448
+
449
+ this.collisions++;
450
+ this.rewinds++;
451
+ logger.info(`rewinding message collision ${this.collisions} ${ndx} ${bytes.length} ${JSON.stringify(buff)}`);
452
+ this.readPacket(buff);
453
+ return ndx;
454
+ //return this.padding.length + this.preamble.length;
455
+ }
456
+ public readPacket(bytes: number[]): number {
457
+ //logger.info(`BYTES: ${JSON.stringify(bytes)}`);
458
+ var ndx = this.readHeader(bytes, 0);
459
+ if (this.isValid && this.header.length > 0) ndx = this.readPayload(bytes, ndx);
460
+ if (this.isValid && this.header.length > 0) ndx = this.readChecksum(bytes, ndx);
461
+ if (this.isComplete && !this.isValid) return this.rewind(bytes, ndx);
462
+ return ndx;
463
+ }
464
+ public mergeBytes(bytes) {
465
+ var ndx = 0;
466
+ if (this.header.length === 0) ndx = this.readHeader(bytes, ndx);
467
+ if (this.isValid && this.header.length > 0) ndx = this.readPayload(bytes, ndx);
468
+ if (this.isValid && this.header.length > 0) ndx = this.readChecksum(bytes, ndx);
469
+ //if (this.isComplete && !this.isValid) return this.rewind(bytes, ndx);
470
+ return ndx;
471
+ }
472
+ public readHeader(bytes: number[], ndx: number): number {
473
+ // start over to include the padding bytes.
474
+ //if (this.protocol !== Protocol.Unknown) {
475
+ // logger.warn(`${this.protocol} resulted in an empty message header ${JSON.stringify(this.header)}`);
476
+ //}
477
+ let ndxStart = ndx;
478
+ // RKS: 05-30-22 -- OMG we have not been dealing with short headers. As a result it was restarting
479
+ // the header process even after it had identified it.
480
+ if (this.protocol === Protocol.Unknown) {
481
+ while (ndx < bytes.length) {
482
+ if (this.testBroadcastHeader(bytes, ndx)) {
483
+ this.protocol = Protocol.Broadcast;
484
+ break;
485
+ }
486
+ if (this.testUnidentifiedHeader(bytes, ndx)) {
487
+ this.protocol = Protocol.Unidentified;
488
+ break;
489
+ }
490
+ if (this.testChlorHeader(bytes, ndx)) {
491
+ this.protocol = Protocol.Chlorinator;
492
+ break;
493
+ }
494
+ if (this.testAquaLinkHeader(bytes, ndx)) {
495
+ this.protocol = Protocol.AquaLink;
496
+ break;
497
+ }
498
+ if (this.testHaywardHeader(bytes, ndx)) {
499
+ this.protocol = Protocol.Hayward;
500
+ break;
501
+ }
502
+ if (this.testRegalModbusHeader(bytes, ndx)) {
503
+ this.protocol = Protocol.RegalModbus;
504
+ logger.debug(`RegalModbus header detected. ${JSON.stringify(bytes)}`);
505
+ break;
506
+ }
507
+ this.padding.push(bytes[ndx++]);
508
+ }
509
+ }
510
+ // When the code above finds a protocol, ndx will be at the start of that
511
+ // header. If it is not identified then it will rewind to the initial
512
+ // start position until we get more bytes. This is the default case below.
513
+ let ndxHeader = ndx;
514
+ switch (this.protocol) {
515
+ case Protocol.Pump:
516
+ case Protocol.IntelliChem:
517
+ case Protocol.IntelliValve:
518
+ case Protocol.Broadcast:
519
+ case Protocol.Heater:
520
+ case Protocol.Unidentified:
521
+ ndx = this.pushBytes(this.preamble, bytes, ndx, 3);
522
+ ndx = this.pushBytes(this.header, bytes, ndx, 6);
523
+ if (this.header.length < 6) {
524
+ // We actually don't have a complete header yet so just return.
525
+ // we will pick it up next go around.
526
+ // logger.debug(`We have an incoming message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
527
+ //logger.info(`We don't have a complete header ${JSON.stringify(this.header)}`);
528
+ this.preamble = [];
529
+ this.header = [];
530
+ return ndxHeader;
531
+ }
532
+ if (this.source >= 96 && this.source <= 111) this.protocol = Protocol.Pump;
533
+ else if (this.dest >= 96 && this.dest <= 111) this.protocol = Protocol.Pump;
534
+ else if (this.source >= 112 && this.source <= 127) this.protocol = Protocol.Heater;
535
+ else if (this.dest >= 112 && this.dest <= 127) this.protocol = Protocol.Heater;
536
+ else if (this.dest >= 144 && this.dest <= 158) this.protocol = Protocol.IntelliChem;
537
+ else if (this.source >= 144 && this.source <= 158) this.protocol = Protocol.IntelliChem;
538
+ else if (this.source == 12 || this.dest == 12) this.protocol = Protocol.IntelliValve;
539
+ if (this.datalen > 75) {
540
+ //this.isValid = false;
541
+ logger.debug(`Broadcast length ${this.datalen} exceeded 75 bytes for ${this.protocol} message. Message rewound ${this.header}`);
542
+ this.padding.push(...this.preamble);
543
+ this.padding.push(...this.header.slice(0, 1));
544
+ this.preamble = [];
545
+ this.header = [];
546
+ this.collisions++;
547
+ this.rewinds++;
548
+ return ndxHeader + 1;
549
+ }
550
+ break;
551
+ case Protocol.Chlorinator:
552
+ // RKS: 06-06-20 We occasionally get messages where the 16, 2 is interrupted. The message below
553
+ // has an IntelliValve broadcast embedded within as well as a chlorinator status request. So
554
+ // in the instance below we have two messages being tossed because something on the bus interrupted
555
+ // the chlorinator. The first 240 byte does not belong to the chlorinator nor does it belong to
556
+ // the IntelliValve
557
+ //[][16, 2, 240][255, 0, 255, 165, 1, 16, 12, 82, 8, 0, 128, 216, 128, 57, 64, 25, 166, 4, 44, 16, 2, 80, 17, 0][115, 16, 3]
558
+ //[][16, 2, 80, 17][0][115, 16, 3]
559
+ ndx = this.pushBytes(this.header, bytes, ndx, 4);
560
+ if (this.header.length < 4) {
561
+ // We actually don't have a complete header yet so just return.
562
+ // we will pick it up next go around.
563
+ logger.debug(`We have an incoming chlorinator message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
564
+ this.preamble = [];
565
+ this.header = [];
566
+ return ndxHeader;
567
+ }
568
+ break;
569
+ case Protocol.Hayward:
570
+ ndx = this.pushBytes(this.header, bytes, ndx, 5);
571
+ if (this.header.length < 4) {
572
+ // We actually don't have a complete header yet so just return.
573
+ // we will pick it up next go around.
574
+ logger.debug(`We have an incoming Hayward message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
575
+ this.preamble = [];
576
+ this.header = [];
577
+ return ndxHeader;
578
+ }
579
+ break;
580
+ case Protocol.AquaLink:
581
+ ndx = this.pushBytes(this.header, bytes, ndx, 5);
582
+ if (this.header.length < 5) {
583
+ // We actually don't have a complete header yet so just return.
584
+ // we will pick it up next go around.
585
+ logger.debug(`We have an incoming AquaLink message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
586
+ this.preamble = [];
587
+ this.header = [];
588
+ return ndxHeader;
589
+ }
590
+ break;
591
+ case Protocol.RegalModbus:
592
+ ndx = this.pushBytes(this.header, bytes, ndx, 3);
593
+ if (this.header.length < 3) {
594
+ // We actually don't have a complete header yet so just return.
595
+ // we will pick it up next go around.
596
+ logger.debug(`We have an incoming RegalModbus message but the serial port hasn't given a complete header. [${this.padding}][${this.preamble}][${this.header}]`);
597
+ this.preamble = [];
598
+ this.header = [];
599
+ return ndxHeader;
600
+ }
601
+ break;
602
+ default:
603
+ // We didn't get a message signature. don't do anything with it.
604
+ ndx = ndxStart;
605
+ if (bytes.length > 24) {
606
+ // The length of the incoming bytes have exceeded 24 bytes. This is very likely
607
+ // flat out garbage on the serial port. Strip off all but the last 5 preamble + signature bytes and move on. Heck we aren't even
608
+ // going to keep them.
609
+ // 255, 255, 255, 0, 255
610
+ ndx = bytes.length - 5;
611
+ let arr = bytes.slice(0, ndx);
612
+ // Remove all but the last 4 bytes. This will result in nothing anyway.
613
+ logger.verbose(`[Port ${this.portId}] Tossed Inbound Bytes ${arr} due to an unrecoverable collision.`);
614
+ }
615
+ this.padding = [];
616
+ break;
617
+ }
618
+ return ndx;
619
+ }
620
+ public readPayload(bytes: number[], ndx: number): number {
621
+ //if (!this.isValid) return bytes.length;
622
+ if (!this.isValid) return ndx;
623
+ switch (this.protocol) {
624
+ case Protocol.Broadcast:
625
+ case Protocol.Pump:
626
+ case Protocol.IntelliChem:
627
+ case Protocol.IntelliValve:
628
+ case Protocol.Heater:
629
+ case Protocol.Unidentified:
630
+ if (this.datalen - this.payload.length <= 0) {
631
+ let buff = bytes.slice(ndx - 1);
632
+ //logger.info(`We don't need any more payload ${this.datalen - this.payload.length} ${ndx} ${JSON.stringify(buff)};`);
633
+ return ndx; // We don't need any more payload.
634
+ }
635
+ ndx = this.pushBytes(this.payload, bytes, ndx, this.datalen - this.payload.length);
636
+ break;
637
+ case Protocol.Chlorinator:
638
+ // We need to deal with chlorinator packets where the terminator is actually split meaning only the first byte or
639
+ // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
640
+ while (ndx + 3 <= bytes.length && !this.testChlorTerm(bytes, ndx)) {
641
+ this.payload.push(bytes[ndx++]);
642
+ if (this.payload.length > 25) {
643
+ this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
644
+ logger.debug(`Chlorinator message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
645
+ break;
646
+ }
647
+ }
648
+ break;
649
+ case Protocol.AquaLink:
650
+ // We need to deal with AquaLink packets where the terminator is actually split meaning only the first byte or
651
+ // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
652
+ while (ndx + 3 <= bytes.length && !this.testAquaLinkTerm(bytes, ndx)) {
653
+ this.payload.push(bytes[ndx++]);
654
+ if (this.payload.length > 25) {
655
+ this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
656
+ logger.debug(`AquaLink message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
657
+ break;
658
+ }
659
+ }
660
+ break;
661
+ case Protocol.Hayward:
662
+ // We need to deal with AquaLink packets where the terminator is actually split meaning only the first byte or
663
+ // two of the total payload is provided for the term. We need at least 3 bytes to make this determination.
664
+ while (ndx + 4 <= bytes.length && !this.testHaywardTerm(bytes, ndx)) {
665
+ this.payload.push(bytes[ndx++]);
666
+ if (this.payload.length > 25) {
667
+ this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
668
+ logger.debug(`Hayward message marked as invalid after not finding 16,3 in payload after ${this.payload.length} bytes`);
669
+ break;
670
+ }
671
+ }
672
+ break;
673
+ case Protocol.RegalModbus:
674
+ // RegalModbus protocol: header, function, ack, payload, crcLo, crcHi
675
+ while (ndx + 3 <= bytes.length) {
676
+ this.payload.push(bytes[ndx++]);
677
+ if (this.payload.length > 11) {
678
+ this.isValid = false; // We have a runaway packet. Some collision occurred so lets preserve future packets.
679
+ logger.debug(`RegalModbus message marked as invalid due to payload more than 11 bytes`);
680
+ break;
681
+ }
682
+ }
683
+ break;
684
+
685
+ }
686
+ return ndx;
687
+ }
688
+ public readChecksum(bytes: number[], ndx: number): number {
689
+ if (!this.isValid) return bytes.length;
690
+ if (ndx >= bytes.length) return ndx;
691
+ switch (this.protocol) {
692
+ case Protocol.Broadcast:
693
+ case Protocol.Pump:
694
+ case Protocol.IntelliValve:
695
+ case Protocol.IntelliChem:
696
+ case Protocol.Heater:
697
+ case Protocol.RegalModbus:
698
+ case Protocol.Unidentified:
699
+ // If we don't have enough bytes to make the terminator then continue on and
700
+ // hope we get them on the next go around.
701
+ if (this.payload.length >= this.datalen && ndx + 2 <= bytes.length) {
702
+ this._complete = true;
703
+ ndx = this.pushBytes(this.term, bytes, ndx, 2);
704
+ this.isValid = this.isValidChecksum();
705
+ }
706
+ break;
707
+ case Protocol.Chlorinator:
708
+ if (ndx + 3 <= bytes.length && this.testChlorTerm(bytes, ndx)) {
709
+ this._complete = true;
710
+ ndx = this.pushBytes(this.term, bytes, ndx, 3);
711
+ this.isValid = this.isValidChecksum();
712
+ }
713
+ break;
714
+ case Protocol.AquaLink:
715
+ if (ndx + 3 <= bytes.length && this.testAquaLinkTerm(bytes, ndx)) {
716
+ this._complete = true;
717
+ ndx = this.pushBytes(this.term, bytes, ndx, 3);
718
+ this.isValid = this.isValidChecksum();
719
+ }
720
+ break;
721
+ case Protocol.Hayward:
722
+ if (ndx + 4 <= bytes.length && this.testHaywardTerm(bytes, ndx)) {
723
+ this._complete = true;
724
+ ndx = this.pushBytes(this.term, bytes, ndx, 4);
725
+ this.isValid = this.isValidChecksum();
726
+ }
727
+ break;
728
+
729
+ }
730
+ return ndx;
731
+ }
732
+ public extractPayloadString(start: number, length: number) {
733
+ var s = '';
734
+ for (var i = start; i < this.payload.length && i < start + length; i++) {
735
+ if (this.payload[i] <= 0) break;
736
+ s += String.fromCharCode(this.payload[i]);
737
+ }
738
+ return s;
739
+ }
740
+ // return Little Endian Int
741
+ public extractPayloadInt(ndx: number, def?: number) {
742
+ return ndx + 1 < this.payload.length ? (this.payload[ndx + 1] * 256) + this.payload[ndx] : def;
743
+
744
+ }
745
+ // return Big Endian Int
746
+ public extractPayloadIntBE(ndx: number, endian = 'le', def?: number) {
747
+ return ndx + 1 < this.payload.length ? (this.payload[ndx] * 256) + this.payload[ndx + 1] : def;
748
+ }
749
+ public extractPayloadByte(ndx: number, def?: number) {
750
+ return ndx < this.payload.length ? this.payload[ndx] : def;
751
+ }
752
+ private processBroadcast(): void {
753
+ if (this.action !== 2 && !state.isInitialized) {
754
+ // RKS: This is a placeholder for now so that messages aren't processed until we
755
+ // are certain who is on the other end of the wire. Once the system config is normalized
756
+ // we won't need this check here anymore.
757
+ return;
758
+ }
759
+ switch (sys.controllerType) {
760
+ // RKS: 10-10-20 - We have a message somewhere that is ending up in a process for one of the other controllers. This
761
+ // makes sure we are processing every message and alerting when a message is not being processed.
762
+ case ControllerType.IntelliCenter:
763
+ switch (this.action) {
764
+ case 1: // ACK
765
+ VersionMessage.processAction168Ack(this);
766
+ break;
767
+ case 2:
768
+ case 204:
769
+ EquipmentStateMessage.process(this);
770
+ break;
771
+ case 30:
772
+ ConfigMessage.process(this);
773
+ break;
774
+ case 147: // Not sure whether this is only for *Touch. If it is not then it probably should have been caught by the protocol.
775
+ IntelliChemStateMessage.process(this);
776
+ break;
777
+ case 164:
778
+ VersionMessage.process(this);
779
+ break;
780
+ case 168:
781
+ ExternalMessage.processIntelliCenter(this);
782
+ break;
783
+ case 179: // v3.004+ Heartbeat request - handled by EquipmentStateMessage
784
+ EquipmentStateMessage.process(this);
785
+ break;
786
+ case 180: // v3.004+ Heartbeat response/status (may be sent by other devices)
787
+ // No processing required; mark as handled to avoid noisy "not processed" logs.
788
+ this.isProcessed = true;
789
+ break;
790
+ case 184: // v3.004+ Circuit control from wireless remote (replaces Action 134)
791
+ // Wireless remote sends this to control circuits
792
+ // Currently handled by EquipmentStateMessage for logging
793
+ EquipmentStateMessage.process(this);
794
+ break;
795
+ case 217: // v3.004+ Device list broadcast
796
+ EquipmentStateMessage.process(this);
797
+ break;
798
+ case 222: // A panel is asking for action 30s
799
+ this.isProcessed = true;
800
+ break;
801
+ case 228: // A panel is asking for the current version
802
+ VersionMessage.processVersionRequest(this);
803
+ break;
804
+ case 251: // v3.004+ Device announcement/registration request
805
+ // Devices send this to announce presence to OCP
806
+ // Payload byte 0: device address
807
+ // Response: Action 253
808
+ this.isProcessed = true;
809
+ break;
810
+ case 253: // v3.004+ Device registration confirmation
811
+ // OCP sends this in response to Action 251
812
+ // Payload byte 2: 1 = registered, 0 = not registered
813
+ logger.info(`Device registration confirmed: ${this.toPacket()}`);
814
+ this.isProcessed = true;
815
+ break;
816
+ default:
817
+ logger.info(`An unprocessed message was received ${this.toPacket()}`)
818
+ break;
819
+
820
+ }
821
+ if (!this.isProcessed) logger.info(`The message was not processed ${this.action} - ${this.toPacket()}`);
822
+ break;
823
+ default:
824
+ switch (this.action) {
825
+ case 1: // Ack
826
+ break;
827
+ case 2: // Shared IntelliCenter/IntelliTouch
828
+ case 5:
829
+ case 8:
830
+ case 96: // intellibrite lights
831
+ EquipmentStateMessage.process(this);
832
+ break;
833
+ // IntelliTouch
834
+ case 10:
835
+ CustomNameMessage.process(this);
836
+ break;
837
+ case 11:
838
+ CircuitMessage.processTouch(this);
839
+ break;
840
+ case 25:
841
+ ChlorinatorMessage.processTouch(this);
842
+ break;
843
+ case 153:
844
+ ExternalMessage.processTouchChlorinator(this);
845
+ break;
846
+ case 17:
847
+ case 145:
848
+ ScheduleMessage.process(this);
849
+ break;
850
+ case 18:
851
+ IntellichemMessage.process(this);
852
+ break;
853
+ case 24:
854
+ case 27:
855
+ case 152:
856
+ case 155:
857
+ PumpMessage.process(this);
858
+ break;
859
+ case 30:
860
+ switch (sys.controllerType) {
861
+ case ControllerType.Unknown:
862
+ break;
863
+ case ControllerType.SunTouch:
864
+ ScheduleMessage.processSunTouch(this);
865
+ break;
866
+ default:
867
+ OptionsMessage.process(this);
868
+ break;
869
+ }
870
+ break;
871
+ case 22:
872
+ case 32:
873
+ case 33:
874
+ RemoteMessage.process(this);
875
+ break;
876
+ case 29:
877
+ case 35:
878
+ ValveMessage.process(this);
879
+ break;
880
+ case 39:
881
+ case 167:
882
+ CircuitMessage.processTouch(this);
883
+ break;
884
+ case 40:
885
+ case 168:
886
+ OptionsMessage.process(this);
887
+ break;
888
+ case 41:
889
+ CircuitGroupMessage.process(this);
890
+ break;
891
+ case 197:
892
+ EquipmentStateMessage.process(this); // Date/Time request
893
+ break;
894
+ case 252:
895
+ EquipmentMessage.process(this);
896
+ break;
897
+ case 9:
898
+ case 16:
899
+ case 34:
900
+ case 137:
901
+ case 144:
902
+ case 162:
903
+ HeaterMessage.process(this);
904
+ break;
905
+ case 114:
906
+ case 115:
907
+ HeaterStateMessage.process(this);
908
+ break
909
+ case 147:
910
+ IntellichemMessage.process(this);
911
+ break;
912
+ case 136:
913
+ ExternalMessage.processTouchSetHeatMode(this);
914
+ break;
915
+ default:
916
+ if (this.action === 109 && this.payload[1] === 3) break;
917
+ if (this.source === 17 && this.payload[0] === 109) break;
918
+ logger.debug(`Packet not processed: ${this.toPacket()}`);
919
+ break;
920
+ }
921
+ break;
922
+ }
923
+ }
924
+ public process() {
925
+ let port = conn.findPortById(this.portId);
926
+ if (this.portId === sys.anslq25.portId) {
927
+ return MessagesMock.process(this);
928
+ }
929
+ if (port.mock && port.hasAssignedEquipment()){
930
+ return MessagesMock.process(this);
931
+ }
932
+ switch (this.protocol) {
933
+ case Protocol.Broadcast:
934
+ this.processBroadcast();
935
+ break;
936
+ case Protocol.IntelliValve:
937
+ IntelliValveStateMessage.process(this);
938
+ break;
939
+ case Protocol.IntelliChem:
940
+ IntelliChemStateMessage.process(this);
941
+ break;
942
+ case Protocol.Pump:
943
+ if ((this.source >= 96 && this.source <= 111) || (this.dest >= 96 && this.dest <= 111))
944
+ PumpStateMessage.process(this);
945
+ else
946
+ this.processBroadcast();
947
+ break;
948
+ case Protocol.Heater:
949
+ HeaterStateMessage.process(this);
950
+ break;
951
+ case Protocol.Chlorinator:
952
+ ChlorinatorStateMessage.process(this);
953
+ break;
954
+ case Protocol.Hayward:
955
+ PumpStateMessage.processHayward(this);
956
+ break;
957
+ case Protocol.RegalModbus:
958
+ RegalModbusStateMessage.process(this);
959
+ break;
960
+ default:
961
+ logger.debug(`Unprocessed Message ${this.toPacket()}`)
962
+ break;
963
+ }
964
+ }
965
+ }
966
+ class OutboundCommon extends Message {
967
+ public set sub(val: number) { if (this.protocol !== Protocol.Chlorinator && this.protocol !== Protocol.AquaLink) this.header[1] = val; }
968
+ public get sub() { return super.sub; }
969
+ public set dest(val: number) {
970
+ if (this.protocol === Protocol.Chlorinator) this.header[2] = val;
971
+ else if (this.protocol === Protocol.Hayward) this.header[4] = val;
972
+ else if (this.protocol === Protocol.RegalModbus) this.header[0] = val;
973
+ else this.header[2] = val;
974
+ }
975
+ public get dest() { return super.dest; }
976
+ public set source(val: number) {
977
+ switch (this.protocol) {
978
+ case Protocol.Chlorinator:
979
+ break;
980
+ case Protocol.Hayward:
981
+ this.header[3] = val;
982
+ break;
983
+ case Protocol.RegalModbus:
984
+ break;
985
+ default:
986
+ this.header[3] = val;
987
+ break;
988
+ }
989
+ //if (this.protocol === Protocol.Hayward) this.header[2] = val;
990
+ //else if (this.protocol !== Protocol.Chlorinator) this.header[3] = val;
991
+ }
992
+ public get source() { return super.source; }
993
+ public set action(val: number) {
994
+ switch (this.protocol) {
995
+ case Protocol.Chlorinator:
996
+ this.header[3] = val;
997
+ break;
998
+ case Protocol.Hayward:
999
+ this.header[2] = val;
1000
+ break;
1001
+ case Protocol.RegalModbus:
1002
+ this.header[1] = val;
1003
+ break;
1004
+ default:
1005
+ this.header[4] = val;
1006
+ break;
1007
+ }
1008
+ }
1009
+ public get action() { return super.action; }
1010
+ public set datalen(val: number) {
1011
+ if (this.protocol !== Protocol.Chlorinator && this.protocol !== Protocol.Hayward && this.protocol !== Protocol.RegalModbus) {
1012
+ this.header[5] = val;
1013
+ }
1014
+ }
1015
+ public get datalen() { return super.datalen; }
1016
+ public set chkHi(val: number) { if (this.protocol !== Protocol.Chlorinator) this.term[0] = val; }
1017
+ public get chkHi() { return super.chkHi; }
1018
+ public set chkLo(val: number) { if (this.protocol !== Protocol.Chlorinator) this.term[1] = val; else this.term[0] = val; }
1019
+ public get chkLo() { return super.chkLo; }
1020
+ // Methods
1021
+ public calcChecksum() {
1022
+ this.datalen = this.payload.length;
1023
+ let sum: number = this.checksum;
1024
+ switch (this.protocol) {
1025
+ case Protocol.Pump:
1026
+ case Protocol.Broadcast:
1027
+ case Protocol.IntelliValve:
1028
+ case Protocol.Unidentified:
1029
+ case Protocol.IntelliChem:
1030
+ case Protocol.Heater:
1031
+ case Protocol.Hayward:
1032
+ this.chkHi = Math.floor(sum / 256);
1033
+ this.chkLo = (sum - (this.chkHi * 256));
1034
+ break;
1035
+ case Protocol.AquaLink:
1036
+ case Protocol.Chlorinator:
1037
+ this.term[0] = sum % 256;
1038
+ break;
1039
+ case Protocol.RegalModbus:
1040
+ // Calculate checksum using the CRC16 algorithm and set chkHi and chkLo.
1041
+ // This.payload is expected to be an array of numbers (byte values 0–255)
1042
+ // combine header and payload for CRC calculation
1043
+ let data: number[] = this.header.concat(this.payload);
1044
+ const crc: number = computeCRC16(data);
1045
+ // Extract the high and low bytes from the 16-bit CRC:
1046
+ this.chkLo = (crc >> 8) & 0xFF;
1047
+ this.chkHi = crc & 0xFF;
1048
+ break;
1049
+ }
1050
+ }
1051
+ }
1052
+ export class Outbound extends OutboundCommon {
1053
+ constructor(proto: Protocol, source: number, dest: number, action: number, payload: number[], retries?: number, response?: Response | boolean, scope?: string) {
1054
+ super();
1055
+ this.id = Message.nextMessageId;
1056
+ this.protocol = proto;
1057
+ this.direction = Direction.Out;
1058
+ this.retries = retries || 0;
1059
+ this.preamble.length = 0;
1060
+ this.header.length = 0;
1061
+ this.term.length = 0;
1062
+ this.payload.length = 0;
1063
+ if (proto === Protocol.Chlorinator || proto === Protocol.AquaLink) {
1064
+ this.header.push.apply(this.header, [16, 2, 0, 0]);
1065
+ this.term.push.apply(this.term, [0, 16, 3]);
1066
+ }
1067
+ else if (proto === Protocol.Broadcast) {
1068
+ this.preamble.push.apply(this.preamble, [255, 0, 255]);
1069
+ this.header.push.apply(this.header, [165, Message.headerSubByte, 15, Message.pluginAddress, 0, 0]);
1070
+ this.term.push.apply(this.term, [0, 0]);
1071
+ }
1072
+ else if (proto === Protocol.Pump || proto === Protocol.IntelliValve || proto === Protocol.IntelliChem || proto === Protocol.Heater) {
1073
+ this.preamble.push.apply(this.preamble, [255, 0, 255]);
1074
+ this.header.push.apply(this.header, [165, 0, 15, Message.pluginAddress, 0, 0]);
1075
+ this.term.push.apply(this.term, [0, 0]);
1076
+ }
1077
+ else if (proto === Protocol.Hayward) {
1078
+ this.header.push.apply(this.header, [16, 2, 0, 0, 0]);
1079
+ this.term.push.apply(this.term, [0, 0, 16, 3]);
1080
+ }
1081
+ else if (proto === Protocol.RegalModbus) {
1082
+ this.header.push.apply(this.header, [this.dest, this.action, 0x20]);
1083
+ }
1084
+ this.scope = scope;
1085
+ this.source = source;
1086
+ this.dest = dest;
1087
+ this.action = action;
1088
+ this.payload.push.apply(this.payload, payload);
1089
+ this.calcChecksum();
1090
+ if (typeof response === "boolean" && response)
1091
+ this.response = Response.create({ protocol: this.protocol, response: true });
1092
+ else
1093
+ this.response = response as Response;
1094
+ }
1095
+ // Factory
1096
+ public static create(obj?: any) {
1097
+ let o = extend({
1098
+ protocol: Protocol.Broadcast,
1099
+ source: sys.board.commandSourceAddress || Message.pluginAddress,
1100
+ dest: sys.board.commandDestAddress || 16,
1101
+ action: 0,
1102
+ payload: [],
1103
+ retries: 0,
1104
+ response: false,
1105
+ }, obj, true);
1106
+ let out = new Outbound(o.protocol, o.source, o.dest, o.action, o.payload, o.retries, o.response, o.scope);
1107
+ //let out = new Outbound(obj.protocol || Protocol.Broadcast,
1108
+ // obj.source || sys.board.commandSourceAddress || Message.pluginAddress, obj.dest || sys.board.commandDestAddress || 16, obj.action || 0, obj.payload || [], obj.retries || 0, obj.response || false, obj.scope || undefined);
1109
+ out.portId = obj.portId || 0;
1110
+ out.onComplete = obj.onComplete;
1111
+ out.onAbort = obj.onAbort;
1112
+ out.timeout = obj.timeout;
1113
+ for (let i = 0; i < out.header.length; i++) {
1114
+ if (out.header[i] >= 0 && out.header[i] <= 255 && out.header[i] !== null && typeof out.header[i] !== 'undefined') continue;
1115
+ throw new OutboundMessageError(out, `Invalid header detected: ${out.toShortPacket()}`);
1116
+ }
1117
+ for (let i = 0; i < out.payload.length; i++) {
1118
+ if (out.payload[i] >= 0 && out.payload[i] <= 255 && out.payload[i] !== null && typeof out.payload[i] !== 'undefined') continue;
1119
+ throw new OutboundMessageError(out, `Invalid payload detected: ${out.toShortPacket()}`);
1120
+ }
1121
+ return out;
1122
+ }
1123
+ public static createMessage(action: number, payload: number[], retries?: number, response?: Response | boolean): Outbound {
1124
+ return new Outbound(Protocol.Broadcast, sys.board.commandSourceAddress || Message.pluginAddress, sys.board.commandDestAddress || 16, action, payload, retries, response);
1125
+ }
1126
+ public async sendAsync() {
1127
+ return conn.queueSendMessageAsync(this);
1128
+ }
1129
+ // Fields
1130
+ public retries: number = 0;
1131
+ public tries: number = 0;
1132
+ public timeout: number = 1000;
1133
+ public response: Response;
1134
+ public failed: boolean = false;
1135
+ public onComplete: (error: Error, msg: Inbound) => void;
1136
+ public onAbort: () => void;
1137
+ // Properties
1138
+ public get requiresResponse(): boolean {
1139
+ if (typeof this.response === 'undefined' || (typeof this.response === 'boolean' && !this.response)) return false;
1140
+ if (this.response instanceof Response || typeof this.response === 'function') { return true; }
1141
+ return false;
1142
+ }
1143
+ public get remainingTries(): number { return this.retries - this.tries + 1; } // Always allow 1 try.
1144
+ public setPayloadByte(ndx: number, value: number, def?: number) {
1145
+ if (typeof value === 'undefined' || isNaN(value)) value = def;
1146
+ if (ndx < this.payload.length) this.payload[ndx] = value;
1147
+ return this;
1148
+ }
1149
+ public appendPayloadByte(value: number, def?: number) {
1150
+ if (typeof value === 'undefined' || isNaN(value)) value = def;
1151
+ this.payload.push(value);
1152
+ return this;
1153
+ }
1154
+ public appendPayloadBytes(value: number, len: number) {
1155
+ for (let i = 0; i < len; i++) this.payload.push(value);
1156
+ return this;
1157
+ }
1158
+ public setPayloadBytes(value: number, len: number) {
1159
+ for (let i = 0; i < len; i++) {
1160
+ if (i < this.payload.length) this.payload[i] = value;
1161
+ }
1162
+ return this;
1163
+ }
1164
+ public insertPayloadBytes(ndx: number, value: number, len: number) {
1165
+ let buf = [];
1166
+ for (let i = 0; i < len; i++) {
1167
+ buf.push(value);
1168
+ }
1169
+ this.payload.splice(ndx, 0, ...buf);
1170
+ return this;
1171
+ }
1172
+ public setPayloadInt(ndx: number, value: number, def?: number) {
1173
+ if (typeof value === 'undefined' || isNaN(value)) value = def;
1174
+ let b1 = Math.floor(value / 256);
1175
+ let b0 = value - (b1 * 256);
1176
+ if (ndx < this.payload.length) this.payload[ndx] = b0;
1177
+ if (ndx + 1 < this.payload.length) this.payload[ndx + 1] = b1;
1178
+ return this;
1179
+ }
1180
+ public appendPayloadInt(value: number, def?: number) {
1181
+ if (typeof value === 'undefined' || isNaN(value)) value = def;
1182
+ let b1 = Math.floor(value / 256);
1183
+ let b0 = value - (b1 * 256);
1184
+ this.payload.push(b0);
1185
+ this.payload.push(b1);
1186
+ return this;
1187
+ }
1188
+ public insertPayloadInt(ndx: number, value: number, def?: number) {
1189
+ if (typeof value === 'undefined' || isNaN(value)) value = def;
1190
+ let b1 = Math.floor(value / 256);
1191
+ let b0 = (value - b1) * 256;
1192
+ this.payload.splice(ndx, 0, b0, b1);
1193
+ return this;
1194
+ }
1195
+ public setPayloadString(s: string, len?: number, def?: string) {
1196
+ if (typeof s === 'undefined') s = def;
1197
+ for (var i = 0; i < s.length; i++) {
1198
+ if (i < this.payload.length) this.payload[i] = s.charCodeAt(i);
1199
+ }
1200
+ if (typeof (len) !== 'undefined') {
1201
+ for (var j = i; j < len; j++)
1202
+ if (i < this.payload.length) this.payload[i] = 0;
1203
+ }
1204
+ return this;
1205
+ }
1206
+ public appendPayloadString(s: string, len?: number, def?: string) {
1207
+ if (typeof s === 'undefined') s = def;
1208
+ for (var i = 0; i < s.length; i++) {
1209
+ if (typeof (len) !== 'undefined' && i >= len) break;
1210
+ this.payload.push(s.charCodeAt(i));
1211
+ }
1212
+ if (typeof (len) !== 'undefined') {
1213
+ for (var j = i; j < len; j++) this.payload.push(0);
1214
+ }
1215
+ return this;
1216
+ }
1217
+ public insertPayloadString(start: number, s: string, len?: number, def?: string) {
1218
+ if (typeof s === 'undefined') s = def;
1219
+ let l = typeof len === 'undefined' ? s.length : len;
1220
+ let buf = [];
1221
+ for (let i = 0; i < l; i++) {
1222
+ if (i < s.length) buf.push(s.charCodeAt(i));
1223
+ else buf.push(0);
1224
+ }
1225
+ this.payload.splice(start, l, ...buf);
1226
+ return this;
1227
+ }
1228
+ public toPacket(): number[] {
1229
+ var pkt = [];
1230
+ this.calcChecksum();
1231
+ pkt.push.apply(pkt, this.padding);
1232
+ pkt.push.apply(pkt, this.preamble);
1233
+ pkt.push.apply(pkt, this.header);
1234
+ pkt.push.apply(pkt, this.payload);
1235
+ pkt.push.apply(pkt, this.term);
1236
+ return pkt;
1237
+ }
1238
+ public processMock(){
1239
+ // When the port is a mock port, we are no longer sending an
1240
+ // outbound message but converting it to an inbound and
1241
+ // skipping the actual send/receive part of the comms.
1242
+ let inbound = Message.convertOutboundToInbound(this);
1243
+ let port = conn.findPortById(this.portId);
1244
+ if (port.hasAssignedEquipment() || this.portId === sys.anslq25.portId){
1245
+ MessagesMock.process(inbound);
1246
+ }
1247
+ else {
1248
+ inbound.process();
1249
+ }
1250
+
1251
+ }
1252
+ }
1253
+ export class Ack extends Outbound {
1254
+ constructor(byte: number) {
1255
+ super(Protocol.Broadcast, Message.pluginAddress, 15, 1, [byte]);
1256
+ }
1257
+ }
1258
+ export class Response extends OutboundCommon {
1259
+ /*
1260
+ RG 6-2021: This class is now purely for identifying inbound messages and it is a property of the Outbound message.
1261
+ This can be created by passing response: Response.create({}) or response: boolean to the Outbound message.
1262
+ Response used to accept a function but that is deprecated.
1263
+ Response also no longer needs to be passed msgOut because that is the parent object/message and can be
1264
+ accessed via the internal symbol parent.
1265
+ */
1266
+ public message: Inbound;
1267
+ // rsg moved accessors here because we won't have a full header; just set/check the individual byte.
1268
+ public set action(val: number) { (this.protocol !== Protocol.Chlorinator) ? this.header[4] = val : this.header[3] = val; }
1269
+ public get action(): number {
1270
+ if (this.protocol === Protocol.Chlorinator) return this.header[3];
1271
+ else if (typeof this.header[4] !== 'undefined') return this.header[4]
1272
+ else return -1;
1273
+ }
1274
+ constructor(proto: Protocol, source: number, dest: number, action?: number, payload?: number[], ack?: number, callback?: (err, msg?: Outbound) => void) {
1275
+ super();
1276
+ this.protocol = proto;
1277
+ this.direction = Direction.In;
1278
+ this.source = source;
1279
+ this.dest = dest;
1280
+ this.action = action;
1281
+ if (typeof payload !== 'undefined' && payload.length > 0) this.payload.push(...payload);
1282
+ if (typeof ack !== 'undefined' && ack !== null) this.ack = new Ack(ack);
1283
+ this.callback = callback;
1284
+ }
1285
+ public static create(obj?: any) {
1286
+ let res = new Response(obj.protocol || Protocol.Broadcast,
1287
+ obj.source || Message.pluginAddress, obj.dest || 16, obj.action || 0, obj.payload || [], obj.ack, obj.callback);
1288
+ res.responseBool = obj.response;
1289
+ if (typeof obj.action !== 'undefined') res.responseBool = true;
1290
+ return res;
1291
+ }
1292
+ // Fields
1293
+ public ack: Ack;
1294
+ public callback: (err, msg?: Outbound) => void;
1295
+ public responseBool: boolean; // if `response: true|false` is passed to the Outbound message we will store that input here
1296
+
1297
+ // Methods
1298
+ public isResponse(msgIn: Inbound, msgOut?: Outbound): boolean {
1299
+ let bresp = false;;
1300
+ try {
1301
+ if (typeof this.responseBool === 'boolean' && this.responseBool) bresp = this.evalResponse(msgIn, msgOut);
1302
+ else return bresp;
1303
+ if (bresp === true && typeof msgOut !== 'undefined') {
1304
+ msgIn.responseFor.push(msgOut.id);
1305
+ logger.silly(`Message in ${msgIn.id} is a response for message out ${msgOut.id}`);
1306
+ }
1307
+ return bresp;
1308
+ }
1309
+ catch (err) { }
1310
+ }
1311
+
1312
+ public evalResponse(msgIn: Inbound, msgOut?: Outbound): boolean {
1313
+ // this holds the logic to determine if an inbound message is a response.
1314
+ // Aka is this Response object
1315
+ // a response to the parent message of Outbound class.
1316
+ if (typeof msgOut === 'undefined') return false;
1317
+ if (msgIn.protocol !== msgOut.protocol) { return false; }
1318
+ if (typeof msgIn === 'undefined') { return false; } // getting here on msg send failure
1319
+
1320
+ // if these properties were set on the Response (this) object via creation,
1321
+ // then use the passed in values. Otherwise, use the msgIn/msgOut matching rules
1322
+ // IntelliCenter config queue uses (action,payload-prefix) matching for Action 30 responses.
1323
+ // Keep this stricter prefix matching scoped to IntelliCenter to avoid unintended effects on other controllers.
1324
+ if (this.action > 0 && sys.controllerType === ControllerType.IntelliCenter) {
1325
+ if (this.action !== msgIn.action) return false;
1326
+ // If no payload prefix is provided, action match is sufficient (e.g. v3 Action 30 with empty payload).
1327
+ if (this.payload.length === 0) return true;
1328
+ if (msgIn.payload.length < this.payload.length) return false;
1329
+ for (let i = 0; i < this.payload.length; i++) {
1330
+ if (this.payload[i] !== msgIn.payload[i]) return false;
1331
+ }
1332
+ return true;
1333
+ }
1334
+ else if (msgOut.protocol === Protocol.Pump) {
1335
+ switch (msgIn.action) {
1336
+ case 7:
1337
+ // Scenario 1. Request for pump status.
1338
+ // Msg In: [165,0,16, 96, 7,15], [4,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0,17,31], [1,95]
1339
+ // Msg Out: [165,0,96, 16, 7, 0],[1,28]
1340
+ if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; }
1341
+ if (msgIn.action === 7 && msgOut.action === 7) { return true; }
1342
+ return false;
1343
+ default:
1344
+ //Scenario 2, pump messages are mimics of each other but the dest/src are swapped
1345
+ if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; }
1346
+ // sub-case
1347
+ // Msg In: [165,0,16, 96, 1, 2], [3,32],[1,59]
1348
+ // Msg Out: [165,0,96,16, 1,4],[3,39, 3,32], [1,103]
1349
+ if (msgIn.payload[0] === msgOut.payload[2] && msgIn.payload[1] === msgOut.payload[3]) { return true; }
1350
+ // else mimics
1351
+ if (JSON.stringify(msgIn.payload) === JSON.stringify(msgOut.payload)) { return true; }
1352
+ return false;
1353
+ }
1354
+ }
1355
+ else if (msgIn.protocol === Protocol.RegalModbus) {
1356
+ // RegalModbus is a little different. The action is the function code and the payload is the data.
1357
+ // We are looking for a match on the action an ack of 0x10.
1358
+ if (msgIn.action === msgOut.action && msgIn.header[2] === 0x10) return true;
1359
+ return false;
1360
+ }
1361
+ else if (msgIn.protocol === Protocol.Chlorinator) {
1362
+ switch (msgIn.action) {
1363
+ case 1:
1364
+ return msgOut.action === 0 ? true : false;
1365
+ case 3:
1366
+ return msgOut.action === 20 ? true : false;
1367
+ case 18:
1368
+ case 21:
1369
+ case 22:
1370
+ return msgOut.action === 17 ? true : false;
1371
+ default:
1372
+ return false;
1373
+ }
1374
+ }
1375
+ else if (msgIn.protocol === Protocol.IntelliChem) {
1376
+ switch (msgIn.action) {
1377
+ case 1: // ack
1378
+ if (msgIn.source === msgOut.dest && msgIn.payload[0] === msgOut.action) return true;
1379
+ break;
1380
+ default:
1381
+ // in: 18; out 210 fits parent & 0x63 pattern
1382
+ if (msgIn.action === (msgOut.action & 63) && msgIn.source === msgOut.dest) return true;
1383
+ return false;
1384
+ }
1385
+ }
1386
+ else if (sys.controllerType !== ControllerType.IntelliCenter) {
1387
+ switch (msgIn.action) {
1388
+ // these responses have multiple items so match the 1st payload byte
1389
+ case 1: // ack
1390
+ if (msgIn.payload[0] === msgOut.action) return true;
1391
+ break;
1392
+ case 10:
1393
+ case 11:
1394
+ case 17:
1395
+ if (msgIn.action === (msgOut.action & 63) && msgIn.payload[0] === msgOut.payload[0]) return true;
1396
+ break;
1397
+ case 252:
1398
+ if (msgOut.action === 253) return true;
1399
+ break;
1400
+ default:
1401
+ if (msgIn.action === (msgOut.action & 63)) return true;
1402
+ }
1403
+ return false;
1404
+ }
1405
+ else if (sys.controllerType === ControllerType.IntelliCenter) {
1406
+ // intellicenter packets
1407
+ if (this.dest >= 0 && msgIn.dest !== this.dest) return false;
1408
+ for (let i = 0; i < this.payload.length; i++) {
1409
+ if (i > msgIn.payload.length - 1)
1410
+ return false;
1411
+ //console.log({ msg: 'Checking response', p1: msgIn.payload[i], pd: this.payload[i] });
1412
+ if (msgIn.payload[i] !== this.payload[i]) return false;
1413
+ }
1414
+ return true;
1415
+ }
1416
+ }
1417
+ }
1418
+
1419
+ /**
1420
+ * Computes the CRC16 checksum over an array of bytes using the RegalModbus algorithm.
1421
+ * @param data - The array of byte values (numbers between 0 and 255).
1422
+ * @returns The computed 16-bit checksum.
1423
+ */
1424
+ export function computeCRC16(data: number[]): number {
1425
+ let crc = 0xFFFF;
1426
+ for (const byte of data) {
1427
+ crc ^= byte;
1428
+ for (let j = 0; j < 8; j++) {
1429
+ crc = (crc & 0x0001) ? (crc >> 1) ^ 0xA001 : crc >> 1;
1430
+ }
1431
+ }
1432
+ return crc;
1433
+ }