e84: per-port FSM via E84PortStore
E84 (Parallel I/O) is fundamentally per-load-port: each port has its own ten-wire handshake with the AMHS. Earlier revisions modeled it as a single equipment-wide FSM; this commit refactors to a per-port store, so multi-LP tools can run independent handshakes in parallel. Public API change in EquipmentDataModel: E84StateMachine e84; -> removed E84PortStore e84_ports; // create(port_id), get(port_id), ... Convenience pass-throughs: E84PortStore::on_signal_change auto-creates the port on first use (ergonomic for demos); applications should call create() explicitly with their full port set. The two existing callsites (test_gem300_scenario, test_e87_wire_scenarios) are updated. The multi-LP test now demonstrates the actual win: interleaved LP1 load + LP2 unload handshakes that reach their respective Ready states without sequencing, and an ES on LP1 that does NOT affect LP2 — exactly the failure mode the previous design couldn't catch. Five new dedicated tests in test_e84_ports.cpp for the store itself. COMPLIANCE.md §4i updated: row now reflects per-port design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,7 @@ add_executable(secsgem_tests
|
|||||||
tests/test_sml.cpp
|
tests/test_sml.cpp
|
||||||
tests/test_s9_fallback.cpp
|
tests/test_s9_fallback.cpp
|
||||||
tests/test_e84.cpp
|
tests/test_e84.cpp
|
||||||
|
tests/test_e84_ports.cpp
|
||||||
tests/test_gem300_scenario.cpp
|
tests/test_gem300_scenario.cpp
|
||||||
tests/test_wire_ceid_emission.cpp
|
tests/test_wire_ceid_emission.cpp
|
||||||
tests/test_live_gem300.cpp
|
tests/test_live_gem300.cpp
|
||||||
|
|||||||
+1
-1
@@ -196,7 +196,7 @@ Legend:
|
|||||||
|
|
||||||
| Capability | Status | Spec ref | Messages | Notes |
|
| 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)
|
## 4j. E5 §13 Wafer Maps (S12)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "secsgem/gem/store/cem_objects.hpp"
|
#include "secsgem/gem/store/cem_objects.hpp"
|
||||||
#include "secsgem/gem/store/clock.hpp"
|
#include "secsgem/gem/store/clock.hpp"
|
||||||
#include "secsgem/gem/e84_state.hpp"
|
#include "secsgem/gem/e84_state.hpp"
|
||||||
|
#include "secsgem/gem/store/e84_ports.hpp"
|
||||||
#include "secsgem/gem/ept_state.hpp"
|
#include "secsgem/gem/ept_state.hpp"
|
||||||
#include "secsgem/gem/store/control_jobs.hpp"
|
#include "secsgem/gem/store/control_jobs.hpp"
|
||||||
#include "secsgem/gem/store/equipment_constants.hpp"
|
#include "secsgem/gem/store/equipment_constants.hpp"
|
||||||
@@ -48,7 +49,7 @@ struct EquipmentDataModel {
|
|||||||
EptStateMachine ept;
|
EptStateMachine ept;
|
||||||
CemObjectStore cem;
|
CemObjectStore cem;
|
||||||
ModuleStore modules;
|
ModuleStore modules;
|
||||||
E84StateMachine e84;
|
E84PortStore e84_ports;
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<void(uint8_t port_id, E84State from, E84State to,
|
||||||
|
E84Signal trigger)>;
|
||||||
|
|
||||||
|
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<E84StateMachine>();
|
||||||
|
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<uint8_t> ids() const {
|
||||||
|
std::vector<uint8_t> out;
|
||||||
|
out.reserve(ports_.size());
|
||||||
|
for (const auto& kv : ports_) out.push_back(kv.first);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<uint8_t, std::unique_ptr<E84StateMachine>> ports_;
|
||||||
|
StateChangeHandler on_change_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace secsgem::gem
|
||||||
@@ -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 <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<std::pair<uint8_t, E84State>> 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);
|
||||||
|
}
|
||||||
@@ -222,35 +222,28 @@ TEST_CASE("Multi-LP: 4 ports are independent — events on one don't affect anot
|
|||||||
gem::SlotMapStatus::NotRead);
|
gem::SlotMapStatus::NotRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Multi-LP: simultaneous E84 handshakes on two ports stay independent") {
|
TEST_CASE("Multi-LP: per-port E84 FSMs run truly in parallel") {
|
||||||
// No wire here — the E84 FSM is a per-port axis in the equipment
|
// E84 is now per-port (E84PortStore). Two ports can be in completely
|
||||||
// model; we test that two separate ports running their handshakes in
|
// different states at the same time without sequencing.
|
||||||
// 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.
|
|
||||||
gem::EquipmentDataModel m;
|
gem::EquipmentDataModel m;
|
||||||
m.load_ports.create(1);
|
m.load_ports.create(1);
|
||||||
m.load_ports.create(2);
|
m.load_ports.create(2);
|
||||||
|
|
||||||
// LP1 load handshake.
|
// LP1: interleave load-handshake bring-up with LP2: interleave
|
||||||
m.e84.on_signal_change(gem::E84Signal::CS_0, true);
|
// unload-handshake bring-up. Both reach their respective Ready
|
||||||
m.e84.on_signal_change(gem::E84Signal::VALID, true);
|
// states with no cross-contamination.
|
||||||
m.e84.on_signal_change(gem::E84Signal::L_REQ, true);
|
m.e84_ports.on_signal_change(1, gem::E84Signal::CS_0, true);
|
||||||
CHECK(m.e84.state() == gem::E84State::LoadReady);
|
m.e84_ports.on_signal_change(2, gem::E84Signal::CS_0, true);
|
||||||
m.e84.on_signal_change(gem::E84Signal::BUSY, true);
|
m.e84_ports.on_signal_change(1, gem::E84Signal::VALID, true);
|
||||||
m.e84.on_signal_change(gem::E84Signal::BUSY, false);
|
m.e84_ports.on_signal_change(2, gem::E84Signal::VALID, true);
|
||||||
m.e84.on_signal_change(gem::E84Signal::COMPT, true);
|
m.e84_ports.on_signal_change(1, gem::E84Signal::L_REQ, true);
|
||||||
CHECK(m.e84.state() == gem::E84State::Complete);
|
m.e84_ports.on_signal_change(2, gem::E84Signal::U_REQ, true);
|
||||||
|
|
||||||
// Reset for LP2.
|
CHECK(m.e84_ports.get(1)->state() == gem::E84State::LoadReady);
|
||||||
m.e84.reset();
|
CHECK(m.e84_ports.get(2)->state() == gem::E84State::UnloadReady);
|
||||||
CHECK(m.e84.state() == gem::E84State::Idle);
|
|
||||||
|
|
||||||
// LP2 unload handshake.
|
// ES on LP1 must not affect LP2.
|
||||||
m.e84.on_signal_change(gem::E84Signal::CS_0, true);
|
m.e84_ports.on_signal_change(1, gem::E84Signal::ES, true);
|
||||||
m.e84.on_signal_change(gem::E84Signal::VALID, true);
|
CHECK(m.e84_ports.get(1)->state() == gem::E84State::EmergencyStop);
|
||||||
m.e84.on_signal_change(gem::E84Signal::U_REQ, true);
|
CHECK(m.e84_ports.get(2)->state() == gem::E84State::UnloadReady);
|
||||||
CHECK(m.e84.state() == gem::E84State::UnloadReady);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,15 +48,15 @@ TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") {
|
|||||||
// ---- 2. E84 load handshake at LP1 --------------------------------------
|
// ---- 2. E84 load handshake at LP1 --------------------------------------
|
||||||
// AMHS asserts CS_0, then VALID; equipment asserts L_REQ; AMHS asserts
|
// AMHS asserts CS_0, then VALID; equipment asserts L_REQ; AMHS asserts
|
||||||
// BUSY, then drops BUSY + asserts COMPT.
|
// BUSY, then drops BUSY + asserts COMPT.
|
||||||
m.e84.on_signal_change(E84Signal::CS_0, true);
|
m.e84_ports.on_signal_change(1,E84Signal::CS_0, true);
|
||||||
m.e84.on_signal_change(E84Signal::VALID, true);
|
m.e84_ports.on_signal_change(1,E84Signal::VALID, true);
|
||||||
m.e84.on_signal_change(E84Signal::L_REQ, true);
|
m.e84_ports.on_signal_change(1,E84Signal::L_REQ, true);
|
||||||
CHECK(m.e84.state() == E84State::LoadReady);
|
CHECK(m.e84_ports.get(1)->state() == E84State::LoadReady);
|
||||||
|
|
||||||
m.e84.on_signal_change(E84Signal::BUSY, true);
|
m.e84_ports.on_signal_change(1,E84Signal::BUSY, true);
|
||||||
m.e84.on_signal_change(E84Signal::BUSY, false);
|
m.e84_ports.on_signal_change(1,E84Signal::BUSY, false);
|
||||||
m.e84.on_signal_change(E84Signal::COMPT, true);
|
m.e84_ports.on_signal_change(1,E84Signal::COMPT, true);
|
||||||
CHECK(m.e84.state() == E84State::Complete);
|
CHECK(m.e84_ports.get(1)->state() == E84State::Complete);
|
||||||
|
|
||||||
// Load port transitions on the equipment side.
|
// Load port transitions on the equipment side.
|
||||||
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartLoading);
|
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 -------------------------------------
|
// ---- 12. Carrier unloads via E84 -------------------------------------
|
||||||
m.e84.reset();
|
m.e84_ports.reset(1);
|
||||||
m.e84.on_signal_change(E84Signal::CS_0, true);
|
m.e84_ports.on_signal_change(1,E84Signal::CS_0, true);
|
||||||
m.e84.on_signal_change(E84Signal::VALID, true);
|
m.e84_ports.on_signal_change(1,E84Signal::VALID, true);
|
||||||
m.e84.on_signal_change(E84Signal::U_REQ, true);
|
m.e84_ports.on_signal_change(1,E84Signal::U_REQ, true);
|
||||||
CHECK(m.e84.state() == E84State::UnloadReady);
|
CHECK(m.e84_ports.get(1)->state() == E84State::UnloadReady);
|
||||||
m.e84.on_signal_change(E84Signal::BUSY, true);
|
m.e84_ports.on_signal_change(1,E84Signal::BUSY, true);
|
||||||
m.e84.on_signal_change(E84Signal::BUSY, false);
|
m.e84_ports.on_signal_change(1,E84Signal::BUSY, false);
|
||||||
m.e84.on_signal_change(E84Signal::COMPT, true);
|
m.e84_ports.on_signal_change(1,E84Signal::COMPT, true);
|
||||||
CHECK(m.e84.state() == E84State::Complete);
|
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::StartUnloading);
|
||||||
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteUnloading);
|
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteUnloading);
|
||||||
|
|||||||
Reference in New Issue
Block a user