#include "secsgem/gem/process_job_state.hpp" namespace secsgem::gem { // PRJOBSTATE on the wire is a single byte per E40-0705 §10.3.2. Pin the // enum values to the spec so a future reorder fails the build instead of // silently corrupting S16F9 emissions. static_assert(static_cast(ProcessJobState::Queued) == 0); static_assert(static_cast(ProcessJobState::SettingUp) == 1); static_assert(static_cast(ProcessJobState::WaitingForStart) == 2); static_assert(static_cast(ProcessJobState::Processing) == 3); static_assert(static_cast(ProcessJobState::ProcessComplete) == 4); static_assert(static_cast(ProcessJobState::Paused) == 5); static_assert(static_cast(ProcessJobState::Stopping) == 6); static_assert(static_cast(ProcessJobState::Aborting) == 7); const char* process_job_state_name(ProcessJobState s) { switch (s) { case ProcessJobState::Queued: return "Queued"; case ProcessJobState::SettingUp: return "SettingUp"; case ProcessJobState::WaitingForStart: return "WaitingForStart"; case ProcessJobState::Processing: return "Processing"; case ProcessJobState::ProcessComplete: return "ProcessComplete"; case ProcessJobState::Paused: return "Paused"; case ProcessJobState::Stopping: return "Stopping"; case ProcessJobState::Aborting: return "Aborting"; case ProcessJobState::NoState: return "NoState"; } return "?"; } std::optional parse_process_job_state(const std::string& s) { if (s == "Queued") return ProcessJobState::Queued; if (s == "SettingUp") return ProcessJobState::SettingUp; if (s == "WaitingForStart") return ProcessJobState::WaitingForStart; if (s == "Processing") return ProcessJobState::Processing; if (s == "ProcessComplete") return ProcessJobState::ProcessComplete; if (s == "Paused") return ProcessJobState::Paused; if (s == "Stopping") return ProcessJobState::Stopping; if (s == "Aborting") return ProcessJobState::Aborting; if (s == "NoState") return ProcessJobState::NoState; return std::nullopt; } const char* process_job_event_name(ProcessJobEvent e) { switch (e) { case ProcessJobEvent::Created: return "Created"; case ProcessJobEvent::Select: return "Select"; case ProcessJobEvent::SetupComplete: return "SetupComplete"; case ProcessJobEvent::Start: return "Start"; case ProcessJobEvent::Pause: return "Pause"; case ProcessJobEvent::Resume: return "Resume"; case ProcessJobEvent::Stop: return "Stop"; case ProcessJobEvent::Abort: return "Abort"; case ProcessJobEvent::HeadOfQueue: return "HeadOfQueue"; case ProcessJobEvent::ProcessComplete: return "ProcessComplete"; case ProcessJobEvent::AbortComplete: return "AbortComplete"; } return "?"; } std::optional parse_process_job_event(const std::string& s) { // `Created` is intentionally not parseable — it's a synthetic observer // signal, never a table row. Returning nullopt makes the loader reject // `on: created` with "unknown `on` event". if (s == "select") return ProcessJobEvent::Select; if (s == "setup_complete") return ProcessJobEvent::SetupComplete; if (s == "start") return ProcessJobEvent::Start; if (s == "pause") return ProcessJobEvent::Pause; if (s == "resume") return ProcessJobEvent::Resume; if (s == "stop") return ProcessJobEvent::Stop; if (s == "abort") return ProcessJobEvent::Abort; if (s == "hoq") return ProcessJobEvent::HeadOfQueue; if (s == "process_complete") return ProcessJobEvent::ProcessComplete; if (s == "abort_complete") return ProcessJobEvent::AbortComplete; return std::nullopt; } std::optional pr_cmd_to_event(const std::string& prcmd) { // PRCMD strings per E40-0705 §10.2.5. if (prcmd == "PJSTART" || prcmd == "START") return ProcessJobEvent::Start; if (prcmd == "PJPAUSE" || prcmd == "PAUSE") return ProcessJobEvent::Pause; if (prcmd == "PJRESUME" || prcmd == "RESUME") return ProcessJobEvent::Resume; if (prcmd == "PJSTOP" || prcmd == "STOP") return ProcessJobEvent::Stop; if (prcmd == "PJABORT" || prcmd == "ABORT") return ProcessJobEvent::Abort; if (prcmd == "PJHOQ" || prcmd == "HOQ") return ProcessJobEvent::HeadOfQueue; return std::nullopt; } void ProcessJobTransitionTable::add(ProcessJobTransition row) { rows_.push_back(row); } const ProcessJobTransition* ProcessJobTransitionTable::find( ProcessJobState from, ProcessJobEvent on) const { for (const auto& r : rows_) { if (r.from == from && r.on == on) return &r; } return nullptr; } ProcessJobTransitionTable ProcessJobTransitionTable::default_table() { using S = ProcessJobState; using E = ProcessJobEvent; ProcessJobTransitionTable t; // QUEUED: t.add({S::Queued, E::Select, S::SettingUp, std::nullopt}); t.add({S::Queued, E::HeadOfQueue, std::nullopt, std::nullopt}); // reorder only // Stop/Abort on a Queued PJ routes through Aborting so the host // observes PRJOBSTATE=7 on the wire (E40-0705 §6.3). t.add({S::Queued, E::Stop, S::Aborting, std::nullopt}); t.add({S::Queued, E::Abort, S::Aborting, std::nullopt}); // SETTING-UP: t.add({S::SettingUp, E::SetupComplete, S::WaitingForStart, std::nullopt}); t.add({S::SettingUp, E::Stop, S::Stopping, std::nullopt}); t.add({S::SettingUp, E::Abort, S::Aborting, std::nullopt}); // WAITING-FOR-START: t.add({S::WaitingForStart, E::Start, S::Processing, std::nullopt}); t.add({S::WaitingForStart, E::Stop, S::Stopping, std::nullopt}); t.add({S::WaitingForStart, E::Abort, S::Aborting, std::nullopt}); // PROCESSING: t.add({S::Processing, E::Pause, S::Paused, std::nullopt}); t.add({S::Processing, E::Stop, S::Stopping, std::nullopt}); t.add({S::Processing, E::Abort, S::Aborting, std::nullopt}); t.add({S::Processing, E::ProcessComplete, S::ProcessComplete, std::nullopt}); // PAUSED: t.add({S::Paused, E::Resume, S::Processing, std::nullopt}); t.add({S::Paused, E::Stop, S::Stopping, std::nullopt}); t.add({S::Paused, E::Abort, S::Aborting, std::nullopt}); // STOPPING: t.add({S::Stopping, E::ProcessComplete, S::ProcessComplete, std::nullopt}); t.add({S::Stopping, E::Abort, S::Aborting, std::nullopt}); // ABORTING: t.add({S::Aborting, E::AbortComplete, S::ProcessComplete, std::nullopt}); // PROCESS-COMPLETE: terminal. No further transitions; deletion is via // the ProcessJobStore (which removes the FSM entirely). return t; } ProcessJobStateMachine::ProcessJobStateMachine() : ProcessJobStateMachine(ProcessJobTransitionTable::default_table(), ProcessJobState::Queued) {} ProcessJobStateMachine::ProcessJobStateMachine(ProcessJobTransitionTable table, ProcessJobState initial) : table_(std::move(table)), state_(initial) {} void ProcessJobStateMachine::transition(ProcessJobState next, ProcessJobEvent trigger) { if (state_ == next) return; const ProcessJobState prev = state_; state_ = next; if (on_change_) on_change_(prev, next, trigger); } const ProcessJobTransition* ProcessJobStateMachine::fire(ProcessJobEvent on) { const ProcessJobTransition* row = table_.find(state_, on); if (!row) return nullptr; if (row->to) transition(*row->to, on); return row; } HostCmdAck ProcessJobStateMachine::on_host_command(ProcessJobEvent event) { const auto* row = fire(event); if (!row) return HostCmdAck::CannotDoNow; if (row->ack_code) return static_cast(*row->ack_code); return HostCmdAck::Accept; } bool ProcessJobStateMachine::on_internal(ProcessJobEvent event) { return fire(event) != nullptr; } } // namespace secsgem::gem