F1: EptStateMachine (E116 Equipment Performance Tracking)
Adds the six E116-0712 §6.2 buckets for classifying equipment time: NonScheduledTime (0) not scheduled to operate ScheduledDowntime (1) planned maintenance window UnscheduledDowntime (2) faults / unplanned stoppage Engineering (3) engineering / qualification time Standby (4) idle but available Productive (5) actively producing Wire-byte values pinned via static_assert to E116 §10.3. The FSM is a classifier rather than a strict lifecycle — every (state, event) pair is legal — but it remains data-driven through the shared CarrierTransitionTable template so the default cross-product is expressible declaratively. The state-change handler also surfaces dwell time (how long the previous state was held) computed off std::chrono::steady_clock, so accounting code can compute MTBF / availability / utilization from a single source without maintaining a parallel timestamp log. 4 test cases cover the initial state, every event firing, dwell-time reporting, and the no-op same-state event (no handler call). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
#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()) {}
|
||||
|
||||
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_);
|
||||
state_ = *row->to;
|
||||
entered_ = now;
|
||||
if (on_change_) on_change_(prev, state_, e, dwell);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user