diff --git a/src/components/AmperageBudgetManager.cpp b/src/components/AmperageBudgetManager.cpp index 703e615c..fb0d0c53 100644 --- a/src/components/AmperageBudgetManager.cpp +++ b/src/components/AmperageBudgetManager.cpp @@ -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 devicesToHeat; - for (size_t i = 0; i < urgentDevices.size() && i < m_maxSimultaneousHeating.getValue(); ++i) + uint32_t now_s = now / 1000; + std::vector 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 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(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 \ No newline at end of file diff --git a/src/components/AmperageBudgetManager.h b/src/components/AmperageBudgetManager.h index 6d61be79..ec956955 100644 --- a/src/components/AmperageBudgetManager.h +++ b/src/components/AmperageBudgetManager.h @@ -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 &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 m_minHeatingDurationS; NetworkValue m_maxHeatingDurationS; - NetworkValue m_maxHeatingDurationOscillatingS; NetworkValue m_maxSimultaneousHeating; NetworkValue m_windowOffset; NetworkValue m_mode; - NetworkValue m_postHeatupMode; NetworkValue m_startIndex; NetworkValue m_endIndex; NetworkValue 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; diff --git a/src/components/Loadcell.cpp b/src/components/Loadcell.cpp index 72a2cd50..c1db3708 100644 --- a/src/components/Loadcell.cpp +++ b/src/components/Loadcell.cpp @@ -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); diff --git a/src/components/OmronE5.cpp b/src/components/OmronE5.cpp index 7630e68b..c5669fd9 100644 --- a/src/components/OmronE5.cpp +++ b/src/components/OmronE5.cpp @@ -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; } diff --git a/src/components/OmronE5.h b/src/components/OmronE5.h index 43da53b6..b799e524 100644 --- a/src/components/OmronE5.h +++ b/src/components/OmronE5.h @@ -18,6 +18,7 @@ using OmronValue = NetworkValue; using OmronBoolValue = NetworkValue; #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 diff --git a/src/components/PressCylinder.h b/src/components/PressCylinder.h index 518315be..b5c35812 100644 --- a/src/components/PressCylinder.h +++ b/src/components/PressCylinder.h @@ -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 { diff --git a/src/profiles/PlotBase.cpp b/src/profiles/PlotBase.cpp index 5dbe3f8b..ea961b4a 100644 --- a/src/profiles/PlotBase.cpp +++ b/src/profiles/PlotBase.cpp @@ -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(PlotBaseRegisterOffset::TIME_OVERRIDE)) + { + int16_t currentOverride = m_timeOverride.getValue(); + int16_t newOverride = static_cast(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; } diff --git a/src/profiles/PlotBase.h b/src/profiles/PlotBase.h index e3f110ba..7b76585f 100644 --- a/src/profiles/PlotBase.h +++ b/src/profiles/PlotBase.h @@ -16,7 +16,7 @@ #include "config.h" #include "net/commons.h" -#define PLOT_BASE_BLOCK_COUNT static_cast(PlotBaseRegisterOffset::_COUNT) +#define PLOT_BASE_BLOCK_COUNT (static_cast(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(E_NetworkValueFeatureFlags::E_NVFF_ALL)), m_elapsed(this, componentId, "Elapsed", static_cast(E_NetworkValueFeatureFlags::E_NVFF_ALL)), m_remaining(this, componentId, "Remaining", static_cast(E_NetworkValueFeatureFlags::E_NVFF_ALL)), - m_command(this, componentId, "Command", static_cast(E_NetworkValueFeatureFlags::E_NVFF_ALL)) + m_command(this, componentId, "Command", static_cast(E_NetworkValueFeatureFlags::E_NVFF_ALL)), + m_timeOverride(this, componentId, "TimeOverride", static_cast(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 m_elapsed; NetworkValue m_remaining; NetworkValue m_command; + NetworkValue m_timeOverride; virtual const char *getModbusNamePrefix() const { return getOwnPrefix(); } virtual const char *getModbusGroupName() const { return this->name.c_str(); } diff --git a/src/profiles/TemperatureProfile.cpp b/src/profiles/TemperatureProfile.cpp index d5d6c177..10e05f4d 100644 --- a/src/profiles/TemperatureProfile.cpp +++ b/src/profiles/TemperatureProfile.cpp @@ -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 diff --git a/src/profiles/TemperatureProfile.h b/src/profiles/TemperatureProfile.h index 7c7c46a0..81446542 100644 --- a/src/profiles/TemperatureProfile.h +++ b/src/profiles/TemperatureProfile.h @@ -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;