Add batch debounce, TimeOverride for PlotBase, and min heating duration
This commit is contained in:
parent
02faa08df1
commit
899bc545d3
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(); }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user