nodejs-poolcontroller 8.3.0 → 8.4.1

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