Files
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

131 lines
5.1 KiB
C++

#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