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>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user