Add batch debounce, TimeOverride for PlotBase, and min heating duration

This commit is contained in:
babayaga 2026-01-20 07:12:41 +01:00
parent 02faa08df1
commit 899bc545d3
10 changed files with 317 additions and 112 deletions

View File

@ -12,7 +12,8 @@
using namespace JsonUtils;
#define SEQ_LOOP_INTERVAL_MS 50
#define SEQ_LOOP_INTERVAL_MS 60
#define REVERT_TO_HEATUP_DEADBAND 15
short AmperageBudgetManager::reset()
{
@ -23,6 +24,7 @@ short AmperageBudgetManager::reset()
{
_deviceInHeatup[i] = false;
}
_batchDoneConfirmationCount = 0;
return E_OK;
}
@ -41,11 +43,9 @@ AmperageBudgetManager::AmperageBudgetManager(Component *owner, uint16_t baseAddr
_currentIndex(0),
m_minHeatingDurationS(this, this->id, "MinHeatingDurationS"),
m_maxHeatingDurationS(this, this->id, "MaxHeatingDurationS"),
m_maxHeatingDurationOscillatingS(this, this->id, "MaxHeatingDurationOscillatingS"),
m_maxSimultaneousHeating(this, this->id, "MaxSimultaneousHeating"),
m_windowOffset(this, this->id, "WindowOffset"),
m_mode(this, this->id, "Mode(0:Cycle All,1:Cycle SP,2:Any SP,3:Most Urgent (Recommended))"),
m_postHeatupMode(this, this->id, "PostHeatupMode"),
m_startIndex(this, this->id, "StartIndex"),
m_endIndex(this, this->id, "EndIndex"),
m_opFlags(this, this->id, "OpFlags"),
@ -54,7 +54,8 @@ AmperageBudgetManager::AmperageBudgetManager(Component *owner, uint16_t baseAddr
_heatupPhaseComplete(false),
_canUseCallback(nullptr),
_stoppingIndex(-1),
_lastStopTimestamp(0)
_lastStopTimestamp(0),
_batchDoneConfirmationCount(0)
{
pFlags = E_PersistenceFlags::E_PF_ENABLED;
for (uint8_t i = 0; i < MAX_MANAGED_DEVICES; ++i)
@ -107,10 +108,6 @@ short AmperageBudgetManager::setup()
m_maxHeatingDurationS.initModbus(baseAddr + REG_OFFSET_MAX_TIME, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "MaxTime", this->name.c_str());
registerBlock(m_maxHeatingDurationS.getRegisterInfo());
m_maxHeatingDurationOscillatingS.initNotify(DEFAULT_MAX_HEATING_OSCILLATING_S, 1, NetworkValue_ThresholdMode::DIFFERENCE);
m_maxHeatingDurationOscillatingS.initModbus(baseAddr + REG_OFFSET_MAX_TIME_OSCILLATING, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "MaxTimeOscillating", this->name.c_str());
registerBlock(m_maxHeatingDurationOscillatingS.getRegisterInfo());
m_maxSimultaneousHeating.initNotify(DEFAULT_MAX_SIMULTANEOUS_HEATING, 1, NetworkValue_ThresholdMode::DIFFERENCE);
m_maxSimultaneousHeating.initModbus(baseAddr + REG_OFFSET_MAX_SIM, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "MaxSim", this->name.c_str());
registerBlock(m_maxSimultaneousHeating.getRegisterInfo());
@ -123,10 +120,6 @@ short AmperageBudgetManager::setup()
m_mode.initModbus(baseAddr + REG_OFFSET_MODE, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "Mode(0:Cycle All,1:Cycle SP,2:Any SP,3:Most Urgent)", this->name.c_str());
registerBlock(m_mode.getRegisterInfo());
m_postHeatupMode.initNotify(E_AM_CYCLE_ALL, (E_AMPERAGE_MODE)1, NetworkValue_ThresholdMode::DIFFERENCE);
m_postHeatupMode.initModbus(baseAddr + REG_OFFSET_POST_HEATUP_MODE, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "PostHeatupMode", this->name.c_str());
registerBlock(m_postHeatupMode.getRegisterInfo());
m_startIndex.initNotify(DEFAULT_START_INDEX, 1, NetworkValue_ThresholdMode::DIFFERENCE);
m_startIndex.initModbus(baseAddr + REG_OFFSET_START_INDEX, 1, this->id, this->slaveId, FN_WRITE_HOLD_REGISTER, "StartIndex", this->name.c_str());
registerBlock(m_startIndex.getRegisterInfo());
@ -161,7 +154,9 @@ bool AmperageBudgetManager::_checkHeatup(OmronE5 *device)
{
return false;
}
if (device->hasError())
// Fix: hasError() is cumulative. We only want to stop if the device is truly unreachable (Timeout).
// Other errors (CRC, Collision) are transient and should be ignored to prevent premature abortion.
if (device->getLastErrorCode() == (uint16_t)MB_Error::Timeout)
{
return false;
}
@ -206,7 +201,7 @@ void AmperageBudgetManager::_loopCycleAll()
while (devicesInWindow < effectiveMaxSimultaneous)
{
if (tempIndex < _numDevices && _devices[tempIndex] && _devices[tempIndex]->enabled())
if (tempIndex < _numDevices && _devices[tempIndex] && _devices[tempIndex]->enabled() && _checkHeatup(_devices[tempIndex]))
{
windowDeviceIndices.push_back(tempIndex);
devicesInWindow++;
@ -290,7 +285,7 @@ void AmperageBudgetManager::_loopCycleAll()
}
bool isPastHeatup = !_deviceInHeatup[deviceIndex];
uint32_t maxDuration = isPastHeatup ? m_maxHeatingDurationOscillatingS.getValue() : m_maxHeatingDurationS.getValue();
uint32_t maxDuration = m_maxHeatingDurationS.getValue();
if (isPastHeatup && !device->isHeating())
{
@ -371,7 +366,7 @@ void AmperageBudgetManager::_loopCycleSp()
while (devicesInWindow < effectiveMaxSimultaneous)
{
if (tempIndex < _numDevices && _devices[tempIndex] && _devices[tempIndex]->enabled())
if (tempIndex < _numDevices && _devices[tempIndex] && _devices[tempIndex]->enabled() && _checkHeatup(_devices[tempIndex]))
{
indices.push_back(tempIndex);
devicesInWindow++;
@ -436,8 +431,18 @@ void AmperageBudgetManager::_loopCycleSp()
if (allInWindowAreDone)
{
advanceCurrentIndex();
windowIndices = getWindowDeviceIndices();
// Debounce: Only advance if the "Done" state persists to avoid deadband flutter
_batchDoneConfirmationCount++;
if (_batchDoneConfirmationCount >= BATCH_DONE_THRESHOLD)
{
advanceCurrentIndex();
windowIndices = getWindowDeviceIndices();
_batchDoneConfirmationCount = 0;
}
}
else
{
_batchDoneConfirmationCount = 0;
}
for (uint8_t i = 0; i < _numDevices; i++)
@ -472,12 +477,6 @@ void AmperageBudgetManager::_loopCycleSp()
{
_devices[i]->stop();
_deviceHeating[i] = false;
// Note: Stop also counts as an action?
// If we are strictly sequential, yes.
// But stops are often "safe defaults".
// Given "break may flip", turning ON is the danger.
// But user said "updates ( stop / run )".
// So we return here too.
return;
}
}
@ -526,6 +525,13 @@ void AmperageBudgetManager::_loopCycleSpAny()
{
if (_deviceHeating[i])
{
// Enforce minimum heating duration to prevent rapid cycling
uint32_t now_s = now / 1000;
if (now_s - _deviceStartTimes[i] < m_minHeatingDurationS.getValue())
{
continue; // Keep running
}
_devices[i]->stop();
_deviceHeating[i] = false;
return; // One action per cycle
@ -565,13 +571,51 @@ void AmperageBudgetManager::_loopCycleSpMostUrgent()
std::sort(urgentDevices.begin(), urgentDevices.end());
std::vector<uint8_t> devicesToHeat;
for (size_t i = 0; i < urgentDevices.size() && i < m_maxSimultaneousHeating.getValue(); ++i)
uint32_t now_s = now / 1000;
std::vector<uint8_t> lingeringIndices;
// 1. Identify "Lingering" devices (must keep running due to MinDuration)
for (uint8_t i = 0; i < _numDevices; i++)
{
devicesToHeat.push_back(urgentDevices[i].index);
if (_devices[i] && _devices[i]->enabled() && _deviceHeating[i])
{
if (now_s - _deviceStartTimes[i] < m_minHeatingDurationS.getValue())
{
lingeringIndices.push_back(i);
}
}
}
uint32_t now_s = now / 1000;
std::vector<uint8_t> devicesToHeat;
// 2. Reserve slots for lingering devices
for (uint8_t idx : lingeringIndices)
{
devicesToHeat.push_back(idx);
}
// 3. Fill remaining slots with most urgent devices
for (size_t i = 0; i < urgentDevices.size(); ++i)
{
if (devicesToHeat.size() >= m_maxSimultaneousHeating.getValue())
{
break;
}
bool isLingering = false;
for (uint8_t lIdx : lingeringIndices)
{
if (urgentDevices[i].index == lIdx)
{
isLingering = true;
break;
}
}
if (!isLingering)
{
devicesToHeat.push_back(urgentDevices[i].index);
}
}
for (uint8_t i = 0; i < _numDevices; i++)
{
@ -598,13 +642,27 @@ void AmperageBudgetManager::_loopCycleSpMostUrgent()
if (_deviceHeating[i])
{
bool isPastHeatup = !_deviceInHeatup[i];
uint32_t maxDuration = isPastHeatup ? m_maxHeatingDurationOscillatingS.getValue() : m_maxHeatingDurationS.getValue();
uint32_t maxDuration = m_maxHeatingDurationS.getValue();
uint32_t elapsed = now_s - _deviceStartTimes[i];
if (!shouldHeat || (now_s - _deviceStartTimes[i] >= maxDuration))
if (elapsed >= maxDuration)
{
_devices[i]->stop();
_deviceHeating[i] = false;
return; // One action per cycle
return; // Action taken
}
if (!shouldHeat)
{
// Enforce minimum heating duration
if (elapsed < m_minHeatingDurationS.getValue())
{
continue; // Keep running
}
_devices[i]->stop();
_deviceHeating[i] = false;
return; // Action taken
}
}
else
@ -732,11 +790,12 @@ short AmperageBudgetManager::loop()
short AmperageBudgetManager::info(short val0, short val1)
{
/*
L_INFO(F("[%s] Devices: %d/%d, Current Index: %d\n"),
_name.c_str(), _numDevices, MAX_MANAGED_DEVICES, _currentIndex);
L_INFO(F(" Min Time: %lu s, Max Time: %lu s, Max Oscillating Time: %lu s\n"),
m_minHeatingDurationS.getValue(), m_maxHeatingDurationS.getValue(), m_maxHeatingDurationOscillatingS.getValue());
L_INFO(F(" Min Time: %lu s, Max Time: %lu s\n"),
m_minHeatingDurationS.getValue(), m_maxHeatingDurationS.getValue());
L_INFO(F(" Max Simultaneous: %d, Window Offset: %d\n"),
m_maxSimultaneousHeating.getValue(), m_windowOffset.getValue());
L_INFO(F(" Start Index: %d, End Index: %d\n"),
@ -825,7 +884,7 @@ short AmperageBudgetManager::info(short val0, short val1)
Log.notice(F(" Device %d: Not heating\n"), i);
}
}
*/
return 0;
}
@ -950,38 +1009,7 @@ short AmperageBudgetManager::mb_tcp_write(MB_Registers *reg, short value)
return E_INVALID_PARAMETER;
}
}
else if (address == (_baseAddress + REG_OFFSET_MAX_TIME_OSCILLATING))
{
if (_validateMaxTimeOscillating(value))
{
if (m_maxHeatingDurationOscillatingS.getValue() != (uint16_t)value)
{
m_maxHeatingDurationOscillatingS.update(value);
reset();
changed = true;
}
}
else
{
return E_INVALID_PARAMETER;
}
}
else if (address == (_baseAddress + REG_OFFSET_POST_HEATUP_MODE))
{
if (value >= E_AM_CYCLE_ALL && value <= E_AM_CYCLE_SP_MOST_URGENT)
{
if (m_postHeatupMode.getValue() != (E_AMPERAGE_MODE)value)
{
m_postHeatupMode.update((E_AMPERAGE_MODE)value);
// No reset needed for this one as per original code
changed = true;
}
}
else
{
return E_INVALID_PARAMETER;
}
}
else if (address == (_baseAddress + REG_OFFSET_OP_FLAGS))
{
if (m_opFlags.getValue() != (uint16_t)value)
@ -1059,14 +1087,7 @@ short AmperageBudgetManager::mb_tcp_read(MB_Registers *reg)
{
return m_mode.getValue();
}
if (address == (_baseAddress + REG_OFFSET_MAX_TIME_OSCILLATING))
{
return m_maxHeatingDurationOscillatingS.getValue();
}
if (address == (_baseAddress + REG_OFFSET_POST_HEATUP_MODE))
{
return m_postHeatupMode.getValue();
}
if (address == (_baseAddress + REG_OFFSET_OP_FLAGS))
{
return m_opFlags.getValue();
@ -1100,7 +1121,7 @@ void AmperageBudgetManager::toJson(JsonDocument &doc) const
obj["minHeatingDurationS"] = m_minHeatingDurationS.getValue();
obj["maxHeatingDurationS"] = m_maxHeatingDurationS.getValue();
obj["maxHeatingDurationOscillatingS"] = m_maxHeatingDurationOscillatingS.getValue();
obj["maxSimultaneousHeating"] = m_maxSimultaneousHeating.getValue();
obj["windowOffset"] = m_windowOffset.getValue();
obj["enabled"] = const_cast<AmperageBudgetManager *>(this)->enabled();
@ -1114,7 +1135,6 @@ void AmperageBudgetManager::toJson(JsonDocument &doc) const
{
obj["mode"] = m_mode.getValue();
}
obj["postHeatupMode"] = m_postHeatupMode.getValue();
obj["opFlags"] = m_opFlags.getValue();
}
@ -1127,7 +1147,7 @@ bool AmperageBudgetManager::fromJson(const JsonObject &json)
}
uint32_t minHeatingDurationS = m_minHeatingDurationS.getValue();
uint32_t maxHeatingDurationS = m_maxHeatingDurationS.getValue();
uint32_t maxHeatingDurationOscillatingS = m_maxHeatingDurationOscillatingS.getValue();
uint8_t maxSimultaneousHeating = m_maxSimultaneousHeating.getValue();
uint8_t windowOffset = m_windowOffset.getValue();
@ -1135,8 +1155,7 @@ bool AmperageBudgetManager::fromJson(const JsonObject &json)
m_minHeatingDurationS.applyUpdate(minHeatingDurationS);
JsonUtils::parseJsonFieldUint32(json, "maxHeatingDurationS", maxHeatingDurationS, "maxHeatingDurationS", _name.c_str());
m_maxHeatingDurationS.applyUpdate(maxHeatingDurationS);
JsonUtils::parseJsonFieldUint32(json, "maxHeatingDurationOscillatingS", maxHeatingDurationOscillatingS, "maxHeatingDurationOscillatingS", _name.c_str());
m_maxHeatingDurationOscillatingS.applyUpdate(maxHeatingDurationOscillatingS);
JsonUtils::parseJsonFieldUint8(json, "maxSimultaneousHeating", maxSimultaneousHeating, "maxSimultaneousHeating", _name.c_str());
m_maxSimultaneousHeating.applyUpdate(maxSimultaneousHeating);
JsonUtils::parseJsonFieldUint8(json, "windowOffset", windowOffset, "windowOffset", _name.c_str());
@ -1153,13 +1172,6 @@ bool AmperageBudgetManager::fromJson(const JsonObject &json)
JsonUtils::parseJsonFieldUint32(json, "opFlags", tempOpFlags32, "opFlags", _name.c_str());
m_opFlags.applyUpdate((uint16_t)tempOpFlags32);
uint8_t tempPostHeatupMode = m_postHeatupMode.getValue();
JsonUtils::parseJsonFieldUint8(json, "postHeatupMode", tempPostHeatupMode, "postHeatupMode", _name.c_str());
if (tempPostHeatupMode >= E_AM_CYCLE_ALL && tempPostHeatupMode <= E_AM_CYCLE_SP_MOST_URGENT)
{
m_postHeatupMode.applyUpdate((E_AMPERAGE_MODE)tempPostHeatupMode);
}
uint8_t tempStartIndex = m_startIndex.getValue();
uint8_t tempEndIndex = m_endIndex.getValue();
JsonUtils::parseJsonFieldUint8(json, "startIndex", tempStartIndex, "startIndex", _name.c_str());
@ -1271,15 +1283,15 @@ void AmperageBudgetManager::onHeatupComplete()
{
if (!_heatupPhaseComplete)
{
L_INFO(F("[%s] First heat-up complete. Switching mode from %d to %d"), _name.c_str(), m_mode.getValue(), m_postHeatupMode.getValue());
_initialMode = m_mode.getValue();
m_mode.update(m_postHeatupMode.getValue());
_heatupPhaseComplete = true;
}
}
void AmperageBudgetManager::_checkAllDevicesForHeatupCompletion()
{
bool anyDeviceInHeatup = false;
bool excessiveDropDetected = false;
for (uint8_t i = 0; i < _numDevices; ++i)
{
OmronE5 *device = _devices[i];
@ -1287,17 +1299,90 @@ void AmperageBudgetManager::_checkAllDevicesForHeatupCompletion()
continue;
bool isCurrentlyInHeatup = _checkHeatup(device);
if (_deviceInHeatup[i] && !isCurrentlyInHeatup)
_deviceInHeatup[i] = isCurrentlyInHeatup;
if (isCurrentlyInHeatup)
{
anyDeviceInHeatup = true;
if (_heatupPhaseComplete)
{
// A device has fallen behind after the initial heatup phase was complete.
// Only revert if the drop is significant to avoid oscillation.
uint16_t sp, pv;
// Access OmronE5 SP/PV safely
if (device->getSP(sp) && device->getPV(pv))
{
// Unsigned arithmetic safety check
if (sp > pv && (sp - pv) > REVERT_TO_HEATUP_DEADBAND)
{
excessiveDropDetected = true;
L_INFO(F("[%s] Device %d dropped significantly below SP (Diff > %d). Reverting to initial mode %d"), _name.c_str(), i, REVERT_TO_HEATUP_DEADBAND, _initialMode);
}
}
}
}
}
if (!_heatupPhaseComplete)
{
if (!anyDeviceInHeatup)
{
onHeatupComplete();
}
else if (_heatupPhaseComplete && isCurrentlyInHeatup && !_deviceInHeatup[i])
}
else
{
if (excessiveDropDetected)
{
// A device has fallen behind after the initial heatup phase was complete.
m_mode.update(_initialMode);
_heatupPhaseComplete = false; // Allow the cycle to complete again
}
_deviceInHeatup[i] = isCurrentlyInHeatup;
}
}
size_t AmperageBudgetManager::getBinaryState(uint8_t *buffer, size_t maxLen)
{
if (maxLen < sizeof(StatePacket))
{
return 0;
}
StatePacket *packet = (StatePacket *)buffer;
packet->timestamp = millis();
packet->currentIdx = _currentIndex;
packet->numDevices = _numDevices;
packet->heatupComplete = _heatupPhaseComplete ? 1 : 0;
// Zero out devices first
memset(packet->devices, 0, sizeof(packet->devices));
uint32_t now_s = millis() / 1000;
for (uint8_t i = 0; i < _numDevices && i < NUM_OMRON_DEVICES; ++i)
{
packet->devices[i].index = i;
if (_devices[i])
{
packet->devices[i].enabled = _devices[i]->enabled() ? 1 : 0;
}
else
{
packet->devices[i].enabled = 0;
}
packet->devices[i].heating = _deviceHeating[i] ? 1 : 0;
packet->devices[i].heatup = _deviceInHeatup[i] ? 1 : 0;
if (_deviceHeating[i])
{
packet->devices[i].elapsed = now_s - _deviceStartTimes[i];
}
else
{
packet->devices[i].elapsed = 0;
}
}
return sizeof(StatePacket);
}
#endif

View File

@ -26,8 +26,8 @@ class OmronE5;
#define DEFAULT_WINDOW_OFFSET 1 // Default window offset
#define DEFAULT_START_INDEX 0 // Default start index for cycling
#define DEFAULT_END_INDEX (MAX_MANAGED_DEVICES - 1) // Default end index for cycling
#define DEFAULT_MAX_HEATING_OSCILLATING_S 15 // 60 seconds
#define STOP_ALL_DEVICES_WAIT_MS 50 // Wait time after stopping all devices
#define STOP_ALL_DEVICES_WAIT_MS 50 // Wait time after stopping all devices
// Modbus write boundaries
#define MB_MAX_TIME_MIN_S 1 // Minimum max time: 1s
@ -36,7 +36,7 @@ class OmronE5;
#define MB_MIN_TIME_MIN_S 1 // Minimum min time: 1s
#define MB_MIN_TIME_MAX_S 60 // Maximum min time: 60s
#define AMP_BUDGET_MB_COUNT 12 // m_enabled + 11 custom values
#define AMP_BUDGET_MB_COUNT 11 // m_enabled + 10 custom values
enum E_AMPERAGE_MODE
{
@ -70,8 +70,6 @@ public:
REG_OFFSET_START_INDEX,
REG_OFFSET_END_INDEX,
REG_OFFSET_MODE,
REG_OFFSET_MAX_TIME_OSCILLATING,
REG_OFFSET_POST_HEATUP_MODE,
REG_OFFSET_OP_FLAGS
};
@ -98,6 +96,26 @@ public:
virtual void onCycleEnd(const std::vector<OmronE5 *> &activeDevices);
virtual void onHeatupComplete();
struct DeviceStatePacket
{
uint8_t index;
uint8_t enabled;
uint8_t heating;
uint8_t heatup;
uint32_t elapsed;
} __attribute__((packed));
struct StatePacket
{
uint32_t timestamp;
uint8_t currentIdx;
uint8_t numDevices;
uint8_t heatupComplete;
DeviceStatePacket devices[NUM_OMRON_DEVICES];
} __attribute__((packed));
size_t getBinaryState(uint8_t *buffer, size_t maxLen);
// Max simultaneous heating control
uint8_t getMaxSimultaneousHeating() const { return m_maxSimultaneousHeating.getValue(); }
void setMaxSimultaneousHeating(uint8_t value)
@ -134,11 +152,9 @@ private:
// Configurable parameters
NetworkValue<uint16_t> m_minHeatingDurationS;
NetworkValue<uint16_t> m_maxHeatingDurationS;
NetworkValue<uint16_t> m_maxHeatingDurationOscillatingS;
NetworkValue<uint8_t> m_maxSimultaneousHeating;
NetworkValue<uint8_t> m_windowOffset;
NetworkValue<E_AMPERAGE_MODE> m_mode;
NetworkValue<E_AMPERAGE_MODE> m_postHeatupMode;
NetworkValue<uint8_t> m_startIndex;
NetworkValue<uint8_t> m_endIndex;
NetworkValue<uint16_t> m_opFlags;
@ -168,6 +184,10 @@ private:
void _loopCycleSpAny();
void _loopCycleSpMostUrgent();
// Batch mode debounce
uint16_t _batchDoneConfirmationCount;
static const uint16_t BATCH_DONE_THRESHOLD = 20; // ~1.2s at 60ms loop
// Validation methods
bool _validateMaxTime(short value) const
{
@ -177,10 +197,7 @@ private:
{
return value >= MB_MIN_TIME_MIN_S && value <= MB_MIN_TIME_MAX_S;
}
bool _validateMaxTimeOscillating(short value) const
{
return value >= 1 && value <= 3600; // 1s to 1hr
}
bool _validateMaxSim(short value) const
{
return value >= 1 && value <= MAX_MANAGED_DEVICES;

View File

@ -108,6 +108,7 @@ short Loadcell::info()
bool zero_volt_ok = getZeroVoltage(zero_volt_val);
L_INFO(F("--- Loadcell[%d] Info ---"), slaveId);
L_INFO(F(" State: %s, Mode: %d"), getStateString(), _mode);
L_INFO(F(" Last Error: %d"), getLastErrorCode());
L_INFO(F(" Real-time Net Weight: %s (%lu) raw: H=%d L=%d"), rt_ok ? "OK" : "Error/Missing", rt_ok ? rt_val : 0, _weightHigh, _weightLow);
L_INFO(F(" Real-time Voltage: %s (%lu) raw: H=%u L=%u"), volt_ok ? "OK" : "Error/Missing", volt_ok ? volt_val : 0, _voltageHigh, _voltageLow);
L_INFO(F(" Zero Voltage: %s (%lu) raw: H=%u L=%u"), zero_volt_ok ? "OK" : "Error/Missing", zero_volt_ok ? zero_volt_val : 0, _zeroVoltageHigh, _zeroVoltageLow);

View File

@ -41,7 +41,8 @@ OmronE5::OmronE5(Component *owner, uint8_t slaveId, millis_t readInterval)
INIT_MODBUS_NETWORK_VALUE(m_statusHigh, "Status High", 0, 1, NetworkValue_ThresholdMode::DIFFERENCE, nullptr),
INIT_MODBUS_NETWORK_VALUE(m_runState, "Run/Stop", true, 1, NetworkValue_ThresholdMode::DIFFERENCE, nullptr), // true = stopped
INIT_MODBUS_NETWORK_VALUE(m_enabled, "Enabled", true, 1, NetworkValue_ThresholdMode::DIFFERENCE, nullptr),
INIT_MODBUS_NETWORK_VALUE(m_commsWritingEnabled, "CommsWrite", true, 1, NetworkValue_ThresholdMode::DIFFERENCE, nullptr)
INIT_MODBUS_NETWORK_VALUE(m_commsWritingEnabled, "CommsWrite", true, 1, NetworkValue_ThresholdMode::DIFFERENCE, nullptr),
_lastRunStopCmdTime(0)
{
m_modbusHelper.init(this);
type = COMPONENT_TYPE::COMPONENT_TYPE_PID;
@ -354,8 +355,7 @@ short OmronE5::setup()
#endif
this->addOutputRegister(OR_E5_SWR_SP, E_FN_CODE::FN_WRITE_HOLD_REGISTER, 0);
this->addOutputRegister(OR_E5_OPERATION_COMMAND_REGISTER, E_FN_CODE::FN_WRITE_HOLD_REGISTER, BUILD_OMRON_OP_COMMAND(OP_CODE_COMMS_WRITE, COMMS_WRITE_ENABLED), PRIORITY_HIGHEST);
this->addOutputRegister(OR_E5_OPERATION_COMMAND_REGISTER, E_FN_CODE::FN_WRITE_HOLD_REGISTER, BUILD_OMRON_OP_COMMAND(OP_CODE_COMMS_WRITE, COMMS_WRITE_ENABLED), PRIORITY_MEDIUM);
// Ensure communication writing is enabled on setup
// setCommsWriting(true);
@ -582,16 +582,47 @@ bool OmronE5::setSP(uint16_t value)
setOutputRegisterValue(spAddr, clampedValue);
return true;
}
bool OmronE5::run()
bool OmronE5::run(bool force)
{
if (!force)
{
if (isRunning())
{
return true;
}
if (millis() - _lastRunStopCmdTime < OMRON_RUN_STOP_THROTTLE_MS)
{
return true; // Throttle
}
}
// L_INFO(F("OmronE5[%d]::run - Sending RUN_CMD"), slaveId);
_sendOperationCommand(OP_CODE_RUN_STOP, RUN_CMD);
_lastRunStopCmdTime = millis();
return true;
}
short OmronE5::stop()
{
return stop(false);
}
short OmronE5::stop(bool force)
{
if (!force)
{
if (!isRunning())
{
return true;
}
if (millis() - _lastRunStopCmdTime < OMRON_RUN_STOP_THROTTLE_MS)
{
return true; // Throttle
}
}
// L_INFO(F("OmronE5[%d]::stop - Sending STOP_CMD"), slaveId);
_sendOperationCommand(OP_CODE_RUN_STOP, STOP_CMD);
_lastRunStopCmdTime = millis();
return true;
}
@ -648,6 +679,7 @@ bool OmronE5::onRegisterUpdate(uint16_t address, uint16_t newValue)
if (RTU_Base::onRegisterUpdate(address, newValue))
updated = true;
return updated;
}

View File

@ -18,6 +18,7 @@ using OmronValue = NetworkValue<uint16_t>;
using OmronBoolValue = NetworkValue<bool>;
#define HEATUP_DEADBAND 5
#define OMRON_RUN_STOP_THROTTLE_MS 250
#define OMRON_E5_READ_BLOCK_START_ADDR 0x0000
#define OMRON_E5_READ_BLOCK_REG_COUNT 6
@ -92,8 +93,9 @@ public:
uint32_t getTotalWh() const;
// --- Setters (Optional - Implement if needed) ---
bool setSP(uint16_t value);
bool run();
bool run(bool force = false);
short stop() override;
short stop(bool force);
bool setCommsWriting(bool enabled);
uint32_t getConsumption() const;
@ -181,6 +183,7 @@ private:
private:
void _sendOperationCommand(OR_E5_OPERATION_CODE code, uint8_t data);
millis_t _lastRunStopCmdTime = 0;
};
#endif // ENABLE_RS485

View File

@ -125,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_CHECK_LOADCELL
E_PC_OP_ALL = E_PC_OP_CHECK_MAX_TIME
};
enum E_PC_OutputMode
{

View File

@ -772,6 +772,22 @@ short PlotBase::mb_tcp_write(MB_Registers *reg, short value)
m_elapsed.update(value);
return E_OK;
}
else if (offset == static_cast<short>(PlotBaseRegisterOffset::TIME_OVERRIDE))
{
int16_t currentOverride = m_timeOverride.getValue();
int16_t newOverride = static_cast<int16_t>(value);
int16_t deltaMinutes = newOverride - currentOverride;
if (deltaMinutes != 0)
{
// Calculate delta in milliseconds
int32_t deltaMs = (int32_t)deltaMinutes * 60000;
slipTime(deltaMs);
m_timeOverride.update(newOverride);
Log.noticeln("PlotBase %s: Time override applied. Delta: %d min, New Override: %d min", name.c_str(), deltaMinutes, newOverride);
}
return E_OK;
}
// For any other write, let the base NetworkComponent handle it.
// This will find the corresponding NetworkValue and update it.
@ -797,6 +813,11 @@ short PlotBase::setup()
m_duration.initNotify(0, 1, NetworkValue_ThresholdMode::DIFFERENCE);
registerBlock(m_duration.getRegisterInfo());
m_timeOverride.initModbus(_baseAddress + (ushort)PlotBaseRegisterOffset::TIME_OVERRIDE, 1, this->id, this->slaveId, E_FN_CODE::FN_WRITE_HOLD_REGISTER, "TimeOverride", group);
m_timeOverride.initNotify(0, 1, NetworkValue_ThresholdMode::DIFFERENCE);
registerBlock(m_timeOverride.getRegisterInfo());
m_timeOverride.update(0);
m_elapsed.initModbus(_baseAddress + (ushort)PlotBaseRegisterOffset::ELAPSED, 1, this->id, this->slaveId, E_FN_CODE::FN_WRITE_HOLD_REGISTER, "Elapsed", group);
m_elapsed.initNotify(0, 1, NetworkValue_ThresholdMode::DIFFERENCE);
registerBlock(m_elapsed.getRegisterInfo());
@ -808,6 +829,7 @@ short PlotBase::setup()
m_command.initModbus(_baseAddress + (ushort)PlotBaseRegisterOffset::COMMAND, 1, this->id, this->slaveId, E_FN_CODE::FN_WRITE_HOLD_REGISTER, "Command", group);
m_command.initNotify(0, 1, NetworkValue_ThresholdMode::DIFFERENCE);
registerBlock(m_command.getRegisterInfo());
m_command.update(0);
return E_OK;
}

View File

@ -16,7 +16,7 @@
#include "config.h"
#include "net/commons.h"
#define PLOT_BASE_BLOCK_COUNT static_cast<uint16_t>(PlotBaseRegisterOffset::_COUNT)
#define PLOT_BASE_BLOCK_COUNT (static_cast<uint16_t>(PlotBaseRegisterOffset::_COUNT) + 3)
//------------------------------------------------------------------------------
// Status Enum
@ -58,6 +58,7 @@ enum class PlotBaseRegisterOffset : uint8_t
ELAPSED,
REMAINING,
COMMAND,
TIME_OVERRIDE,
_COUNT
};
@ -88,7 +89,8 @@ public:
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)),
m_remaining(this, componentId, "Remaining", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_command(this, componentId, "Command", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
m_command(this, componentId, "Command", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL)),
m_timeOverride(this, componentId, "TimeOverride", static_cast<uint8_t>(E_NetworkValueFeatureFlags::E_NVFF_ALL))
{
this->type = COMPONENT_TYPE_PLOT;
for (int i = 0; i < MAX_PLOTS; ++i)
@ -104,6 +106,7 @@ public:
addNetworkValue(&m_elapsed);
addNetworkValue(&m_remaining);
addNetworkValue(&m_command);
addNetworkValue(&m_timeOverride);
}
virtual ~PlotBase() = default;
@ -308,6 +311,7 @@ protected:
NetworkValue<uint16_t> m_elapsed;
NetworkValue<uint16_t> m_remaining;
NetworkValue<uint16_t> m_command;
NetworkValue<int16_t> m_timeOverride;
virtual const char *getModbusNamePrefix() const { return getOwnPrefix(); }
virtual const char *getModbusGroupName() const { return this->name.c_str(); }

View File

@ -305,4 +305,42 @@ void TemperatureProfile::clearTargetOffsets()
_targetOffsets.reserve(NUM_OMRON_DEVICES);
#endif
}
void TemperatureProfile::resolveLinkedProfiles()
{
PHApp *app = (PHApp *)owner;
if (!app)
{
return;
}
#ifdef ENABLE_PROFILE_PRESSURE
if (_pressureProfileSlotId >= 0 && _pressureProfileSlotId < PROFILE_PRESSURE_COUNT)
{
PressureProfile *p = app->pressureProfiles[_pressureProfileSlotId];
if (p)
{
addPlot(p);
}
}
#endif
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
if (_signalPlotSlotId >= 0 && _signalPlotSlotId < PROFILE_SIGNAL_PLOT_COUNT)
{
SignalPlot *s = app->signalPlots[_signalPlotSlotId];
if (s)
{
addPlot(s);
}
}
#endif
}
void TemperatureProfile::onStart()
{
resolveLinkedProfiles();
PlotBase::onStart();
}
#endif // ENABLE_PROFILE_TEMPERATURE

View File

@ -51,10 +51,12 @@ public:
short loop() override;
short info() override;
// short start() override;
void onStart() override;
void sample();
// void mb_tcp_register(ModbusTCP *manager) override;
// short mb_tcp_write(MB_Registers *reg, short value) override;
/**
* @brief Loads temperature profile specific data (controlPoints) from JSON.
@ -107,6 +109,7 @@ public:
void setPressureProfileSlotId(short slotId) { _pressureProfileSlotId = slotId; }
// void updateOmronSetpoints(PHApp* app, uint16_t value);
void resolveLinkedProfiles();
protected:
const char *getOwnPrefix() const override;