Files
raphael f56639ba17 persistence: CarrierStore + LoadPortStore enable_persistence(dir)
Mirrors SpoolStore: per-record file with atomic .tmp+rename, magic+
version-prefixed binary layout, replay on enable, delete on remove.
FSMs gain a restore_state() that bypasses the transition table and
handlers since a replay isn't a transition.

Six new tests cover write+restart+replay across every CIDS/CSMS/CAS
axis, remove-deletes-journal, malformed-record drop-not-poison, and
the persistence-disabled no-op path.

Closes #1 in the test-gap backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 10:25:50 +02:00

171 lines
6.1 KiB
C++

#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);
// Direct state-restore (persistence replay only). Bypasses the
// transition tables and does NOT fire change handlers — a restored
// state isn't a transition, it's a checkpoint. Callers must ensure
// the (id, sm, acc) triple was a legal state at write time.
void restore_state(CarrierIDStatus id, SlotMapStatus sm,
CarrierAccessStatus acc) {
id_state_ = id;
sm_state_ = sm;
acc_state_ = acc;
}
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