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
+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