f56639ba17
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>
171 lines
6.1 KiB
C++
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
|