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
|
@@ -0,0 +1,411 @@
|
|
|
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 { Inbound, Outbound, Protocol } from "../Messages";
|
|
19
|
+
import { state } from "../../../State";
|
|
20
|
+
import { sys, ControllerType } from "../../../Equipment";
|
|
21
|
+
import { conn } from "../../Comms";
|
|
22
|
+
import { logger } from "../../../../logger/Logger";
|
|
23
|
+
|
|
24
|
+
// Create a fault object to hold the fault codes and descriptions
|
|
25
|
+
let faultCodes = {
|
|
26
|
+
0x21: "Software overcurrent",
|
|
27
|
+
0x22: "DC overvoltage",
|
|
28
|
+
0x23: "DC undervoltage",
|
|
29
|
+
0x26: "Hardware overcurrent",
|
|
30
|
+
0x2A: "Startup failure",
|
|
31
|
+
0x2D: "Processor - Fatal",
|
|
32
|
+
0x2E: "IGBT over temperature",
|
|
33
|
+
0x2F: "Loss of phase",
|
|
34
|
+
0x30: "Low power",
|
|
35
|
+
0x31: "Processor - Registers",
|
|
36
|
+
0x32: "Processor - Program counter",
|
|
37
|
+
0x33: "Processor - Interrupt/Execution",
|
|
38
|
+
0x34: "Processor - Clock",
|
|
39
|
+
0x35: "Processor - Flash Memory",
|
|
40
|
+
0x36: "Ras fault",
|
|
41
|
+
0x37: "Processor - ADC",
|
|
42
|
+
0x3C: "Keypad fault",
|
|
43
|
+
0x3D: "LVB data flash fault",
|
|
44
|
+
0x3E: "Comm loss fault - LVB & Drive",
|
|
45
|
+
0x3F: "Generic fault",
|
|
46
|
+
0x40: "Coherence fault",
|
|
47
|
+
0x41: "UL fault",
|
|
48
|
+
0x42: "SVRS fault type 1",
|
|
49
|
+
0x43: "SVRS fault type 2",
|
|
50
|
+
0x44: "SVRS fault type 13",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let nackErrors = {
|
|
54
|
+
0x01: "Command not recognized / illegal",
|
|
55
|
+
0x02: "Operand out of allowed range",
|
|
56
|
+
0x03: "Data out of range",
|
|
57
|
+
0x04: "General failure: fault mode",
|
|
58
|
+
0x05: "Incorrect command length",
|
|
59
|
+
0x06: "Command cannot be executed now",
|
|
60
|
+
0x09: "Buffer error (not used)",
|
|
61
|
+
0x0A: "Running parameters incomplete (not used)",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class RegalModbusStateMessage {
|
|
65
|
+
public static process(msg: Inbound) {
|
|
66
|
+
|
|
67
|
+
// debug log the message object
|
|
68
|
+
logger.debug(`RegalModbusStateMessage.process ${JSON.stringify(msg)}`);
|
|
69
|
+
|
|
70
|
+
let addr = msg.header[0];
|
|
71
|
+
let functionCode = msg.header[1];
|
|
72
|
+
let ack = msg.header[2];
|
|
73
|
+
|
|
74
|
+
if (ack == 0x20) return;
|
|
75
|
+
if (ack in nackErrors) {
|
|
76
|
+
logger.debug(`RegalModbusStateMessage.process NACK: ${nackErrors[ack]} (Address: ${addr})`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (ack != 0x10) {
|
|
80
|
+
logger.debug(`RegalModbusStateMessage.process Unknown ACK: ${ack} (Address: ${addr})`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If we're here, we have an ack=0x10 message
|
|
85
|
+
|
|
86
|
+
let pumpCfg = sys.pumps.getPumpByAddress(addr, false, { isActive: false });
|
|
87
|
+
let pumpId = pumpCfg.id;
|
|
88
|
+
let pumpType = sys.board.valueMaps.pumpTypes.transform(pumpCfg.type);
|
|
89
|
+
let pumpState = state.pumps.getItemById(pumpId, pumpCfg.isActive === true);
|
|
90
|
+
|
|
91
|
+
logger.debug(`RegalModbusStateMessage.process.pstate ${JSON.stringify(pumpState)}`);
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
switch (functionCode) {
|
|
95
|
+
case 0x41: { // Go
|
|
96
|
+
logger.debug(`RegalModbusStateMessage.process Go (Address: ${addr})`);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 0x42: { // Stop
|
|
100
|
+
logger.debug(`RegalModbusStateMessage.process Stop (Address: ${addr})`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 0x43: { // Status
|
|
104
|
+
let status = msg.extractPayloadByte(0);
|
|
105
|
+
switch (status) {
|
|
106
|
+
case 0x00: { // stop mode - motor stopped
|
|
107
|
+
logger.debug(`RegalModbusStateMessage.process Status: Stop (Address: ${addr})`);
|
|
108
|
+
pumpState.driveState = 0;
|
|
109
|
+
pumpState.command = 4; // dashPanel assumes command = 10 in running state
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 0x09: { // run mode - boot (motor is getting ready to spin)
|
|
113
|
+
logger.debug(`RegalModbusStateMessage.process Status: Boot (Address: ${addr})`);
|
|
114
|
+
pumpState.driveState = 1;
|
|
115
|
+
pumpState.command = 10; // dashPanel assumes command = 10 in running state
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 0x0B: { // run mode - vector
|
|
119
|
+
logger.debug(`RegalModbusStateMessage.process Status: Vector (Address: ${addr})`);
|
|
120
|
+
pumpState.driveState = 2;
|
|
121
|
+
pumpState.command = 10; // dashPanel assumes command = 10 in running state
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 0x20: { // fault mode - motor stopped
|
|
125
|
+
logger.debug(`RegalModbusStateMessage.process Status: Fault (Address: ${addr})`);
|
|
126
|
+
pumpState.driveState = 4;
|
|
127
|
+
pumpState.command = 4; // dashPanel assumes command = 10 in running state
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 0x44: { // Set demand
|
|
134
|
+
let mode = msg.extractPayloadByte(0);
|
|
135
|
+
let demandLo = msg.extractPayloadByte(1);
|
|
136
|
+
let demandHi = msg.extractPayloadByte(2);
|
|
137
|
+
|
|
138
|
+
switch (mode) {
|
|
139
|
+
case 0: { // Speed control, demand = RPM * 4
|
|
140
|
+
let rpm = RegalModbusStateMessage.demandToRPM(demandLo, demandHi);
|
|
141
|
+
logger.debug(`RegalModbusStateMessage.process Speed: ${rpm} (Address: ${addr})`);
|
|
142
|
+
pumpState.rpm = rpm;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 1: { // Torque control, demand = lbf-ft * 1200
|
|
146
|
+
logger.debug(`RegalModbusStateMessage.process Ignoring torque: ${demandLo}, ${demandHi} (Address: ${addr})`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 2: { // Reserved (used to be flow)
|
|
150
|
+
logger.debug(`RegalModbusStateMessage.process Ignoring reserved demand mode ${mode}: ${demandLo}, ${demandHi} (Address: ${addr})`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 3: { // Reserved
|
|
154
|
+
logger.debug(`RegalModbusStateMessage.process Ignoring reserved demand mode ${mode}: ${demandLo}, ${demandHi} (Address: ${addr})`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 0x45: { // Read sensor
|
|
161
|
+
let page = msg.extractPayloadByte(0);
|
|
162
|
+
let sensorAddr = msg.extractPayloadByte(1);
|
|
163
|
+
let valueLo = msg.extractPayloadByte(2);
|
|
164
|
+
let valueHi = msg.extractPayloadByte(3);
|
|
165
|
+
let raw_value = (valueHi << 8) + valueLo;
|
|
166
|
+
|
|
167
|
+
let scaleValue = (value: number, scale: number) => {
|
|
168
|
+
return value / scale;
|
|
169
|
+
};
|
|
170
|
+
let scaled_value;
|
|
171
|
+
|
|
172
|
+
switch (page) {
|
|
173
|
+
case 0: {
|
|
174
|
+
switch (sensorAddr) {
|
|
175
|
+
case 0x00: { // Motor speed
|
|
176
|
+
scaled_value = scaleValue(raw_value, 4);
|
|
177
|
+
logger.debug(`RegalModbusStateMessage.process Motor speed: ${scaled_value} (Address: ${addr})`);
|
|
178
|
+
pumpState.rpm = scaled_value;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 0x01: { // Motor current
|
|
182
|
+
scaled_value = scaleValue(raw_value, 1000);
|
|
183
|
+
logger.debug(`RegalModbusStateMessage.process Motor current: ${scaled_value} (Address: ${addr})`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 0x02: { // Operating mode
|
|
187
|
+
switch (raw_value) {
|
|
188
|
+
case 0: { // Speed control
|
|
189
|
+
logger.debug(`RegalModbusStateMessage.process Operating mode: Speed control (Address: ${addr})`);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 1: { // Torque control
|
|
193
|
+
logger.debug(`RegalModbusStateMessage.process Operating mode: Torque control (Address: ${addr})`);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 0x03: { // Demand sent to motor
|
|
200
|
+
logger.debug(`RegalModbusStateMessage.process Raw (unscaled) demand sent to motor: ${raw_value} (Address: ${addr})`);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 0x04: { // Torque
|
|
204
|
+
scaled_value = scaleValue(raw_value, 1200);
|
|
205
|
+
logger.debug(`RegalModbusStateMessage.process Torque: ${scaled_value} (Address: ${addr})`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 0x05: { // Inverter input power
|
|
209
|
+
logger.debug(`RegalModbusStateMessage.process Raw (unscaled) inverter input power: ${raw_value} (Address: ${addr})`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 0x06: { // DC bus voltage
|
|
213
|
+
scaled_value = scaleValue(raw_value, 64);
|
|
214
|
+
logger.debug(`RegalModbusStateMessage.process DC bus voltage: ${scaled_value} (Address: ${addr})`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case 0x07: { // Ambient temperature
|
|
218
|
+
scaled_value = scaleValue(raw_value, 128);
|
|
219
|
+
logger.debug(`RegalModbusStateMessage.process Ambient temperature: ${scaled_value} (Address: ${addr})`);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 0x08: { // Status
|
|
223
|
+
switch (raw_value) {
|
|
224
|
+
case 0x00: { // stop mode - motor stopped
|
|
225
|
+
logger.debug(`RegalModbusStateMessage.process Status: Stop (Address: ${addr})`);
|
|
226
|
+
pumpState.driveState = 0;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 0x09: { // run mode - boot (motor is getting ready to spin)
|
|
230
|
+
logger.debug(`RegalModbusStateMessage.process Status: Boot (Address: ${addr})`);
|
|
231
|
+
pumpState.driveState = 1;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 0x0B: { // run mode - vector
|
|
235
|
+
logger.debug(`RegalModbusStateMessage.process Status: Vector (Address: ${addr})`);
|
|
236
|
+
pumpState.driveState = 2;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 0x20: { // fault mode - motor stopped
|
|
240
|
+
logger.debug(`RegalModbusStateMessage.process Status: Fault (Address: ${addr})`);
|
|
241
|
+
pumpState.driveState = 4;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 0x09: { // Previous fault
|
|
248
|
+
if (raw_value in faultCodes) {
|
|
249
|
+
logger.debug(`RegalModbusStateMessage.process Previous fault: ${faultCodes[raw_value]} (Address: ${addr})`);
|
|
250
|
+
} else {
|
|
251
|
+
logger.debug(`RegalModbusStateMessage.process Previous fault: Unknown fault code ${raw_value} (Address: ${addr})`);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 0X0A: { // Output power
|
|
256
|
+
scaled_value = scaleValue(raw_value, 1);
|
|
257
|
+
logger.debug(`RegalModbusStateMessage.process Shaft power (W): ${scaled_value} (Address: ${addr})`);
|
|
258
|
+
pumpState.watts = scaled_value;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case 0x0B: { // SVRS Bypass Status
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case 0x0C: { // Number of current faults
|
|
265
|
+
logger.debug(`RegalModbusStateMessage.process Number of current faults: ${raw_value} (Address: ${addr})`);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case 0x0D: { // Motor line voltage
|
|
269
|
+
logger.debug(`RegalModbusStateMessage.process Raw (unscaled) motor line voltage: ${raw_value} (Address: ${addr})`);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 0x0E: { // Ramp status
|
|
273
|
+
logger.debug(`RegalModbusStateMessage.process Ramp status: ${raw_value} (Address: ${addr})`);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 0x0F: { // Number of total fault
|
|
277
|
+
logger.debug(`RegalModbusStateMessage.process Number of total faults: ${raw_value} (Address: ${addr})`);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 0x10: { // Prime status
|
|
281
|
+
switch (raw_value) {
|
|
282
|
+
case 0: { // Not priming
|
|
283
|
+
logger.debug(`RegalModbusStateMessage.process Prime status: Not priming (Address: ${addr})`);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 1: { // Priming running
|
|
287
|
+
logger.debug(`RegalModbusStateMessage.process Prime status: Priming running (Address: ${addr})`);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 2: { // Priming completed
|
|
291
|
+
logger.debug(`RegalModbusStateMessage.process Prime status: Priming completed (Address: ${addr})`);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 0x11: { // Motor input power
|
|
298
|
+
logger.debug(`RegalModbusStateMessage.process Raw (unscaled) motor input power: ${raw_value} (Address: ${addr})`);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 0x12: { // IGBT temperature
|
|
302
|
+
scaled_value = scaleValue(raw_value, 128);
|
|
303
|
+
logger.debug(`RegalModbusStateMessage.process IGBT temperature: ${scaled_value} (Address: ${addr})`);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case 0x13: { // PCB temperature
|
|
307
|
+
logger.debug(`RegalModbusStateMessage.process Raw (unscaled) PCB temperature: ${raw_value} (Address: ${addr})`);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 0x14: { // Status of external input
|
|
311
|
+
switch (raw_value) {
|
|
312
|
+
case 0: { // No external input
|
|
313
|
+
logger.debug(`RegalModbusStateMessage.process External input: No external input (Address: ${addr})`);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 3: { // PWM
|
|
317
|
+
logger.debug(`RegalModbusStateMessage.process External input: PWM (Address: ${addr})`);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case 4: { // DI_1 present
|
|
321
|
+
logger.debug(`RegalModbusStateMessage.process External input: DI_1 present (Address: ${addr})`);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case 5: { // DI_2 present
|
|
325
|
+
logger.debug(`RegalModbusStateMessage.process External input: DI_2 present (Address: ${addr})`);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case 6: { // DI_3 present
|
|
329
|
+
logger.debug(`RegalModbusStateMessage.process External input: DI_3 present (Address: ${addr})`);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 7: { // DI_4 present
|
|
333
|
+
logger.debug(`RegalModbusStateMessage.process External input: DI_4 present (Address: ${addr})`);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case 8: { // Serial input
|
|
337
|
+
logger.debug(`RegalModbusStateMessage.process External input: Serial input (Address: ${addr})`);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case 0x15: { // Reference speed
|
|
344
|
+
scaled_value = scaleValue(raw_value, 4);
|
|
345
|
+
logger.debug(`RegalModbusStateMessage.process Reference speed: ${scaled_value} (Address: ${addr})`);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
default: {
|
|
352
|
+
logger.debug(`RegalModbusStateMessage.process Page 1: ${page} (Address: ${addr}) - Not yet implemented`);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case 0x46: { // Read identification
|
|
359
|
+
logger.debug(`RegalModbusStateMessage.process Read identification (Address: ${addr}) - Not yet implemented`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 0x64: { // Read/write configuration
|
|
363
|
+
logger.debug(`RegalModbusStateMessage.process Read/write configuration (Address: ${addr}) - Not yet implemented`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
default: {
|
|
367
|
+
logger.debug(`RegalModbusStateMessage.process Unknown function code: ${functionCode} (Address: ${addr})`);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
state.emitEquipmentChanges();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public static rpmToDemand(rpm: number): [number, number] {
|
|
375
|
+
/**
|
|
376
|
+
* Converts an RPM value to a RegalModbus demand payload in speed control mode.
|
|
377
|
+
*
|
|
378
|
+
* @param {number} rpm - Desired motor speed in RPM.
|
|
379
|
+
* @returns {[number, number]} - [demand_lo, demand_hi]
|
|
380
|
+
* - demand_lo: lower byte of demand
|
|
381
|
+
* - demand_hi: upper byte of demand
|
|
382
|
+
* @throws {Error} - If RPM is out of valid range for RegalModbus demand (0–16383).
|
|
383
|
+
*/
|
|
384
|
+
if (rpm < 0 || rpm * 4 > 0xFFFF) {
|
|
385
|
+
throw new Error("RPM is out of valid range for RegalModbus demand (0–16383)");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const rawDemand = Math.round(rpm * 4); // Scale RPM by 4
|
|
389
|
+
const demandLo = rawDemand & 0xFF;
|
|
390
|
+
const demandHi = (rawDemand >> 8) & 0xFF;
|
|
391
|
+
|
|
392
|
+
return [demandLo, demandHi];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
public static demandToRPM(demandLo: number, demandHi: number): number {
|
|
396
|
+
/**
|
|
397
|
+
* Converts a RegalModbus demand payload to an RPM value.
|
|
398
|
+
*
|
|
399
|
+
* @param {number} demandLo - Lower byte of demand.
|
|
400
|
+
* @param {number} demandHi - Upper byte of demand.
|
|
401
|
+
* @returns {number} - Motor speed in RPM.
|
|
402
|
+
* @throws {Error} - If demand is out of valid range for RPM (0–16383).
|
|
403
|
+
**/
|
|
404
|
+
const rawDemand = (demandHi << 8) | demandLo; // Combine high and low bytes
|
|
405
|
+
if (rawDemand < 0 || rawDemand > 0xFFFF) {
|
|
406
|
+
throw new Error("Demand is out of valid range for RPM (0–16383)");
|
|
407
|
+
}
|
|
408
|
+
const rpm = Math.round(rawDemand / 4); // Scale back to RPM
|
|
409
|
+
return rpm;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -137,6 +137,8 @@ export class NixiePumpCollection extends NixieEquipmentCollection<NixiePump> {
|
|
|
137
137
|
return new NixiePumpHWVS(this.controlPanel, pump);
|
|
138
138
|
case 'hwrly':
|
|
139
139
|
return new NixiePumpHWRLY(this.controlPanel, pump);
|
|
140
|
+
case 'regalmodbus':
|
|
141
|
+
return new NixiePumpRegalModbus(this.controlPanel, pump);
|
|
140
142
|
default:
|
|
141
143
|
throw new EquipmentNotFoundError(`NCP: Cannot create pump ${pump.name}.`, type);
|
|
142
144
|
}
|
|
@@ -235,6 +237,7 @@ export class NixiePump extends NixieEquipment {
|
|
|
235
237
|
case 'hwvs':
|
|
236
238
|
case 'vssvrs':
|
|
237
239
|
case 'vs':
|
|
240
|
+
case 'regalmodbus':
|
|
238
241
|
c.units = sys.board.valueMaps.pumpUnits.getValue('rpm');
|
|
239
242
|
break;
|
|
240
243
|
case 'ss':
|
|
@@ -994,3 +997,198 @@ export class NixiePumpHWVS extends NixiePumpRS485 {
|
|
|
994
997
|
|
|
995
998
|
};
|
|
996
999
|
}
|
|
1000
|
+
|
|
1001
|
+
export class NixiePumpRegalModbus extends NixiePump {
|
|
1002
|
+
|
|
1003
|
+
constructor(ncp: INixieControlPanel, pump: Pump) {
|
|
1004
|
+
super(ncp, pump);
|
|
1005
|
+
// this.pump = pump;
|
|
1006
|
+
// this._targetSpeed = 0;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
public setTargetSpeed(pumpState: PumpState) {
|
|
1010
|
+
let newSpeed = 0;
|
|
1011
|
+
if (!pumpState.pumpOnDelay) {
|
|
1012
|
+
let circuitConfigs = this.pump.circuits.get();
|
|
1013
|
+
for (let i = 0; i < circuitConfigs.length; i++) {
|
|
1014
|
+
let circuitConfig = circuitConfigs[i];
|
|
1015
|
+
let circ = state.circuits.getInterfaceById(circuitConfig.circuit);
|
|
1016
|
+
if (circ.isOn) newSpeed = Math.max(newSpeed, circuitConfig.speed);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (isNaN(newSpeed)) newSpeed = 0;
|
|
1020
|
+
this._targetSpeed = newSpeed;
|
|
1021
|
+
if (this._targetSpeed !== 0) Math.min(Math.max(this.pump.minSpeed, this._targetSpeed), this.pump.maxSpeed);
|
|
1022
|
+
if (this._targetSpeed !== newSpeed) logger.info(`NCP: Setting Pump ${this.pump.name} to ${newSpeed} RPM.`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
public async setServiceModeAsync() {
|
|
1026
|
+
this._targetSpeed = 0;
|
|
1027
|
+
await this.setDriveStateAsync(false);
|
|
1028
|
+
// await this.setPumpToRemoteControlAsync(false);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
public async setPumpStateAsync(pstate: PumpState) {
|
|
1032
|
+
// Don't poll while we are seting the state.
|
|
1033
|
+
this.suspendPolling = true;
|
|
1034
|
+
try {
|
|
1035
|
+
let pt = sys.board.valueMaps.pumpTypes.get(this.pump.type);
|
|
1036
|
+
if (state.mode === 0) {
|
|
1037
|
+
// Since these process are async the closing flag can be set
|
|
1038
|
+
// between calls. We need to check it in between each call.
|
|
1039
|
+
if (!this.closing) {
|
|
1040
|
+
if (this._targetSpeed >= pt.minSpeed && this._targetSpeed <= pt.maxSpeed) await this.setPumpRPMAsync();
|
|
1041
|
+
}
|
|
1042
|
+
if (!this.closing) await this.setDriveStateAsync();
|
|
1043
|
+
;
|
|
1044
|
+
// if (!this.closing && pt.name !== 'vsf' && pt.name !== 'vs') await this.setPumpFeatureAsync(6);;
|
|
1045
|
+
if (!this.closing) await setTimeout(1000);;
|
|
1046
|
+
if (!this.closing) await this.requestPumpStatusAsync();;
|
|
1047
|
+
// if (!this.closing) await this.setPumpToRemoteControlAsync();;
|
|
1048
|
+
}
|
|
1049
|
+
return new InterfaceServerResponse(200, 'Success');
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
logger.error(`Error running pump sequence for ${this.pump.name}: ${err.message}`);
|
|
1053
|
+
return Promise.reject(err);
|
|
1054
|
+
}
|
|
1055
|
+
finally { this.suspendPolling = false; }
|
|
1056
|
+
};
|
|
1057
|
+
protected async setDriveStateAsync(isRunning: boolean = true) {
|
|
1058
|
+
let functionCode = this._targetSpeed > 0 ? 0x41 : 0x42;
|
|
1059
|
+
logger.debug(`NixiePumpRegalModbus: setDriveStateAsync ${this.pump.name} ${functionCode == 0x41 ? 'RUN' : functionCode == 0x42 ? 'STOP' : 'UNKNOWN'}`);
|
|
1060
|
+
try {
|
|
1061
|
+
if (conn.isPortEnabled(this.pump.portId || 0)) {
|
|
1062
|
+
let functionCode = this._targetSpeed > 0 ? 0x41 : 0x42;
|
|
1063
|
+
let out = Outbound.create({
|
|
1064
|
+
portId: this.pump.portId || 0,
|
|
1065
|
+
protocol: Protocol.RegalModbus,
|
|
1066
|
+
dest: this.pump.address,
|
|
1067
|
+
action: functionCode,
|
|
1068
|
+
payload: [],
|
|
1069
|
+
retries: 1,
|
|
1070
|
+
response: true
|
|
1071
|
+
});
|
|
1072
|
+
try {
|
|
1073
|
+
await out.sendAsync();
|
|
1074
|
+
}
|
|
1075
|
+
catch (err) {
|
|
1076
|
+
logger.error(`Error sending setDriveState for ${this.pump.name}: ${err.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
let pumpState = state.pumps.getItemById(this.pump.id);
|
|
1081
|
+
pumpState.command = pumpState.rpm > 0 || pumpState.flow > 0 ? 10 : 0; // dashPanel needs this to be set to 10 for running.
|
|
1082
|
+
}
|
|
1083
|
+
} catch (err) { logger.error(`Error setting driveState for ${this.pump.name}: ${err.message}`); }
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
protected async requestPumpDriveStateAsync() {
|
|
1087
|
+
if (conn.isPortEnabled(this.pump.portId || 0)) {
|
|
1088
|
+
let out = Outbound.create({
|
|
1089
|
+
portId: this.pump.portId || 0,
|
|
1090
|
+
protocol: Protocol.RegalModbus,
|
|
1091
|
+
dest: this.pump.address,
|
|
1092
|
+
action: 0x43,
|
|
1093
|
+
payload: [],
|
|
1094
|
+
retries: 2,
|
|
1095
|
+
response: true,
|
|
1096
|
+
});
|
|
1097
|
+
try {
|
|
1098
|
+
await out.sendAsync();
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
logger.error(`Error sending requestPumpDriveState for ${this.pump.name}: ${err.message}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
protected async requestSensorAsync(page: number, sensorAddr: number, retries: number = 2) {
|
|
1107
|
+
if (conn.isPortEnabled(this.pump.portId || 0)) {
|
|
1108
|
+
let out = Outbound.create({
|
|
1109
|
+
portId: this.pump.portId || 0,
|
|
1110
|
+
protocol: Protocol.RegalModbus,
|
|
1111
|
+
dest: this.pump.address,
|
|
1112
|
+
action: 0x45,
|
|
1113
|
+
payload: [page, sensorAddr],
|
|
1114
|
+
retries: retries,
|
|
1115
|
+
response: true,
|
|
1116
|
+
});
|
|
1117
|
+
try {
|
|
1118
|
+
await out.sendAsync();
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
logger.error(`Error sending requestSensor for ${this.pump.name} page ${page} sensor ${sensorAddr}: ${err.message}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
protected async requestPumpStatusAsync() {
|
|
1127
|
+
|
|
1128
|
+
await this.requestPumpDriveStateAsync();
|
|
1129
|
+
await this.requestSensorAsync(0, 0x00); // motor speed
|
|
1130
|
+
// await this.requestSensorAsync(0, 0x01); // motor current
|
|
1131
|
+
// await this.requestSensorAsync(0, 0x04); // torque
|
|
1132
|
+
// await this.requestSensorAsync(0, 0x05); // inverter input power
|
|
1133
|
+
// await this.requestSensorAsync(0, 0x06); // DC bus voltage
|
|
1134
|
+
// await this.requestSensorAsync(0, 0x07); // ambient temperature
|
|
1135
|
+
await this.requestSensorAsync(0, 0x0A); // output power
|
|
1136
|
+
// await this.requestSensorAsync(0, 0x0D); // motor line voltage
|
|
1137
|
+
// await this.requestSensorAsync(0, 0x0E); // ramp status
|
|
1138
|
+
// await this.requestSensorAsync(0, 0x0F); // no of total fault
|
|
1139
|
+
// await this.requestSensorAsync(0, 0x10); // prime status
|
|
1140
|
+
// await this.requestSensorAsync(0, 0x11); // motor input power
|
|
1141
|
+
// await this.requestSensorAsync(0, 0x12); // IGBT temperature
|
|
1142
|
+
// await this.requestSensorAsync(0, 0x13); // PCB temperature
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
protected async setPumpRPMAsync() {
|
|
1146
|
+
logger.debug(`NixiePumpRegalModbus: setPumpRPMAsync ${this.pump.name} ${this._targetSpeed}`);
|
|
1147
|
+
if (conn.isPortEnabled(this.pump.portId || 0)) {
|
|
1148
|
+
// get demand bytes from rpm
|
|
1149
|
+
const mode = 0x00; // 0x00 for speed control mode
|
|
1150
|
+
const demandLo = Math.round(this._targetSpeed * 4) & 0xFF;
|
|
1151
|
+
const demandHi = (Math.round(this._targetSpeed * 4) >> 8) & 0xFF;
|
|
1152
|
+
let out = Outbound.create({
|
|
1153
|
+
portId: this.pump.portId || 0,
|
|
1154
|
+
protocol: Protocol.RegalModbus,
|
|
1155
|
+
dest: this.pump.address,
|
|
1156
|
+
action: 0x44,
|
|
1157
|
+
payload: [mode, demandLo, demandHi],
|
|
1158
|
+
retries: 1,
|
|
1159
|
+
// timeout: 250,
|
|
1160
|
+
response: true
|
|
1161
|
+
});
|
|
1162
|
+
try {
|
|
1163
|
+
await out.sendAsync();
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
logger.error(`Error sending setPumpRPMAsync for ${this.pump.name}: ${err.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
public async closeAsync() {
|
|
1171
|
+
try {
|
|
1172
|
+
this.suspendPolling = true;
|
|
1173
|
+
logger.info(`Nixie Pump closing ${this.pump.name}.`)
|
|
1174
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
1175
|
+
this._pollTimer = null;
|
|
1176
|
+
this.closing = true;
|
|
1177
|
+
let pumpType = sys.board.valueMaps.pumpTypes.get(this.pump.type);
|
|
1178
|
+
let pumpState = state.pumps.getItemById(this.pump.id);
|
|
1179
|
+
this._targetSpeed = 0;
|
|
1180
|
+
await this.setDriveStateAsync(false);
|
|
1181
|
+
// if (!this.closing && pt.name !== 'vsf' && pt.name !== 'vs') await this.setPumpFeatureAsync();
|
|
1182
|
+
//await this.setPumpFeature();
|
|
1183
|
+
//await this.setDriveStateAsync(false);
|
|
1184
|
+
// await this.setPumpToRemoteControlAsync(false);
|
|
1185
|
+
// Make sure the polling timer is dead after we have closed this all off. That way we do not
|
|
1186
|
+
// have another process that revives it from the dead.
|
|
1187
|
+
if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
|
|
1188
|
+
this._pollTimer = null;
|
|
1189
|
+
pumpState.emitEquipmentChange();
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) { logger.error(`Nixie Pump closeAsync: ${err.message}`); return Promise.reject(err); }
|
|
1192
|
+
finally { this.suspendPolling = false; }
|
|
1193
|
+
}
|
|
1194
|
+
}
|
package/defaultConfig.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
services:
|
|
2
|
+
njspc:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
image: njspc:local
|
|
7
|
+
container_name: njspc
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
ports:
|
|
10
|
+
- "4200:4200"
|
|
11
|
+
environment:
|
|
12
|
+
- TZ=${TZ:-UTC}
|
|
13
|
+
- NODE_ENV=production
|
|
14
|
+
# Example overrides as supported by Config.getEnvVariables()
|
|
15
|
+
# - POOL_WEB_SERVERS_HTTP_PORT=4200
|
|
16
|
+
# Disable network connection and use local serial (rs485)
|
|
17
|
+
- POOL_NET_CONNECT=false
|
|
18
|
+
# Map RS-485 USB adapter if present, adjust device path
|
|
19
|
+
devices:
|
|
20
|
+
- /dev/ttyACM0:/dev/ttyUSB0
|
|
21
|
+
# Persistence (create host directories/files first)
|
|
22
|
+
volumes:
|
|
23
|
+
- ./server-config.json:/app/config.json # Persisted config file on host
|
|
24
|
+
- njspc-data:/app/data # State & equipment snapshots
|
|
25
|
+
- njspc-backups:/app/backups # Backup archives
|
|
26
|
+
- njspc-logs:/app/logs # Logs
|
|
27
|
+
- njspc-bindings:/app/web/bindings/custom # Custom bindings
|
|
28
|
+
volumes:
|
|
29
|
+
njspc-data:
|
|
30
|
+
njspc-backups:
|
|
31
|
+
njspc-logs:
|
|
32
|
+
njspc-bindings:
|