77197b9c1e
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>
177 lines
8.0 KiB
C++
177 lines
8.0 KiB
C++
// GEM300 end-to-end conformance scenario: drives every top-level FSM
|
|
// through a coordinated lifecycle that mirrors what a real fab acceptance
|
|
// test would exercise. No HSMS wire here — the test drives the in-memory
|
|
// data model directly so the FSMs and stores are validated as a system.
|
|
//
|
|
// Scenario:
|
|
// 1. Equipment goes Productive (E116 EPT)
|
|
// 2. Carrier arrives at LP1 via E84 handshake (load handshake)
|
|
// 3. Carrier read: CIDS NotConfirmed -> Confirmed via host ProceedWithCarrier
|
|
// 4. Slot map: CSMS NotRead -> Read
|
|
// 5. Two substrates created bound to slots 1 and 2
|
|
// 6. Each substrate ID confirmed (E90 IDS)
|
|
// 7. Two process jobs created referencing a recipe
|
|
// 8. Control job created containing both PJs
|
|
// 9. CJ executes: PJs cascade through their lifecycle
|
|
// 10. Each substrate goes AtSource -> AtWork -> AtDestination, processing
|
|
// goes NeedsProcessing -> InProcess -> Processed
|
|
// 11. A module steps through StartGeneral -> StartStep -> CompleteStep
|
|
// 12. CJ completes; carrier unloads via E84 (unload handshake)
|
|
// 13. Equipment returns to Standby
|
|
//
|
|
// At every stage we assert the expected state across multiple FSMs so a
|
|
// regression in any one of them surfaces here.
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include "secsgem/gem/data_model.hpp"
|
|
#include "secsgem/gem/e84_state.hpp"
|
|
#include "secsgem/gem/ept_state.hpp"
|
|
#include "secsgem/gem/substrate_state.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") {
|
|
EquipmentDataModel m;
|
|
|
|
// Pre-create the recipe the PJs will reference.
|
|
m.recipes.add("RECIPE-A", "step1; step2");
|
|
m.load_ports.create(1);
|
|
m.modules.create("MOD-A");
|
|
|
|
// ---- 1. Equipment goes Productive --------------------------------------
|
|
CHECK(m.ept.state() == EptState::NonScheduledTime);
|
|
m.ept.on_event(EptEvent::EnterStandby);
|
|
m.ept.on_event(EptEvent::EnterProductive);
|
|
CHECK(m.ept.state() == EptState::Productive);
|
|
|
|
// ---- 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_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_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);
|
|
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteLoading);
|
|
|
|
// ---- 3. Carrier read and ID-confirmed ---------------------------------
|
|
m.carriers.create("CAR-1", /*port=*/1, /*capacity=*/4);
|
|
REQUIRE(m.carriers.has("CAR-1"));
|
|
m.load_ports.associate(1, "CAR-1");
|
|
CHECK(m.load_ports.get(1)->fsm->association_state() ==
|
|
LoadPortAssociationStatus::Associated);
|
|
|
|
// Host ProceedWithCarrier -> CIDS::Confirmed.
|
|
m.carriers.fire_id_event("CAR-1", CarrierIDEvent::ProceedWithCarrier);
|
|
CHECK(m.carriers.get("CAR-1")->fsm->id_status() == CarrierIDStatus::Confirmed);
|
|
|
|
// ---- 4. Slot map verified ---------------------------------------------
|
|
// Equipment fills the slot map (slots 1 and 2 populated, 3 and 4 empty).
|
|
m.carriers.get("CAR-1")->slots[0].state = 1;
|
|
m.carriers.get("CAR-1")->slots[1].state = 1;
|
|
m.carriers.get("CAR-1")->slots[2].state = 0;
|
|
m.carriers.get("CAR-1")->slots[3].state = 0;
|
|
m.carriers.fire_slot_map_event("CAR-1", SlotMapEvent::Read);
|
|
CHECK(m.carriers.get("CAR-1")->fsm->slot_map_status() == SlotMapStatus::Read);
|
|
|
|
// ---- 5. Substrates created from the carrier ---------------------------
|
|
REQUIRE(m.substrates.create("W-1", "CAR-1", /*slot=*/1) ==
|
|
SubstrateStore::CreateResult::Created);
|
|
REQUIRE(m.substrates.create("W-2", "CAR-1", /*slot=*/2) ==
|
|
SubstrateStore::CreateResult::Created);
|
|
|
|
// ---- 6. Each substrate's ID is confirmed (E90 IDS) --------------------
|
|
for (auto* id : {"W-1", "W-2"}) {
|
|
auto* s = m.substrates.get(id);
|
|
REQUIRE(s);
|
|
s->fsm->on_id_event(SubstrateIDEvent::Read);
|
|
s->fsm->on_id_event(SubstrateIDEvent::Confirm);
|
|
CHECK(s->fsm->id_state() == SubstrateIDStatus::Confirmed);
|
|
}
|
|
|
|
// ---- 7-8. Process jobs + Control job ---------------------------------
|
|
CHECK(m.process_jobs.create(
|
|
"PJ-1", "RECIPE-A", {"W-1", "W-2"},
|
|
[&](const std::string& ppid) { return m.recipes.get(ppid).has_value(); }) ==
|
|
ProcessJobStore::CreateResult::Created);
|
|
CHECK(m.control_jobs.create("CJ-1", {"PJ-1"},
|
|
[&](const std::string& id) {
|
|
return m.process_jobs.has(id);
|
|
}) == ControlJobStore::CreateResult::Created);
|
|
|
|
// ---- 9-11. CJ executes; PJs + substrates + module cascade -----------
|
|
m.control_jobs.fire_internal("CJ-1", ControlJobEvent::Select);
|
|
m.control_jobs.fire_internal("CJ-1", ControlJobEvent::SetupComplete);
|
|
m.control_jobs.fire_internal("CJ-1", ControlJobEvent::Start);
|
|
CHECK(m.control_jobs.get("CJ-1")->fsm->state() == ControlJobState::Executing);
|
|
|
|
m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::Select);
|
|
m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::SetupComplete);
|
|
m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::Start);
|
|
CHECK(m.process_jobs.get("PJ-1")->fsm->state() == ProcessJobState::Processing);
|
|
|
|
// Per-substrate processing cascade.
|
|
for (auto* id : {"W-1", "W-2"}) {
|
|
m.substrates.fire_location_event(id, SubstrateEvent::Acquire, "MOD-A");
|
|
}
|
|
|
|
// Module steps through.
|
|
m.modules.bind("MOD-A", "W-1", "step1");
|
|
m.modules.fire("MOD-A", ModuleEvent::StartGeneral);
|
|
m.modules.fire("MOD-A", ModuleEvent::StartStep);
|
|
|
|
for (auto* id : {"W-1", "W-2"}) {
|
|
m.substrates.fire_processing_event(id, SubstrateProcessingEvent::StartProcessing);
|
|
m.substrates.fire_processing_event(id, SubstrateProcessingEvent::EndProcessing);
|
|
m.substrates.fire_location_event(id, SubstrateEvent::Release, "DEST");
|
|
}
|
|
|
|
m.modules.fire("MOD-A", ModuleEvent::CompleteStep);
|
|
CHECK(m.modules.get("MOD-A")->fsm->state() == ModuleState::StepCompleted);
|
|
|
|
m.process_jobs.fire_internal("PJ-1", ProcessJobEvent::ProcessComplete);
|
|
CHECK(m.process_jobs.get("PJ-1")->fsm->state() == ProcessJobState::ProcessComplete);
|
|
|
|
m.control_jobs.fire_internal("CJ-1", ControlJobEvent::AllJobsComplete);
|
|
CHECK(m.control_jobs.get("CJ-1")->fsm->state() == ControlJobState::Completed);
|
|
|
|
// Each substrate is at destination and processed.
|
|
for (auto* id : {"W-1", "W-2"}) {
|
|
CHECK(m.substrates.get(id)->fsm->location_state() == SubstrateState::AtDestination);
|
|
CHECK(m.substrates.get(id)->fsm->processing_state() ==
|
|
SubstrateProcessingState::Processed);
|
|
// History should have recorded both axes' transitions.
|
|
CHECK(m.substrates.history(id)->size() >= 4);
|
|
}
|
|
|
|
// ---- 12. Carrier unloads via E84 -------------------------------------
|
|
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);
|
|
m.load_ports.disassociate(1);
|
|
|
|
// ---- 13. Equipment returns to Standby --------------------------------
|
|
m.ept.on_event(EptEvent::EnterStandby);
|
|
CHECK(m.ept.state() == EptState::Standby);
|
|
|
|
// Time accounting should show productive time accumulated.
|
|
CHECK(m.ept.accumulated(EptState::Productive).count() >= 0);
|
|
}
|