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:
@@ -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
|
||||
Reference in New Issue
Block a user