nodejs-poolcontroller 8.1.2 → 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 +123 -5
- 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/NixieBoard.ts +31 -16
- 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
package/controller/Equipment.ts
CHANGED
|
@@ -1478,7 +1478,7 @@ export class Pump extends EqItem {
|
|
|
1478
1478
|
public set id(val: number) { this.setDataVal('id', val); }
|
|
1479
1479
|
public get portId(): number { return this.data.portId; }
|
|
1480
1480
|
public set portId(val: number) { this.setDataVal('portId', val); }
|
|
1481
|
-
public get address(): number { return this.data.address
|
|
1481
|
+
public get address(): number { return this.data.address; }
|
|
1482
1482
|
public set address(val: number) { this.setDataVal('address', val); }
|
|
1483
1483
|
public get name(): string { return this.data.name; }
|
|
1484
1484
|
public set name(val: string) { this.setDataVal('name', val); }
|
package/controller/State.ts
CHANGED
|
@@ -393,8 +393,18 @@ export class State implements IState {
|
|
|
393
393
|
self.data.time = self._dt.format();
|
|
394
394
|
self.hasChanged = true;
|
|
395
395
|
self.heliotrope.date = self._dt.toDate();
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
// Provide safe access & environment fallback for coordinates
|
|
397
|
+
const loc = sys?.general?.location || {} as any;
|
|
398
|
+
let lon = loc.longitude;
|
|
399
|
+
let lat = loc.latitude;
|
|
400
|
+
if (typeof lon !== 'number' || typeof lat !== 'number') {
|
|
401
|
+
const envLat = process.env.POOL_LATITUDE ? parseFloat(process.env.POOL_LATITUDE) : undefined;
|
|
402
|
+
const envLon = process.env.POOL_LONGITUDE ? parseFloat(process.env.POOL_LONGITUDE) : undefined;
|
|
403
|
+
if (typeof lon !== 'number' && typeof envLon === 'number' && !isNaN(envLon)) lon = envLon;
|
|
404
|
+
if (typeof lat !== 'number' && typeof envLat === 'number' && !isNaN(envLat)) lat = envLat;
|
|
405
|
+
}
|
|
406
|
+
self.heliotrope.longitude = lon;
|
|
407
|
+
self.heliotrope.latitude = lat;
|
|
398
408
|
let times = self.heliotrope.calculatedTimes;
|
|
399
409
|
self.data.sunrise = times.isValid ? Timestamp.toISOLocal(times.sunrise) : '';
|
|
400
410
|
self.data.sunset = times.isValid ? Timestamp.toISOLocal(times.sunset) : '';
|
|
@@ -947,7 +957,7 @@ export class PumpState extends EqState {
|
|
|
947
957
|
}
|
|
948
958
|
public get id(): number { return this.data.id; }
|
|
949
959
|
public set id(val: number) { this.data.id = val; }
|
|
950
|
-
public get address(): number { return this.data.address
|
|
960
|
+
public get address(): number { return this.data.address; }
|
|
951
961
|
public set address(val: number) { this.setDataVal('address', val); }
|
|
952
962
|
public get name(): string { return this.data.name; }
|
|
953
963
|
public set name(val: string) { this.setDataVal('name', val); }
|
|
@@ -1042,6 +1052,7 @@ export class PumpState extends EqState {
|
|
|
1042
1052
|
case 'hwvs':
|
|
1043
1053
|
case 'vssvrs':
|
|
1044
1054
|
case 'vs':
|
|
1055
|
+
case 'regalmodbus':
|
|
1045
1056
|
c.units = sys.board.valueMaps.pumpUnits.transformByName('rpm');
|
|
1046
1057
|
break;
|
|
1047
1058
|
case 'ss':
|
|
@@ -29,6 +29,12 @@ import { webApp } from "../../web/Server";
|
|
|
29
29
|
import { setTimeout } from 'timers/promises';
|
|
30
30
|
import { setTimeout as setTimeoutSync } from 'timers';
|
|
31
31
|
|
|
32
|
+
const addrsPentairPump = Object.freeze([96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]);
|
|
33
|
+
const addrsRegalModbusPump = Object.freeze(
|
|
34
|
+
Array.from({ length: ((0xF7 - 0x15) / 2) + 1 }, (_, i) => 0x15 + i * 2) // Odd numbers fro 0x15 through 0xF7
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
export class NixieBoard extends SystemBoard {
|
|
33
39
|
constructor (system: PoolSystem){
|
|
34
40
|
super(system);
|
|
@@ -76,14 +82,15 @@ export class NixieBoard extends SystemBoard {
|
|
|
76
82
|
[17, { name: 'watercolors', desc: 'WaterColors', isLight: true, theme: 'watercolors' }],
|
|
77
83
|
]);
|
|
78
84
|
this.valueMaps.pumpTypes = new byteValueMap([
|
|
79
|
-
[1, { name: 'ss', desc: 'Single Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }]}],
|
|
80
|
-
[2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2, relays: [{ id: 1, name: 'Low Speed' }, { id: 2, name: 'High Speed' }]}],
|
|
81
|
-
[3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
|
|
82
|
-
[4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
|
|
83
|
-
[5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
|
|
84
|
-
[6, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
|
|
85
|
-
[7, { name: 'hwrly', desc: 'Hayward Relay VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, maxSpeeds: 8, relays: [{ id: 1, name: 'Step #1' }, { id: 2, name: 'Step #2'}, { id: 3, name: 'Step #3' }, { id: 4, name: 'Pump On' }] }],
|
|
86
|
-
[100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 4, relays: [{ id: 1, name: 'Program #1' }, { id: 2, name: 'Program #2' }, { id: 3, name: 'Program #3' }, { id: 4, name: 'Program #4' }]}]
|
|
85
|
+
[1, { name: 'ss', desc: 'Single Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }], addresses: []}],
|
|
86
|
+
[2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2, relays: [{ id: 1, name: 'Low Speed' }, { id: 2, name: 'High Speed' }], addresses: []}],
|
|
87
|
+
[3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }],
|
|
88
|
+
[4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }],
|
|
89
|
+
[5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }],
|
|
90
|
+
[6, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }],
|
|
91
|
+
[7, { name: 'hwrly', desc: 'Hayward Relay VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, maxSpeeds: 8, relays: [{ id: 1, name: 'Step #1' }, { id: 2, name: 'Step #2'}, { id: 3, name: 'Step #3' }, { id: 4, name: 'Pump On' }], addresses: [] }],
|
|
92
|
+
[100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 4, relays: [{ id: 1, name: 'Program #1' }, { id: 2, name: 'Program #2' }, { id: 3, name: 'Program #3' }, { id: 4, name: 'Program #4' }], addresses: [] }],
|
|
93
|
+
[200, { name: 'regalmodbus', desc: 'Regal Modbus', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsRegalModbusPump}],
|
|
87
94
|
]);
|
|
88
95
|
// RSG - same as systemBoard definition; can delete.
|
|
89
96
|
this.valueMaps.heatModes = new byteValueMap([
|
|
@@ -101,12 +108,20 @@ export class NixieBoard extends SystemBoard {
|
|
|
101
108
|
[6, { name: 'sat', desc: 'Saturday', dow: 6, bitval: 32 }],
|
|
102
109
|
[7, { name: 'sun', desc: 'Sunday', dow: 0, bitval: 64 }]
|
|
103
110
|
]);
|
|
111
|
+
/**
|
|
112
|
+
* groupCircuitStates value map:
|
|
113
|
+
* 1: 'on' - Circuit should be ON when group is ON, OFF when group is OFF.
|
|
114
|
+
* 2: 'off' - Circuit should be OFF when group is ON, ON when group is OFF.
|
|
115
|
+
* 3: 'ignore' - Circuit is ignored by group state changes.
|
|
116
|
+
* 4: 'on+ignore' - Circuit should be ON when group is ON, ignored when group is OFF.
|
|
117
|
+
* 5: 'off+ignore' - Circuit should be OFF when group is ON, ignored when group is OFF.
|
|
118
|
+
*/
|
|
104
119
|
this.valueMaps.groupCircuitStates = new byteValueMap([
|
|
105
|
-
[1, { name: 'on', desc: 'On/Off' }],
|
|
106
|
-
[2, { name: 'off', desc: 'Off/On' }],
|
|
107
|
-
[3, { name: 'ignore', desc: 'Ignore' }],
|
|
108
|
-
[4, { name: 'on+ignore', desc: 'On/Ignore' }],
|
|
109
|
-
[5, { name: 'off+ignore', desc: 'Off/Ignore' }]
|
|
120
|
+
[1, { name: 'on', desc: 'On/Off' }], // 1: ON when group ON, OFF when group OFF
|
|
121
|
+
[2, { name: 'off', desc: 'Off/On' }], // 2: OFF when group ON, ON when group OFF
|
|
122
|
+
[3, { name: 'ignore', desc: 'Ignore' }], // 3: Ignored by group state
|
|
123
|
+
[4, { name: 'on+ignore', desc: 'On/Ignore' }], // 4: ON when group ON, ignored when group OFF
|
|
124
|
+
[5, { name: 'off+ignore', desc: 'Off/Ignore' }] // 5: OFF when group ON, ignored when group OFF
|
|
110
125
|
]);
|
|
111
126
|
this.valueMaps.chlorinatorModel = new byteValueMap([
|
|
112
127
|
[0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }],
|
|
@@ -1231,7 +1246,7 @@ export class NixieCircuitCommands extends CircuitCommands {
|
|
|
1231
1246
|
}
|
|
1232
1247
|
public async deleteCircuitGroupAsync(obj: any): Promise<CircuitGroup> {
|
|
1233
1248
|
let id = parseInt(obj.id, 10);
|
|
1234
|
-
if (isNaN(id)) return Promise.reject(new
|
|
1249
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid group id: ${obj.id}`, obj.id, 'CircuitGroup'));
|
|
1235
1250
|
if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return;
|
|
1236
1251
|
if (typeof obj.id !== 'undefined') {
|
|
1237
1252
|
let group = sys.circuitGroups.getItemById(id, false);
|
|
@@ -1249,7 +1264,7 @@ export class NixieCircuitCommands extends CircuitCommands {
|
|
|
1249
1264
|
}
|
|
1250
1265
|
public async deleteLightGroupAsync(obj: any): Promise<LightGroup> {
|
|
1251
1266
|
let id = parseInt(obj.id, 10);
|
|
1252
|
-
if (isNaN(id)) return Promise.reject(new
|
|
1267
|
+
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid group id: ${obj.id}`, obj.id, 'LightGroup'));
|
|
1253
1268
|
if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return;
|
|
1254
1269
|
if (typeof obj.id !== 'undefined') {
|
|
1255
1270
|
let group = sys.lightGroups.getItemById(id, false);
|
|
@@ -1762,7 +1777,7 @@ export class NixieValveCommands extends ValveCommands {
|
|
|
1762
1777
|
state.valves.removeItemById(id);
|
|
1763
1778
|
ncp.valves.removeById(id);
|
|
1764
1779
|
return valve;
|
|
1765
|
-
} catch (err) {
|
|
1780
|
+
} catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteValveAsync')); }
|
|
1766
1781
|
}
|
|
1767
1782
|
public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) {
|
|
1768
1783
|
try {
|
|
@@ -139,7 +139,7 @@ export class Connection {
|
|
|
139
139
|
else {
|
|
140
140
|
if (!await existing.closeAsync()) {
|
|
141
141
|
existing.closing = false; // if closing fails, reset flag so user can try again
|
|
142
|
-
return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port
|
|
142
|
+
return Promise.reject(new InvalidOperationError(`Unable to close the current RS485 port (Try to save the port again as it usually works the second time).`, 'setPortAsync'));
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
config.setSection(section, pdata);
|
|
@@ -526,6 +526,8 @@ export class RS485Port {
|
|
|
526
526
|
private procTimer: NodeJS.Timeout;
|
|
527
527
|
public writeTimer: NodeJS.Timeout
|
|
528
528
|
private _processing: boolean = false;
|
|
529
|
+
private _lastTx: number = 0;
|
|
530
|
+
private _lastRx: number = 0;
|
|
529
531
|
private _inBytes: number[] = [];
|
|
530
532
|
private _inBuffer: number[] = [];
|
|
531
533
|
private _outBuffer: Outbound[] = [];
|
|
@@ -897,7 +899,9 @@ export class RS485Port {
|
|
|
897
899
|
}
|
|
898
900
|
// make public for now; should enable writing directly to mock port at Conn level...
|
|
899
901
|
public pushIn(pkt: Buffer) {
|
|
900
|
-
this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data);
|
|
902
|
+
this._inBuffer.push.apply(this._inBuffer, pkt.toJSON().data);
|
|
903
|
+
this._lastRx = Date.now();
|
|
904
|
+
if (sys.isReady) setImmediate(() => { this.processPackets(); });
|
|
901
905
|
}
|
|
902
906
|
private pushOut(msg) {
|
|
903
907
|
this._outBuffer.push(msg); setImmediate(() => { this.processPackets(); });
|
|
@@ -988,9 +992,11 @@ export class RS485Port {
|
|
|
988
992
|
// but this condition would be eval'd before the callback of port.write was calls and the outbound packet
|
|
989
993
|
// would be sitting idle for eternity.
|
|
990
994
|
if (this._outBuffer.length > 0 || typeof this._waitingPacket !== 'undefined' || this._waitingPacket || typeof msg !== 'undefined') {
|
|
991
|
-
//
|
|
995
|
+
// Configurable inter-frame delay (default 30ms) overrides fixed 100ms.
|
|
996
|
+
const dCfg = (config.getSection('controller').txDelays || {});
|
|
997
|
+
const interFrame = Math.max(0, Number(dCfg.interFrameDelayMs || 30));
|
|
992
998
|
let self = this;
|
|
993
|
-
this.procTimer = setTimeout(() => self.processPackets(),
|
|
999
|
+
this.procTimer = setTimeout(() => self.processPackets(), interFrame);
|
|
994
1000
|
}
|
|
995
1001
|
}
|
|
996
1002
|
private writeMessage(msg: Outbound) {
|
|
@@ -1022,10 +1028,44 @@ export class RS485Port {
|
|
|
1022
1028
|
this.isRTS = true;
|
|
1023
1029
|
return;
|
|
1024
1030
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1031
|
+
const dCfg = (config.getSection('controller').txDelays || {});
|
|
1032
|
+
const idleBeforeTx = Math.max(0, Number(dCfg.idleBeforeTxMs || 0));
|
|
1033
|
+
const interByte = Math.max(0, Number(dCfg.interByteDelayMs || 0));
|
|
1034
|
+
const now = Date.now();
|
|
1035
|
+
const idleElapsed = now - Math.max(this._lastTx, this._lastRx);
|
|
1036
|
+
const doWrite = () => {
|
|
1037
|
+
this.counter.bytesSent += bytes.length;
|
|
1038
|
+
msg.timestamp = new Date();
|
|
1039
|
+
logger.packet(msg);
|
|
1040
|
+
if (interByte > 0 && bytes.length > 1 && this._port && (this._port instanceof SerialPort || this._port instanceof SerialPortMock)) {
|
|
1041
|
+
// Manual inter-byte pacing
|
|
1042
|
+
let idx = 0;
|
|
1043
|
+
const writeNext = () => {
|
|
1044
|
+
if (idx >= bytes.length) {
|
|
1045
|
+
this._lastTx = Date.now();
|
|
1046
|
+
completeWrite(undefined);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const b = Buffer.from([bytes[idx++]]);
|
|
1050
|
+
(this._port as any).write(b, (err) => {
|
|
1051
|
+
if (err) {
|
|
1052
|
+
this._lastTx = Date.now();
|
|
1053
|
+
completeWrite(err);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (interByte > 0) setTimeout(writeNext, interByte);
|
|
1057
|
+
else setImmediate(writeNext);
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
writeNext();
|
|
1061
|
+
} else {
|
|
1062
|
+
this.write(msg, (err) => {
|
|
1063
|
+
this._lastTx = Date.now();
|
|
1064
|
+
completeWrite(err);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const completeWrite = (err?: Error) => {
|
|
1029
1069
|
clearTimeout(this.writeTimer);
|
|
1030
1070
|
this.writeTimer = null;
|
|
1031
1071
|
msg.tries++;
|
|
@@ -1042,7 +1082,6 @@ export class RS485Port {
|
|
|
1042
1082
|
self._waitingPacket = null;
|
|
1043
1083
|
self.counter.sndAborted++;
|
|
1044
1084
|
}
|
|
1045
|
-
return;
|
|
1046
1085
|
}
|
|
1047
1086
|
else {
|
|
1048
1087
|
logger.verbose(`Wrote packet [Port ${this.portId} id: ${msg.id}] [${bytes}].Retries remaining: ${msg.remainingTries} `);
|
|
@@ -1053,15 +1092,17 @@ export class RS485Port {
|
|
|
1053
1092
|
self._waitingPacket = null;
|
|
1054
1093
|
self.counter.sndSuccess++;
|
|
1055
1094
|
if (typeof msg.onComplete === 'function') msg.onComplete(err, undefined);
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
else if (msg.remainingTries >= 0) {
|
|
1059
|
-
self._waitingPacket = msg;
|
|
1060
1095
|
}
|
|
1096
|
+
else if (msg.remainingTries >= 0) self._waitingPacket = msg;
|
|
1061
1097
|
}
|
|
1062
1098
|
self.counter.updatefailureRate();
|
|
1063
1099
|
self.emitPortStats();
|
|
1064
|
-
}
|
|
1100
|
+
};
|
|
1101
|
+
// Honor idle-before-TX if not enough bus quiet time has elapsed
|
|
1102
|
+
if (idleBeforeTx > 0 && idleElapsed < idleBeforeTx) {
|
|
1103
|
+
const wait = idleBeforeTx - idleElapsed;
|
|
1104
|
+
setTimeout(doWrite, wait);
|
|
1105
|
+
} else doWrite();
|
|
1065
1106
|
}
|
|
1066
1107
|
}
|
|
1067
1108
|
catch (err) {
|
|
@@ -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
|
+
}
|