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:
2026-06-08 00:09:42 +02:00
parent c163d2060f
commit 94c26c0771
6 changed files with 702 additions and 0 deletions
+3
View File
@@ -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
+159
View File
@@ -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
+119
View File
@@ -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
+171
View File
@@ -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
+142
View File
@@ -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
+108
View File
@@ -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);
}