b1772cfefd
Semantics settled and documented: v1 is observe-and-report. The engine keeps acking S16/S3/S7/S2F15 from its FSM tables — exactly the behaviour both reference implementations validated — while the tool observes lifecycle events on the Subscribe stream and reports physical progress back. Gating stays the documented v2 deferred-reply item. Engine: two new store observers (HandlerSlot pattern) — RecipeStore fires (ppid, body) after an add (S7F3 downloads), EquipmentConstantStore fires (id, value) on ACCEPTED S2F15 writes only. Unit-tested. Daemon: the service registers PJ/recipe/EC observers (io thread; add_ observers coexist with register_default_handlers' primaries) and fans the new HostRequest variants out via push_request (fire-and-forget, no- buffering contract). ProcessJob carries action (Start->START, Resume-> RESUME, Paused->PAUSE, Stopping->STOP, Aborting->ABORT) + recipe + material bindings read store-side on the io thread. ReportProcessJob maps SETTING_UP ->SetupComplete, COMPLETE->ProcessComplete, ABORTED->AbortComplete via read_sync; PROCESSING is informational; unknown job => INVALID_OBJECT, table-rejected transition => CANNOT_DO_NOW. Carriers deferred (CarrierStore has no observer machinery; ReportCarrier stays UNIMPLEMENTED) — roadmap. Python client: on_process_job / on_recipe / on_constant_change decorators + report_job(job_id, state); ProcessJob dataclass exported. Tests: daemon suite 141 -> 175 assertions — the full in-process loop (S16F11 create -> tool setup -> S16F5 PJSTART -> stream ProcessJob with recipe+carriers -> ReportProcessJob(COMPLETE) -> FSM at ProcessComplete), rejection paths, S7F3 -> ProcessProgram, S2F15 -> ConstantChange with the configured name. Core 475/3097 (observer units). Live regression: daemon interop 20 checks + pyclient 13 checks still green against the running daemon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
3.5 KiB
C++
112 lines
3.5 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/handler_slot.hpp"
|
|
#include "secsgem/secs2/item.hpp"
|
|
|
|
namespace secsgem::gem {
|
|
|
|
enum class ProcessProgramAck : uint8_t {
|
|
Accept = 0,
|
|
PermissionNotGranted = 1,
|
|
LengthError = 2,
|
|
MatrixOverflow = 3,
|
|
PpidNotFound = 4,
|
|
ModeUnsupported = 5,
|
|
PerformanceError = 6,
|
|
};
|
|
|
|
// One parameter on a formatted-PP step (E42). Name + arbitrary
|
|
// SECS-II Item value.
|
|
struct FormattedRecipeParam {
|
|
std::string name;
|
|
secs2::Item value;
|
|
};
|
|
|
|
// One step of a formatted PP: a CCODE (command name) and its
|
|
// parameter list. Matches the wire shape of S7F23/F26 row entries.
|
|
struct FormattedRecipeStep {
|
|
std::string ccode;
|
|
std::vector<FormattedRecipeParam> params;
|
|
};
|
|
|
|
// Equipment / model metadata that travels with a formatted PP. Set
|
|
// on add_formatted; round-tripped through S7F23/F26.
|
|
struct FormattedRecipe {
|
|
std::string mdln; // model name
|
|
std::string softrev; // software revision
|
|
std::vector<FormattedRecipeStep> steps;
|
|
};
|
|
|
|
class RecipeStore {
|
|
public:
|
|
// Observe recipe arrivals (e.g. a host S7F3 download): fires after the
|
|
// store is updated, with (ppid, body). Multi-observer via HandlerSlot —
|
|
// the gRPC daemon forwards these onto its Subscribe stream.
|
|
using AddedHandler = std::function<void(const std::string&, const std::string&)>;
|
|
void add_added_handler(AddedHandler h) { on_added_.add(std::move(h)); }
|
|
|
|
void add(std::string ppid, std::string body) {
|
|
auto it = by_ppid_.insert_or_assign(std::move(ppid), std::move(body)).first;
|
|
if (on_added_) on_added_(it->first, it->second);
|
|
}
|
|
std::optional<std::string> get(const std::string& ppid) const {
|
|
auto it = by_ppid_.find(ppid);
|
|
if (it == by_ppid_.end()) return std::nullopt;
|
|
return it->second;
|
|
}
|
|
std::vector<std::string> list() const {
|
|
std::vector<std::string> out;
|
|
out.reserve(by_ppid_.size() + formatted_.size());
|
|
for (const auto& [k, _] : by_ppid_) out.push_back(k);
|
|
for (const auto& [k, _] : formatted_) {
|
|
if (!by_ppid_.count(k)) out.push_back(k);
|
|
}
|
|
return out;
|
|
}
|
|
ProcessProgramAck remove(const std::string& ppid) {
|
|
bool found = (by_ppid_.erase(ppid) > 0) | (formatted_.erase(ppid) > 0);
|
|
return found ? ProcessProgramAck::Accept : ProcessProgramAck::PpidNotFound;
|
|
}
|
|
// Unique PPID count. A PPID with both opaque and formatted views
|
|
// counts once.
|
|
std::size_t size() const {
|
|
std::size_t n = by_ppid_.size();
|
|
for (const auto& [k, _] : formatted_) {
|
|
if (!by_ppid_.count(k)) ++n;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// ---- E42 formatted PPs ------------------------------------------------
|
|
// A formatted recipe lives alongside any opaque-body PP with the same
|
|
// PPID. Most tools pick one form or the other; storing both
|
|
// independently lets a host poke at the formatted view while a legacy
|
|
// app reads the opaque view.
|
|
void add_formatted(std::string ppid, FormattedRecipe r) {
|
|
formatted_.insert_or_assign(std::move(ppid), std::move(r));
|
|
}
|
|
std::optional<FormattedRecipe> get_formatted(const std::string& ppid) const {
|
|
auto it = formatted_.find(ppid);
|
|
if (it == formatted_.end()) return std::nullopt;
|
|
return it->second;
|
|
}
|
|
bool has_formatted(const std::string& ppid) const {
|
|
return formatted_.count(ppid) > 0;
|
|
}
|
|
|
|
private:
|
|
std::map<std::string, std::string> by_ppid_;
|
|
HandlerSlot<const std::string&, const std::string&> on_added_;
|
|
std::map<std::string, FormattedRecipe> formatted_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|