E1: SubstrateStateMachine + SubstrateStore (E90 §6)

Per-substrate dual FSM with two orthogonal axes:

  Location (STS):
    AtSource -> AtWork (Acquire) -> AtDestination (Release)
    AtWork  -> AtSource (Return; processing aborted before completion)

  Processing:
    NeedsProcessing -> InProcess (Start) -> Processed (End)
    InProcess -> {Aborted, Stopped, Rejected, Lost} terminal
    NeedsProcessing -> {Skipped, Lost} terminal

Wire-byte values pinned via static_assert to E90-0716 §10.3.

SubstrateStore mirrors the CarrierStore pattern: non-movable, per-row
SubstrateStateMachine heap-allocated with handlers dispatching through
the store's location/processing callbacks; fire_location_event accepts
an optional new_location string so the application can carry
equipment-specific module names alongside the FSM state.

Joins EquipmentDataModel alongside carriers / load_ports.  9 test
cases cover initial state, full location lifecycle, all five
processing exits, and store-level dual-axis observer firing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:49:31 +02:00
parent ff1a6b3726
commit 7c726ed9ba
6 changed files with 435 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
#include <doctest/doctest.h>
#include <vector>
#include "secsgem/gem/store/substrates.hpp"
#include "secsgem/gem/substrate_state.hpp"
using namespace secsgem::gem;
TEST_CASE("SubstrateStateMachine: initial AtSource / NeedsProcessing") {
SubstrateStateMachine s;
CHECK(s.location_state() == SubstrateState::AtSource);
CHECK(s.processing_state() == SubstrateProcessingState::NeedsProcessing);
}
TEST_CASE("Substrate location: AtSource -> AtWork -> AtDestination") {
SubstrateStateMachine s;
CHECK(s.on_location_event(SubstrateEvent::Acquire));
CHECK(s.location_state() == SubstrateState::AtWork);
CHECK(s.on_location_event(SubstrateEvent::Release));
CHECK(s.location_state() == SubstrateState::AtDestination);
}
TEST_CASE("Substrate location: Return from AtWork sends back to AtSource") {
SubstrateStateMachine s;
s.on_location_event(SubstrateEvent::Acquire);
CHECK(s.on_location_event(SubstrateEvent::Return));
CHECK(s.location_state() == SubstrateState::AtSource);
}
TEST_CASE("Substrate processing: happy path Start -> End -> Processed") {
SubstrateStateMachine s;
CHECK(s.on_processing_event(SubstrateProcessingEvent::StartProcessing));
CHECK(s.processing_state() == SubstrateProcessingState::InProcess);
CHECK(s.on_processing_event(SubstrateProcessingEvent::EndProcessing));
CHECK(s.processing_state() == SubstrateProcessingState::Processed);
}
TEST_CASE("Substrate processing: Abort/Stop/Reject/Lost are terminal") {
for (auto ev : {SubstrateProcessingEvent::Abort,
SubstrateProcessingEvent::Stop,
SubstrateProcessingEvent::Reject,
SubstrateProcessingEvent::ReportLost}) {
SubstrateStateMachine s;
s.on_processing_event(SubstrateProcessingEvent::StartProcessing);
CHECK(s.on_processing_event(ev));
// Terminal: no further transitions accepted.
CHECK_FALSE(s.on_processing_event(SubstrateProcessingEvent::EndProcessing));
}
}
TEST_CASE("Substrate processing: Skip from NeedsProcessing") {
SubstrateStateMachine s;
CHECK(s.on_processing_event(SubstrateProcessingEvent::Skip));
CHECK(s.processing_state() == SubstrateProcessingState::Skipped);
}
// ---- SubstrateStore -----------------------------------------------------
TEST_CASE("SubstrateStore: create + dedup + location update") {
SubstrateStore s;
CHECK(s.create("W-001", "CAR-A", /*slot=*/1) ==
SubstrateStore::CreateResult::Created);
CHECK(s.create("W-001") == SubstrateStore::CreateResult::Denied_AlreadyExists);
REQUIRE(s.has("W-001"));
CHECK(s.get("W-001")->carrierid == "CAR-A");
CHECK(s.get("W-001")->slot == 1);
CHECK(s.fire_location_event("W-001", SubstrateEvent::Acquire, "MOD-A"));
CHECK(s.get("W-001")->location == "MOD-A");
CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork);
}
TEST_CASE("SubstrateStore: change handlers observe both axes independently") {
SubstrateStore s;
std::vector<std::pair<std::string, SubstrateState>> loc_log;
std::vector<std::pair<std::string, SubstrateProcessingState>> proc_log;
s.set_location_handler([&](const std::string& id, SubstrateState,
SubstrateState to, SubstrateEvent) {
loc_log.emplace_back(id, to);
});
s.set_processing_handler([&](const std::string& id, SubstrateProcessingState,
SubstrateProcessingState to,
SubstrateProcessingEvent) {
proc_log.emplace_back(id, to);
});
s.create("W-X");
s.fire_location_event("W-X", SubstrateEvent::Acquire);
s.fire_processing_event("W-X", SubstrateProcessingEvent::StartProcessing);
s.fire_processing_event("W-X", SubstrateProcessingEvent::EndProcessing);
s.fire_location_event("W-X", SubstrateEvent::Release);
CHECK(loc_log.size() == 2);
CHECK(proc_log.size() == 2);
CHECK(loc_log.back().second == SubstrateState::AtDestination);
CHECK(proc_log.back().second == SubstrateProcessingState::Processed);
}