Files
raphael b1772cfefd feat(daemon): Phase D — GEM300 in-the-loop (jobs, recipes, EC changes)
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>
2026-06-10 23:53:45 +02:00

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