diff --git a/CMakeLists.txt b/CMakeLists.txt index 274c7e3..044f1bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ add_library(secsgem src/gem/exception_state.cpp src/gem/carrier_state.cpp src/gem/load_port_state.cpp + src/gem/substrate_state.cpp src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp @@ -102,6 +103,7 @@ add_executable(secsgem_tests tests/test_exceptions.cpp tests/test_carrier_state.cpp tests/test_carriers.cpp + tests/test_substrates.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 1833b8b..9dac587 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -10,6 +10,7 @@ #include "secsgem/gem/store/host_commands.hpp" #include "secsgem/gem/store/limits.hpp" #include "secsgem/gem/store/process_jobs.hpp" +#include "secsgem/gem/store/substrates.hpp" #include "secsgem/gem/store/recipes.hpp" #include "secsgem/gem/store/spool.hpp" #include "secsgem/gem/store/status_variables.hpp" @@ -39,6 +40,7 @@ struct EquipmentDataModel { ExceptionStore exceptions; CarrierStore carriers; LoadPortStore load_ports; + SubstrateStore substrates; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/store/substrates.hpp b/include/secsgem/gem/store/substrates.hpp new file mode 100644 index 0000000..dca2d54 --- /dev/null +++ b/include/secsgem/gem/store/substrates.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "secsgem/gem/substrate_state.hpp" + +namespace secsgem::gem { + +struct Substrate { + std::string substid; // SUBSTID — E90 wafer identifier + std::string carrierid; // CARRIERID where this wafer originated + uint8_t slot = 0; // 1-based slot in the source carrier + std::string location; // free-form: equipment-defined module name + std::unique_ptr fsm; +}; + +class SubstrateStore { + public: + using LocationChangeHandler = + std::function; + using ProcessingChangeHandler = + std::function; + + SubstrateStore() = default; + SubstrateStore(const SubstrateStore&) = delete; + SubstrateStore& operator=(const SubstrateStore&) = delete; + SubstrateStore(SubstrateStore&&) = delete; + SubstrateStore& operator=(SubstrateStore&&) = delete; + + void set_location_handler(LocationChangeHandler h) { on_loc_ = std::move(h); } + void set_processing_handler(ProcessingChangeHandler h) { on_proc_ = std::move(h); } + + enum class CreateResult { Created, Denied_AlreadyExists }; + + CreateResult create(std::string substid, std::string carrierid = "", + uint8_t slot = 0) { + if (subs_.count(substid)) return CreateResult::Denied_AlreadyExists; + auto fsm = std::make_unique(); + const std::string id = substid; + fsm->set_location_handler( + [this, id](SubstrateState f, SubstrateState t, SubstrateEvent e) { + if (on_loc_) on_loc_(id, f, t, e); + }); + fsm->set_processing_handler( + [this, id](SubstrateProcessingState f, SubstrateProcessingState t, + SubstrateProcessingEvent e) { + if (on_proc_) on_proc_(id, f, t, e); + }); + subs_.emplace(substid, + Substrate{substid, std::move(carrierid), slot, "", std::move(fsm)}); + return CreateResult::Created; + } + + bool has(const std::string& id) const { return subs_.count(id) > 0; } + const Substrate* get(const std::string& id) const { + auto it = subs_.find(id); + return it == subs_.end() ? nullptr : &it->second; + } + Substrate* get(const std::string& id) { + auto it = subs_.find(id); + return it == subs_.end() ? nullptr : &it->second; + } + + bool fire_location_event(const std::string& id, SubstrateEvent e, + std::string new_location = "") { + auto* s = get(id); + if (!s) return false; + if (!new_location.empty()) s->location = std::move(new_location); + return s->fsm->on_location_event(e); + } + bool fire_processing_event(const std::string& id, SubstrateProcessingEvent e) { + auto* s = get(id); + return s && s->fsm->on_processing_event(e); + } + + bool remove(const std::string& id) { return subs_.erase(id) > 0; } + std::size_t size() const { return subs_.size(); } + std::vector ids() const { + std::vector out; + out.reserve(subs_.size()); + for (const auto& kv : subs_) out.push_back(kv.first); + return out; + } + + private: + std::map subs_; + LocationChangeHandler on_loc_; + ProcessingChangeHandler on_proc_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/substrate_state.hpp b/include/secsgem/gem/substrate_state.hpp new file mode 100644 index 0000000..ce3d1f6 --- /dev/null +++ b/include/secsgem/gem/substrate_state.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "secsgem/gem/carrier_state.hpp" // shared CarrierTransition template + +// E90 §6 Substrate Tracking — per-substrate state model. +// +// E90 actually defines several related state models per substrate: +// +// STS Substrate State the processing lifecycle +// SLS Substrate Location where the wafer physically is +// IDS Substrate ID Status has the ID been verified +// +// This first pass covers the most-used STS model (E90-0716 §10.3.1) at +// full fidelity and exposes a Location string the application can +// update as it sees substrate moves. IDS / cross-state guards mirror +// the CIDS pattern from E87 and remain a follow-on. +namespace secsgem::gem { + +// Wire-byte values per E90-0716 §10.3.1 STS. +enum class SubstrateState : uint8_t { + AtSource = 0, + AtWork = 1, // in-process at a module + AtDestination = 2, // moved to final location + NoState = 255, +}; + +const char* substrate_state_name(SubstrateState s); +std::optional parse_substrate_state(const std::string& s); + +// "Processing" lifecycle (E90 §6.4.4); orthogonal to STS. +enum class SubstrateProcessingState : uint8_t { + NeedsProcessing = 0, + InProcess = 1, + Processed = 2, + Aborted = 3, + Stopped = 4, + Rejected = 5, + Lost = 6, + Skipped = 7, + NoState = 255, +}; + +const char* substrate_processing_state_name(SubstrateProcessingState s); + +enum class SubstrateEvent { + Acquire, // AtSource -> AtWork (machine picked up the wafer) + Release, // AtWork -> AtDestination (deposited) + Return, // AtWork -> AtSource (returned without processing) +}; + +const char* substrate_event_name(SubstrateEvent e); + +enum class SubstrateProcessingEvent { + StartProcessing, // NeedsProcessing -> InProcess + EndProcessing, // InProcess -> Processed + Abort, // any -> Aborted + Stop, // any -> Stopped + Reject, // any -> Rejected + ReportLost, // any -> Lost + Skip, // any -> Skipped +}; + +const char* substrate_processing_event_name(SubstrateProcessingEvent e); + +using SubstrateTable = + CarrierTransitionTable; +using SubstrateProcessingTable = + CarrierTransitionTable; + +SubstrateTable default_substrate_table(); +SubstrateProcessingTable default_substrate_processing_table(); + +class SubstrateStateMachine { + public: + using LocationChangeHandler = + std::function; + using ProcessingChangeHandler = + std::function; + + SubstrateStateMachine(); + + SubstrateState location_state() const { return loc_; } + SubstrateProcessingState processing_state() const { return proc_; } + + void set_location_handler(LocationChangeHandler h) { on_loc_ = std::move(h); } + void set_processing_handler(ProcessingChangeHandler h) { on_proc_ = std::move(h); } + + bool on_location_event(SubstrateEvent e); + bool on_processing_event(SubstrateProcessingEvent e); + + private: + SubstrateTable loc_table_; + SubstrateProcessingTable proc_table_; + SubstrateState loc_ = SubstrateState::AtSource; + SubstrateProcessingState proc_ = SubstrateProcessingState::NeedsProcessing; + LocationChangeHandler on_loc_; + ProcessingChangeHandler on_proc_; +}; + +} // namespace secsgem::gem diff --git a/src/gem/substrate_state.cpp b/src/gem/substrate_state.cpp new file mode 100644 index 0000000..b1ad243 --- /dev/null +++ b/src/gem/substrate_state.cpp @@ -0,0 +1,126 @@ +#include "secsgem/gem/substrate_state.hpp" + +namespace secsgem::gem { + +static_assert(static_cast(SubstrateState::AtSource) == 0); +static_assert(static_cast(SubstrateState::AtWork) == 1); +static_assert(static_cast(SubstrateState::AtDestination) == 2); +static_assert(static_cast(SubstrateProcessingState::NeedsProcessing) == 0); +static_assert(static_cast(SubstrateProcessingState::InProcess) == 1); +static_assert(static_cast(SubstrateProcessingState::Processed) == 2); +static_assert(static_cast(SubstrateProcessingState::Aborted) == 3); +static_assert(static_cast(SubstrateProcessingState::Stopped) == 4); +static_assert(static_cast(SubstrateProcessingState::Rejected) == 5); +static_assert(static_cast(SubstrateProcessingState::Lost) == 6); +static_assert(static_cast(SubstrateProcessingState::Skipped) == 7); + +const char* substrate_state_name(SubstrateState s) { + switch (s) { + case SubstrateState::AtSource: return "AtSource"; + case SubstrateState::AtWork: return "AtWork"; + case SubstrateState::AtDestination: return "AtDestination"; + case SubstrateState::NoState: return "NoState"; + } + return "?"; +} + +std::optional parse_substrate_state(const std::string& s) { + if (s == "AtSource") return SubstrateState::AtSource; + if (s == "AtWork") return SubstrateState::AtWork; + if (s == "AtDestination") return SubstrateState::AtDestination; + if (s == "NoState") return SubstrateState::NoState; + return std::nullopt; +} + +const char* substrate_processing_state_name(SubstrateProcessingState s) { + switch (s) { + case SubstrateProcessingState::NeedsProcessing: return "NeedsProcessing"; + case SubstrateProcessingState::InProcess: return "InProcess"; + case SubstrateProcessingState::Processed: return "Processed"; + case SubstrateProcessingState::Aborted: return "Aborted"; + case SubstrateProcessingState::Stopped: return "Stopped"; + case SubstrateProcessingState::Rejected: return "Rejected"; + case SubstrateProcessingState::Lost: return "Lost"; + case SubstrateProcessingState::Skipped: return "Skipped"; + case SubstrateProcessingState::NoState: return "NoState"; + } + return "?"; +} + +const char* substrate_event_name(SubstrateEvent e) { + switch (e) { + case SubstrateEvent::Acquire: return "Acquire"; + case SubstrateEvent::Release: return "Release"; + case SubstrateEvent::Return: return "Return"; + } + return "?"; +} + +const char* substrate_processing_event_name(SubstrateProcessingEvent e) { + switch (e) { + case SubstrateProcessingEvent::StartProcessing: return "StartProcessing"; + case SubstrateProcessingEvent::EndProcessing: return "EndProcessing"; + case SubstrateProcessingEvent::Abort: return "Abort"; + case SubstrateProcessingEvent::Stop: return "Stop"; + case SubstrateProcessingEvent::Reject: return "Reject"; + case SubstrateProcessingEvent::ReportLost: return "ReportLost"; + case SubstrateProcessingEvent::Skip: return "Skip"; + } + return "?"; +} + +SubstrateTable default_substrate_table() { + using S = SubstrateState; + using E = SubstrateEvent; + SubstrateTable t; + t.add({S::AtSource, E::Acquire, S::AtWork, std::nullopt}); + t.add({S::AtWork, E::Release, S::AtDestination, std::nullopt}); + t.add({S::AtWork, E::Return, S::AtSource, std::nullopt}); + return t; +} + +SubstrateProcessingTable default_substrate_processing_table() { + using S = SubstrateProcessingState; + using E = SubstrateProcessingEvent; + SubstrateProcessingTable t; + // From NeedsProcessing. + t.add({S::NeedsProcessing, E::StartProcessing, S::InProcess, std::nullopt}); + t.add({S::NeedsProcessing, E::Skip, S::Skipped, std::nullopt}); + t.add({S::NeedsProcessing, E::ReportLost, S::Lost, std::nullopt}); + // From InProcess. + t.add({S::InProcess, E::EndProcessing, S::Processed, std::nullopt}); + t.add({S::InProcess, E::Abort, S::Aborted, std::nullopt}); + t.add({S::InProcess, E::Stop, S::Stopped, std::nullopt}); + t.add({S::InProcess, E::Reject, S::Rejected, std::nullopt}); + t.add({S::InProcess, E::ReportLost, S::Lost, std::nullopt}); + // Processed/Aborted/etc are terminal. + return t; +} + +SubstrateStateMachine::SubstrateStateMachine() + : loc_table_(default_substrate_table()), + proc_table_(default_substrate_processing_table()) {} + +bool SubstrateStateMachine::on_location_event(SubstrateEvent e) { + const auto* row = loc_table_.find(loc_, e); + if (!row) return false; + if (row->to && *row->to != loc_) { + auto prev = loc_; + loc_ = *row->to; + if (on_loc_) on_loc_(prev, loc_, e); + } + return true; +} + +bool SubstrateStateMachine::on_processing_event(SubstrateProcessingEvent e) { + const auto* row = proc_table_.find(proc_, e); + if (!row) return false; + if (row->to && *row->to != proc_) { + auto prev = proc_; + proc_ = *row->to; + if (on_proc_) on_proc_(prev, proc_, e); + } + return true; +} + +} // namespace secsgem::gem diff --git a/tests/test_substrates.cpp b/tests/test_substrates.cpp new file mode 100644 index 0000000..2bc37f2 --- /dev/null +++ b/tests/test_substrates.cpp @@ -0,0 +1,96 @@ +#include + +#include + +#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> loc_log; + std::vector> 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); +}