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.
@@ -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 || this.data.id + 95; }
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); }
@@ -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
- self.heliotrope.longitude = sys.general.location.longitude;
397
- self.heliotrope.latitude = sys.general.location.latitude;
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 || this.data.id + 95; }
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 EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'CircuitGroup'));
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 EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'LightGroup'));
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) { logger.error(`Nixie: Error removing valve from system ${obj.id}: ${err.message}`); return Promise.reject(new Error(`Nixie: Error removing valve from system ${ obj.id }: ${ err.message }`)); }
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`, 'setPortAsync'));
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); if (sys.isReady) setImmediate(() => { this.processPackets(); });
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
- // Come back later as we still have items to send.
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(), 100);
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
- this.counter.bytesSent += bytes.length;
1026
- msg.timestamp = new Date();
1027
- logger.packet(msg);
1028
- this.write(msg, (err) => {
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 { return this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink || this.protocol === Protocol.Hayward ? this.payload.length : this.header.length > 5 ? this.header[5] : -1; }
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
- if (this.protocol === Protocol.Chlorinator || this.protocol === Protocol.AquaLink) return this.checksum % 256 === this.chkLo;
253
- return (this.chkHi * 256) + this.chkLo === this.checksum;
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) { if (this.protocol !== Protocol.Chlorinator && this.protocol !== Protocol.Hayward) this.header[5] = val; }
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
+ }