From 82fac6fd17d52da0f07f83d8749d80e10ef07656 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 03:37:20 +0200 Subject: [PATCH] =?UTF-8?q?H1:=20ModuleStateMachine=20+=20ModuleStore=20(E?= =?UTF-8?q?157=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-module process-tracking state machine. An E157 instance models a single recipe step at a single module, with the canonical lifecycle: NotExecuting -> GeneralExecuting (StartGeneral) -> StepExecuting (StartStep) -> StepCompleted (CompleteStep) Plus universal escape hatches: Reset returns any state to NotExecuting; Abort terminates from any state to StepCompleted. ModuleStore wraps the FSM with the now-standard pattern: - non-movable (this-capture lambdas) - per-module bind() carries current_substid + recipe_step - fire(module_id, event) delegates to the FSM - set_state_change_handler observes every transition with module_id Joins EquipmentDataModel. 5 test cases cover happy path, Reset from each interior state, Abort, store-level create dedup + bind, and the multi-module change handler keying. Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 2 + include/secsgem/gem/data_model.hpp | 2 + include/secsgem/gem/store/modules.hpp | 86 +++++++++++++++++++++++++++ src/gem/module_state.cpp | 71 ++++++++++++++++++++++ tests/test_modules.cpp | 73 +++++++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 include/secsgem/gem/store/modules.hpp create mode 100644 src/gem/module_state.cpp create mode 100644 tests/test_modules.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 92f3333..152f414 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(secsgem src/gem/substrate_state.cpp src/gem/ept_state.cpp src/gem/cem_objects.cpp + src/gem/module_state.cpp src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp @@ -108,6 +109,7 @@ add_executable(secsgem_tests tests/test_substrates.cpp tests/test_ept.cpp tests/test_cem_objects.cpp + tests/test_modules.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index faf5960..ffe7690 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -11,6 +11,7 @@ #include "secsgem/gem/store/exceptions.hpp" #include "secsgem/gem/store/host_commands.hpp" #include "secsgem/gem/store/limits.hpp" +#include "secsgem/gem/store/modules.hpp" #include "secsgem/gem/store/process_jobs.hpp" #include "secsgem/gem/store/substrates.hpp" #include "secsgem/gem/store/recipes.hpp" @@ -45,6 +46,7 @@ struct EquipmentDataModel { SubstrateStore substrates; EptStateMachine ept; CemObjectStore cem; + ModuleStore modules; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/store/modules.hpp b/include/secsgem/gem/store/modules.hpp new file mode 100644 index 0000000..eb3ba8b --- /dev/null +++ b/include/secsgem/gem/store/modules.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/gem/module_state.hpp" + +namespace secsgem::gem { + +struct Module { + std::string module_id; + std::string recipe_step; // RCPSPEC the step is executing + std::string current_substid; // SUBSTID being processed; empty when idle + std::unique_ptr fsm; +}; + +class ModuleStore { + public: + using StateChangeHandler = + std::function; + + ModuleStore() = default; + ModuleStore(const ModuleStore&) = delete; + ModuleStore& operator=(const ModuleStore&) = delete; + ModuleStore(ModuleStore&&) = delete; + ModuleStore& operator=(ModuleStore&&) = delete; + + void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } + + bool create(std::string module_id) { + if (mods_.count(module_id)) return false; + auto fsm = std::make_unique(); + const std::string id = module_id; + fsm->set_state_change_handler( + [this, id](ModuleState f, ModuleState t, ModuleEvent e) { + if (on_change_) on_change_(id, f, t, e); + }); + mods_.emplace(module_id, Module{module_id, "", "", std::move(fsm)}); + return true; + } + + bool has(const std::string& mid) const { return mods_.count(mid) > 0; } + const Module* get(const std::string& mid) const { + auto it = mods_.find(mid); + return it == mods_.end() ? nullptr : &it->second; + } + Module* get(const std::string& mid) { + auto it = mods_.find(mid); + return it == mods_.end() ? nullptr : &it->second; + } + + // Bind a substrate + recipe step to the module for the next step + // (typically called immediately before StartGeneral). + bool bind(const std::string& mid, std::string substid, std::string recipe_step) { + auto* m = get(mid); + if (!m) return false; + m->current_substid = std::move(substid); + m->recipe_step = std::move(recipe_step); + return true; + } + + bool fire(const std::string& mid, ModuleEvent e) { + auto* m = get(mid); + return m && m->fsm->on_event(e); + } + + std::size_t size() const { return mods_.size(); } + std::vector ids() const { + std::vector out; + out.reserve(mods_.size()); + for (const auto& kv : mods_) out.push_back(kv.first); + return out; + } + + private: + std::map mods_; + StateChangeHandler on_change_; +}; + +} // namespace secsgem::gem diff --git a/src/gem/module_state.cpp b/src/gem/module_state.cpp new file mode 100644 index 0000000..4e9edf9 --- /dev/null +++ b/src/gem/module_state.cpp @@ -0,0 +1,71 @@ +#include "secsgem/gem/module_state.hpp" + +namespace secsgem::gem { + +static_assert(static_cast(ModuleState::NotExecuting) == 0); +static_assert(static_cast(ModuleState::GeneralExecuting) == 1); +static_assert(static_cast(ModuleState::StepExecuting) == 2); +static_assert(static_cast(ModuleState::StepCompleted) == 3); + +const char* module_state_name(ModuleState s) { + switch (s) { + case ModuleState::NotExecuting: return "NotExecuting"; + case ModuleState::GeneralExecuting: return "GeneralExecuting"; + case ModuleState::StepExecuting: return "StepExecuting"; + case ModuleState::StepCompleted: return "StepCompleted"; + case ModuleState::NoState: return "NoState"; + } + return "?"; +} + +std::optional parse_module_state(const std::string& s) { + if (s == "NotExecuting") return ModuleState::NotExecuting; + if (s == "GeneralExecuting") return ModuleState::GeneralExecuting; + if (s == "StepExecuting") return ModuleState::StepExecuting; + if (s == "StepCompleted") return ModuleState::StepCompleted; + if (s == "NoState") return ModuleState::NoState; + return std::nullopt; +} + +const char* module_event_name(ModuleEvent e) { + switch (e) { + case ModuleEvent::StartGeneral: return "StartGeneral"; + case ModuleEvent::StartStep: return "StartStep"; + case ModuleEvent::CompleteStep: return "CompleteStep"; + case ModuleEvent::Reset: return "Reset"; + case ModuleEvent::Abort: return "Abort"; + } + return "?"; +} + +ModuleTable default_module_table() { + using S = ModuleState; + using E = ModuleEvent; + ModuleTable t; + t.add({S::NotExecuting, E::StartGeneral, S::GeneralExecuting, std::nullopt}); + t.add({S::GeneralExecuting, E::StartStep, S::StepExecuting, std::nullopt}); + t.add({S::StepExecuting, E::CompleteStep, S::StepCompleted, std::nullopt}); + + // Reset and Abort are universal escape hatches. + for (auto src : {S::NotExecuting, S::GeneralExecuting, + S::StepExecuting, S::StepCompleted}) { + t.add({src, E::Reset, S::NotExecuting, std::nullopt}); + t.add({src, E::Abort, S::StepCompleted, std::nullopt}); + } + return t; +} + +ModuleStateMachine::ModuleStateMachine() : table_(default_module_table()) {} + +bool ModuleStateMachine::on_event(ModuleEvent e) { + const auto* row = table_.find(state_, e); + if (!row) return false; + if (row->to && *row->to != state_) { + auto prev = state_; + state_ = *row->to; + if (on_change_) on_change_(prev, state_, e); + } + return true; +} + +} // namespace secsgem::gem diff --git a/tests/test_modules.cpp b/tests/test_modules.cpp new file mode 100644 index 0000000..97cd024 --- /dev/null +++ b/tests/test_modules.cpp @@ -0,0 +1,73 @@ +#include + +#include "secsgem/gem/module_state.hpp" +#include "secsgem/gem/store/modules.hpp" + +using namespace secsgem::gem; + +// ---- FSM --------------------------------------------------------------- + +TEST_CASE("ModuleStateMachine: happy path lifecycle") { + ModuleStateMachine fsm; + CHECK(fsm.state() == ModuleState::NotExecuting); + CHECK(fsm.on_event(ModuleEvent::StartGeneral)); + CHECK(fsm.state() == ModuleState::GeneralExecuting); + CHECK(fsm.on_event(ModuleEvent::StartStep)); + CHECK(fsm.state() == ModuleState::StepExecuting); + CHECK(fsm.on_event(ModuleEvent::CompleteStep)); + CHECK(fsm.state() == ModuleState::StepCompleted); +} + +TEST_CASE("ModuleStateMachine: Reset returns to NotExecuting from any state") { + for (auto start : {ModuleState::GeneralExecuting, ModuleState::StepExecuting, + ModuleState::StepCompleted}) { + ModuleStateMachine fsm; + // Drive into `start`. + if (start != ModuleState::NotExecuting) fsm.on_event(ModuleEvent::StartGeneral); + if (start == ModuleState::StepExecuting || start == ModuleState::StepCompleted) + fsm.on_event(ModuleEvent::StartStep); + if (start == ModuleState::StepCompleted) fsm.on_event(ModuleEvent::CompleteStep); + REQUIRE(fsm.state() == start); + CHECK(fsm.on_event(ModuleEvent::Reset)); + CHECK(fsm.state() == ModuleState::NotExecuting); + } +} + +TEST_CASE("ModuleStateMachine: Abort lands in StepCompleted") { + ModuleStateMachine fsm; + fsm.on_event(ModuleEvent::StartGeneral); + fsm.on_event(ModuleEvent::StartStep); + CHECK(fsm.on_event(ModuleEvent::Abort)); + CHECK(fsm.state() == ModuleState::StepCompleted); +} + +// ---- Store ------------------------------------------------------------- + +TEST_CASE("ModuleStore: create + dedup + bind + fire") { + ModuleStore s; + CHECK(s.create("MOD-A")); + CHECK_FALSE(s.create("MOD-A")); + CHECK(s.bind("MOD-A", "W-1", "STEP-1")); + CHECK(s.get("MOD-A")->current_substid == "W-1"); + CHECK(s.get("MOD-A")->recipe_step == "STEP-1"); + CHECK(s.fire("MOD-A", ModuleEvent::StartGeneral)); + CHECK(s.get("MOD-A")->fsm->state() == ModuleState::GeneralExecuting); +} + +TEST_CASE("ModuleStore: state-change handler observes per-module") { + ModuleStore s; + std::vector> log; + s.set_state_change_handler( + [&](const std::string& id, ModuleState, ModuleState to, ModuleEvent) { + log.emplace_back(id, to); + }); + s.create("M1"); + s.create("M2"); + s.fire("M1", ModuleEvent::StartGeneral); + s.fire("M2", ModuleEvent::StartGeneral); + s.fire("M1", ModuleEvent::StartStep); + REQUIRE(log.size() == 3); + CHECK(log[0].first == "M1"); + CHECK(log[2].first == "M1"); + CHECK(log[2].second == ModuleState::StepExecuting); +}