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:
2026-06-08 02:17:16 +02:00
parent 777fa5e9f9
commit 7bff01c363
4 changed files with 235 additions and 0 deletions
+98
View File
@@ -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