#pragma once #include #include #include #include #include #include "secsgem/gem/store/alarms.hpp" // AlarmAck (ACKC5) // E5 §9 Exception lifecycle. // // An *exception* (distinct from a plain *alarm*) is a recoverable // equipment fault whose handling involves a host/equipment dialogue: // the equipment Posts an S5F9 with one or more candidate EXRECVRA // recovery actions; the host picks one via S5F13 to drive equipment // recovery; equipment Notifies completion via S5F15 (with EXRESULT // signalling success or failure); the host may abort an in-progress // recovery via S5F17. // // This state machine lives per-EXID inside an `ExceptionStore`; the // transitions mirror the request/response cadence on the wire so the // store can be wired into the server's S5F13 / S5F17 / internal // recovery hooks with a single dispatch through `on_host_command` // (host-initiated) or `on_internal` (equipment-initiated). // // The Posted/Cleared distinction matches E5 §9.3 figure 9 — when an // exception's underlying condition clears autonomously without ever // being recovered (rare but legal), the FSM transitions Posted → // Cleared directly via the `Clear` event. namespace secsgem::gem { enum class ExceptionState : uint8_t { Posted = 0, // S5F9 sent, awaiting host action or autonomous clear Recovering = 1, // S5F13 accepted; equipment running the recovery RecoverFailed = 2, // S5F15 reported a failed recovery; retry possible Cleared = 3, // exception resolved (terminal; store removes the row) NoState = 255, }; const char* exception_state_name(ExceptionState s); std::optional parse_exception_state(const std::string& s); enum class ExceptionEvent { Created, // synthetic NoState -> Posted (observer signal) Recover, // host's S5F13 — Posted/RecoverFailed -> Recovering RecoveryComplete, // internal — Recovering -> Cleared RecoveryFailed, // internal — Recovering -> RecoverFailed RecoveryAbort, // host's S5F17 — Recovering -> Posted Clear, // internal — Posted/RecoverFailed -> Cleared }; const char* exception_event_name(ExceptionEvent e); std::optional parse_exception_event(const std::string& s); struct ExceptionTransition { ExceptionState from; ExceptionEvent on; std::optional to; std::optional ack_code; // ACKC5 (AlarmAck) when applicable }; class ExceptionTransitionTable { public: void add(ExceptionTransition row); const ExceptionTransition* find(ExceptionState from, ExceptionEvent on) const; std::size_t size() const { return rows_.size(); } const std::vector& rows() const { return rows_; } // The default table matches data/exception_state.yaml (when present); // kept in code so tests don't depend on the YAML being readable. static ExceptionTransitionTable default_table(); private: std::vector rows_; }; class ExceptionStateMachine { public: using StateChangeHandler = std::function; ExceptionStateMachine(); explicit ExceptionStateMachine(ExceptionTransitionTable table, ExceptionState initial = ExceptionState::Posted); ExceptionState state() const { return state_; } void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); } // Host-initiated event (S5F13 / S5F17). Returns AlarmAck::Accept on a // valid transition, AlarmAck::Error when no row matches. The default // table is permissive — every host event either advances state or is // silently accepted. AlarmAck on_host_command(ExceptionEvent event); // Equipment-internal event (RecoveryComplete / RecoveryFailed / Clear). // Returns true if a row fired. bool on_internal(ExceptionEvent event); private: const ExceptionTransition* fire(ExceptionEvent on); void transition(ExceptionState next, ExceptionEvent trigger); ExceptionTransitionTable table_; ExceptionState state_; StateChangeHandler on_change_; }; } // namespace secsgem::gem