From a52d44ade55049266ac670a58440d41b7c73b020 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 08:46:10 +0200 Subject: [PATCH] K2: SubstrateIDStatus (third E90 axis) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the substrate-ID verification FSM that E90 §6.4.6 calls for: NotConfirmed initial; equipment hasn't read the ID yet WaitingForHost ID has been read; awaiting host accept/reject Confirmed host confirmed (or force-bound) Mismatched host rejected — recoverable via Bind Events: Read NotConfirmed -> WaitingForHost Confirm WaitingForHost -> Confirmed Mismatch WaitingForHost -> Mismatched Bind any -> Confirmed (force-bind) Reset any -> NotConfirmed Wire-byte values pinned via static_assert. The third axis is now exposed on SubstrateStateMachine alongside location_state() and processing_state(); set_id_handler() observes transitions. Existing two-axis API is unchanged. 4 new test cases cover the happy path, Mismatch+Bind recovery, Reset from any state, and same-state event handler suppression. Co-Authored-By: Claude Opus 4.7 --- include/secsgem/gem/substrate_state.hpp | 34 ++++++++++++++ src/gem/substrate_state.cpp | 59 ++++++++++++++++++++++++- tests/test_substrates.cpp | 42 ++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/include/secsgem/gem/substrate_state.hpp b/include/secsgem/gem/substrate_state.hpp index ce3d1f6..1f0fa02 100644 --- a/include/secsgem/gem/substrate_state.hpp +++ b/include/secsgem/gem/substrate_state.hpp @@ -68,13 +68,38 @@ enum class SubstrateProcessingEvent { const char* substrate_processing_event_name(SubstrateProcessingEvent e); +// Substrate ID Status (E90 §6.4.6) — the third independent axis. +// Captures whether the equipment has read & confirmed the wafer's ID. +enum class SubstrateIDStatus : uint8_t { + NotConfirmed = 0, + WaitingForHost = 1, // ID read; awaiting host confirmation + Confirmed = 2, + Mismatched = 3, + NoState = 255, +}; + +const char* substrate_id_status_name(SubstrateIDStatus s); + +enum class SubstrateIDEvent { + Read, // NotConfirmed -> WaitingForHost (equipment scanned ID) + Confirm, // WaitingForHost -> Confirmed (host accepted) + Mismatch, // WaitingForHost -> Mismatched (host rejected) + Bind, // any -> Confirmed (host force-bind) + Reset, // any -> NotConfirmed +}; + +const char* substrate_id_event_name(SubstrateIDEvent e); + using SubstrateTable = CarrierTransitionTable; using SubstrateProcessingTable = CarrierTransitionTable; +using SubstrateIDTable = + CarrierTransitionTable; SubstrateTable default_substrate_table(); SubstrateProcessingTable default_substrate_processing_table(); +SubstrateIDTable default_substrate_id_table(); class SubstrateStateMachine { public: @@ -84,25 +109,34 @@ class SubstrateStateMachine { std::function; + using IDChangeHandler = + std::function; SubstrateStateMachine(); SubstrateState location_state() const { return loc_; } SubstrateProcessingState processing_state() const { return proc_; } + SubstrateIDStatus id_state() const { return id_; } void set_location_handler(LocationChangeHandler h) { on_loc_ = std::move(h); } void set_processing_handler(ProcessingChangeHandler h) { on_proc_ = std::move(h); } + void set_id_handler(IDChangeHandler h) { on_id_ = std::move(h); } bool on_location_event(SubstrateEvent e); bool on_processing_event(SubstrateProcessingEvent e); + bool on_id_event(SubstrateIDEvent e); private: SubstrateTable loc_table_; SubstrateProcessingTable proc_table_; + SubstrateIDTable id_table_; SubstrateState loc_ = SubstrateState::AtSource; SubstrateProcessingState proc_ = SubstrateProcessingState::NeedsProcessing; + SubstrateIDStatus id_ = SubstrateIDStatus::NotConfirmed; LocationChangeHandler on_loc_; ProcessingChangeHandler on_proc_; + IDChangeHandler on_id_; }; } // namespace secsgem::gem diff --git a/src/gem/substrate_state.cpp b/src/gem/substrate_state.cpp index b1ad243..8ddc953 100644 --- a/src/gem/substrate_state.cpp +++ b/src/gem/substrate_state.cpp @@ -13,6 +13,10 @@ static_assert(static_cast(SubstrateProcessingState::Stopped) == static_assert(static_cast(SubstrateProcessingState::Rejected) == 5); static_assert(static_cast(SubstrateProcessingState::Lost) == 6); static_assert(static_cast(SubstrateProcessingState::Skipped) == 7); +static_assert(static_cast(SubstrateIDStatus::NotConfirmed) == 0); +static_assert(static_cast(SubstrateIDStatus::WaitingForHost) == 1); +static_assert(static_cast(SubstrateIDStatus::Confirmed) == 2); +static_assert(static_cast(SubstrateIDStatus::Mismatched) == 3); const char* substrate_state_name(SubstrateState s) { switch (s) { @@ -56,6 +60,28 @@ const char* substrate_event_name(SubstrateEvent e) { return "?"; } +const char* substrate_id_status_name(SubstrateIDStatus s) { + switch (s) { + case SubstrateIDStatus::NotConfirmed: return "NotConfirmed"; + case SubstrateIDStatus::WaitingForHost: return "WaitingForHost"; + case SubstrateIDStatus::Confirmed: return "Confirmed"; + case SubstrateIDStatus::Mismatched: return "Mismatched"; + case SubstrateIDStatus::NoState: return "NoState"; + } + return "?"; +} + +const char* substrate_id_event_name(SubstrateIDEvent e) { + switch (e) { + case SubstrateIDEvent::Read: return "Read"; + case SubstrateIDEvent::Confirm: return "Confirm"; + case SubstrateIDEvent::Mismatch: return "Mismatch"; + case SubstrateIDEvent::Bind: return "Bind"; + case SubstrateIDEvent::Reset: return "Reset"; + } + return "?"; +} + const char* substrate_processing_event_name(SubstrateProcessingEvent e) { switch (e) { case SubstrateProcessingEvent::StartProcessing: return "StartProcessing"; @@ -97,9 +123,29 @@ SubstrateProcessingTable default_substrate_processing_table() { return t; } +SubstrateIDTable default_substrate_id_table() { + using S = SubstrateIDStatus; + using E = SubstrateIDEvent; + SubstrateIDTable t; + t.add({S::NotConfirmed, E::Read, S::WaitingForHost, std::nullopt}); + t.add({S::WaitingForHost, E::Confirm, S::Confirmed, std::nullopt}); + t.add({S::WaitingForHost, E::Mismatch, S::Mismatched, std::nullopt}); + t.add({S::Mismatched, E::Bind, S::Confirmed, std::nullopt}); + // Bind from any state force-confirms. + t.add({S::NotConfirmed, E::Bind, S::Confirmed, std::nullopt}); + t.add({S::WaitingForHost, E::Bind, S::Confirmed, std::nullopt}); + // Reset returns any state to NotConfirmed. + for (auto src : {S::NotConfirmed, S::WaitingForHost, + S::Confirmed, S::Mismatched}) { + t.add({src, E::Reset, S::NotConfirmed, std::nullopt}); + } + return t; +} + SubstrateStateMachine::SubstrateStateMachine() : loc_table_(default_substrate_table()), - proc_table_(default_substrate_processing_table()) {} + proc_table_(default_substrate_processing_table()), + id_table_(default_substrate_id_table()) {} bool SubstrateStateMachine::on_location_event(SubstrateEvent e) { const auto* row = loc_table_.find(loc_, e); @@ -123,4 +169,15 @@ bool SubstrateStateMachine::on_processing_event(SubstrateProcessingEvent e) { return true; } +bool SubstrateStateMachine::on_id_event(SubstrateIDEvent e) { + const auto* row = id_table_.find(id_, e); + if (!row) return false; + if (row->to && *row->to != id_) { + auto prev = id_; + id_ = *row->to; + if (on_id_) on_id_(prev, id_, e); + } + return true; +} + } // namespace secsgem::gem diff --git a/tests/test_substrates.cpp b/tests/test_substrates.cpp index 7ad5a0e..6a4e57c 100644 --- a/tests/test_substrates.cpp +++ b/tests/test_substrates.cpp @@ -71,6 +71,48 @@ TEST_CASE("SubstrateStore: create + dedup + location update") { CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork); } +// ---- Substrate ID Status (E90 §6.4.6) ----------------------------------- + +TEST_CASE("Substrate IDS: NotConfirmed -> WaitingForHost -> Confirmed") { + SubstrateStateMachine s; + CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed); + CHECK(s.on_id_event(SubstrateIDEvent::Read)); + CHECK(s.id_state() == SubstrateIDStatus::WaitingForHost); + CHECK(s.on_id_event(SubstrateIDEvent::Confirm)); + CHECK(s.id_state() == SubstrateIDStatus::Confirmed); +} + +TEST_CASE("Substrate IDS: Mismatch + Bind recovers") { + SubstrateStateMachine s; + s.on_id_event(SubstrateIDEvent::Read); + CHECK(s.on_id_event(SubstrateIDEvent::Mismatch)); + CHECK(s.id_state() == SubstrateIDStatus::Mismatched); + CHECK(s.on_id_event(SubstrateIDEvent::Bind)); + CHECK(s.id_state() == SubstrateIDStatus::Confirmed); +} + +TEST_CASE("Substrate IDS: Reset from any state returns to NotConfirmed") { + SubstrateStateMachine s; + s.on_id_event(SubstrateIDEvent::Read); + s.on_id_event(SubstrateIDEvent::Confirm); + CHECK(s.on_id_event(SubstrateIDEvent::Reset)); + CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed); +} + +TEST_CASE("Substrate IDS: change handler observes transitions only") { + SubstrateStateMachine s; + int calls = 0; + s.set_id_handler([&](SubstrateIDStatus, SubstrateIDStatus, SubstrateIDEvent) { + ++calls; + }); + s.on_id_event(SubstrateIDEvent::Read); + s.on_id_event(SubstrateIDEvent::Confirm); + CHECK(calls == 2); + // Re-confirming from Confirmed: no row, no call. + s.on_id_event(SubstrateIDEvent::Confirm); + CHECK(calls == 2); +} + TEST_CASE("SubstrateStore: history records every transition") { SubstrateStore s; s.create("W-H", "CAR-X", 3);