Files
secs-gem/src/gem/carrier_state.cpp
T
raphael 94c26c0771 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>
2026-06-08 00:09:42 +02:00

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