94c26c0771
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>
172 lines
6.4 KiB
C++
172 lines
6.4 KiB
C++
#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
|