#pragma once #include #include #include #include #include namespace secsgem::gem { // E30 §6.5 — GEM Communication State Model. // // This is a *separate* state machine from the HSMS connection state // (NOT-SELECTED / SELECTED) and from the E30 control state model. It // governs whether the equipment is currently in a "communicating" // relationship with the host as established by the S1F13 / S1F14 // handshake. Per E30 §6.5: // // DISABLED // +-- (operator enables comms) ----------+ // | v // ENABLED.NOT-COMMUNICATING.WAIT-CRA (sent S1F13; awaiting S1F14) // | ^ | // | | (T_DELAY elapses) | (S1F14 COMMACK=Accept) // | | v // ENABLED.NOT-COMMUNICATING.WAIT-DELAY ENABLED.COMMUNICATING // ^ ^ | // | +------ (T_CRA timeout, or / // | COMMACK!=Accept) ---------+ // +-- (any event below; comms re-attempted on T_DELAY) // // (anywhere in ENABLED) -- (operator disables) --> DISABLED // // Timers (E30 §6.5): // T_CRA — Communication Response Awaited; default 30 s. // Bounds how long we'll wait for S1F14 after sending S1F13. // T_DELAY — Delay between retry attempts; default 10 s. // // This class is pure logic. It owns NO sockets and NO timers — instead // it asks its embedder to arm a timer via the `OnTimer` callback (and // cancel via OnCancelTimer), and the embedder reports the timer firing // by calling on_cra_timeout() / on_delay_elapsed(). That keeps the // state machine testable without Asio. enum class CommState : uint8_t { Disabled, // operator-disabled; no comm attempts WaitCRA, // S1F13 sent, waiting for S1F14 WaitDelay, // S1F14 failed or timed out; waiting before retry Communicating, // S1F13/F14 handshake succeeded }; const char* comm_state_name(CommState s); // Optional listener for state changes (mainly for logging). using CommStateChangeHandler = std::function; struct CommTimers { std::chrono::milliseconds t_cra{std::chrono::seconds(30)}; std::chrono::milliseconds t_delay{std::chrono::seconds(10)}; }; // Embedder callbacks. arm_t_cra() / arm_t_delay() must schedule a // one-shot callback that calls on_cra_timeout() / on_delay_elapsed() // respectively when the duration elapses. cancel_timers() must cancel // any pending arming. We deliberately do not embed an asio::steady_timer // here so the state machine is unit-testable. struct CommEnvironment { std::function arm_t_cra; std::function arm_t_delay; std::function cancel_timers; // Asked to send the equipment-initiated S1F13. May be empty; only // equipment-initiated establishment uses this. std::function send_s1f13; }; class CommunicationStateMachine { public: // The communication-establishment direction. Per E30 the host may // also initiate by sending S1F13 first; in that case we go directly // from DISABLED to COMMUNICATING on receipt of a successful S1F13. enum class Initiator { Equipment, // we send S1F13 ourselves on enable Host, // we wait for host's S1F13 }; explicit CommunicationStateMachine(CommTimers timers = {}, Initiator initiator = Initiator::Equipment); // Injection point for the embedder. Required before enable() can fire // the equipment-initiated S1F13. void set_environment(CommEnvironment env) { env_ = std::move(env); } void set_state_change_handler(CommStateChangeHandler h) { on_change_ = std::move(h); } CommState state() const { return state_; } bool communicating() const { return state_ == CommState::Communicating; } // Operator actions. void enable(); // DISABLED -> WaitCRA (equipment-initiated) or stays // in DISABLED-equivalent-NotCommunicating (host-initiated) void disable(); // any -> DISABLED // ---- Events from the message layer / timer layer -------------------- // Inbound S1F14 with the given COMMACK byte. Accept (0) transitions // us to COMMUNICATING; anything else drops us to WAIT-DELAY for a // retry. Only meaningful in WAIT-CRA; ignored elsewhere. void on_s1f14_received(uint8_t commack); // Inbound S1F13 from the host. The equipment must reply with S1F14; // we transition to COMMUNICATING immediately (the reply is the // embedder's responsibility — typically via a Router handler). void on_s1f13_received(); // The transport layer dropped (HSMS Connection closed). Per E30 any // active communications transition back into NOT-COMMUNICATING. void on_connection_lost(); // Timer firings — called by the embedder's scheduled callbacks. void on_cra_timeout(); void on_delay_elapsed(); // Manual retry, mostly for tests. Equivalent to "T_DELAY elapsed // right now"; only meaningful in WAIT-DELAY. void retry_now() { on_delay_elapsed(); } Initiator initiator() const { return initiator_; } const CommTimers& timers() const { return timers_; } private: void transition(CommState next, const std::string& reason); CommTimers timers_; Initiator initiator_; CommState state_ = CommState::Disabled; CommEnvironment env_; CommStateChangeHandler on_change_; }; } // namespace secsgem::gem