#pragma once #include #include #include #include #include #include "secsgem/gem/handler_slot.hpp" namespace secsgem::gem { // E30 §6.2 Control State Model. enum class ControlState { EquipmentOffline, AttemptOnline, HostOffline, OnlineLocal, OnlineRemote, }; const char* control_state_name(ControlState s); std::optional parse_control_state(const std::string& s); bool is_online(ControlState s); enum class ControlEvent { OperatorSwitchOnline, OperatorSwitchOffline, OperatorSwitchLocal, OperatorSwitchRemote, AttemptComplete, AttemptFailed, HostRequestOnline, HostRequestOffline, }; const char* control_event_name(ControlEvent e); std::optional parse_control_event(const std::string& s); enum class OnlineAck : uint8_t { Accept = 0, NotAccept = 1, AlreadyOnline = 2, }; enum class OfflineAck : uint8_t { Accept = 0, }; enum class CommAck : uint8_t { Accept = 0, Denied = 1, }; // One row of the control-state transition table (E30 §6.2 + extensions). // `to` and `then` are both optional: a row may produce no transition (e.g. // host_request_online while already OnlineRemote — ack only); a row may also // chain straight through the transient AttemptOnline state via `then`. // `ack_code` is the raw uint8_t carried by S1F18 (ONLACK) or S1F16 (OFLACK); // its interpretation depends on the triggering event. struct ControlTransition { ControlState from; ControlEvent on; std::optional to; std::optional then; std::optional ack_code; }; // Pure data table — no behaviour. Lookup is first-match on (from, on). class ControlTransitionTable { public: void add(ControlTransition row); const ControlTransition* find(ControlState from, ControlEvent on) const; std::size_t size() const { return rows_.size(); } const std::vector& rows() const { return rows_; } // Built-in default table, matching data/control_state.yaml exactly. // Used by tests so they don't depend on the YAML file being present. static ControlTransitionTable default_table(); private: std::vector rows_; }; // The E30 control state machine, driven by a transition table. All // behavioural rules live in the table; this class is just the engine. class ControlStateMachine { public: using StateChangeHandler = std::function; ControlStateMachine(); explicit ControlStateMachine(ControlTransitionTable table, ControlState initial = ControlState::HostOffline); ControlState state() const { return state_; } bool online() const { return is_online(state_); } // Replaces the primary handler (legacy single-slot semantics). void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } // Appends an observer that survives set_state_change_handler calls. void add_state_change_handler(StateChangeHandler h) { on_change_.add(std::move(h)); } // Operator actions. Return true if a transition (or self-ack) was found. bool operator_online(); bool operator_offline(); bool operator_local(); bool operator_remote(); // Host requests. Return the ack code from the matching table row. OnlineAck on_host_request_online(); OfflineAck on_host_request_offline(); private: // Apply the matching row (if any) for (state_, event); returns the row. const ControlTransition* fire(ControlEvent on); void transition(ControlState next, ControlEvent trigger); ControlTransitionTable table_; ControlState state_; HandlerSlot on_change_; }; } // namespace secsgem::gem