nodejs-poolcontroller 8.1.1 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +63 -0
- package/.github/workflows/ghcr-publish.yml +67 -0
- package/Changelog +27 -0
- package/Dockerfile +52 -9
- package/README.md +127 -9
- package/config/Config.ts +57 -7
- package/config/VersionCheck.ts +63 -35
- package/controller/Equipment.ts +1 -1
- package/controller/State.ts +14 -3
- package/controller/boards/EasyTouchBoard.ts +10 -10
- package/controller/boards/IntelliCenterBoard.ts +20 -3
- package/controller/boards/NixieBoard.ts +31 -16
- package/controller/boards/SunTouchBoard.ts +2 -0
- package/controller/boards/SystemBoard.ts +1 -1
- package/controller/comms/Comms.ts +55 -14
- package/controller/comms/messages/Messages.ts +169 -6
- package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
- package/controller/nixie/pumps/Pump.ts +198 -0
- package/defaultConfig.json +5 -0
- package/docker-compose.yml +32 -0
- package/package.json +23 -25
- package/types/express-multer.d.ts +32 -0
- package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
|
@@ -42,6 +42,7 @@ import { IntellichemMessage } from "./config/IntellichemMessage";
|
|
|
42
42
|
import { TouchScheduleCommands } from "controller/boards/EasyTouchBoard";
|
|
43
43
|
import { IntelliValveStateMessage } from "./status/IntelliValveStateMessage";
|
|
44
44
|
import { IntelliChemStateMessage } from "./status/IntelliChemStateMessage";
|
|
45
|
+
import { RegalModbusStateMessage } from "./status/RegalModbusStateMessage";
|
|
45
46
|
import { OutboundMessageError } from "../../Errors";
|
|
46
47
|
import { conn } from "../Comms"
|
|
47
48
|
import extend = require("extend");
|
|
@@ -61,7 +62,8 @@ export enum Protocol {
|
|
|
61
62
|
Heater = 'heater',
|
|
62
63
|
AquaLink = 'aqualink',
|
|
63
64
|
Hayward = 'hayward',
|
|
64
|
-
Unidentified = 'unidentified'
|
|
65
|
+
Unidentified = 'unidentified',
|
|
66
|
+
RegalModbus = 'regalmodbus'
|
|
65
67
|
}
|
|
66
68
|
export class Message {
|
|
67
69
|
constructor() { }
|
|
@@ -105,6 +107,9 @@ export class Message {
|
|
|
105
107
|
//0x10, 0x02, 0x00, 0x0C, 0x00, 0x00, 0x2D, 0x02, 0x36, 0x00, 0x83, 0x10, 0x03 -- Response from pump
|
|
106
108
|
return this.header.length > 4 ? this.header[2] : -1;
|
|
107
109
|
}
|
|
110
|
+
else if (this.protocol === Protocol.RegalModbus) {
|
|
111
|
+
return this.header.length > 0 ? this.header[0] : -1;
|
|
112
|
+
}
|
|
108
113
|
else return this.header.length > 2 ? this.header[2] : -1;
|
|
109
114
|
}
|
|
110
115
|
else return -1;
|
|
@@ -128,6 +133,10 @@ export class Message {
|
|
|
128
133
|
//0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
|
|
129
134
|
return this.header.length > 4 ? this.header[4] : -1;
|
|
130
135
|
}
|
|
136
|
+
else if (this.protocol === Protocol.RegalModbus) {
|
|
137
|
+
// No source address in RegalModbus.
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
131
140
|
if (this.header.length > 3) return this.header[3];
|
|
132
141
|
else return -1;
|
|
133
142
|
}
|
|
@@ -141,10 +150,57 @@ export class Message {
|
|
|
141
150
|
//0x10, 0x02, 0x0C, 0x01, 0x02, 0x2D, 0x00, 0x4E, 0x10, 0x03 -- Command to AUX2 Pump
|
|
142
151
|
return this.header.length > 3 ? this.header[3] || this.header[2] : -1;
|
|
143
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;
|
|
144
158
|
if (this.header.length > 4) return this.header[4];
|
|
145
159
|
else return -1;
|
|
146
160
|
}
|
|
147
|
-
public get datalen(): number {
|
|
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
|
+
}
|
|
148
204
|
public get chkHi(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? 0 : this.term.length > 0 ? this.term[0] : -1; }
|
|
149
205
|
public get chkLo(): number { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink ? this.term[0] : this.term[1]; }
|
|
150
206
|
public get checksum(): number {
|
|
@@ -249,8 +305,19 @@ export class Inbound extends Message {
|
|
|
249
305
|
public rewinds: number = 0;
|
|
250
306
|
// Private methods
|
|
251
307
|
private isValidChecksum(): boolean {
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|
|
254
321
|
}
|
|
255
322
|
public toLog() {
|
|
256
323
|
if (this.responseFor.length > 0)
|
|
@@ -284,6 +351,26 @@ export class Inbound extends Message {
|
|
|
284
351
|
}
|
|
285
352
|
return false;
|
|
286
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
|
+
}
|
|
287
374
|
private testAquaLinkHeader(bytes: number[], ndx: number): boolean {
|
|
288
375
|
if (bytes.length > ndx + 4 && sys.controllerType === 'aqualink') {
|
|
289
376
|
if (bytes[ndx] === 16 && bytes[ndx + 1] === 2) {
|
|
@@ -412,6 +499,11 @@ export class Inbound extends Message {
|
|
|
412
499
|
this.protocol = Protocol.Hayward;
|
|
413
500
|
break;
|
|
414
501
|
}
|
|
502
|
+
if (this.testRegalModbusHeader(bytes, ndx)) {
|
|
503
|
+
this.protocol = Protocol.RegalModbus;
|
|
504
|
+
logger.debug(`RegalModbus header detected. ${JSON.stringify(bytes)}`);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
415
507
|
this.padding.push(bytes[ndx++]);
|
|
416
508
|
}
|
|
417
509
|
}
|
|
@@ -496,6 +588,17 @@ export class Inbound extends Message {
|
|
|
496
588
|
return ndxHeader;
|
|
497
589
|
}
|
|
498
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;
|
|
499
602
|
default:
|
|
500
603
|
// We didn't get a message signature. don't do anything with it.
|
|
501
604
|
ndx = ndxStart;
|
|
@@ -567,6 +670,17 @@ export class Inbound extends Message {
|
|
|
567
670
|
}
|
|
568
671
|
}
|
|
569
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;
|
|
570
684
|
|
|
571
685
|
}
|
|
572
686
|
return ndx;
|
|
@@ -580,6 +694,7 @@ export class Inbound extends Message {
|
|
|
580
694
|
case Protocol.IntelliValve:
|
|
581
695
|
case Protocol.IntelliChem:
|
|
582
696
|
case Protocol.Heater:
|
|
697
|
+
case Protocol.RegalModbus:
|
|
583
698
|
case Protocol.Unidentified:
|
|
584
699
|
// If we don't have enough bytes to make the terminator then continue on and
|
|
585
700
|
// hope we get them on the next go around.
|
|
@@ -810,6 +925,9 @@ export class Inbound extends Message {
|
|
|
810
925
|
case Protocol.Hayward:
|
|
811
926
|
PumpStateMessage.processHayward(this);
|
|
812
927
|
break;
|
|
928
|
+
case Protocol.RegalModbus:
|
|
929
|
+
RegalModbusStateMessage.process(this);
|
|
930
|
+
break;
|
|
813
931
|
default:
|
|
814
932
|
logger.debug(`Unprocessed Message ${this.toPacket()}`)
|
|
815
933
|
break;
|
|
@@ -822,6 +940,7 @@ class OutboundCommon extends Message {
|
|
|
822
940
|
public set dest(val: number) {
|
|
823
941
|
if (this.protocol === Protocol.Chlorinator) this.header[2] = val;
|
|
824
942
|
else if (this.protocol === Protocol.Hayward) this.header[4] = val;
|
|
943
|
+
else if (this.protocol === Protocol.RegalModbus) this.header[0] = val;
|
|
825
944
|
else this.header[2] = val;
|
|
826
945
|
}
|
|
827
946
|
public get dest() { return super.dest; }
|
|
@@ -832,6 +951,8 @@ class OutboundCommon extends Message {
|
|
|
832
951
|
case Protocol.Hayward:
|
|
833
952
|
this.header[3] = val;
|
|
834
953
|
break;
|
|
954
|
+
case Protocol.RegalModbus:
|
|
955
|
+
break;
|
|
835
956
|
default:
|
|
836
957
|
this.header[3] = val;
|
|
837
958
|
break;
|
|
@@ -848,13 +969,20 @@ class OutboundCommon extends Message {
|
|
|
848
969
|
case Protocol.Hayward:
|
|
849
970
|
this.header[2] = val;
|
|
850
971
|
break;
|
|
972
|
+
case Protocol.RegalModbus:
|
|
973
|
+
this.header[1] = val;
|
|
974
|
+
break;
|
|
851
975
|
default:
|
|
852
976
|
this.header[4] = val;
|
|
853
977
|
break;
|
|
854
978
|
}
|
|
855
979
|
}
|
|
856
980
|
public get action() { return super.action; }
|
|
857
|
-
public set datalen(val: number) {
|
|
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
|
+
}
|
|
858
986
|
public get datalen() { return super.datalen; }
|
|
859
987
|
public set chkHi(val: number) { if (this.protocol !== Protocol.Chlorinator) this.term[0] = val; }
|
|
860
988
|
public get chkHi() { return super.chkHi; }
|
|
@@ -879,6 +1007,16 @@ class OutboundCommon extends Message {
|
|
|
879
1007
|
case Protocol.Chlorinator:
|
|
880
1008
|
this.term[0] = sum % 256;
|
|
881
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;
|
|
882
1020
|
}
|
|
883
1021
|
}
|
|
884
1022
|
}
|
|
@@ -911,6 +1049,9 @@ export class Outbound extends OutboundCommon {
|
|
|
911
1049
|
this.header.push.apply(this.header, [16, 2, 0, 0, 0]);
|
|
912
1050
|
this.term.push.apply(this.term, [0, 0, 16, 3]);
|
|
913
1051
|
}
|
|
1052
|
+
else if (proto === Protocol.RegalModbus) {
|
|
1053
|
+
this.header.push.apply(this.header, [this.dest, this.action, 0x20]);
|
|
1054
|
+
}
|
|
914
1055
|
this.scope = scope;
|
|
915
1056
|
this.source = source;
|
|
916
1057
|
this.dest = dest;
|
|
@@ -1184,6 +1325,12 @@ export class Response extends OutboundCommon {
|
|
|
1184
1325
|
return false;
|
|
1185
1326
|
}
|
|
1186
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
|
+
}
|
|
1187
1334
|
else if (msgIn.protocol === Protocol.Chlorinator) {
|
|
1188
1335
|
switch (msgIn.action) {
|
|
1189
1336
|
case 1:
|
|
@@ -1240,4 +1387,20 @@ export class Response extends OutboundCommon {
|
|
|
1240
1387
|
return true;
|
|
1241
1388
|
}
|
|
1242
1389
|
}
|
|
1243
|
-
}
|
|
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
|
+
}
|