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:
@@ -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
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<EptState> 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<EptState, EptEvent>;
|
||||
EptTable default_ept_table();
|
||||
|
||||
class EptStateMachine {
|
||||
public:
|
||||
using StateChangeHandler =
|
||||
std::function<void(EptState from, EptState to, EptEvent,
|
||||
std::chrono::milliseconds dwell_before_change)>;
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#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<std::pair<EptState, std::chrono::milliseconds>> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user