/* nodejs-poolController. An application to control pool equipment.
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
Russell Goldin, tagyoureit. russ.goldin@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import { Inbound } from "../Messages";
import { state, PumpState } from "../../../State";
import { sys, Pump } from "../../../Equipment";
import { logger } from "../../../../logger/Logger";
type PendingRead = {
startAddr: number;
quantity: number;
requestedAt: number;
};
export class NeptuneModbusStateMessage {
private static pendingReads: Map = new Map();
public static enqueueReadRequest(address: number, startAddr: number, quantity: number) {
const queue = this.pendingReads.get(address) || [];
queue.push({ startAddr, quantity, requestedAt: Date.now() });
this.pendingReads.set(address, queue);
}
public static clearReadRequests(address: number) {
this.pendingReads.delete(address);
}
private static dequeueReadRequest(address: number): PendingRead {
const queue = this.pendingReads.get(address);
if (!queue || queue.length === 0) return undefined;
const request = queue.shift();
if (queue.length === 0) this.pendingReads.delete(address);
else this.pendingReads.set(address, queue);
return request;
}
private static getNeptunePumpByAddress(address: number): Pump {
for (let i = 0; i < sys.pumps.length; i++) {
const pump = sys.pumps.getItemByIndex(i);
const typeName = sys.board.valueMaps.pumpTypes.getName(pump.type);
if (typeName === 'neptunemodbus' && pump.address === address) return pump;
}
return undefined;
}
private static toSigned16(value: number): number {
return value > 0x7FFF ? value - 0x10000 : value;
}
private static decodeModbusException(code: number): string {
const modbusExceptions = {
0x01: 'Illegal function',
0x02: 'Illegal data address',
0x03: 'Illegal data value',
0x04: 'Server device failure',
0x05: 'Acknowledge',
0x06: 'Server device busy',
0x08: 'Memory parity error',
0x0A: 'Gateway path unavailable',
0x0B: 'Gateway target failed to respond',
};
return modbusExceptions[code] || `Unknown Modbus exception ${code}`;
}
private static processReadInput(msg: Inbound, address: number, pumpState: PumpState) {
const request = this.dequeueReadRequest(address);
const byteCount = msg.extractPayloadByte(0, 0);
if (byteCount <= 0 || (byteCount % 2) !== 0) {
logger.debug(`NeptuneModbusStateMessage.processReadInput invalid byte count ${byteCount} (Address: ${address})`);
return;
}
if (msg.payload.length < byteCount + 1) {
logger.debug(`NeptuneModbusStateMessage.processReadInput short payload (Address: ${address})`);
return;
}
let motorFaultCode = 0;
let interfaceFaultCode = 0;
let stoppedState: number = undefined;
for (let i = 0; i < byteCount; i += 2) {
const value = (msg.payload[i + 1] << 8) | msg.payload[i + 2];
const offset = i / 2;
const registerAddr = request ? request.startAddr + offset : -1;
switch (registerAddr) {
case 0: // 30001 Current Speed
pumpState.rpm = value;
break;
case 3: // 30004 Motor Power
pumpState.watts = value;
break;
case 5: // 30006 Motor Fault Status
logger.debug(`Neptune motor fault status ${value} (Address: ${address})`);
break;
case 6: // 30007 Motor Fault Code
motorFaultCode = value;
break;
case 30: // 30031 Interface Fault State
logger.debug(`Neptune interface fault state ${value} (Address: ${address})`);
break;
case 31: // 30032 Interface Fault Code
interfaceFaultCode = value;
break;
case 113: // 30114 Stopped State bit image
stoppedState = value;
break;
case 119: // 30120 Target shaft speed (signed)
pumpState.targetSpeed = Math.abs(this.toSigned16(value));
break;
default:
// Keep MVP mapping focused on existing pump state fields.
break;
}
}
const hasFault = motorFaultCode > 0 || interfaceFaultCode > 0;
if (hasFault) {
logger.warn(`Neptune fault detected (Address: ${address}) motorFault=${motorFaultCode} interfaceFault=${interfaceFaultCode}`);
pumpState.status = 16;
pumpState.driveState = 4;
pumpState.command = 4;
return;
}
if (typeof stoppedState !== 'undefined') {
const isStopped = (stoppedState & 0x01) === 1;
pumpState.driveState = isStopped ? 0 : 2;
pumpState.command = isStopped ? 4 : 10;
pumpState.status = isStopped ? 0 : 1;
}
else if (pumpState.rpm > 0) {
pumpState.driveState = 2;
pumpState.command = 10;
pumpState.status = 1;
}
else {
pumpState.driveState = 0;
pumpState.command = 4;
pumpState.status = 0;
}
}
private static processWriteSingle(msg: Inbound, address: number, pumpState: PumpState) {
if (msg.payload.length < 4) return;
const registerAddr = (msg.payload[0] << 8) | msg.payload[1];
const registerValue = (msg.payload[2] << 8) | msg.payload[3];
switch (registerAddr) {
case 0: // 40001 Motor On/Off
if (registerValue === 0) {
pumpState.driveState = 0;
pumpState.command = 4;
pumpState.status = 0;
}
else if (registerValue === 1) {
pumpState.driveState = 2;
pumpState.command = 10;
pumpState.status = 1;
}
break;
case 1: // 40002 Manual speed RPM
pumpState.rpm = registerValue;
break;
default:
logger.debug(`Neptune write ack for unhandled register ${registerAddr}=${registerValue} (Address: ${address})`);
break;
}
}
public static process(msg: Inbound) {
const address = msg.dest;
const functionCode = msg.action;
const pumpCfg = this.getNeptunePumpByAddress(address);
if (typeof pumpCfg === 'undefined') {
logger.debug(`NeptuneModbusStateMessage.process ignored unconfigured address ${address}`);
return;
}
const pumpState = state.pumps.getItemById(pumpCfg.id, pumpCfg.isActive === true);
if ((functionCode & 0x80) === 0x80) {
const exceptionCode = msg.extractPayloadByte(0, 0);
logger.warn(`Neptune Modbus exception response fn=0x${functionCode.toString(16)} code=${exceptionCode} (${this.decodeModbusException(exceptionCode)}) address=${address}`);
pumpState.status = 16;
state.emitEquipmentChanges();
return;
}
switch (functionCode) {
case 0x04: // Read input registers
this.processReadInput(msg, address, pumpState);
break;
case 0x06: // Write single register
this.processWriteSingle(msg, address, pumpState);
break;
case 0x10: // Write multiple registers
logger.debug(`Neptune write-multiple response (Address: ${address})`);
break;
default:
logger.debug(`NeptuneModbusStateMessage.process unhandled function 0x${functionCode.toString(16)} (Address: ${address})`);
break;
}
state.emitEquipmentChanges();
}
}