Files
secs-gem/include/secsgem/gem/communication_state.hpp
T
raphael 90c177b7ce E40 Process Jobs + E94 Control Jobs + E30 communication state
GEM300 layer: SEMI E40-0705 Process Job and E94-0705 Control Job
state machines, plus the E30 §6.1 communication-state machine that
sits between HSMS SELECT and full GEM communication. Data-driven
via data/process_job_state.yaml and data/control_job_state.yaml,
mirroring the existing control_state.yaml pattern.

Wire coverage:
  S14F9/F10   CreateObject (CJ)              host -> equipment
  S14F11/F12  DeleteObject (CJ)              host -> equipment
  S16F5/F6    PRJobCommand                   host -> equipment
  S16F9       PRJobAlert                     equipment -> host
  S16F11/F12  PRJobCreate (simplified body)  host -> equipment
  S16F13/F14  PRJobDequeue                   host -> equipment
  S16F27/F28  CJobCommand                    host -> equipment

Process Job FSM exposes 8 states matching PRJOBSTATE bytes (E40 §10.3.2);
HOQ is reorder-aware (move-to-head against an insertion-order vector);
Stop/Abort on a Queued PJ routes through ABORTING so the host observes
PRJOBSTATE=7 on the wire (§6.3); alert_enabled is settable per-PJ for
PRALERT control; FSM dispatches through ProcessJobStore::on_change_
dynamically so a late set_state_change_handler() reaches existing PJs.

Hardening: loader rejects NoState (sentinel) as initial/from/to and
rejects `on: created` rows; static_asserts pin enum values to wire
bytes; ProcessJobStore is non-movable to keep the per-PJ this-capture
safe.

Server simulator cascades the full CJ -> PJ lifecycle on CJSTART so
the wire trace exercises every legal state. CEIDs 400/401 fire on CJ
state changes via the existing event-report pipeline.

Tests: 60+ new assertions across test_process_jobs, test_control_jobs,
test_communication_state, test_hsms_connection, plus loader and
messages round-trip coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:00:32 +02:00

141 lines
5.4 KiB
C++

#pragma once
#include <chrono>
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
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<void(CommState from, CommState to, const std::string& reason)>;
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<void(std::chrono::milliseconds)> arm_t_cra;
std::function<void(std::chrono::milliseconds)> arm_t_delay;
std::function<void()> cancel_timers;
// Asked to send the equipment-initiated S1F13. May be empty; only
// equipment-initiated establishment uses this.
std::function<void()> 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