#pragma once #include #include #include #include #include #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 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 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 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 to; std::optional 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& 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 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; 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