D1: E87 carrier and load-port state machines
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<State, Event> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,8 @@ add_library(secsgem
|
|||||||
src/gem/process_job_state.cpp
|
src/gem/process_job_state.cpp
|
||||||
src/gem/control_job_state.cpp
|
src/gem/control_job_state.cpp
|
||||||
src/gem/exception_state.cpp
|
src/gem/exception_state.cpp
|
||||||
|
src/gem/carrier_state.cpp
|
||||||
|
src/gem/load_port_state.cpp
|
||||||
src/gem/host_handler.cpp
|
src/gem/host_handler.cpp
|
||||||
src/config/loader.cpp
|
src/config/loader.cpp
|
||||||
src/endpoint.cpp
|
src/endpoint.cpp
|
||||||
@@ -98,6 +100,7 @@ add_executable(secsgem_tests
|
|||||||
tests/test_process_jobs.cpp
|
tests/test_process_jobs.cpp
|
||||||
tests/test_control_jobs.cpp
|
tests/test_control_jobs.cpp
|
||||||
tests/test_exceptions.cpp
|
tests/test_exceptions.cpp
|
||||||
|
tests/test_carrier_state.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
target_link_libraries(secsgem_tests PRIVATE secsgem doctest::doctest)
|
||||||
target_compile_definitions(secsgem_tests PRIVATE
|
target_compile_definitions(secsgem_tests PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 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<CarrierIDStatus> 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 <typename State, typename Event>
|
||||||
|
struct CarrierTransition {
|
||||||
|
State from;
|
||||||
|
Event on;
|
||||||
|
std::optional<State> to;
|
||||||
|
std::optional<uint8_t> ack_code;
|
||||||
|
};
|
||||||
|
|
||||||
|
using CarrierIDTransition = CarrierTransition<CarrierIDStatus, CarrierIDEvent>;
|
||||||
|
using SlotMapTransition = CarrierTransition<SlotMapStatus, SlotMapEvent>;
|
||||||
|
using CarrierAccessTransition = CarrierTransition<CarrierAccessStatus, CarrierAccessEvent>;
|
||||||
|
|
||||||
|
template <typename State, typename Event>
|
||||||
|
class CarrierTransitionTable {
|
||||||
|
public:
|
||||||
|
using Row = CarrierTransition<State, Event>;
|
||||||
|
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<Row>& rows() const { return rows_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Row> rows_;
|
||||||
|
};
|
||||||
|
|
||||||
|
using CarrierIDTable = CarrierTransitionTable<CarrierIDStatus, CarrierIDEvent>;
|
||||||
|
using SlotMapTable = CarrierTransitionTable<SlotMapStatus, SlotMapEvent>;
|
||||||
|
using CarrierAccessTable = CarrierTransitionTable<CarrierAccessStatus, CarrierAccessEvent>;
|
||||||
|
|
||||||
|
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<void(CarrierIDStatus from, CarrierIDStatus to, CarrierIDEvent)>;
|
||||||
|
using SlotMapChangeHandler =
|
||||||
|
std::function<void(SlotMapStatus from, SlotMapStatus to, SlotMapEvent)>;
|
||||||
|
using AccessChangeHandler =
|
||||||
|
std::function<void(CarrierAccessStatus from, CarrierAccessStatus to,
|
||||||
|
CarrierAccessEvent)>;
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<LoadPortTransferState, LoadPortTransferEvent>;
|
||||||
|
using LoadPortReservationTable =
|
||||||
|
CarrierTransitionTable<LoadPortReservationStatus, LoadPortReservationEvent>;
|
||||||
|
using LoadPortAssociationTable =
|
||||||
|
CarrierTransitionTable<LoadPortAssociationStatus, LoadPortAssociationEvent>;
|
||||||
|
|
||||||
|
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<void(LoadPortTransferState from, LoadPortTransferState to,
|
||||||
|
LoadPortTransferEvent)>;
|
||||||
|
using ReservationChangeHandler =
|
||||||
|
std::function<void(LoadPortReservationStatus from, LoadPortReservationStatus to,
|
||||||
|
LoadPortReservationEvent)>;
|
||||||
|
using AssociationChangeHandler =
|
||||||
|
std::function<void(LoadPortAssociationStatus from, LoadPortAssociationStatus to,
|
||||||
|
LoadPortAssociationEvent)>;
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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<uint8_t>(CarrierIDStatus::NotConfirmed) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(CarrierIDStatus::Confirmed) == 1);
|
||||||
|
static_assert(static_cast<uint8_t>(CarrierIDStatus::Mismatched) == 2);
|
||||||
|
static_assert(static_cast<uint8_t>(CarrierIDStatus::Unknown) == 3);
|
||||||
|
static_assert(static_cast<uint8_t>(SlotMapStatus::NotRead) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(SlotMapStatus::Read) == 1);
|
||||||
|
static_assert(static_cast<uint8_t>(SlotMapStatus::Mismatched) == 2);
|
||||||
|
static_assert(static_cast<uint8_t>(CarrierAccessStatus::NotAccessed) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(CarrierAccessStatus::InAccess) == 1);
|
||||||
|
static_assert(static_cast<uint8_t>(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<CarrierIDStatus> 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
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include "secsgem/gem/load_port_state.hpp"
|
||||||
|
|
||||||
|
namespace secsgem::gem {
|
||||||
|
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortTransferState::OutOfService) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortTransferState::InService) == 1);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortTransferState::Loading) == 2);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortTransferState::Unloading) == 3);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortReservationStatus::NotReserved) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortReservationStatus::Reserved) == 1);
|
||||||
|
static_assert(static_cast<uint8_t>(LoadPortAssociationStatus::NotAssociated) == 0);
|
||||||
|
static_assert(static_cast<uint8_t>(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
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user