cfa2d1e531
Replaces the simplified <L,3 PRJOBID PPID MTRLOUTSPEC> demo body with
the full SEMI E40-0705 §10.2 shape:
<L,5 PRJOBID MF PRRECIPEMETHOD
<L,2 PPID <L,n <L,2 RCPPARNM RCPPARVAL>>>
<L,n MTRLOUTSPEC>
<L,n <L,2 PARAMNAME PARAMVAL>>>
ProcessJob now carries the extra fields (MaterialFlag, ProcessRecipeMethod,
RcpVar[], ProcessParam[]) so a tool's recipe engine can later consume
the recipe-variable overrides and per-job process parameters. Server
S16F11 dispatch populates them via the new ProcessJobStore::set_e40_extras
helper after a successful create.
MaterialFlag + ProcessRecipeMethod enums live in their own tiny header
(`e40_constants.hpp`) so process_jobs.hpp (the store) can use them
without dragging in messages_helpers.hpp (which would create a circular
include via data_model.hpp).
The simplified 3-arg HostHandler::send_create_process_job convenience
remains; it constructs a sensible-default PRJobCreateRequest internally.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
228 lines
8.5 KiB
C++
228 lines
8.5 KiB
C++
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/e40_constants.hpp"
|
|
#include "secsgem/gem/process_job_state.hpp"
|
|
#include "secsgem/secs2/item.hpp"
|
|
|
|
namespace secsgem::gem {
|
|
|
|
// One Process Job record. The FSM is heap-allocated through unique_ptr so
|
|
// the per-PJ state-change handler can capture a stable pointer to
|
|
// `this`-style state without invalidation on map rehash.
|
|
//
|
|
// The MF / recipe-method / rcp-vars / process-params fields are the
|
|
// optional E40-0705 §10.2 trailers on S16F11. Simple callers leave
|
|
// them defaulted; tools that actually need recipe-variable tuning or
|
|
// per-job process parameters populate them.
|
|
struct RcpVar {
|
|
std::string name;
|
|
secs2::Item value;
|
|
};
|
|
|
|
struct ProcessParam {
|
|
std::string name;
|
|
secs2::Item value;
|
|
};
|
|
|
|
struct ProcessJob {
|
|
std::string prjobid;
|
|
std::string ppid; // recipe identifier
|
|
std::vector<std::string> mtrloutspec; // material identifiers
|
|
bool alert_enabled = true; // S16F9 alerts on/off
|
|
MaterialFlag mf = MaterialFlag::Substrate;
|
|
ProcessRecipeMethod prrecipemethod = ProcessRecipeMethod::RecipeOnly;
|
|
std::vector<RcpVar> rcpvars;
|
|
std::vector<ProcessParam> prprocessparams;
|
|
std::unique_ptr<ProcessJobStateMachine> fsm;
|
|
};
|
|
|
|
class ProcessJobStore {
|
|
public:
|
|
using TransitionTableFactory =
|
|
std::function<ProcessJobTransitionTable()>;
|
|
using StateChangeHandler =
|
|
std::function<void(const std::string& prjobid,
|
|
ProcessJobState from, ProcessJobState to,
|
|
ProcessJobEvent trigger)>;
|
|
|
|
ProcessJobStore()
|
|
: factory_([] { return ProcessJobTransitionTable::default_table(); }) {}
|
|
|
|
// The per-PJ FSM closes over `this`, so the store must keep a stable
|
|
// address. unique_ptr makes copying impossible anyway; moves would
|
|
// silently dangle the per-PJ lambdas — disallow them explicitly.
|
|
ProcessJobStore(const ProcessJobStore&) = delete;
|
|
ProcessJobStore& operator=(const ProcessJobStore&) = delete;
|
|
ProcessJobStore(ProcessJobStore&&) = delete;
|
|
ProcessJobStore& operator=(ProcessJobStore&&) = delete;
|
|
|
|
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_InvalidPpid,
|
|
};
|
|
|
|
// Validate `ppid` against the optional callback; if accepted, create
|
|
// the PJ in Queued and fire the Created -> Queued change handler.
|
|
CreateResult create(std::string prjobid, std::string ppid,
|
|
std::vector<std::string> materials,
|
|
const std::function<bool(const std::string&)>& ppid_exists =
|
|
[](const std::string&) { return true; }) {
|
|
if (jobs_.count(prjobid)) return CreateResult::Denied_AlreadyExists;
|
|
if (!ppid_exists(ppid)) return CreateResult::Denied_InvalidPpid;
|
|
auto fsm = std::make_unique<ProcessJobStateMachine>(factory_(),
|
|
ProcessJobState::Queued);
|
|
// Dispatch through *this so a later set_state_change_handler() takes
|
|
// effect for existing PJs (the captured-at-create snapshot pattern
|
|
// would otherwise pin the old handler on jobs already in the map).
|
|
fsm->set_state_change_handler(
|
|
[this, id = prjobid](ProcessJobState from, ProcessJobState to,
|
|
ProcessJobEvent trig) {
|
|
if (on_change_) on_change_(id, from, to, trig);
|
|
});
|
|
order_.push_back(prjobid);
|
|
ProcessJob pj;
|
|
pj.prjobid = prjobid;
|
|
pj.ppid = std::move(ppid);
|
|
pj.mtrloutspec = std::move(materials);
|
|
pj.alert_enabled = true;
|
|
pj.fsm = std::move(fsm);
|
|
jobs_.emplace(prjobid, std::move(pj));
|
|
// Synthetic NoState -> Queued so subscribers observe creation. The
|
|
// server filters this so it doesn't emit a bogus S16F9 for a PJ
|
|
// that's still being acked.
|
|
if (on_change_) {
|
|
on_change_(jobs_.find(prjobid)->first, ProcessJobState::NoState,
|
|
ProcessJobState::Queued, ProcessJobEvent::Created);
|
|
}
|
|
return CreateResult::Created;
|
|
}
|
|
|
|
// After `create`, populate the optional E40-0705 fields on the new PJ.
|
|
// Returns true if the PJ exists. These fields don't influence the FSM;
|
|
// they're carried so the tool's recipe engine can read them later.
|
|
bool set_e40_extras(const std::string& prjobid,
|
|
MaterialFlag mf,
|
|
ProcessRecipeMethod prrecipemethod,
|
|
std::vector<RcpVar> rcpvars,
|
|
std::vector<ProcessParam> params) {
|
|
auto it = jobs_.find(prjobid);
|
|
if (it == jobs_.end()) return false;
|
|
it->second.mf = mf;
|
|
it->second.prrecipemethod = prrecipemethod;
|
|
it->second.rcpvars = std::move(rcpvars);
|
|
it->second.prprocessparams = std::move(params);
|
|
return true;
|
|
}
|
|
|
|
bool has(const std::string& prjobid) const {
|
|
return jobs_.count(prjobid) > 0;
|
|
}
|
|
|
|
const ProcessJob* get(const std::string& prjobid) const {
|
|
auto it = jobs_.find(prjobid);
|
|
return it == jobs_.end() ? nullptr : &it->second;
|
|
}
|
|
ProcessJob* get(const std::string& prjobid) {
|
|
auto it = jobs_.find(prjobid);
|
|
return it == jobs_.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
ProcessJobState state(const std::string& prjobid) const {
|
|
auto it = jobs_.find(prjobid);
|
|
return it == jobs_.end() ? ProcessJobState::NoState
|
|
: it->second.fsm->state();
|
|
}
|
|
|
|
// Host-initiated S16F5 PRJobCommand. Returns InvalidObject for unknown
|
|
// PJs; CannotDoNow when the current PJ state has no row for the event.
|
|
// For HOQ the FSM gates legality (Queued only) and the store performs
|
|
// the actual reorder so the next CJ Select picks this PJ first.
|
|
HostCmdAck on_host_command(const std::string& prjobid, ProcessJobEvent event) {
|
|
auto* pj = get(prjobid);
|
|
if (!pj) return HostCmdAck::InvalidObject;
|
|
HostCmdAck ack = pj->fsm->on_host_command(event);
|
|
if (ack == HostCmdAck::Accept && event == ProcessJobEvent::HeadOfQueue) {
|
|
move_to_head(prjobid);
|
|
}
|
|
return ack;
|
|
}
|
|
|
|
// Position of `prjobid` in the queue order (insertion order; HOQ rewrites
|
|
// it). Returns -1 if unknown. Exposed for tests and CJ promotion logic.
|
|
int position(const std::string& prjobid) const {
|
|
auto it = std::find(order_.begin(), order_.end(), prjobid);
|
|
return it == order_.end() ? -1 : static_cast<int>(it - order_.begin());
|
|
}
|
|
|
|
// Internal events from the application (recipe runner etc.).
|
|
bool fire_internal(const std::string& prjobid, ProcessJobEvent event) {
|
|
auto* pj = get(prjobid);
|
|
if (!pj) return false;
|
|
return pj->fsm->on_internal(event);
|
|
}
|
|
|
|
// Dequeue S16F13: only legal while QUEUED.
|
|
HostCmdAck dequeue(const std::string& prjobid) {
|
|
auto it = jobs_.find(prjobid);
|
|
if (it == jobs_.end()) return HostCmdAck::InvalidObject;
|
|
if (it->second.fsm->state() != ProcessJobState::Queued)
|
|
return HostCmdAck::CannotDoNow;
|
|
erase_from_order(prjobid);
|
|
jobs_.erase(it);
|
|
return HostCmdAck::Accept;
|
|
}
|
|
|
|
// Remove a terminal job from the store (caller responsibility to ensure
|
|
// ProcessComplete before deleting). Used after CJ Completed cleanup.
|
|
bool remove(const std::string& prjobid) {
|
|
erase_from_order(prjobid);
|
|
return jobs_.erase(prjobid) > 0;
|
|
}
|
|
|
|
// Per-PJ S16F9 alert gate. E40 §10.3 leaves PRALERT control to the host
|
|
// (S16F1/F2 in the full multi-create form, which we don't model yet);
|
|
// exposed here so application code can toggle alerts directly.
|
|
bool set_alert(const std::string& prjobid, bool enabled) {
|
|
auto* pj = get(prjobid);
|
|
if (!pj) return false;
|
|
pj->alert_enabled = enabled;
|
|
return true;
|
|
}
|
|
|
|
std::size_t size() const { return jobs_.size(); }
|
|
|
|
// Insertion-order list of PRJOBIDs, mutated by HOQ. Used by CJ promotion
|
|
// logic and by tests that need to assert ordering.
|
|
const std::vector<std::string>& ids() const { return order_; }
|
|
|
|
private:
|
|
void erase_from_order(const std::string& prjobid) {
|
|
auto it = std::find(order_.begin(), order_.end(), prjobid);
|
|
if (it != order_.end()) order_.erase(it);
|
|
}
|
|
void move_to_head(const std::string& prjobid) {
|
|
auto it = std::find(order_.begin(), order_.end(), prjobid);
|
|
if (it == order_.end() || it == order_.begin()) return;
|
|
std::rotate(order_.begin(), it, it + 1);
|
|
}
|
|
|
|
std::map<std::string, ProcessJob> jobs_;
|
|
std::vector<std::string> order_; // queue position (E40 HOQ-aware)
|
|
TransitionTableFactory factory_;
|
|
StateChangeHandler on_change_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|