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,130 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/store/host_commands.hpp" // HostCmdAck
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
// E40 §6 Process Job state model.
|
||||
//
|
||||
// Per E40-0705 §6.3 the PJ lifecycle is a directed graph over the eight
|
||||
// states below. The values match the PRJOBSTATE byte that S16F9 carries
|
||||
// on the wire (E40-0705 §10.3.2): 0 QUEUED, 1 SETTING-UP,
|
||||
// 2 WAITING-FOR-START, 3 PROCESSING, 4 PROCESS-COMPLETE, 5 PAUSED,
|
||||
// 6 STOPPING, 7 ABORTING. NoState (255) is our sentinel for "doesn't
|
||||
// exist yet / freshly deleted" so the same enum can be used in the API.
|
||||
enum class ProcessJobState : uint8_t {
|
||||
Queued = 0,
|
||||
SettingUp = 1,
|
||||
WaitingForStart = 2,
|
||||
Processing = 3,
|
||||
ProcessComplete = 4,
|
||||
Paused = 5,
|
||||
Stopping = 6,
|
||||
Aborting = 7,
|
||||
NoState = 255,
|
||||
};
|
||||
|
||||
const char* process_job_state_name(ProcessJobState s);
|
||||
std::optional<ProcessJobState> parse_process_job_state(const std::string& s);
|
||||
|
||||
// Inputs that drive the PJ FSM. Names mirror the E40 spec verbs: the
|
||||
// host-initiated PJSTART / PJPAUSE / ... arrive as S16F5 PRCMD strings;
|
||||
// the internal SetupComplete / ProcessComplete fire from the equipment's
|
||||
// application logic (recipe runner, abort controller, etc.).
|
||||
//
|
||||
// `Created` is a synthetic observer signal — the store fires it through
|
||||
// the change handler when a PJ first lands in Queued, so subscribers can
|
||||
// distinguish "created in Queued" from "transitioned into Queued" (the
|
||||
// latter never happens, but the signal is still useful for bookkeeping).
|
||||
// It deliberately does NOT appear in the transition table and the YAML
|
||||
// loader rejects `on: created` rows.
|
||||
enum class ProcessJobEvent {
|
||||
Created, // synthetic NoState -> Queued (observer only)
|
||||
Select, // QUEUED -> SETTING-UP (CJ promoted this PJ)
|
||||
SetupComplete, // SETTING-UP -> WAITING-FOR-START
|
||||
Start, // WAITING-FOR-START -> PROCESSING (host: PJSTART)
|
||||
Pause, // PROCESSING -> PAUSED (host: PJPAUSE)
|
||||
Resume, // PAUSED -> PROCESSING (host: PJRESUME)
|
||||
Stop, // ... -> STOPPING (host: PJSTOP)
|
||||
Abort, // ... -> ABORTING (host: PJABORT)
|
||||
HeadOfQueue, // QUEUED -> QUEUED (host: PJHOQ; reorder only)
|
||||
ProcessComplete, // PROCESSING/STOPPING -> PROCESS-COMPLETE
|
||||
AbortComplete, // ABORTING -> PROCESS-COMPLETE
|
||||
};
|
||||
|
||||
const char* process_job_event_name(ProcessJobEvent e);
|
||||
std::optional<ProcessJobEvent> parse_process_job_event(const std::string& s);
|
||||
|
||||
// Wire-name -> event mapping for the PRCMD string on S16F5. Returns
|
||||
// nullopt for unknown verbs (server should reply HCACK=InvalidCommand).
|
||||
std::optional<ProcessJobEvent> pr_cmd_to_event(const std::string& prcmd);
|
||||
|
||||
// One row of the PJ transition table. Same shape conventions as
|
||||
// ControlTransition (see control_state.hpp): `to` absent means the row
|
||||
// only validates the event (e.g. HOQ is a reorder, not a state change),
|
||||
// and `ack_code` is the HCACK the equipment should return to the host
|
||||
// when the event came from S16F5.
|
||||
struct ProcessJobTransition {
|
||||
ProcessJobState from;
|
||||
ProcessJobEvent on;
|
||||
std::optional<ProcessJobState> to;
|
||||
std::optional<uint8_t> ack_code; // HostCmdAck when applicable
|
||||
};
|
||||
|
||||
class ProcessJobTransitionTable {
|
||||
public:
|
||||
void add(ProcessJobTransition row);
|
||||
const ProcessJobTransition* find(ProcessJobState from,
|
||||
ProcessJobEvent on) const;
|
||||
std::size_t size() const { return rows_.size(); }
|
||||
const std::vector<ProcessJobTransition>& rows() const { return rows_; }
|
||||
|
||||
// Built-in default table matching data/process_job_state.yaml exactly.
|
||||
// Tests use this so they don't depend on the YAML being present.
|
||||
static ProcessJobTransitionTable default_table();
|
||||
|
||||
private:
|
||||
std::vector<ProcessJobTransition> rows_;
|
||||
};
|
||||
|
||||
// PJ FSM engine. One instance per Process Job — the ProcessJobStore
|
||||
// keeps them keyed by PRJOBID. Behaviour rules live in the table.
|
||||
class ProcessJobStateMachine {
|
||||
public:
|
||||
using StateChangeHandler =
|
||||
std::function<void(ProcessJobState from, ProcessJobState to,
|
||||
ProcessJobEvent trigger)>;
|
||||
|
||||
ProcessJobStateMachine();
|
||||
explicit ProcessJobStateMachine(ProcessJobTransitionTable table,
|
||||
ProcessJobState initial = ProcessJobState::Queued);
|
||||
|
||||
ProcessJobState state() const { return state_; }
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
// Apply the host-initiated event from S16F5. Returns the HCACK the
|
||||
// equipment should reply with: Accept if a row exists with no
|
||||
// ack_code override, otherwise the row's ack. CannotDoNow if the
|
||||
// (state, event) pair has no matching row.
|
||||
HostCmdAck on_host_command(ProcessJobEvent event);
|
||||
|
||||
// Apply an internal event (SetupComplete, ProcessComplete,
|
||||
// AbortComplete). Returns true if a transition fired.
|
||||
bool on_internal(ProcessJobEvent event);
|
||||
|
||||
private:
|
||||
const ProcessJobTransition* fire(ProcessJobEvent on);
|
||||
void transition(ProcessJobState next, ProcessJobEvent trigger);
|
||||
|
||||
ProcessJobTransitionTable table_;
|
||||
ProcessJobState state_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user