06f664dfab
EptStateMachine now retains per-state cumulative dwell time so the host can read it as SVIDs. The implementation is zero-overhead while the FSM is idle (no timers, no background work) — on every transition we add the prior state's dwell to its bucket and reset the entered_ timestamp. Live dwell in the current state is included in accumulated() via a now-vs-entered_ delta at read time. New public API: accumulated(EptState) per-state cumulative ms (incl. live dwell) total_elapsed() denominator for utilization ratios reset_history() S2F43-style history clear This closes the gap I called out: previously we emitted CEIDs on transition but didn't accumulate the bucket the host actually queries for utilization metrics. Wiring these into specific SVIDs is the application's job (equipment.yaml declares SVIDs against any read callable); the runtime data is now there. 4 new test cases cover accumulation, live-dwell inclusion, and reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
124 lines
4.7 KiB
C++
124 lines
4.7 KiB
C++
#include "secsgem/gem/ept_state.hpp"
|
|
|
|
namespace secsgem::gem {
|
|
|
|
static_assert(static_cast<uint8_t>(EptState::NonScheduledTime) == 0);
|
|
static_assert(static_cast<uint8_t>(EptState::ScheduledDowntime) == 1);
|
|
static_assert(static_cast<uint8_t>(EptState::UnscheduledDowntime) == 2);
|
|
static_assert(static_cast<uint8_t>(EptState::Engineering) == 3);
|
|
static_assert(static_cast<uint8_t>(EptState::Standby) == 4);
|
|
static_assert(static_cast<uint8_t>(EptState::Productive) == 5);
|
|
|
|
const char* ept_state_name(EptState s) {
|
|
switch (s) {
|
|
case EptState::NonScheduledTime: return "NonScheduledTime";
|
|
case EptState::ScheduledDowntime: return "ScheduledDowntime";
|
|
case EptState::UnscheduledDowntime: return "UnscheduledDowntime";
|
|
case EptState::Engineering: return "Engineering";
|
|
case EptState::Standby: return "Standby";
|
|
case EptState::Productive: return "Productive";
|
|
case EptState::NoState: return "NoState";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
std::optional<EptState> parse_ept_state(const std::string& s) {
|
|
if (s == "NonScheduledTime") return EptState::NonScheduledTime;
|
|
if (s == "ScheduledDowntime") return EptState::ScheduledDowntime;
|
|
if (s == "UnscheduledDowntime") return EptState::UnscheduledDowntime;
|
|
if (s == "Engineering") return EptState::Engineering;
|
|
if (s == "Standby") return EptState::Standby;
|
|
if (s == "Productive") return EptState::Productive;
|
|
if (s == "NoState") return EptState::NoState;
|
|
return std::nullopt;
|
|
}
|
|
|
|
const char* ept_event_name(EptEvent e) {
|
|
switch (e) {
|
|
case EptEvent::EnterNonScheduled: return "EnterNonScheduled";
|
|
case EptEvent::EnterScheduledDown: return "EnterScheduledDown";
|
|
case EptEvent::EnterUnscheduledDown: return "EnterUnscheduledDown";
|
|
case EptEvent::EnterEngineering: return "EnterEngineering";
|
|
case EptEvent::EnterStandby: return "EnterStandby";
|
|
case EptEvent::EnterProductive: return "EnterProductive";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
namespace {
|
|
// Each event has exactly one target state regardless of source.
|
|
EptState target_of(EptEvent e) {
|
|
switch (e) {
|
|
case EptEvent::EnterNonScheduled: return EptState::NonScheduledTime;
|
|
case EptEvent::EnterScheduledDown: return EptState::ScheduledDowntime;
|
|
case EptEvent::EnterUnscheduledDown: return EptState::UnscheduledDowntime;
|
|
case EptEvent::EnterEngineering: return EptState::Engineering;
|
|
case EptEvent::EnterStandby: return EptState::Standby;
|
|
case EptEvent::EnterProductive: return EptState::Productive;
|
|
}
|
|
return EptState::NoState;
|
|
}
|
|
} // namespace
|
|
|
|
EptTable default_ept_table() {
|
|
EptTable t;
|
|
// Cross-product: every source state accepts every event. The FSM is
|
|
// a classifier, not a strict workflow, so all transitions are legal.
|
|
for (auto src : {EptState::NonScheduledTime, EptState::ScheduledDowntime,
|
|
EptState::UnscheduledDowntime, EptState::Engineering,
|
|
EptState::Standby, EptState::Productive}) {
|
|
for (auto ev : {EptEvent::EnterNonScheduled, EptEvent::EnterScheduledDown,
|
|
EptEvent::EnterUnscheduledDown, EptEvent::EnterEngineering,
|
|
EptEvent::EnterStandby, EptEvent::EnterProductive}) {
|
|
t.add({src, ev, target_of(ev), std::nullopt});
|
|
}
|
|
}
|
|
return t;
|
|
}
|
|
|
|
EptStateMachine::EptStateMachine()
|
|
: table_(default_ept_table()),
|
|
entered_(std::chrono::steady_clock::now()),
|
|
reset_anchor_(entered_) {}
|
|
|
|
bool EptStateMachine::on_event(EptEvent e) {
|
|
const auto* row = table_.find(state_, e);
|
|
if (!row) return false;
|
|
if (row->to && *row->to != state_) {
|
|
auto prev = state_;
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto dwell = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now - entered_);
|
|
buckets_[static_cast<size_t>(prev)] += dwell;
|
|
state_ = *row->to;
|
|
entered_ = now;
|
|
if (on_change_) on_change_(prev, state_, e, dwell);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::chrono::milliseconds EptStateMachine::accumulated(EptState s) const {
|
|
const auto idx = static_cast<size_t>(s);
|
|
if (idx >= buckets_.size()) return std::chrono::milliseconds{0};
|
|
auto base = buckets_[idx];
|
|
if (state_ == s) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
base += std::chrono::duration_cast<std::chrono::milliseconds>(now - entered_);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
std::chrono::milliseconds EptStateMachine::total_elapsed() const {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - reset_anchor_);
|
|
}
|
|
|
|
void EptStateMachine::reset_history() {
|
|
buckets_.fill(std::chrono::milliseconds{0});
|
|
const auto now = std::chrono::steady_clock::now();
|
|
entered_ = now;
|
|
reset_anchor_ = now;
|
|
}
|
|
|
|
} // namespace secsgem::gem
|