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/carrier_state.cpp
|
||||||
src/gem/load_port_state.cpp
|
src/gem/load_port_state.cpp
|
||||||
src/gem/substrate_state.cpp
|
src/gem/substrate_state.cpp
|
||||||
|
src/gem/ept_state.cpp
|
||||||
src/gem/host_handler.cpp
|
src/gem/host_handler.cpp
|
||||||
src/config/loader.cpp
|
src/config/loader.cpp
|
||||||
src/endpoint.cpp
|
src/endpoint.cpp
|
||||||
@@ -104,6 +105,7 @@ add_executable(secsgem_tests
|
|||||||
tests/test_carrier_state.cpp
|
tests/test_carrier_state.cpp
|
||||||
tests/test_carriers.cpp
|
tests/test_carriers.cpp
|
||||||
tests/test_substrates.cpp
|
tests/test_substrates.cpp
|
||||||
|
tests/test_ept.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
||||||
target_compile_definitions(secsgem_tests PRIVATE
|
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