Add binary WebSocket updates and lag compensation; extend PlotStatus

This commit is contained in:
babayaga 2026-01-17 06:25:31 +01:00
parent 2f92442b00
commit 357e36b24d
14 changed files with 1626 additions and 1463 deletions

View File

@ -14,7 +14,7 @@
#define NETWORKVALUE_PERSISTENCE_ENABLED
#ifdef NETWORKVALUE_PERSISTENCE_ENABLED
#include <ArduinoJson.h>
#include <ArduinoJson.h>
#endif
/*
@ -44,7 +44,7 @@
#include "NetworkValuePB.h"
#define NETWORKVALUE_PROTOBUF_INHERITANCE , public maybe<NETWORKVALUE_ENABLE_PROTOBUF, NV_Protobuf>
#else
#define NETWORKVALUE_PROTOBUF_INHERITANCE
#define NETWORKVALUE_PROTOBUF_INHERITANCE
#endif
enum class NetworkValue_ThresholdMode : uint8_t
@ -87,10 +87,10 @@ public:
}
template <typename... Args>
void log(int level, const char *componentName, const char *fmt, Args... args) const { }
void log(int level, const char *componentName, const char *fmt, Args... args) const {}
void setup_feature() {}
void loop_feature() {}
void info_feature() const { }
void info_feature() const {}
};
/**
@ -124,7 +124,6 @@ public:
void loop_feature() {}
void info_feature() const
{
}
};
@ -222,7 +221,8 @@ public:
bool applyUpdate(const T &newValue)
{
if (!checkChanged(newValue)) {
if (!checkChanged(newValue))
{
return false;
}
T oldValue = m_lastValueOnUpdate;
@ -236,7 +236,7 @@ public:
}
T getValue() const { return m_value; }
T& getValueRef() { return m_value; }
T &getValueRef() { return m_value; }
void setup_feature() {}
void loop_feature() {}
@ -281,8 +281,10 @@ public:
// This specialized version of checkChanged now compares the new value against the last known state.
bool checkChanged(const std::array<T, N> &newValue) const
{
for (size_t i = 0; i < N; ++i) {
if (newValue[i] != m_lastValueOnUpdate[i]) {
for (size_t i = 0; i < N; ++i)
{
if (newValue[i] != m_lastValueOnUpdate[i])
{
return true;
}
}
@ -291,7 +293,8 @@ public:
bool applyUpdate(const std::array<T, N> &newValue)
{
if (!checkChanged(newValue)) {
if (!checkChanged(newValue))
{
return false;
}
std::array<T, N> oldValue = m_lastValueOnUpdate;
@ -305,7 +308,7 @@ public:
}
std::array<T, N> getValue() const { return m_value; }
std::array<T, N>& getValueRef() { return m_value; }
std::array<T, N> &getValueRef() { return m_value; }
void setup_feature() {}
void loop_feature() {}
@ -349,8 +352,10 @@ public:
bool checkChanged(const std::array<bool, N> &newValue) const
{
for (size_t i = 0; i < N; ++i) {
if (newValue[i] != m_lastValueOnUpdate[i]) {
for (size_t i = 0; i < N; ++i)
{
if (newValue[i] != m_lastValueOnUpdate[i])
{
return true;
}
}
@ -359,7 +364,8 @@ public:
bool applyUpdate(const std::array<bool, N> &newValue)
{
if (!checkChanged(newValue)) {
if (!checkChanged(newValue))
{
return false;
}
std::array<bool, N> oldValue = m_lastValueOnUpdate;
@ -373,7 +379,7 @@ public:
}
std::array<bool, N> getValue() const { return m_value; }
std::array<bool, N>& getValueRef() { return m_value; }
std::array<bool, N> &getValueRef() { return m_value; }
void setup_feature() {}
void loop_feature() {}
@ -399,8 +405,7 @@ template <typename T>
class NetworkValue : public NetworkValueBase,
public maybe<NETWORKVALUE_ENABLE_LOGGING, NV_Logging>,
public maybe<NETWORKVALUE_ENABLE_MODBUS, NV_Modbus>,
public maybe<NETWORKVALUE_ENABLE_NOTIFY, NV_Notify<T>>
NETWORKVALUE_PROTOBUF_INHERITANCE
public maybe<NETWORKVALUE_ENABLE_NOTIFY, NV_Notify<T>> NETWORKVALUE_PROTOBUF_INHERITANCE
{
private:
// --- Tag Dispatching for update() method ---
@ -429,18 +434,22 @@ private:
if (this->owner)
{
#if (NETWORKVALUE_ENABLE_PROTOBUF == 1)
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_PROTOBUF)) {
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_PROTOBUF))
{
uint8_t buffer[64]; // Static buffer for PB encoding
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
MB_Registers regInfo = this->getRegisterInfo();
if (this->encode(&stream, regInfo, newValue)) {
if (this->encode(&stream, regInfo, newValue))
{
PB_UpdateData pb_msg;
pb_msg.data = buffer;
pb_msg.len = stream.bytes_written;
pb_msg.componentId = this->id;
this->owner->onMessage(this->id, E_CALLS::EC_PROTOBUF_UPDATE, E_MessageFlags::E_MF_NONE, &pb_msg, this->owner);
} else {
}
else
{
Log.warningln(F(" [update_impl] Protobuf encoding failed for '%s'"), this->name.c_str());
}
}
@ -471,18 +480,22 @@ private:
if (this->owner)
{
#if (NETWORKVALUE_ENABLE_PROTOBUF == 1)
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_PROTOBUF)) {
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_PROTOBUF))
{
uint8_t buffer[256]; // Larger buffer for arrays
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
MB_Registers regInfo = this->getRegisterInfo();
if (this->encode(&stream, regInfo, newValue)) {
if (this->encode(&stream, regInfo, newValue))
{
PB_UpdateData pb_msg;
pb_msg.data = buffer;
pb_msg.len = stream.bytes_written;
pb_msg.componentId = this->id;
this->owner->onMessage(this->id, E_CALLS::EC_PROTOBUF_UPDATE, E_MessageFlags::E_MF_NONE, &pb_msg, this->owner);
} else {
}
else
{
Log.warningln(F(" [update_impl] Protobuf encoding failed for array '%s'"), this->name.c_str());
}
}
@ -518,12 +531,10 @@ private:
}
public:
NetworkValue(Component *owner, ushort id,
const char *name,
T initial, T threshold, NetworkValue_ThresholdMode mode, void (*cb)(const T &, const T &) = nullptr,
uint8_t featureFlags = static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
NetworkValue(Component *owner, ushort id,
const char *name,
T initial, T threshold, NetworkValue_ThresholdMode mode, void (*cb)(const T &, const T &) = nullptr,
uint8_t featureFlags = static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
: NetworkValueBase(name, id, COMPONENT_DEFAULT, owner, featureFlags)
{
initNotify(initial, threshold, mode, cb);
@ -539,10 +550,10 @@ public:
}
#endif
}
NetworkValue(Component *owner, ushort id,
const char *name,
uint8_t featureFlags = static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
NetworkValue(Component *owner, ushort id,
const char *name,
uint8_t featureFlags = static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
: NetworkValueBase(name, id, COMPONENT_DEFAULT, owner, featureFlags)
{
#if (NETWORKVALUE_ENABLE_LOGGING == 1)
@ -610,7 +621,8 @@ public:
return E_OK;
}
short info() override {
short info() override
{
Component::info();
#if (NETWORKVALUE_ENABLE_LOGGING == 1)
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_LOGGING))
@ -629,10 +641,11 @@ public:
template <typename U = T>
bool update(const U &newValue, E_PRIORITY priority = E_PRIORITY::E_PRIORITY_LOWEST)
{
if (!this->applyUpdate(newValue)) {
if (!this->applyUpdate(newValue))
{
return false; // If no change, do nothing further.
}
#if (NETWORKVALUE_ENABLE_LOGGING == 1)
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_LOGGING))
{
@ -641,10 +654,11 @@ public:
#endif
// Dispatch notifications
if (this->owner) {
if (this->owner)
{
update_impl(newValue, typename get_update_tag<U>::type(), priority);
}
return true;
}
@ -685,18 +699,20 @@ public:
if (NETWORKVALUE_ENABLE_NOTIFY && hasFeature(E_NetworkValueFeatureFlags::E_NVFF_NOTIFY))
{
NV_Notify<T>::init_feature(initial, threshold, mode, cb);
}
}
}
bool applyUpdate(const T& newValue) {
if (NETWORKVALUE_ENABLE_NOTIFY && hasFeature(E_NetworkValueFeatureFlags::E_NVFF_NOTIFY)) {
bool applyUpdate(const T &newValue)
{
if (NETWORKVALUE_ENABLE_NOTIFY && hasFeature(E_NetworkValueFeatureFlags::E_NVFF_NOTIFY))
{
return NV_Notify<T>::applyUpdate(newValue);
}
return false;
}
T getValue() const { return NV_Notify<T>::getValue(); }
T& getValueRef() { return NV_Notify<T>::getValueRef(); }
T &getValueRef() { return NV_Notify<T>::getValueRef(); }
using NV_Notify<T>::getValue;
using NV_Notify<T>::getValueRef;
@ -750,14 +766,14 @@ public:
void fromJSON(const JsonObjT &obj)
{
#if NETWORKVALUE_ENABLE_LOGGING
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_LOGGING) && obj.containsKey("logging_enabled"))
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_LOGGING) && !obj["logging_enabled"].isNull())
{
this->enableLogging(obj["logging_enabled"].template as<bool>());
}
#endif
#if NETWORKVALUE_ENABLE_MODBUS
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_MODBUS) && obj.containsKey("modbus"))
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_MODBUS) && !obj["modbus"].isNull())
{
auto mod = obj["modbus"].template as<ArduinoJson::JsonObjectConst>();
ushort startAddress = mod["startAddress"].template as<ushort>();
@ -771,7 +787,7 @@ public:
#endif
#if NETWORKVALUE_ENABLE_NOTIFY
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_NOTIFY) && obj.containsKey("notify"))
if (hasFeature(E_NetworkValueFeatureFlags::E_NVFF_NOTIFY) && !obj["notify"].isNull())
{
auto notify = obj["notify"].template as<ArduinoJson::JsonObjectConst>();
T threshold = notify["threshold"].template as<T>();

View File

@ -54,10 +54,9 @@ enum class E_DELTA_MS300_FAULT_STATUS_FLAGS : uint16_t
// MS300 Frequency Command Register (2001H)
#define DELTA_REG_SET_FREQ static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_FREQUENCY_COMMAND)
// Enum for TCP Offsets is now in DELTA_VFD.h
DELTA_VFD::DELTA_VFD(Component* owner, uint8_t slaveId, millis_t readInterval)
DELTA_VFD::DELTA_VFD(Component *owner, uint8_t slaveId, millis_t readInterval)
: VFD_Base(owner, slaveId, COMPONENT_KEY_DELTA_VFD, readInterval)
{
componentId = id;
@ -87,7 +86,7 @@ DELTA_VFD::DELTA_VFD(Component* owner, uint8_t slaveId, millis_t readInterval)
short DELTA_VFD::setup()
{
L_INFO(F("DELTA_VFD[%d]: Setting up..."), slaveId);
// --- Configure Mandatory Read Blocks ---
// Block 1: Status and monitoring registers (0x2100-0x2106) - Status Codes, Fault Status, Freq Cmd, Output Freq, Current, DC Bus, Output Voltage
ModbusReadBlock *statusMonitorBlock = addMandatoryReadBlock(
@ -117,12 +116,12 @@ short DELTA_VFD::setup()
}
#endif
// Add output registers for VFD control
this->addOutputRegister(DELTA_REG_SET_FREQ, E_FN_CODE::FN_WRITE_HOLD_REGISTER, 0, PRIORITY_HIGH);
this->addOutputRegister(static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_CONTROL_COMMAND),
this->addOutputRegister(DELTA_REG_SET_FREQ, E_FN_CODE::FN_WRITE_HOLD_REGISTER, 0, PRIORITY_HIGH);
this->addOutputRegister(static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_CONTROL_COMMAND),
E_FN_CODE::FN_WRITE_HOLD_REGISTER,
0, // Default to no command (stopped)
PRIORITY_HIGH);
return E_OK;
}
@ -181,7 +180,6 @@ bool DELTA_VFD::onRegisterUpdate(uint16_t address, uint16_t newValue)
bool updated = false;
// Update local state based on the register address using MS300 registers
switch (address)
{
case static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MON_OUTPUT_FREQ):
@ -214,10 +212,11 @@ bool DELTA_VFD::onRegisterUpdate(uint16_t address, uint16_t newValue)
updated = true;
break;
case static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_STATUS_CODES): // 0x2100 - Status Codes
_faultCode = newValue & 0xFF; // Low byte = error code
_faultCode = newValue & 0xFF; // Low byte = error code
_faultValid = true;
// Check for specific fault flags if high byte has fault status flags
if (newValue & 0xFF00) {
if (newValue & 0xFF00)
{
_faultStatusFlags = (newValue >> 8) & 0xFF; // High byte contains fault status flags
}
_updateVfdState();
@ -225,7 +224,7 @@ bool DELTA_VFD::onRegisterUpdate(uint16_t address, uint16_t newValue)
break;
default:
// Check if it's within the monitoring range
if (address >= static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MON_FREQ_CMD) &&
if (address >= static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MON_FREQ_CMD) &&
address <= static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MON_OUTPUT_TORQUE))
{
// Log.traceln(F("DELTA_VFD[%d]: MS300 monitoring register update (Addr: 0x%04X) -> %d"), slaveId, address, newValue);
@ -234,9 +233,10 @@ bool DELTA_VFD::onRegisterUpdate(uint16_t address, uint16_t newValue)
}
// Call base class method
if (RTU_Base::onRegisterUpdate(address, newValue)) updated = true;
if (RTU_Base::onRegisterUpdate(address, newValue))
updated = true;
//L_INFO(F("DELTA_VFD[%d]: Register Update (Addr: 0x%04X) -> %d"), slaveId, address, newValue);
// L_INFO(F("DELTA_VFD[%d]: Register Update (Addr: 0x%04X) -> %d"), slaveId, address, newValue);
return updated;
}
@ -299,7 +299,7 @@ bool DELTA_VFD::hasFaultType(E_DELTA_MS300_FAULT_STATUS_FLAGS faultType) const
{
if (!_faultValid)
return false;
return (_faultStatusFlags & static_cast<uint8_t>(faultType)) != 0;
}
@ -324,8 +324,7 @@ bool DELTA_VFD::run()
static_cast<uint8_t>(E_DELTA_MS300_CMD_ACCEL_DECEL::ACCEL_DECEL_1),
static_cast<uint8_t>(E_DELTA_MS300_CMD_SPEED_SELECT::MASTER_SPEED),
static_cast<uint8_t>(E_DELTA_MS300_CMD_ENABLE::DISABLE),
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING)
);
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING));
uint16_t reg = static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_CONTROL_COMMAND);
this->setOutputRegisterValue(reg, cmd);
return true;
@ -339,8 +338,7 @@ bool DELTA_VFD::reverse()
static_cast<uint8_t>(E_DELTA_MS300_CMD_ACCEL_DECEL::ACCEL_DECEL_1),
static_cast<uint8_t>(E_DELTA_MS300_CMD_SPEED_SELECT::MASTER_SPEED),
static_cast<uint8_t>(E_DELTA_MS300_CMD_ENABLE::DISABLE),
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING)
);
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING));
uint16_t reg = static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_CONTROL_COMMAND);
this->setOutputRegisterValue(reg, cmd);
return true;
@ -354,8 +352,7 @@ short DELTA_VFD::stop()
static_cast<uint8_t>(E_DELTA_MS300_CMD_ACCEL_DECEL::ACCEL_DECEL_1),
static_cast<uint8_t>(E_DELTA_MS300_CMD_SPEED_SELECT::MASTER_SPEED),
static_cast<uint8_t>(E_DELTA_MS300_CMD_ENABLE::DISABLE),
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING)
);
static_cast<uint8_t>(E_DELTA_MS300_CMD_OPERATION_SOURCE::PR_00_21_SETTING));
uint16_t reg = static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_CONTROL_COMMAND);
this->setOutputRegisterValue(reg, cmd);
return true;
@ -505,7 +502,7 @@ short DELTA_VFD::mb_tcp_read(MB_Registers *reg)
success = _setFrequencyValid; // Corrected: was _setFrequencyValid / 100
value = success ? (_setFrequency / 100) : 0xFFFF; // Display in Hz
break;
case E_DELTA_TCP_OFFSET::CMD_DIRECTION: // Read associated with command write returns current running state?
case E_DELTA_TCP_OFFSET::CMD_DIRECTION: // Read associated with command write returns current running state?
value = _statusValid ? _statusRegister : 0xFFFF; // Return raw status
success = _statusValid;
break;
@ -533,15 +530,15 @@ short DELTA_VFD::mb_tcp_read(MB_Registers *reg)
short DELTA_VFD::setupVFD()
{
RS485 *rs485 = (RS485 *)owner;
// Configure MS300 parameters for communication control
// /rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MASTER_FREQ_CMD_SOURCE), 9, true); // Master Freq Cmd Source (00-20)
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_OPERATION_CMD_SOURCE), 2, true); // Operation Cmd Source (00-21)
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_OPERATION_CMD_SOURCE), 2, true); // Operation Cmd Source (00-21)
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_MAX_OP_FREQ_MOTOR_1), DELTA_MAX_OP_FREQ_MOTOR_1, true); // Max Op Freq Motor 1 (01-00) = 75.00 Hz
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_ACCEL_TIME_1_MOTOR_1), 150, true); // Accel Time 1 (01-12) = 1.50 sec
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_DECEL_TIME_1_MOTOR_1), 150, true); // Decel Time 1 (01-13) = 1.50 sec
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_OUTPUT_VOLTAGE_MOTOR_1), 2200, true); // Motor Voltage (01-02) = 220.0V
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_ACCEL_TIME_1_MOTOR_1), 150, true); // Accel Time 1 (01-12) = 1.50 sec
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_DECEL_TIME_1_MOTOR_1), 150, true); // Decel Time 1 (01-13) = 1.50 sec
rs485->modbus.writeRegister(this->slaveId, static_cast<uint16_t>(E_DELTA_MS300_REGISTERS::E_DELTA_MS300_OUTPUT_VOLTAGE_MOTOR_1), 2200, true); // Motor Voltage (01-02) = 220.0V
return E_OK;
}
@ -583,7 +580,7 @@ short DELTA_VFD::mb_tcp_write(MB_Registers *reg, short value)
case 1: // Run Forward
commandSuccess = run();
break;
case 2: // Run Reverse
case 2: // Run Reverse
commandSuccess = reverse();
break;
case 0: // Stop
@ -604,7 +601,7 @@ short DELTA_VFD::mb_tcp_write(MB_Registers *reg, short value)
// Handle custom commands
E_DELTA_CMD command = static_cast<E_DELTA_CMD>(value);
_lastCommand = command;
switch (command)
{
case E_DELTA_CMD::E_DTC_INFO:
@ -811,7 +808,7 @@ bool DELTA_VFD::getOutputTorquePercent(uint16_t &value) const
}
#endif
void DELTA_VFD::onError(ushort errorCode, const char* errorMessage)
void DELTA_VFD::onError(ushort errorCode, const char *errorMessage)
{
// L_ERROR(F("DELTA_VFD[%d]: Modbus Error %d - %s"), slaveId, errorCode, errorMessage ? errorMessage : "Unknown");
RTU_Base::onError(errorCode, errorMessage);

View File

@ -52,9 +52,11 @@ OmronE5::OmronE5(Component *owner, uint8_t slaveId, millis_t readInterval)
m_pv.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::PV, 1, this->id, this->slaveId, FN_READ_HOLD_REGISTER, "PV", name.c_str());
m_sp.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::SP, 1, this->id, this->slaveId, FN_READ_HOLD_REGISTER, "SP", name.c_str());
m_statusLow.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::STATUS_LOW, 1, this->id, this->slaveId, FN_READ_HOLD_REGISTER, "Status Low", name.c_str());
m_statusHigh.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::STATUS_HIGH, 1, this->id, this->slaveId, FN_READ_HOLD_REGISTER, "Status High", name.c_str());
m_enabled.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::ENABLED, 1, this->id, this->slaveId, FN_WRITE_COIL, "Enabled", name.c_str());
m_commsWritingEnabled.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::CMD_COMMS_WRITE, 1, this->id, this->slaveId, FN_WRITE_COIL, "Comms Write", name.c_str());
m_runState.initModbus(tcpBaseAddr + (ushort)E_OmronTcpOffset::CMD_STOP, 1, this->id, this->slaveId, FN_WRITE_COIL, "Run/Stop Coil", name.c_str());

View File

@ -5,16 +5,26 @@
#include "./OperatorSwitch.h"
// Helper function to convert State enum to string for logging
const char* stateToString(OperatorSwitch::State state) {
switch (state) {
case OperatorSwitch::State::IDLE: return "IDLE";
case OperatorSwitch::State::STOP_PRESSED: return "STOP_PRESSED";
case OperatorSwitch::State::CYCLE_PRESSED: return "CYCLE_PRESSED";
case OperatorSwitch::State::BOTH_PRESSED: return "BOTH_PRESSED";
case OperatorSwitch::State::STOP_HELD: return "STOP_HELD";
case OperatorSwitch::State::CYCLE_HELD: return "CYCLE_HELD";
case OperatorSwitch::State::BOTH_HELD: return "BOTH_HELD";
default: return "UNKNOWN";
const char *stateToString(OperatorSwitch::State state)
{
switch (state)
{
case OperatorSwitch::State::IDLE:
return "IDLE";
case OperatorSwitch::State::STOP_PRESSED:
return "STOP_PRESSED";
case OperatorSwitch::State::CYCLE_PRESSED:
return "CYCLE_PRESSED";
case OperatorSwitch::State::BOTH_PRESSED:
return "BOTH_PRESSED";
case OperatorSwitch::State::STOP_HELD:
return "STOP_HELD";
case OperatorSwitch::State::CYCLE_HELD:
return "CYCLE_HELD";
case OperatorSwitch::State::BOTH_HELD:
return "BOTH_HELD";
default:
return "UNKNOWN";
}
}
@ -116,9 +126,11 @@ short OperatorSwitch::loop()
if (rawInputState != State::IDLE)
{
unsigned long mostRecentPressTime = 0;
if (stopPressed) mostRecentPressTime = max(mostRecentPressTime, buttons[BUTTON_STOP].pressTime);
if (cyclePressed) mostRecentPressTime = max(mostRecentPressTime, buttons[BUTTON_CYCLE].pressTime);
if (stopPressed)
mostRecentPressTime = max(mostRecentPressTime, buttons[BUTTON_STOP].pressTime);
if (cyclePressed)
mostRecentPressTime = max(mostRecentPressTime, buttons[BUTTON_CYCLE].pressTime);
if ((now - mostRecentPressTime >= PRESS_TIME_MS))
{
newState = rawInputState;
@ -143,35 +155,42 @@ short OperatorSwitch::loop()
}
else if (!holdEventTriggered && (now - pressStartTime >= HOLD_TIME_MS))
{
if (currentState == State::STOP_PRESSED) newState = State::STOP_HELD;
else if (currentState == State::CYCLE_PRESSED) newState = State::CYCLE_HELD;
else if (currentState == State::BOTH_PRESSED) newState = State::BOTH_HELD;
if (newState != currentState) {
if (currentState == State::STOP_PRESSED)
newState = State::STOP_HELD;
else if (currentState == State::CYCLE_PRESSED)
newState = State::CYCLE_HELD;
else if (currentState == State::BOTH_PRESSED)
newState = State::BOTH_HELD;
if (newState != currentState)
{
holdEventTriggered = true;
}
}
break;
case State::STOP_HELD:
if (!stopPressed) newState = State::IDLE;
if (!stopPressed)
newState = State::IDLE;
break;
case State::CYCLE_HELD:
if (!cyclePressed) newState = State::IDLE;
if (!cyclePressed)
newState = State::IDLE;
break;
case State::BOTH_HELD:
if (!stopPressed || !cyclePressed) newState = State::IDLE;
if (!stopPressed || !cyclePressed)
newState = State::IDLE;
break;
default:
break;
}
if (newState != currentState) {
if (newState != currentState)
{
m_state.update(newState);
notifyStateChange();
}
return E_OK;
}
@ -184,7 +203,6 @@ short OperatorSwitch::info(short val0, short val1)
void OperatorSwitch::notifyStateChange()
{
Log.verboseln("OperatorSwitch::notifyStateChange, state: %s", stateToString(m_state.getValue()));
Component::notifyStateChange();
}
@ -196,12 +214,13 @@ short OperatorSwitch::mb_tcp_write(MB_Registers *reg, short networkValue)
short OperatorSwitch::mb_tcp_read(MB_Registers *reg)
{
short result = NetworkComponent::mb_tcp_read(reg);
if (result != E_NOT_IMPLEMENTED) {
if (result != E_NOT_IMPLEMENTED)
{
return result;
}
uint16_t address = reg->startAddress;
if (address == (_baseAddress + (ushort)E_MB_Offset::STATE))
{
return (ushort)m_state.getValue();

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
class Loadcell;
class Solenoid;
class PushButton;
// Default pressure thresholds for states
#define PRESS_CYLINDER_DEFAULT_HOMING_THRESHOLD 5
@ -21,22 +22,22 @@ class Solenoid;
#define PRESS_CYLINDER_DEFAULT_MAXLOAD_THRESHOLD 3200
#define PRESS_CYLINDER_DEFAULT_HIGHLOAD_THRESHOLD 3000
#define PRESS_CYLINDER_DEFAULT_OVERLOAD 120
#define PRESS_CYLINDER_DEFAULT_SP_DEADBAND_PERCENT 7
#define PRESS_CYLINDER_DEFAULT_SP_DEADBAND_RAW 100
#define PRESS_CYLINDER_DEFAULT_SP_DEADBAND_PERCENT 5
#define PRESS_CYLINDER_DEFAULT_SP_DEADBAND_RAW 50
#define PRESS_CYLINDER_DEFAULT_MIN_LOAD 10
#define PRESS_CYLINDER_MAX_LOAD_CLAMP 3500
#define PRESS_MAX_PRESS_TIME 35000
#define PRESS_AUTO_TIMEOUT 35000
#define PRESS_CYLINDER_AUTO_MODE_HOLD_DURATION_MS 1500
#define BALANCE_INTERVAL 3000
#define BALANCE_MIN_PV_DIFF 50
#define BALANCE_INTERVAL 500
#define BALANCE_MIN_PV_DIFF 200
#define BALANCE_MAX_PV_DIFF 350
#define BALANCE_MIN_STEP_DIFF 300
#define BALANCE_MIN_STEP_DIFF 200
#define PRESSCYLINDER_INTERVAL 20
#define PRESS_CYLINDER_MAX_STALL_ACTIVATIONS 4
#define PRESS_CYLINDER_MB_COUNT 10
#define PRESS_CYLINDER_MB_COUNT 12
#define PRESS_CYLINDER_MAX_PAIRS 2
#define PRESS_CYLINDER_MAX_LOADCELLS PRESS_CYLINDER_MAX_PAIRS
#define PRESS_CYLINDER_MAX_SOLENOIDS PRESS_CYLINDER_MAX_PAIRS
@ -124,7 +125,7 @@ public:
E_PC_OP_CHECK_MULTI_TIMEOUT = 1 << 5,
E_PC_OP_ENABLE_DOUBLE_CLICK = 1 << 6,
// E_PC_OP_ALL = E_PC_OP_CHECK_MAX_TIME | E_PC_OP_CHECK_STALLED | E_PC_OP_CHECK_BALANCE | E_PC_OP_CHECK_LOADCELL,
E_PC_OP_ALL = E_PC_OP_CHECK_MAX_TIME | E_PC_OP_ENABLE_DOUBLE_CLICK
E_PC_OP_ALL = E_PC_OP_CHECK_MAX_TIME | E_PC_OP_ENABLE_DOUBLE_CLICK | E_PC_OP_CHECK_LOADCELL
};
enum E_PC_OutputMode
{
@ -150,6 +151,7 @@ private:
Loadcell *_loadcells[PRESS_CYLINDER_MAX_PAIRS];
Solenoid *_solenoids[PRESS_CYLINDER_MAX_PAIRS];
Joystick *_joystick;
PushButton *_pushButton;
unsigned long _last_balance_time;
unsigned long _auto_interlocked_start_time;
uint32_t _pvs_raw_previous[PRESS_CYLINDER_MAX_PAIRS];
@ -169,10 +171,11 @@ public:
PressCylinderValue m_error_code;
PressCylinderValue m_cflags;
PressCylinderValue m_outputMode;
PressCylinderValue m_cmd;
NetworkValue<bool> m_interlocked;
PressCylinderSettings _settings;
PressCylinder(Component *owner, short id, short mbAddress, Joystick *joystick);
PressCylinder(Component *owner, short id, short mbAddress, Joystick *joystick, PushButton *pushButton);
short addLoadcell(Loadcell *loadcell);
short addSolenoid(Solenoid *solenoid);
@ -186,6 +189,7 @@ public:
static constexpr unsigned long JOYSTICK_DOUBLE_CLICK_MS = 1200;
static constexpr E_Mode DEFAULT_AUTO_MODE_NORMAL = MODE_AUTO;
static constexpr E_Mode DEFAULT_AUTO_MODE_INTERLOCKED = MODE_AUTO_MULTI_BALANCED;
static constexpr uint8_t MANUAL_VIRTUAL_TARGET_PERCENT = 70;
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
short mb_tcp_read(MB_Registers *reg) override;
@ -203,8 +207,10 @@ public:
private:
void _applyDefaultSettings();
bool pvsOk();
short loopSingle();
short loopMulti();
const char *getErrorString(int error_code);
void onError(int error_code);
short loopSingle(E_Mode mode, uint32_t targetVal);
short loopMulti(E_Mode mode, uint32_t targetVal);
};
#endif // ENABLE_PRESS_CYLINDER

View File

@ -1558,20 +1558,7 @@ void RESTServer::handleWebSocketMessage(AsyncWebSocketClient *client, void *arg,
if (len > 0 && Log.getLevel() >= LOG_LEVEL_VERBOSE)
{
String data_str;
// Limit hex dump to a reasonable size to avoid flooding logs
size_t dump_len = len > 128 ? 128 : len;
for (size_t i = 0; i < dump_len; i++)
{
if (data[i] < 16)
data_str += "0";
data_str += String(data[i], HEX);
data_str += " ";
}
if (len > 128)
{
data_str += "...";
}
// Log.verbose("WS Message len: %d", len); - unsafe string build removed
}
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
@ -1945,8 +1932,8 @@ void RESTServer::handleWebSocketMessage(AsyncWebSocketClient *client, void *arg,
#ifdef ENABLE_WEBSOCKET_PB
else if (command == "get_registers_pb")
{
int currentPage = requestDoc.containsKey("page") ? requestDoc["page"].as<int>() : 0;
int pageSize = requestDoc.containsKey("pageSize") ? requestDoc["pageSize"].as<int>() : DEFAULT_WEBSOCKET_PAGE_SIZE;
int currentPage = !requestDoc["page"].isNull() ? requestDoc["page"].as<int>() : 0;
int pageSize = !requestDoc["pageSize"].isNull() ? requestDoc["pageSize"].as<int>() : DEFAULT_WEBSOCKET_PAGE_SIZE;
if (pageSize <= 0)
pageSize = DEFAULT_WEBSOCKET_PAGE_SIZE;
if (pageSize > MAX_PAGE_SIZE)
@ -2240,37 +2227,37 @@ void RESTServer::broadcast(BroadcastMessageType type, const JsonDocument &data)
case BROADCAST_USER_MESSAGE:
typeStr = "user_message";
break;
case BROADCAST_ERROR_MESSAGE:
typeStr = "error_message";
break;
default:
break;
}
doc["type"] = typeStr;
doc["data"] = data;
if (type == BROADCAST_USER_MESSAGE)
{
doc["timestamp"] = millis();
}
String output;
serializeJson(doc, output);
if (type == BROADCAST_USER_MESSAGE)
if (type == BROADCAST_USER_MESSAGE || type == BROADCAST_ERROR_MESSAGE)
{
doc["timestamp"] = millis();
userMessageHistory.addMessage(output.c_str());
}
queueWsMessage(0, output);
}
#ifdef ENABLE_WEBSOCKET
void RESTServer::broadcastBinary(const uint8_t *data, size_t len)
{
if (!ws.availableForWriteAll())
// Check rate limit if needed, or simply broadcast
if (ws.count() > 0 && ws.availableForWriteAll())
{
return;
ws.binaryAll((uint8_t *)data, len);
}
ws.binaryAll(data, len);
}
#endif
short RESTServer::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *user, Component *src)
{
@ -2284,6 +2271,75 @@ short RESTServer::onMessage(int id, E_CALLS verb, E_MessageFlags flags, void *us
if (verb == E_CALLS::EC_USER && user != nullptr)
{
MB_UpdateData *info = static_cast<MB_UpdateData *>(user);
// --- Binary Protocol Implementation ---
#ifdef USE_BINARY_UPDATES
if (info->functionCode == E_FN_CODE::FN_READ_HOLD_REGISTER ||
info->functionCode == E_FN_CODE::FN_READ_INPUT_REGISTER ||
info->functionCode == E_FN_CODE::FN_WRITE_HOLD_REGISTER ||
info->functionCode == E_FN_CODE::FN_WRITE_MULT_REGISTERS)
{
BinaryRegisterUpdate update;
// update.type is initialized to 0x02
update.slaveId = info->slaveId;
update.fc = (uint8_t)info->functionCode;
update.componentId = src ? src->id : 0;
update.address = info->address;
update.count = info->count;
update.reserved = 0;
// Handle value extraction
if (info->functionCode == E_FN_CODE::FN_WRITE_MULT_REGISTERS && info->userData != nullptr)
{
// For multi-write, we might need a different packet or just send the first one?
// The current spec assumes single register update mostly.
// Let's take the first value for now as the 'value' field.
uint16_t *data = static_cast<uint16_t *>(info->userData);
update.value = data[0];
}
else
{
update.value = (uint16_t)info->value;
}
this->broadcastBinary((uint8_t *)&update, sizeof(update));
lastBroadcastTime = millis();
return E_OK;
}
else if (info->functionCode == E_FN_CODE::FN_READ_COIL ||
info->functionCode == E_FN_CODE::FN_READ_DISCR_INPUT ||
info->functionCode == E_FN_CODE::FN_WRITE_COIL ||
info->functionCode == E_FN_CODE::FN_WRITE_MULT_COILS)
{
BinaryCoilUpdate update;
// update.type is initialized to 0x01
update.slaveId = info->slaveId;
update.fc = (uint8_t)info->functionCode;
update.componentId = src ? src->id : 0;
update.address = info->address;
update.count = info->count;
if (info->functionCode == E_FN_CODE::FN_WRITE_MULT_COILS && info->userData != nullptr)
{
bool *data = static_cast<bool *>(info->userData);
update.value = data[0] ? 1 : 0;
}
else
{
update.value = info->value ? 1 : 0;
}
this->broadcastBinary((uint8_t *)&update, sizeof(update));
lastBroadcastTime = millis();
return E_OK;
}
#endif
// --- End Binary Protocol ---
// --- JSON Fallback (Original Logic) ---
// If USE_BINARY_UPDATES is undefined, OR if the message type wasn't handled above (though all types are covered)
// we fall through to here. Note: If USE_BINARY_UPDATES is defined, we returned E_OK above.
JsonDocument doc;
doc["slaveId"] = info->slaveId;
doc["address"] = info->address;
@ -2358,6 +2414,7 @@ uint8_t RESTServer::getConnectedClientsCount() const
return ws.count();
}
#endif
#ifdef ENABLE_WEBSOCKET
void RESTServer::queueWsMessage(uint32_t clientId, const String &message)
{
@ -2376,8 +2433,10 @@ void RESTServer::queueWsMessage(uint32_t clientId, const String &message)
void RESTServer::processWsQueue()
{
if (millis() - lastWsSendTime < WS_MIN_SEND_INTERVAL_MS)
if (millis() - lastWsSendTime < 100)
{
return;
}
WsMessage msg;
if (wsTxQueue.peek(msg))

View File

@ -33,7 +33,8 @@ typedef enum : uint8_t
BROADCAST_LOG_ENTRY,
BROADCAST_SYSTEM_STATUS,
BROADCAST_USER_DEFINED,
BROADCAST_USER_MESSAGE
BROADCAST_USER_MESSAGE,
BROADCAST_ERROR_MESSAGE
} BroadcastMessageType;
struct WsMessage
@ -42,6 +43,33 @@ struct WsMessage
String message;
};
// --- Binary Protocol Structs ---
#pragma pack(push, 1)
struct BinaryCoilUpdate
{
uint8_t type = 0x01; // BROADCAST_COIL_UPDATE
uint8_t slaveId;
uint8_t fc;
uint8_t value; // 0 or 1
uint16_t componentId;
uint16_t address;
uint16_t count;
};
struct BinaryRegisterUpdate
{
uint8_t type = 0x02; // BROADCAST_REGISTER_UPDATE
uint8_t slaveId;
uint8_t fc;
uint8_t reserved; // Padding
uint16_t componentId;
uint16_t address;
uint16_t count;
uint16_t value;
};
#pragma pack(pop)
// ----------------------------
class WsTxQueue
{
public:

View File

@ -1355,7 +1355,7 @@ void ModbusRTU::onErrorReceived(Error error, uint32_t token)
const char *errorString = modbusErrorToString(mbError);
if (mbError != MB_Error::Timeout && MB_PRINT_ERRORS)
{
Log.errorln("Modbus Error: Token=%u, Error=%d (%s) Slave %d, address %d", token, (int)error, errorString, slaveIdxOperation, address);
// Log.errorln("Modbus Error: Token=%u, Error=%d (%s) Slave %d, address %d", token, (int)error, errorString, slaveIdxOperation, address);
}
onErrorCallback(*operation, (int)error, errorString);
CBI(operation->flags, OP_USED_BIT);

View File

@ -126,6 +126,31 @@ void PlotBase::seek(uint32_t targetMs)
_startTimeMs = ULONG_MAX - (targetMs - now - 1);
}
}
// Recursively seek children
for (int i = 0; i < MAX_PLOTS; ++i)
{
if (_plots[i] != nullptr)
{
_plots[i]->seek(targetMs);
}
}
}
void PlotBase::slipTime(uint32_t deltaMs)
{
if (_running && !_paused)
{
_startTimeMs += deltaMs;
}
// Recursively slip children
for (int i = 0; i < MAX_PLOTS; ++i)
{
if (_plots[i] != nullptr)
{
_plots[i]->slipTime(deltaMs);
}
}
}
uint32_t PlotBase::getElapsedMs() const
@ -216,6 +241,10 @@ PlotStatus PlotBase::getCurrentStatus() const
{
return PlotStatus::PAUSED;
}
if (_waiting && _running)
{
return PlotStatus::WAITING;
}
if (_running)
{
return PlotStatus::RUNNING;
@ -308,6 +337,7 @@ short PlotBase::startPlots()
_plots[i]->start();
}
}
return E_OK;
}
void PlotBase::onStop()
{
@ -755,7 +785,7 @@ short PlotBase::setup()
const char *group = getModbusGroupName();
m_status.initModbus(_baseAddress + (ushort)PlotBaseRegisterOffset::STATUS, 1, this->id, this->slaveId, E_FN_CODE::FN_READ_HOLD_REGISTER, "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED)", group);
m_status.initModbus(_baseAddress + (ushort)PlotBaseRegisterOffset::STATUS, 1, this->id, this->slaveId, E_FN_CODE::FN_READ_HOLD_REGISTER, "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED, 6:WAITING)", group);
m_status.initNotify(PlotStatus::IDLE, PlotStatus::IDLE, NetworkValue_ThresholdMode::DIFFERENCE);
registerBlock(m_status.getRegisterInfo());

View File

@ -28,7 +28,8 @@ enum class PlotStatus : uint8_t
RUNNING, // Actively running
PAUSED, // Started, but currently paused
STOPPED, // Explicitly stopped by user/logic
FINISHED // Reached or exceeded duration
FINISHED, // Reached or exceeded duration
WAITING, // Waiting for active delays to finish
};
enum class PlotType : uint8_t
@ -82,7 +83,7 @@ public:
PlotBase(Component *owner, ushort componentId, ushort modbusAddress, PlotType plotType = PlotType::Base) : NetworkComponent<PLOT_BASE_BLOCK_COUNT>(modbusAddress, "PlotBase", componentId, Component::COMPONENT_DEFAULT, owner),
_durationMs(0), _startTimeMs(0), _elapsedMsAtPause(0), _running(false), _paused(false), _explicitlyStopped(false), _userData(nullptr),
_parent(nullptr), _eventsDelegate(nullptr), _plotType(plotType), _numControlPoints(0), max(0),
m_status(this, componentId, "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED)", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_status(this, componentId, "Status(0:IDLE, 1:INITIALIZING, 2:RUNNING, 3:PAUSED, 4:STOPPED, 5:FINISHED, 6:WAITING)", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_currentValue(this, componentId, "CurrentValue", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_duration(this, componentId, "Duration", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_elapsed(this, componentId, "Elapsed", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
@ -151,6 +152,13 @@ public:
*/
virtual void seek(uint32_t targetMs);
/**
* @brief Slips the start time by the specified delta to compensate for lag.
* Recursively slips all child plots.
* @param deltaMs The amount of time to slip in milliseconds.
*/
virtual void slipTime(uint32_t deltaMs);
void setEventsDelegate(IPlotEvents *delegate) { _eventsDelegate = delegate; }
// --- Sub-Plot Management ---
@ -170,6 +178,17 @@ public:
*/
bool isPaused() const { return _paused; }
/**
* @brief Checks if the plot is currently waiting (lagging).
* @return true if waiting, false otherwise.
*/
bool isWaiting() const { return _waiting; }
/**
* @brief Sets the waiting status of the plot.
* @param waiting True if waiting, false otherwise.
*/
void setWaiting(bool waiting) { _waiting = waiting; }
/**
* @brief Gets the total duration of the plot.
* @return Plot duration in milliseconds.
@ -311,6 +330,7 @@ protected:
uint32_t _startTimeMs;
bool _running; // True if started and not stopped
bool _paused; // True if pause() called while running
bool _waiting = false; // True if waiting for lag compensation
bool _explicitlyStopped; // True if stop() was called and not superseded by start()
void *_userData; // Pointer for arbitrary user data

View File

@ -40,18 +40,18 @@ short SignalPlot::setup()
short SignalPlot::loop()
{
PlotBase::loop();
// Update all relevant NetworkValues from the base class
PlotStatus currentStatus = getCurrentStatus();
m_status.update(currentStatus);
m_duration.update(getDuration() / 1000); // Assuming duration is in seconds for modbus
m_duration.update(getDuration() / 1000); // Assuming duration is in seconds for modbus
m_remaining.update(getRemainingTime() / 1000); // Same for remaining
uint32_t currentElapsedMs = getElapsedMs();
m_elapsed.update(currentElapsedMs / 1000); // Same for elapsed
// Since SignalPlot doesn't have a value curve, we'll just update with 0
m_currentValue.update(0);
m_currentValue.update(0);
if (!isRunning() || !enabled())
{
@ -70,7 +70,8 @@ short SignalPlot::loop()
return E_OK;
}
bool SignalPlot::load(const JsonObject& config) {
bool SignalPlot::load(const JsonObject &config)
{
PlotBase::load(config);
_numControlPoints = 0; // Reset count before loading new points
for (int i = 0; i < MAX_SIGNAL_POINTS; ++i)
@ -242,12 +243,12 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
return;
}
S_SignalControlPoint &cp = _controlPoints[cpIndex];
if (cp.state == E_SIGNAL_STATE::STATE_ON)
{
return; // Already executed, skip
return; // Already executed, skip
}
switch (cp.type)
{
case E_SIGNAL_TYPE::MB_WRITE_COIL:
@ -276,7 +277,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
if (gpioMode == E_GpioWriteMode::DIGITAL)
{
digitalWrite(pin, (value != 0) ? HIGH : LOW);
cp.state = E_SIGNAL_STATE::STATE_ON;
cp.state = E_SIGNAL_STATE::STATE_ON;
}
else if (gpioMode == E_GpioWriteMode::ANALOG_PWM)
{
@ -302,7 +303,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
}
String cmdStr = String((char *)cp.user);
Bridge *bridge = static_cast<Bridge *>(owner->byId(COMPONENT_KEY_MB_BRIDGE));
if (!bridge)
{
@ -320,7 +321,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
}
case E_SIGNAL_TYPE::DISPLAY_MESSAGE:
{
#ifdef ENABLE_WEBSOCKET
#ifdef ENABLE_WEBSOCKET
String message = cp.description;
if (message.isEmpty())
{
@ -336,7 +337,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
phApp->broadcast(BROADCAST_USER_MESSAGE, doc);
}
cp.state = E_SIGNAL_STATE::STATE_ON;
#endif
#endif
break;
}
case E_SIGNAL_TYPE::PAUSE:
@ -369,7 +370,8 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
case E_SIGNAL_TYPE::STOP_PIDS:
{
PlotBase *parent = getParent();
if(parent){
if (parent)
{
PHApp *phApp = static_cast<PHApp *>(owner);
if (phApp)
{
@ -383,7 +385,8 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
case E_SIGNAL_TYPE::START_PIDS:
{
PlotBase *parent = getParent();
if(parent){
if (parent)
{
PHApp *phApp = static_cast<PHApp *>(owner);
if (phApp)
{
@ -393,7 +396,7 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
cp.state = E_SIGNAL_STATE::STATE_ON;
break;
}
#ifdef ENABLE_FEEDBACK_BUZZER
#ifdef ENABLE_FEEDBACK_BUZZER
case E_SIGNAL_TYPE::BUZZER_OFF:
case E_SIGNAL_TYPE::BUZZER_SOLID:
case E_SIGNAL_TYPE::BUZZER_SLOW_BLINK:
@ -431,11 +434,11 @@ void SignalPlot::executeControlPointAction(uint8_t cpIndex)
}
else
{
cp.state = E_SIGNAL_STATE::STATE_ERROR;
}
break;
cp.state = E_SIGNAL_STATE::STATE_ERROR;
}
#endif
break;
}
#endif
case E_SIGNAL_TYPE::IFTTT_WEBHOOK:
{
#ifdef ENABLE_IFTTT

View File

@ -79,6 +79,7 @@ short TemperatureProfile::loop()
auto handleRunning = [&]()
{
// --- Lag Compensation ---
if (now - _lastLoopExecutionMs < TEMPERATURE_PROFILE_LOOP_INTERVAL_MS)
{
return;
@ -91,6 +92,9 @@ short TemperatureProfile::loop()
}
};
// Check if initializing to reset cache if needed, or do it on start.
// Ideally we clear _omron on stop/start to handle config changes, but it's unlikely to change at runtime.
// --- State Machine ---
switch (currentStatus)
{

View File

@ -19,6 +19,7 @@ class ModbusTCP;
#define TEMPERATURE_PROFILE_LOOP_INTERVAL_MS 1000
#define TEMP_PROFILE_ID_BASE 2000
#define TEMP_REGISTER_NAME_PREFIX "TProf"
#define TEMP_MAX_LAG 1000 * 60 * 5 // 5 minutes
const uint16_t TEMP_PROFILE_REGISTER_COUNT = static_cast<uint16_t>(PlotBaseRegisterOffset::_COUNT);
@ -39,6 +40,10 @@ enum class TemperatureProfileCommand : uint16_t
class TemperatureProfile : public PlotBase
{
public:
uint32_t getLagDuration() const { return _lagDurationMs; }
void setLagDuration(uint32_t duration) { _lagDurationMs = duration; }
uint32_t getLastLogMs() const { return _lastLogMs; }
void setLastLogMs(uint32_t timestamp) { _lastLogMs = timestamp; }
TemperatureProfile(Component *owner, short slot, ushort componentId, ushort modbusBaseAddress);
virtual ~TemperatureProfile() = default;
@ -134,6 +139,12 @@ private:
short _pressureProfileSlotId;
PlotStatus _previousStatus;
// Cached pointer to OmronE5 device for lag compensation
Component *_omron = nullptr;
// Track how long we have been slipping due to lag
uint32_t _lagDurationMs = 0;
};
#endif // TEMPERATURE_PROFILE_H