// 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 #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); }