H1: ModuleStateMachine + ModuleStore (E157 §6)

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 03:37:20 +02:00
parent d159bd39d7
commit 82fac6fd17
5 changed files with 234 additions and 0 deletions
+2
View File
@@ -59,6 +59,7 @@ add_library(secsgem
src/gem/substrate_state.cpp src/gem/substrate_state.cpp
src/gem/ept_state.cpp src/gem/ept_state.cpp
src/gem/cem_objects.cpp src/gem/cem_objects.cpp
src/gem/module_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
@@ -108,6 +109,7 @@ add_executable(secsgem_tests
tests/test_substrates.cpp tests/test_substrates.cpp
tests/test_ept.cpp tests/test_ept.cpp
tests/test_cem_objects.cpp tests/test_cem_objects.cpp
tests/test_modules.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
+2
View File
@@ -11,6 +11,7 @@
#include "secsgem/gem/store/exceptions.hpp" #include "secsgem/gem/store/exceptions.hpp"
#include "secsgem/gem/store/host_commands.hpp" #include "secsgem/gem/store/host_commands.hpp"
#include "secsgem/gem/store/limits.hpp" #include "secsgem/gem/store/limits.hpp"
#include "secsgem/gem/store/modules.hpp"
#include "secsgem/gem/store/process_jobs.hpp" #include "secsgem/gem/store/process_jobs.hpp"
#include "secsgem/gem/store/substrates.hpp" #include "secsgem/gem/store/substrates.hpp"
#include "secsgem/gem/store/recipes.hpp" #include "secsgem/gem/store/recipes.hpp"
@@ -45,6 +46,7 @@ struct EquipmentDataModel {
SubstrateStore substrates; SubstrateStore substrates;
EptStateMachine ept; EptStateMachine ept;
CemObjectStore cem; CemObjectStore cem;
ModuleStore modules;
// Convenience: VID -> value lookup spanning SVIDs and DVIDs. // Convenience: VID -> value lookup spanning SVIDs and DVIDs.
std::optional<s2::Item> vid_value(uint32_t vid) const { std::optional<s2::Item> vid_value(uint32_t vid) const {
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#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<ModuleStateMachine> fsm;
};
class ModuleStore {
public:
using StateChangeHandler =
std::function<void(const std::string& module_id, ModuleState from,
ModuleState to, ModuleEvent)>;
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<ModuleStateMachine>();
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<std::string> ids() const {
std::vector<std::string> out;
out.reserve(mods_.size());
for (const auto& kv : mods_) out.push_back(kv.first);
return out;
}
private:
std::map<std::string, Module> mods_;
StateChangeHandler on_change_;
};
} // namespace secsgem::gem
+71
View File
@@ -0,0 +1,71 @@
#include "secsgem/gem/module_state.hpp"
namespace secsgem::gem {
static_assert(static_cast<uint8_t>(ModuleState::NotExecuting) == 0);
static_assert(static_cast<uint8_t>(ModuleState::GeneralExecuting) == 1);
static_assert(static_cast<uint8_t>(ModuleState::StepExecuting) == 2);
static_assert(static_cast<uint8_t>(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<ModuleState> 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
+73
View File
@@ -0,0 +1,73 @@
#include <doctest/doctest.h>
#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<std::pair<std::string, ModuleState>> 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);
}