diff --git a/CMakeLists.txt b/CMakeLists.txt index 8130cc5..2752625 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ add_executable(secsgem_tests tests/test_sml.cpp tests/test_s9_fallback.cpp tests/test_e84.cpp + tests/test_e84_ports.cpp tests/test_gem300_scenario.cpp tests/test_wire_ceid_emission.cpp tests/test_live_gem300.cpp diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 4ee8df7..95eaa80 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -196,7 +196,7 @@ Legend: | Capability | Status | Spec ref | Messages | Notes | |---------------------------------------|--------|----------|----------|-------| -| Handoff state machine | ✅ | E84 | — | Full LOAD / UNLOAD signal vocabulary (CS_0/CS_1, VALID, TR_REQ, BUSY, COMPT, AM_AVBL, ES). | +| Handoff state machine | ✅ | E84 | — | Full LOAD / UNLOAD signal vocabulary (CS_0/CS_1, VALID, TR_REQ, BUSY, COMPT, AM_AVBL, ES). Per-port via `E84PortStore` keyed by `port_id`; independent FSMs run in parallel per load port. | ## 4j. E5 §13 Wafer Maps (S12) diff --git a/include/secsgem/gem/data_model.hpp b/include/secsgem/gem/data_model.hpp index 8b01f64..1b1ae7a 100644 --- a/include/secsgem/gem/data_model.hpp +++ b/include/secsgem/gem/data_model.hpp @@ -5,6 +5,7 @@ #include "secsgem/gem/store/cem_objects.hpp" #include "secsgem/gem/store/clock.hpp" #include "secsgem/gem/e84_state.hpp" +#include "secsgem/gem/store/e84_ports.hpp" #include "secsgem/gem/ept_state.hpp" #include "secsgem/gem/store/control_jobs.hpp" #include "secsgem/gem/store/equipment_constants.hpp" @@ -48,7 +49,7 @@ struct EquipmentDataModel { EptStateMachine ept; CemObjectStore cem; ModuleStore modules; - E84StateMachine e84; + E84PortStore e84_ports; // Convenience: VID -> value lookup spanning SVIDs and DVIDs. std::optional vid_value(uint32_t vid) const { diff --git a/include/secsgem/gem/store/e84_ports.hpp b/include/secsgem/gem/store/e84_ports.hpp new file mode 100644 index 0000000..83587eb --- /dev/null +++ b/include/secsgem/gem/store/e84_ports.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "secsgem/gem/e84_state.hpp" + +namespace secsgem::gem { + +// E84 (Parallel I/O) is per-load-port: each port has its own ten-wire +// handshake with the AMHS. Earlier revisions of this codebase modeled +// E84 as a single equipment-wide FSM; multi-LP tools need one FSM per +// port. E84PortStore keys per-port FSMs by port_id (1-based, matching +// LoadPortStore). +class E84PortStore { + public: + using StateChangeHandler = + std::function; + + E84PortStore() = default; + E84PortStore(const E84PortStore&) = delete; + E84PortStore& operator=(const E84PortStore&) = delete; + E84PortStore(E84PortStore&&) = delete; + E84PortStore& operator=(E84PortStore&&) = delete; + + void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } + + bool create(uint8_t port_id) { + if (ports_.count(port_id)) return false; + auto fsm = std::make_unique(); + const uint8_t pid = port_id; + fsm->set_state_change_handler( + [this, pid](E84State f, E84State t, E84Signal sig) { + if (on_change_) on_change_(pid, f, t, sig); + }); + ports_.emplace(port_id, std::move(fsm)); + return true; + } + + bool has(uint8_t port_id) const { return ports_.count(port_id) > 0; } + + E84StateMachine* get(uint8_t port_id) { + auto it = ports_.find(port_id); + return it == ports_.end() ? nullptr : it->second.get(); + } + const E84StateMachine* get(uint8_t port_id) const { + auto it = ports_.find(port_id); + return it == ports_.end() ? nullptr : it->second.get(); + } + + // Convenience pass-throughs that auto-create the port on first use. + // Real applications should call create() explicitly with their + // expected port set, but the implicit create keeps quick demos + // ergonomic. + bool on_signal_change(uint8_t port_id, E84Signal s, bool value) { + if (!ports_.count(port_id)) create(port_id); + auto* fsm = get(port_id); + if (!fsm) return false; + fsm->on_signal_change(s, value); + return true; + } + bool reset(uint8_t port_id) { + auto* fsm = get(port_id); + if (!fsm) return false; + fsm->reset(); + return true; + } + + std::size_t size() const { return ports_.size(); } + std::vector ids() const { + std::vector out; + out.reserve(ports_.size()); + for (const auto& kv : ports_) out.push_back(kv.first); + return out; + } + + private: + std::map> ports_; + StateChangeHandler on_change_; +}; + +} // namespace secsgem::gem diff --git a/tests/test_e84_ports.cpp b/tests/test_e84_ports.cpp new file mode 100644 index 0000000..54523a5 --- /dev/null +++ b/tests/test_e84_ports.cpp @@ -0,0 +1,98 @@ +// E84PortStore: per-port E84 FSMs. +// +// Each load port owns its own E84 handshake state. These tests +// confirm the store keys correctly, that per-port FSMs are +// independent, and that the change handler reports the right +// port_id alongside the from/to state. + +#include + +#include +#include + +#include "secsgem/gem/store/e84_ports.hpp" + +using namespace secsgem::gem; + +TEST_CASE("E84PortStore: create + get + has + size") { + E84PortStore s; + CHECK(s.size() == 0); + REQUIRE(s.create(1)); + REQUIRE(s.create(2)); + CHECK(s.size() == 2); + CHECK(s.has(1)); + CHECK(s.has(2)); + CHECK_FALSE(s.has(3)); + CHECK(s.get(1) != nullptr); + CHECK(s.get(99) == nullptr); + CHECK_FALSE(s.create(1)); // duplicate +} + +TEST_CASE("E84PortStore: implicit-create on first signal-change") { + // Convenience pass-through: on_signal_change auto-creates the port + // if it doesn't exist yet. + E84PortStore s; + CHECK(s.on_signal_change(7, E84Signal::CS_0, true)); + CHECK(s.has(7)); + CHECK(s.get(7)->signal(E84Signal::CS_0)); +} + +TEST_CASE("E84PortStore: per-port FSMs run independently") { + E84PortStore s; + REQUIRE(s.create(1)); + REQUIRE(s.create(2)); + + // LP1 walks all the way to Complete; LP2 stays idle. + s.on_signal_change(1, E84Signal::CS_0, true); + s.on_signal_change(1, E84Signal::VALID, true); + s.on_signal_change(1, E84Signal::L_REQ, true); + s.on_signal_change(1, E84Signal::BUSY, true); + s.on_signal_change(1, E84Signal::BUSY, false); + s.on_signal_change(1, E84Signal::COMPT, true); + CHECK(s.get(1)->state() == E84State::Complete); + CHECK(s.get(2)->state() == E84State::Idle); + + // Reset LP1; LP2 is unaffected. + s.reset(1); + CHECK(s.get(1)->state() == E84State::Idle); + CHECK(s.get(2)->state() == E84State::Idle); +} + +TEST_CASE("E84PortStore: change handler reports port_id") { + E84PortStore s; + std::vector> seen; + s.set_state_change_handler( + [&](uint8_t pid, E84State, E84State to, E84Signal) { + seen.emplace_back(pid, to); + }); + REQUIRE(s.create(1)); + REQUIRE(s.create(2)); + s.on_signal_change(1, E84Signal::CS_0, true); + s.on_signal_change(2, E84Signal::CS_0, true); + s.on_signal_change(2, E84Signal::VALID, true); + s.on_signal_change(2, E84Signal::U_REQ, true); + + // Should have seen events tagged with the right port_id. + bool saw_lp1_carrier_present = false; + bool saw_lp2_unload_ready = false; + for (auto& [pid, to] : seen) { + if (pid == 1 && to == E84State::CarrierPresent) saw_lp1_carrier_present = true; + if (pid == 2 && to == E84State::UnloadReady) saw_lp2_unload_ready = true; + } + CHECK(saw_lp1_carrier_present); + CHECK(saw_lp2_unload_ready); +} + +TEST_CASE("E84PortStore: ids() returns the registered port set") { + E84PortStore s; + s.create(3); + s.create(1); + s.create(2); + auto ids = s.ids(); + // std::map iteration is ordered by key, so the returned vector is + // sorted. + REQUIRE(ids.size() == 3); + CHECK(ids[0] == 1); + CHECK(ids[1] == 2); + CHECK(ids[2] == 3); +} diff --git a/tests/test_e87_wire_scenarios.cpp b/tests/test_e87_wire_scenarios.cpp index 2bbd755..441a9f2 100644 --- a/tests/test_e87_wire_scenarios.cpp +++ b/tests/test_e87_wire_scenarios.cpp @@ -222,35 +222,28 @@ TEST_CASE("Multi-LP: 4 ports are independent — events on one don't affect anot gem::SlotMapStatus::NotRead); } -TEST_CASE("Multi-LP: simultaneous E84 handshakes on two ports stay independent") { - // No wire here — the E84 FSM is a per-port axis in the equipment - // model; we test that two separate ports running their handshakes in - // overlapping sequences don't interfere. The data model has a - // single E84StateMachine for now; this exercise documents that the - // current design is per-equipment, not per-port — so the test runs - // the second handshake AFTER the first completes, and asserts the - // second one sees the FSM cleanly returned to Idle. +TEST_CASE("Multi-LP: per-port E84 FSMs run truly in parallel") { + // E84 is now per-port (E84PortStore). Two ports can be in completely + // different states at the same time without sequencing. gem::EquipmentDataModel m; m.load_ports.create(1); m.load_ports.create(2); - // LP1 load handshake. - m.e84.on_signal_change(gem::E84Signal::CS_0, true); - m.e84.on_signal_change(gem::E84Signal::VALID, true); - m.e84.on_signal_change(gem::E84Signal::L_REQ, true); - CHECK(m.e84.state() == gem::E84State::LoadReady); - m.e84.on_signal_change(gem::E84Signal::BUSY, true); - m.e84.on_signal_change(gem::E84Signal::BUSY, false); - m.e84.on_signal_change(gem::E84Signal::COMPT, true); - CHECK(m.e84.state() == gem::E84State::Complete); + // LP1: interleave load-handshake bring-up with LP2: interleave + // unload-handshake bring-up. Both reach their respective Ready + // states with no cross-contamination. + m.e84_ports.on_signal_change(1, gem::E84Signal::CS_0, true); + m.e84_ports.on_signal_change(2, gem::E84Signal::CS_0, true); + m.e84_ports.on_signal_change(1, gem::E84Signal::VALID, true); + m.e84_ports.on_signal_change(2, gem::E84Signal::VALID, true); + m.e84_ports.on_signal_change(1, gem::E84Signal::L_REQ, true); + m.e84_ports.on_signal_change(2, gem::E84Signal::U_REQ, true); - // Reset for LP2. - m.e84.reset(); - CHECK(m.e84.state() == gem::E84State::Idle); + CHECK(m.e84_ports.get(1)->state() == gem::E84State::LoadReady); + CHECK(m.e84_ports.get(2)->state() == gem::E84State::UnloadReady); - // LP2 unload handshake. - m.e84.on_signal_change(gem::E84Signal::CS_0, true); - m.e84.on_signal_change(gem::E84Signal::VALID, true); - m.e84.on_signal_change(gem::E84Signal::U_REQ, true); - CHECK(m.e84.state() == gem::E84State::UnloadReady); + // ES on LP1 must not affect LP2. + m.e84_ports.on_signal_change(1, gem::E84Signal::ES, true); + CHECK(m.e84_ports.get(1)->state() == gem::E84State::EmergencyStop); + CHECK(m.e84_ports.get(2)->state() == gem::E84State::UnloadReady); } diff --git a/tests/test_gem300_scenario.cpp b/tests/test_gem300_scenario.cpp index 5dfac55..eba57ef 100644 --- a/tests/test_gem300_scenario.cpp +++ b/tests/test_gem300_scenario.cpp @@ -48,15 +48,15 @@ TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") { // ---- 2. E84 load handshake at LP1 -------------------------------------- // AMHS asserts CS_0, then VALID; equipment asserts L_REQ; AMHS asserts // BUSY, then drops BUSY + asserts COMPT. - m.e84.on_signal_change(E84Signal::CS_0, true); - m.e84.on_signal_change(E84Signal::VALID, true); - m.e84.on_signal_change(E84Signal::L_REQ, true); - CHECK(m.e84.state() == E84State::LoadReady); + m.e84_ports.on_signal_change(1,E84Signal::CS_0, true); + m.e84_ports.on_signal_change(1,E84Signal::VALID, true); + m.e84_ports.on_signal_change(1,E84Signal::L_REQ, true); + CHECK(m.e84_ports.get(1)->state() == E84State::LoadReady); - m.e84.on_signal_change(E84Signal::BUSY, true); - m.e84.on_signal_change(E84Signal::BUSY, false); - m.e84.on_signal_change(E84Signal::COMPT, true); - CHECK(m.e84.state() == E84State::Complete); + m.e84_ports.on_signal_change(1,E84Signal::BUSY, true); + m.e84_ports.on_signal_change(1,E84Signal::BUSY, false); + m.e84_ports.on_signal_change(1,E84Signal::COMPT, true); + CHECK(m.e84_ports.get(1)->state() == E84State::Complete); // Load port transitions on the equipment side. m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartLoading); @@ -153,15 +153,15 @@ TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") { } // ---- 12. Carrier unloads via E84 ------------------------------------- - m.e84.reset(); - m.e84.on_signal_change(E84Signal::CS_0, true); - m.e84.on_signal_change(E84Signal::VALID, true); - m.e84.on_signal_change(E84Signal::U_REQ, true); - CHECK(m.e84.state() == E84State::UnloadReady); - m.e84.on_signal_change(E84Signal::BUSY, true); - m.e84.on_signal_change(E84Signal::BUSY, false); - m.e84.on_signal_change(E84Signal::COMPT, true); - CHECK(m.e84.state() == E84State::Complete); + m.e84_ports.reset(1); + m.e84_ports.on_signal_change(1,E84Signal::CS_0, true); + m.e84_ports.on_signal_change(1,E84Signal::VALID, true); + m.e84_ports.on_signal_change(1,E84Signal::U_REQ, true); + CHECK(m.e84_ports.get(1)->state() == E84State::UnloadReady); + m.e84_ports.on_signal_change(1,E84Signal::BUSY, true); + m.e84_ports.on_signal_change(1,E84Signal::BUSY, false); + m.e84_ports.on_signal_change(1,E84Signal::COMPT, true); + CHECK(m.e84_ports.get(1)->state() == E84State::Complete); m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartUnloading); m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteUnloading);