From 7bff01c3639c44be44abb6f018f57add90b1d871 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 02:17:16 +0200 Subject: [PATCH] F1: EptStateMachine (E116 Equipment Performance Tracking) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CMakeLists.txt | 2 + include/secsgem/gem/ept_state.hpp | 75 +++++++++++++++++++++++ src/gem/ept_state.cpp | 98 +++++++++++++++++++++++++++++++ tests/test_ept.cpp | 60 +++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 include/secsgem/gem/ept_state.hpp create mode 100644 src/gem/ept_state.cpp create mode 100644 tests/test_ept.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 044f1bc..5b24ad5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ add_library(secsgem src/gem/carrier_state.cpp src/gem/load_port_state.cpp src/gem/substrate_state.cpp + src/gem/ept_state.cpp src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp @@ -104,6 +105,7 @@ add_executable(secsgem_tests tests/test_carrier_state.cpp tests/test_carriers.cpp tests/test_substrates.cpp + tests/test_ept.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/include/secsgem/gem/ept_state.hpp b/include/secsgem/gem/ept_state.hpp new file mode 100644 index 0000000..621f97f --- /dev/null +++ b/include/secsgem/gem/ept_state.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "secsgem/gem/carrier_state.hpp" // shared CarrierTransition template + +// E116 §6 Equipment Performance Tracking — top-level equipment state +// model that classifies operating time into productive / non-productive +// categories. Each transition is timestamped so the application can +// compute MTBF / availability / utilization from the same source. +// +// The states correspond directly to the SEMI-defined buckets in E116- +// 0712 §6.2; the wire byte values match the order the spec presents. +namespace secsgem::gem { + +enum class EptState : uint8_t { + NonScheduledTime = 0, // equipment not scheduled to operate + ScheduledDowntime = 1, // scheduled maintenance + UnscheduledDowntime = 2, // unscheduled downtime (faults) + Engineering = 3, // engineering / qualification time + Standby = 4, // idle, available but not producing + Productive = 5, // actively producing + NoState = 255, +}; + +const char* ept_state_name(EptState s); +std::optional parse_ept_state(const std::string& s); + +// Events correspond to transitions the equipment can declare; events +// are intentionally state-neutral (any -> X) since EPT is a "what kind +// of time is this" classifier rather than a strict lifecycle. +enum class EptEvent { + EnterNonScheduled, + EnterScheduledDown, + EnterUnscheduledDown, + EnterEngineering, + EnterStandby, + EnterProductive, +}; + +const char* ept_event_name(EptEvent e); + +using EptTable = CarrierTransitionTable; +EptTable default_ept_table(); + +class EptStateMachine { + public: + using StateChangeHandler = + std::function; + + EptStateMachine(); + + EptState state() const { return state_; } + void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } + + // Fire a transition. Returns true if a row matched. The dwell time + // (how long we sat in the previous state) is computed off the + // monotonic clock and surfaced to the handler so accounting code + // doesn't need a parallel timestamp tracker. + bool on_event(EptEvent e); + + private: + EptTable table_; + EptState state_ = EptState::NonScheduledTime; + std::chrono::steady_clock::time_point entered_; + StateChangeHandler on_change_; +}; + +} // namespace secsgem::gem diff --git a/src/gem/ept_state.cpp b/src/gem/ept_state.cpp new file mode 100644 index 0000000..e3fc548 --- /dev/null +++ b/src/gem/ept_state.cpp @@ -0,0 +1,98 @@ +#include "secsgem/gem/ept_state.hpp" + +namespace secsgem::gem { + +static_assert(static_cast(EptState::NonScheduledTime) == 0); +static_assert(static_cast(EptState::ScheduledDowntime) == 1); +static_assert(static_cast(EptState::UnscheduledDowntime) == 2); +static_assert(static_cast(EptState::Engineering) == 3); +static_assert(static_cast(EptState::Standby) == 4); +static_assert(static_cast(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 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( + now - entered_); + state_ = *row->to; + entered_ = now; + if (on_change_) on_change_(prev, state_, e, dwell); + } + return true; +} + +} // namespace secsgem::gem diff --git a/tests/test_ept.cpp b/tests/test_ept.cpp new file mode 100644 index 0000000..3e90cf4 --- /dev/null +++ b/tests/test_ept.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include + +#include "secsgem/gem/ept_state.hpp" + +using namespace secsgem::gem; + +TEST_CASE("EPT: initial state is NonScheduledTime") { + EptStateMachine ept; + CHECK(ept.state() == EptState::NonScheduledTime); +} + +TEST_CASE("EPT: every event transitions to exactly its target state") { + EptStateMachine ept; + CHECK(ept.on_event(EptEvent::EnterStandby)); + CHECK(ept.state() == EptState::Standby); + CHECK(ept.on_event(EptEvent::EnterProductive)); + CHECK(ept.state() == EptState::Productive); + CHECK(ept.on_event(EptEvent::EnterUnscheduledDown)); + CHECK(ept.state() == EptState::UnscheduledDowntime); + CHECK(ept.on_event(EptEvent::EnterEngineering)); + CHECK(ept.state() == EptState::Engineering); + CHECK(ept.on_event(EptEvent::EnterScheduledDown)); + CHECK(ept.state() == EptState::ScheduledDowntime); + CHECK(ept.on_event(EptEvent::EnterNonScheduled)); + CHECK(ept.state() == EptState::NonScheduledTime); +} + +TEST_CASE("EPT: change handler reports dwell time") { + EptStateMachine ept; + std::vector> transitions; + ept.set_state_change_handler( + [&](EptState, EptState to, EptEvent, std::chrono::milliseconds dwell) { + transitions.emplace_back(to, dwell); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ept.on_event(EptEvent::EnterStandby); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ept.on_event(EptEvent::EnterProductive); + + REQUIRE(transitions.size() == 2); + CHECK(transitions[0].first == EptState::Standby); + CHECK(transitions[0].second.count() >= 1); // dwelled in NonScheduledTime + CHECK(transitions[1].first == EptState::Productive); + CHECK(transitions[1].second.count() >= 1); // dwelled in Standby +} + +TEST_CASE("EPT: same-state event is a no-op (no handler call)") { + EptStateMachine ept; + ept.on_event(EptEvent::EnterStandby); + int calls = 0; + ept.set_state_change_handler( + [&](EptState, EptState, EptEvent, std::chrono::milliseconds) { ++calls; }); + CHECK(ept.on_event(EptEvent::EnterStandby)); // already in Standby + CHECK(calls == 0); +}