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,120 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/gem/control_job_state.hpp"
|
||||
|
||||
namespace secsgem::gem {
|
||||
|
||||
// One Control Job — owns an ordered list of PRJOBIDs (process jobs).
|
||||
struct ControlJob {
|
||||
std::string ctljobid;
|
||||
std::vector<std::string> prjobids;
|
||||
std::unique_ptr<ControlJobStateMachine> fsm;
|
||||
};
|
||||
|
||||
class ControlJobStore {
|
||||
public:
|
||||
using TransitionTableFactory =
|
||||
std::function<ControlJobTransitionTable()>;
|
||||
using StateChangeHandler =
|
||||
std::function<void(const std::string& ctljobid,
|
||||
ControlJobState from, ControlJobState to,
|
||||
ControlJobEvent trigger)>;
|
||||
|
||||
ControlJobStore()
|
||||
: factory_([] { return ControlJobTransitionTable::default_table(); }) {}
|
||||
|
||||
void set_table_factory(TransitionTableFactory f) { factory_ = std::move(f); }
|
||||
void set_state_change_handler(StateChangeHandler h) { on_change_ = std::move(h); }
|
||||
|
||||
enum class CreateResult {
|
||||
Created,
|
||||
Denied_AlreadyExists,
|
||||
Denied_UnknownPRJob,
|
||||
Denied_Empty,
|
||||
};
|
||||
|
||||
CreateResult create(std::string ctljobid,
|
||||
std::vector<std::string> prjobids,
|
||||
const std::function<bool(const std::string&)>& pj_exists =
|
||||
[](const std::string&) { return true; }) {
|
||||
if (jobs_.count(ctljobid)) return CreateResult::Denied_AlreadyExists;
|
||||
if (prjobids.empty()) return CreateResult::Denied_Empty;
|
||||
for (const auto& id : prjobids) {
|
||||
if (!pj_exists(id)) return CreateResult::Denied_UnknownPRJob;
|
||||
}
|
||||
auto fsm = std::make_unique<ControlJobStateMachine>(factory_(),
|
||||
ControlJobState::Queued);
|
||||
const std::string id_for_handler = ctljobid;
|
||||
if (on_change_) {
|
||||
auto cb = on_change_;
|
||||
fsm->set_state_change_handler(
|
||||
[cb, id_for_handler](ControlJobState from, ControlJobState to,
|
||||
ControlJobEvent trig) {
|
||||
cb(id_for_handler, from, to, trig);
|
||||
});
|
||||
}
|
||||
jobs_.emplace(ctljobid, ControlJob{ctljobid, std::move(prjobids),
|
||||
std::move(fsm)});
|
||||
if (on_change_) {
|
||||
on_change_(id_for_handler, ControlJobState::NoState,
|
||||
ControlJobState::Queued, ControlJobEvent::Created);
|
||||
}
|
||||
return CreateResult::Created;
|
||||
}
|
||||
|
||||
bool has(const std::string& ctljobid) const {
|
||||
return jobs_.count(ctljobid) > 0;
|
||||
}
|
||||
const ControlJob* get(const std::string& ctljobid) const {
|
||||
auto it = jobs_.find(ctljobid);
|
||||
return it == jobs_.end() ? nullptr : &it->second;
|
||||
}
|
||||
ControlJob* get(const std::string& ctljobid) {
|
||||
auto it = jobs_.find(ctljobid);
|
||||
return it == jobs_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
ControlJobState state(const std::string& ctljobid) const {
|
||||
auto it = jobs_.find(ctljobid);
|
||||
return it == jobs_.end() ? ControlJobState::NoState
|
||||
: it->second.fsm->state();
|
||||
}
|
||||
|
||||
HostCmdAck on_host_command(const std::string& ctljobid, ControlJobEvent event) {
|
||||
auto* cj = get(ctljobid);
|
||||
if (!cj) return HostCmdAck::InvalidObject;
|
||||
return cj->fsm->on_host_command(event);
|
||||
}
|
||||
|
||||
bool fire_internal(const std::string& ctljobid, ControlJobEvent event) {
|
||||
auto* cj = get(ctljobid);
|
||||
if (!cj) return false;
|
||||
return cj->fsm->on_internal(event);
|
||||
}
|
||||
|
||||
bool remove(const std::string& ctljobid) {
|
||||
return jobs_.erase(ctljobid) > 0;
|
||||
}
|
||||
|
||||
std::size_t size() const { return jobs_.size(); }
|
||||
std::vector<std::string> ids() const {
|
||||
std::vector<std::string> out;
|
||||
out.reserve(jobs_.size());
|
||||
for (const auto& kv : jobs_) out.push_back(kv.first);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, ControlJob> jobs_;
|
||||
TransitionTableFactory factory_;
|
||||
StateChangeHandler on_change_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::gem
|
||||
Reference in New Issue
Block a user