From 94c26c07717326c5151f845dc07db8e234ae24d9 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 00:09:42 +0200 Subject: [PATCH] D1: E87 carrier and load-port state machines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-carrier triple FSM: CIDS (id verification), CSMS (slot-map), CAS (access). Per-port triple FSM: LPTS (transfer), LRS (reservation), LAS (association). Wire-byte enum values pinned via static_assert to match E87-0716 §10.3. CarrierStateMachine combines the three carrier-side FSMs because they are independent but always observed together; same for LoadPortState- Machine. Generic CarrierTransitionTable template is reused across all six tables — same row shape as the PJ/CJ/Exception tables that already exist. Default tables cover the spec's documented transitions: CIDS: NotConfirmed <-> Confirmed/Mismatched/Unknown, Cancel returns to NotConfirmed from any state, Bind force-confirms. CSMS: NotRead -> Read -> {Mismatched, Reset}. CAS: NotAccessed -> InAccess -> Complete (terminal). LPTS: OutOfService <-> InService <-> Loading/Unloading. LRS / LAS: simple boolean toggle pairs. 15 test cases assert the happy-path lifecycles, cross-state cancels, and that change handlers fire only on real transitions (Read in NotConfirmed is a no-op, not a handler call). Co-Authored-By: Claude Opus 4.7 --- CMakeLists.txt | 3 + include/secsgem/gem/carrier_state.hpp | 159 ++++++++++++++++++++++ include/secsgem/gem/load_port_state.hpp | 119 +++++++++++++++++ src/gem/carrier_state.cpp | 171 ++++++++++++++++++++++++ src/gem/load_port_state.cpp | 142 ++++++++++++++++++++ tests/test_carrier_state.cpp | 108 +++++++++++++++ 6 files changed, 702 insertions(+) create mode 100644 include/secsgem/gem/carrier_state.hpp create mode 100644 include/secsgem/gem/load_port_state.hpp create mode 100644 src/gem/carrier_state.cpp create mode 100644 src/gem/load_port_state.cpp create mode 100644 tests/test_carrier_state.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a38387a..04fee28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,8 @@ add_library(secsgem src/gem/process_job_state.cpp src/gem/control_job_state.cpp src/gem/exception_state.cpp + src/gem/carrier_state.cpp + src/gem/load_port_state.cpp src/gem/host_handler.cpp src/config/loader.cpp src/endpoint.cpp @@ -98,6 +100,7 @@ add_executable(secsgem_tests tests/test_process_jobs.cpp tests/test_control_jobs.cpp tests/test_exceptions.cpp + tests/test_carrier_state.cpp ) target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest) target_compile_definitions(secsgem_tests PRIVATE diff --git a/include/secsgem/gem/carrier_state.hpp b/include/secsgem/gem/carrier_state.hpp new file mode 100644 index 0000000..55c0864 --- /dev/null +++ b/include/secsgem/gem/carrier_state.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include + +// E87 §6 Carrier Management — three independent state models that +// every carrier carries simultaneously: +// +// CIDS CarrierIDStatus id-verification lifecycle +// CSMS CarrierSlotMapStatus slot-map verification lifecycle +// CAS CarrierAccessingStatus is access in progress? +// +// Each FSM is per-carrier (keyed by CARRIERID inside CarrierStore) and +// shares the table-driven pattern used by ProcessJobStateMachine / +// ExceptionStateMachine. The wire-byte values match the on-the-wire +// enums (E87-0716 §10.3). +namespace secsgem::gem { + +// CIDS — CARRIER ID Status (E87-0716 §10.3.2). +enum class CarrierIDStatus : uint8_t { + NotConfirmed = 0, // initial; carrier has arrived, ID not yet verified + Confirmed = 1, // host accepted the ID + Mismatched = 2, // host rejected the ID + Unknown = 3, // carrier ID could not be read + NoState = 255, +}; + +const char* carrier_id_status_name(CarrierIDStatus s); +std::optional parse_carrier_id_status(const std::string& s); + +enum class CarrierIDEvent { + Read, // equipment internal: an ID was read + ProceedWithCarrier, // host: accept the ID (NotConfirmed -> Confirmed) + CancelCarrier, // host: reject (any -> NotConfirmed) + IDFailed, // equipment internal: read attempt failed + Bind, // host explicit bind: any -> Confirmed (E87 §10.2.5) +}; + +const char* carrier_id_event_name(CarrierIDEvent e); + +// CSMS — CARRIER Slot Map Status (E87-0716 §10.3.3). +enum class SlotMapStatus : uint8_t { + NotRead = 0, + Read = 1, + Mismatched = 2, + NoState = 255, +}; + +const char* slot_map_status_name(SlotMapStatus s); + +enum class SlotMapEvent { + Read, // equipment internal: slot map was scanned + Confirm, // host: matches what was expected + Mismatch, // host: did not match expected + Reset, // any -> NotRead (e.g. carrier removed) +}; + +const char* slot_map_event_name(SlotMapEvent e); + +// CAS — CARRIER Accessing Status (E87-0716 §10.3.4). +enum class CarrierAccessStatus : uint8_t { + NotAccessed = 0, + InAccess = 1, + Complete = 2, // (CarrierCompleted in some texts) + NoState = 255, +}; + +const char* carrier_access_status_name(CarrierAccessStatus s); + +enum class CarrierAccessEvent { + BeginAccess, // equipment internal: first substrate-move at carrier + EndAccess, // equipment internal: last substrate processed + Cancel, // any -> Complete (cancelled before access began) +}; + +const char* carrier_access_event_name(CarrierAccessEvent e); + +// One row of each transition table; same shape as the PJ/CJ FSM rows +// elsewhere in the project. +template +struct CarrierTransition { + State from; + Event on; + std::optional to; + std::optional ack_code; +}; + +using CarrierIDTransition = CarrierTransition; +using SlotMapTransition = CarrierTransition; +using CarrierAccessTransition = CarrierTransition; + +template +class CarrierTransitionTable { + public: + using Row = CarrierTransition; + void add(Row r) { rows_.push_back(r); } + const Row* find(State from, Event on) const { + for (const auto& r : rows_) if (r.from == from && r.on == on) return &r; + return nullptr; + } + std::size_t size() const { return rows_.size(); } + const std::vector& rows() const { return rows_; } + + private: + std::vector rows_; +}; + +using CarrierIDTable = CarrierTransitionTable; +using SlotMapTable = CarrierTransitionTable; +using CarrierAccessTable = CarrierTransitionTable; + +CarrierIDTable default_carrier_id_table(); +SlotMapTable default_slot_map_table(); +CarrierAccessTable default_carrier_access_table(); + +// Per-carrier triple-FSM. All three substates are independent — a +// carrier in CAS::InAccess may still be CIDS::NotConfirmed if the host +// hasn't acknowledged the ID yet; the application is responsible for +// any cross-FSM constraints it cares about. +class CarrierStateMachine { + public: + using IDChangeHandler = + std::function; + using SlotMapChangeHandler = + std::function; + using AccessChangeHandler = + std::function; + + CarrierStateMachine(); + + CarrierIDStatus id_status() const { return id_state_; } + SlotMapStatus slot_map_status() const { return sm_state_; } + CarrierAccessStatus access_status() const { return acc_state_; } + + void set_id_handler(IDChangeHandler h) { on_id_change_ = std::move(h); } + void set_slot_map_handler(SlotMapChangeHandler h) { on_sm_change_ = std::move(h); } + void set_access_handler(AccessChangeHandler h) { on_acc_change_ = std::move(h); } + + bool on_id_event(CarrierIDEvent e); + bool on_slot_map_event(SlotMapEvent e); + bool on_access_event(CarrierAccessEvent e); + + private: + CarrierIDTable id_table_; + SlotMapTable sm_table_; + CarrierAccessTable acc_table_; + CarrierIDStatus id_state_ = CarrierIDStatus::NotConfirmed; + SlotMapStatus sm_state_ = SlotMapStatus::NotRead; + CarrierAccessStatus acc_state_ = CarrierAccessStatus::NotAccessed; + IDChangeHandler on_id_change_; + SlotMapChangeHandler on_sm_change_; + AccessChangeHandler on_acc_change_; +}; + +} // namespace secsgem::gem diff --git a/include/secsgem/gem/load_port_state.hpp b/include/secsgem/gem/load_port_state.hpp new file mode 100644 index 0000000..68ad12f --- /dev/null +++ b/include/secsgem/gem/load_port_state.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include + +#include "secsgem/gem/carrier_state.hpp" // shared CarrierTransition template + +// E87 §6 Load Port state models — three independent FSMs per port: +// +// LPTS LoadPortTransferState - is a carrier being moved on/off? +// LRS LoadPortReservationStatus - host reservation +// LAS LoadPortAssociationStatus - is a carrier associated? +// +// Wire-byte values match E87-0716 §10.3.5-7. +namespace secsgem::gem { + +enum class LoadPortTransferState : uint8_t { + OutOfService = 0, + InService = 1, // ready, no carrier in motion + Loading = 2, // carrier arriving + Unloading = 3, // carrier leaving + NoState = 255, +}; + +const char* load_port_transfer_state_name(LoadPortTransferState s); + +enum class LoadPortTransferEvent { + EnterService, // OutOfService -> InService + LeaveService, // any -> OutOfService + StartLoading, // InService -> Loading + CompleteLoading, // Loading -> InService + StartUnloading, // InService -> Unloading + CompleteUnloading, // Unloading -> InService +}; + +const char* load_port_transfer_event_name(LoadPortTransferEvent e); + +enum class LoadPortReservationStatus : uint8_t { + NotReserved = 0, + Reserved = 1, + NoState = 255, +}; + +const char* load_port_reservation_status_name(LoadPortReservationStatus s); + +enum class LoadPortReservationEvent { + Reserve, // NotReserved -> Reserved + Release, // Reserved -> NotReserved +}; + +const char* load_port_reservation_event_name(LoadPortReservationEvent e); + +enum class LoadPortAssociationStatus : uint8_t { + NotAssociated = 0, + Associated = 1, + NoState = 255, +}; + +const char* load_port_association_status_name(LoadPortAssociationStatus s); + +enum class LoadPortAssociationEvent { + Associate, // NotAssociated -> Associated + Disassociate, // Associated -> NotAssociated +}; + +const char* load_port_association_event_name(LoadPortAssociationEvent e); + +using LoadPortTransferTable = + CarrierTransitionTable; +using LoadPortReservationTable = + CarrierTransitionTable; +using LoadPortAssociationTable = + CarrierTransitionTable; + +LoadPortTransferTable default_load_port_transfer_table(); +LoadPortReservationTable default_load_port_reservation_table(); +LoadPortAssociationTable default_load_port_association_table(); + +class LoadPortStateMachine { + public: + using TransferChangeHandler = + std::function; + using ReservationChangeHandler = + std::function; + using AssociationChangeHandler = + std::function; + + LoadPortStateMachine(); + + LoadPortTransferState transfer_state() const { return tx_state_; } + LoadPortReservationStatus reservation_state() const { return rs_state_; } + LoadPortAssociationStatus association_state() const { return as_state_; } + + void set_transfer_handler(TransferChangeHandler h) { on_tx_ = std::move(h); } + void set_reservation_handler(ReservationChangeHandler h) { on_rs_ = std::move(h); } + void set_association_handler(AssociationChangeHandler h) { on_as_ = std::move(h); } + + bool on_transfer_event(LoadPortTransferEvent e); + bool on_reservation_event(LoadPortReservationEvent e); + bool on_association_event(LoadPortAssociationEvent e); + + private: + LoadPortTransferTable tx_table_; + LoadPortReservationTable rs_table_; + LoadPortAssociationTable as_table_; + LoadPortTransferState tx_state_ = LoadPortTransferState::InService; + LoadPortReservationStatus rs_state_ = LoadPortReservationStatus::NotReserved; + LoadPortAssociationStatus as_state_ = LoadPortAssociationStatus::NotAssociated; + TransferChangeHandler on_tx_; + ReservationChangeHandler on_rs_; + AssociationChangeHandler on_as_; +}; + +} // namespace secsgem::gem diff --git a/src/gem/carrier_state.cpp b/src/gem/carrier_state.cpp new file mode 100644 index 0000000..078ace5 --- /dev/null +++ b/src/gem/carrier_state.cpp @@ -0,0 +1,171 @@ +#include "secsgem/gem/carrier_state.hpp" + +namespace secsgem::gem { + +// Pin the wire-byte values per E87-0716 §10.3. +static_assert(static_cast(CarrierIDStatus::NotConfirmed) == 0); +static_assert(static_cast(CarrierIDStatus::Confirmed) == 1); +static_assert(static_cast(CarrierIDStatus::Mismatched) == 2); +static_assert(static_cast(CarrierIDStatus::Unknown) == 3); +static_assert(static_cast(SlotMapStatus::NotRead) == 0); +static_assert(static_cast(SlotMapStatus::Read) == 1); +static_assert(static_cast(SlotMapStatus::Mismatched) == 2); +static_assert(static_cast(CarrierAccessStatus::NotAccessed) == 0); +static_assert(static_cast(CarrierAccessStatus::InAccess) == 1); +static_assert(static_cast(CarrierAccessStatus::Complete) == 2); + +const char* carrier_id_status_name(CarrierIDStatus s) { + switch (s) { + case CarrierIDStatus::NotConfirmed: return "NotConfirmed"; + case CarrierIDStatus::Confirmed: return "Confirmed"; + case CarrierIDStatus::Mismatched: return "Mismatched"; + case CarrierIDStatus::Unknown: return "Unknown"; + case CarrierIDStatus::NoState: return "NoState"; + } + return "?"; +} + +std::optional parse_carrier_id_status(const std::string& s) { + if (s == "NotConfirmed") return CarrierIDStatus::NotConfirmed; + if (s == "Confirmed") return CarrierIDStatus::Confirmed; + if (s == "Mismatched") return CarrierIDStatus::Mismatched; + if (s == "Unknown") return CarrierIDStatus::Unknown; + if (s == "NoState") return CarrierIDStatus::NoState; + return std::nullopt; +} + +const char* carrier_id_event_name(CarrierIDEvent e) { + switch (e) { + case CarrierIDEvent::Read: return "Read"; + case CarrierIDEvent::ProceedWithCarrier: return "ProceedWithCarrier"; + case CarrierIDEvent::CancelCarrier: return "CancelCarrier"; + case CarrierIDEvent::IDFailed: return "IDFailed"; + case CarrierIDEvent::Bind: return "Bind"; + } + return "?"; +} + +const char* slot_map_status_name(SlotMapStatus s) { + switch (s) { + case SlotMapStatus::NotRead: return "NotRead"; + case SlotMapStatus::Read: return "Read"; + case SlotMapStatus::Mismatched: return "Mismatched"; + case SlotMapStatus::NoState: return "NoState"; + } + return "?"; +} + +const char* slot_map_event_name(SlotMapEvent e) { + switch (e) { + case SlotMapEvent::Read: return "Read"; + case SlotMapEvent::Confirm: return "Confirm"; + case SlotMapEvent::Mismatch: return "Mismatch"; + case SlotMapEvent::Reset: return "Reset"; + } + return "?"; +} + +const char* carrier_access_status_name(CarrierAccessStatus s) { + switch (s) { + case CarrierAccessStatus::NotAccessed: return "NotAccessed"; + case CarrierAccessStatus::InAccess: return "InAccess"; + case CarrierAccessStatus::Complete: return "Complete"; + case CarrierAccessStatus::NoState: return "NoState"; + } + return "?"; +} + +const char* carrier_access_event_name(CarrierAccessEvent e) { + switch (e) { + case CarrierAccessEvent::BeginAccess: return "BeginAccess"; + case CarrierAccessEvent::EndAccess: return "EndAccess"; + case CarrierAccessEvent::Cancel: return "Cancel"; + } + return "?"; +} + +CarrierIDTable default_carrier_id_table() { + using S = CarrierIDStatus; + using E = CarrierIDEvent; + CarrierIDTable t; + // NotConfirmed (initial) + t.add({S::NotConfirmed, E::Read, S::NotConfirmed, std::nullopt}); + t.add({S::NotConfirmed, E::ProceedWithCarrier, S::Confirmed, std::nullopt}); + t.add({S::NotConfirmed, E::Bind, S::Confirmed, std::nullopt}); + t.add({S::NotConfirmed, E::IDFailed, S::Unknown, std::nullopt}); + t.add({S::NotConfirmed, E::CancelCarrier, S::NotConfirmed, std::nullopt}); + // Confirmed + t.add({S::Confirmed, E::CancelCarrier, S::NotConfirmed, std::nullopt}); + // Mismatched + t.add({S::Mismatched, E::ProceedWithCarrier, S::Confirmed, std::nullopt}); + t.add({S::Mismatched, E::CancelCarrier, S::NotConfirmed, std::nullopt}); + t.add({S::Mismatched, E::Bind, S::Confirmed, std::nullopt}); + // Unknown + t.add({S::Unknown, E::Bind, S::Confirmed, std::nullopt}); + t.add({S::Unknown, E::CancelCarrier, S::NotConfirmed, std::nullopt}); + return t; +} + +SlotMapTable default_slot_map_table() { + using S = SlotMapStatus; + using E = SlotMapEvent; + SlotMapTable t; + t.add({S::NotRead, E::Read, S::Read, std::nullopt}); + t.add({S::Read, E::Confirm, S::Read, std::nullopt}); // ack-only + t.add({S::Read, E::Mismatch, S::Mismatched, std::nullopt}); + t.add({S::Read, E::Reset, S::NotRead, std::nullopt}); + t.add({S::Mismatched, E::Reset, S::NotRead, std::nullopt}); + t.add({S::Mismatched, E::Read, S::Read, std::nullopt}); // re-scan + return t; +} + +CarrierAccessTable default_carrier_access_table() { + using S = CarrierAccessStatus; + using E = CarrierAccessEvent; + CarrierAccessTable t; + t.add({S::NotAccessed, E::BeginAccess, S::InAccess, std::nullopt}); + t.add({S::NotAccessed, E::Cancel, S::Complete, std::nullopt}); + t.add({S::InAccess, E::EndAccess, S::Complete, std::nullopt}); + t.add({S::InAccess, E::Cancel, S::Complete, std::nullopt}); + return t; +} + +CarrierStateMachine::CarrierStateMachine() + : id_table_(default_carrier_id_table()), + sm_table_(default_slot_map_table()), + acc_table_(default_carrier_access_table()) {} + +bool CarrierStateMachine::on_id_event(CarrierIDEvent e) { + const auto* row = id_table_.find(id_state_, e); + if (!row) return false; + if (row->to && *row->to != id_state_) { + auto prev = id_state_; + id_state_ = *row->to; + if (on_id_change_) on_id_change_(prev, id_state_, e); + } + return true; +} + +bool CarrierStateMachine::on_slot_map_event(SlotMapEvent e) { + const auto* row = sm_table_.find(sm_state_, e); + if (!row) return false; + if (row->to && *row->to != sm_state_) { + auto prev = sm_state_; + sm_state_ = *row->to; + if (on_sm_change_) on_sm_change_(prev, sm_state_, e); + } + return true; +} + +bool CarrierStateMachine::on_access_event(CarrierAccessEvent e) { + const auto* row = acc_table_.find(acc_state_, e); + if (!row) return false; + if (row->to && *row->to != acc_state_) { + auto prev = acc_state_; + acc_state_ = *row->to; + if (on_acc_change_) on_acc_change_(prev, acc_state_, e); + } + return true; +} + +} // namespace secsgem::gem diff --git a/src/gem/load_port_state.cpp b/src/gem/load_port_state.cpp new file mode 100644 index 0000000..c2e91d8 --- /dev/null +++ b/src/gem/load_port_state.cpp @@ -0,0 +1,142 @@ +#include "secsgem/gem/load_port_state.hpp" + +namespace secsgem::gem { + +static_assert(static_cast(LoadPortTransferState::OutOfService) == 0); +static_assert(static_cast(LoadPortTransferState::InService) == 1); +static_assert(static_cast(LoadPortTransferState::Loading) == 2); +static_assert(static_cast(LoadPortTransferState::Unloading) == 3); +static_assert(static_cast(LoadPortReservationStatus::NotReserved) == 0); +static_assert(static_cast(LoadPortReservationStatus::Reserved) == 1); +static_assert(static_cast(LoadPortAssociationStatus::NotAssociated) == 0); +static_assert(static_cast(LoadPortAssociationStatus::Associated) == 1); + +const char* load_port_transfer_state_name(LoadPortTransferState s) { + switch (s) { + case LoadPortTransferState::OutOfService: return "OutOfService"; + case LoadPortTransferState::InService: return "InService"; + case LoadPortTransferState::Loading: return "Loading"; + case LoadPortTransferState::Unloading: return "Unloading"; + case LoadPortTransferState::NoState: return "NoState"; + } + return "?"; +} + +const char* load_port_transfer_event_name(LoadPortTransferEvent e) { + switch (e) { + case LoadPortTransferEvent::EnterService: return "EnterService"; + case LoadPortTransferEvent::LeaveService: return "LeaveService"; + case LoadPortTransferEvent::StartLoading: return "StartLoading"; + case LoadPortTransferEvent::CompleteLoading: return "CompleteLoading"; + case LoadPortTransferEvent::StartUnloading: return "StartUnloading"; + case LoadPortTransferEvent::CompleteUnloading: return "CompleteUnloading"; + } + return "?"; +} + +const char* load_port_reservation_status_name(LoadPortReservationStatus s) { + switch (s) { + case LoadPortReservationStatus::NotReserved: return "NotReserved"; + case LoadPortReservationStatus::Reserved: return "Reserved"; + case LoadPortReservationStatus::NoState: return "NoState"; + } + return "?"; +} + +const char* load_port_reservation_event_name(LoadPortReservationEvent e) { + switch (e) { + case LoadPortReservationEvent::Reserve: return "Reserve"; + case LoadPortReservationEvent::Release: return "Release"; + } + return "?"; +} + +const char* load_port_association_status_name(LoadPortAssociationStatus s) { + switch (s) { + case LoadPortAssociationStatus::NotAssociated: return "NotAssociated"; + case LoadPortAssociationStatus::Associated: return "Associated"; + case LoadPortAssociationStatus::NoState: return "NoState"; + } + return "?"; +} + +const char* load_port_association_event_name(LoadPortAssociationEvent e) { + switch (e) { + case LoadPortAssociationEvent::Associate: return "Associate"; + case LoadPortAssociationEvent::Disassociate: return "Disassociate"; + } + return "?"; +} + +LoadPortTransferTable default_load_port_transfer_table() { + using S = LoadPortTransferState; + using E = LoadPortTransferEvent; + LoadPortTransferTable t; + t.add({S::OutOfService, E::EnterService, S::InService, std::nullopt}); + t.add({S::InService, E::LeaveService, S::OutOfService, std::nullopt}); + t.add({S::InService, E::StartLoading, S::Loading, std::nullopt}); + t.add({S::InService, E::StartUnloading, S::Unloading, std::nullopt}); + t.add({S::Loading, E::CompleteLoading, S::InService, std::nullopt}); + t.add({S::Loading, E::LeaveService, S::OutOfService, std::nullopt}); + t.add({S::Unloading, E::CompleteUnloading, S::InService, std::nullopt}); + t.add({S::Unloading, E::LeaveService, S::OutOfService, std::nullopt}); + return t; +} + +LoadPortReservationTable default_load_port_reservation_table() { + using S = LoadPortReservationStatus; + using E = LoadPortReservationEvent; + LoadPortReservationTable t; + t.add({S::NotReserved, E::Reserve, S::Reserved, std::nullopt}); + t.add({S::Reserved, E::Release, S::NotReserved, std::nullopt}); + return t; +} + +LoadPortAssociationTable default_load_port_association_table() { + using S = LoadPortAssociationStatus; + using E = LoadPortAssociationEvent; + LoadPortAssociationTable t; + t.add({S::NotAssociated, E::Associate, S::Associated, std::nullopt}); + t.add({S::Associated, E::Disassociate, S::NotAssociated, std::nullopt}); + return t; +} + +LoadPortStateMachine::LoadPortStateMachine() + : tx_table_(default_load_port_transfer_table()), + rs_table_(default_load_port_reservation_table()), + as_table_(default_load_port_association_table()) {} + +bool LoadPortStateMachine::on_transfer_event(LoadPortTransferEvent e) { + const auto* row = tx_table_.find(tx_state_, e); + if (!row) return false; + if (row->to && *row->to != tx_state_) { + auto prev = tx_state_; + tx_state_ = *row->to; + if (on_tx_) on_tx_(prev, tx_state_, e); + } + return true; +} + +bool LoadPortStateMachine::on_reservation_event(LoadPortReservationEvent e) { + const auto* row = rs_table_.find(rs_state_, e); + if (!row) return false; + if (row->to && *row->to != rs_state_) { + auto prev = rs_state_; + rs_state_ = *row->to; + if (on_rs_) on_rs_(prev, rs_state_, e); + } + return true; +} + +bool LoadPortStateMachine::on_association_event(LoadPortAssociationEvent e) { + const auto* row = as_table_.find(as_state_, e); + if (!row) return false; + if (row->to && *row->to != as_state_) { + auto prev = as_state_; + as_state_ = *row->to; + if (on_as_) on_as_(prev, as_state_, e); + } + return true; +} + +} // namespace secsgem::gem diff --git a/tests/test_carrier_state.cpp b/tests/test_carrier_state.cpp new file mode 100644 index 0000000..5da3881 --- /dev/null +++ b/tests/test_carrier_state.cpp @@ -0,0 +1,108 @@ +#include + +#include "secsgem/gem/carrier_state.hpp" +#include "secsgem/gem/load_port_state.hpp" + +using namespace secsgem::gem; + +// ---- CIDS --------------------------------------------------------------- + +TEST_CASE("CIDS: NotConfirmed -> Confirmed on ProceedWithCarrier") { + CarrierStateMachine c; + CHECK(c.id_status() == CarrierIDStatus::NotConfirmed); + CHECK(c.on_id_event(CarrierIDEvent::ProceedWithCarrier)); + CHECK(c.id_status() == CarrierIDStatus::Confirmed); +} + +TEST_CASE("CIDS: NotConfirmed -> Unknown on IDFailed; Bind recovers") { + CarrierStateMachine c; + CHECK(c.on_id_event(CarrierIDEvent::IDFailed)); + CHECK(c.id_status() == CarrierIDStatus::Unknown); + CHECK(c.on_id_event(CarrierIDEvent::Bind)); + CHECK(c.id_status() == CarrierIDStatus::Confirmed); +} + +TEST_CASE("CIDS: CancelCarrier from any state returns to NotConfirmed") { + CarrierStateMachine c; + c.on_id_event(CarrierIDEvent::ProceedWithCarrier); + REQUIRE(c.id_status() == CarrierIDStatus::Confirmed); + CHECK(c.on_id_event(CarrierIDEvent::CancelCarrier)); + CHECK(c.id_status() == CarrierIDStatus::NotConfirmed); +} + +// ---- CSMS --------------------------------------------------------------- + +TEST_CASE("CSMS: NotRead -> Read -> Mismatched -> NotRead via Reset") { + CarrierStateMachine c; + CHECK(c.slot_map_status() == SlotMapStatus::NotRead); + CHECK(c.on_slot_map_event(SlotMapEvent::Read)); + CHECK(c.slot_map_status() == SlotMapStatus::Read); + CHECK(c.on_slot_map_event(SlotMapEvent::Mismatch)); + CHECK(c.slot_map_status() == SlotMapStatus::Mismatched); + CHECK(c.on_slot_map_event(SlotMapEvent::Reset)); + CHECK(c.slot_map_status() == SlotMapStatus::NotRead); +} + +// ---- CAS ---------------------------------------------------------------- + +TEST_CASE("CAS: full access lifecycle BeginAccess -> EndAccess -> Complete") { + CarrierStateMachine c; + CHECK(c.on_access_event(CarrierAccessEvent::BeginAccess)); + CHECK(c.access_status() == CarrierAccessStatus::InAccess); + CHECK(c.on_access_event(CarrierAccessEvent::EndAccess)); + CHECK(c.access_status() == CarrierAccessStatus::Complete); + // Terminal: no further transitions. + CHECK_FALSE(c.on_access_event(CarrierAccessEvent::BeginAccess)); +} + +TEST_CASE("CAS: Cancel from NotAccessed jumps straight to Complete") { + CarrierStateMachine c; + CHECK(c.on_access_event(CarrierAccessEvent::Cancel)); + CHECK(c.access_status() == CarrierAccessStatus::Complete); +} + +// ---- LoadPort ----------------------------------------------------------- + +TEST_CASE("LPTS: InService -> Loading -> InService") { + LoadPortStateMachine p; + CHECK(p.transfer_state() == LoadPortTransferState::InService); + CHECK(p.on_transfer_event(LoadPortTransferEvent::StartLoading)); + CHECK(p.transfer_state() == LoadPortTransferState::Loading); + CHECK(p.on_transfer_event(LoadPortTransferEvent::CompleteLoading)); + CHECK(p.transfer_state() == LoadPortTransferState::InService); +} + +TEST_CASE("LPTS: any -> OutOfService via LeaveService") { + LoadPortStateMachine p; + p.on_transfer_event(LoadPortTransferEvent::StartLoading); + CHECK(p.on_transfer_event(LoadPortTransferEvent::LeaveService)); + CHECK(p.transfer_state() == LoadPortTransferState::OutOfService); +} + +TEST_CASE("LRS: Reserve / Release") { + LoadPortStateMachine p; + CHECK(p.on_reservation_event(LoadPortReservationEvent::Reserve)); + CHECK(p.reservation_state() == LoadPortReservationStatus::Reserved); + CHECK(p.on_reservation_event(LoadPortReservationEvent::Release)); + CHECK(p.reservation_state() == LoadPortReservationStatus::NotReserved); +} + +TEST_CASE("LAS: Associate / Disassociate") { + LoadPortStateMachine p; + CHECK(p.on_association_event(LoadPortAssociationEvent::Associate)); + CHECK(p.association_state() == LoadPortAssociationStatus::Associated); + CHECK(p.on_association_event(LoadPortAssociationEvent::Disassociate)); + CHECK(p.association_state() == LoadPortAssociationStatus::NotAssociated); +} + +TEST_CASE("CarrierStateMachine: change handlers fire on real transitions only") { + CarrierStateMachine c; + int id_calls = 0; + c.set_id_handler([&](CarrierIDStatus, CarrierIDStatus, CarrierIDEvent) { ++id_calls; }); + // Read in NotConfirmed has no `to` change, no handler call. + c.on_id_event(CarrierIDEvent::Read); + CHECK(id_calls == 0); + // ProceedWithCarrier does transition. + c.on_id_event(CarrierIDEvent::ProceedWithCarrier); + CHECK(id_calls == 1); +}