e6ee927900
The HCACK-4 contract, implemented end to end. For every YAML-declared command the service registers a forwarding handler (new HostCommandRegistry names()/spec() accessors): with a subscribed tool client the command is queued onto the Subscribe stream (id + name + params via from_item) and the host is answered S2F42 HCACK=4 immediately — never blocking the io thread or the T3 window; with NO subscriber the command takes its declarative YAML ack (the honest pre-daemon behaviour). Settled + documented in the proto: v1 is a firehose with no buffering/replay. CompleteCommand correlates the pending id (audit; unknown id => PARAMETER_INVALID). Side effects stay suppressed on HCACK-4 (router applies them only on Accept), so the completion event the TOOL fires is the host's real signal — exactly E30's intent. Tests (daemon suite 101 -> 124 assertions): a real S2F41 dispatched through the full default-handler router ON the io thread under run_async — HCACK 4 with subscriber + params on the stream, declarative Accept without, CompleteCommand known/unknown, fallback restored after unsubscribe. Interop (now 20 checks, all green): the complete conformant loop against the secsgem-py reference host — S2F41 START -> S2F42 HCACK=4 -> tool receives Command(name=START, id=1) -> CompleteCommand -> FireEvent -> host receives S6F11. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
3.6 KiB
C++
105 lines
3.6 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "secsgem/secs2/item.hpp"
|
|
|
|
namespace secsgem::gem {
|
|
|
|
namespace s2 = secsgem::secs2;
|
|
|
|
enum class HostCmdAck : uint8_t {
|
|
Accept = 0,
|
|
InvalidCommand = 1,
|
|
CannotDoNow = 2,
|
|
ParameterInvalid = 3,
|
|
AcceptedWillFinishLater = 4,
|
|
Rejected = 5,
|
|
InvalidObject = 6,
|
|
};
|
|
|
|
// One <CPNAME, CPVAL> entry on S2F41. The messages catalog declares this
|
|
// struct external_struct: true so the codegen references it rather than
|
|
// redefining it.
|
|
struct CommandParameter {
|
|
std::string name;
|
|
s2::Item value;
|
|
};
|
|
|
|
class HostCommandRegistry {
|
|
public:
|
|
// Declarative effect, loaded from YAML.
|
|
struct Spec {
|
|
HostCmdAck ack = HostCmdAck::Accept;
|
|
std::optional<uint32_t> emit_ceid;
|
|
std::optional<uint32_t> set_alarm;
|
|
std::optional<bool> force_spool; // toggles the SpoolStore's flag
|
|
};
|
|
|
|
struct Result {
|
|
HostCmdAck ack = HostCmdAck::InvalidCommand;
|
|
std::optional<uint32_t> emit_ceid;
|
|
std::optional<uint32_t> set_alarm;
|
|
std::optional<bool> force_spool;
|
|
};
|
|
|
|
// Behaviour hook: vendor code that actually *does* something when the host
|
|
// sends a command (run a recipe, move hardware, validate the parameters).
|
|
// The handler receives the live <CPNAME, CPVAL> parameter list and returns
|
|
// the HCACK to send back; returning anything other than Accept suppresses
|
|
// the Spec's declarative side effects, since the router only applies
|
|
// emit_ceid / set_alarm / force_spool on Accept. Without a handler a
|
|
// command stays purely declarative (the YAML Spec's static ack), which is
|
|
// the pre-existing behaviour.
|
|
using Handler = std::function<HostCmdAck(
|
|
const std::string& rcmd, const std::vector<CommandParameter>& params)>;
|
|
|
|
void register_command(std::string rcmd, Spec spec) {
|
|
by_rcmd_.insert_or_assign(std::move(rcmd), std::move(spec));
|
|
}
|
|
// Attach behaviour to an already-registered command. Independent of
|
|
// register_command so the data dictionary (YAML) and the behaviour
|
|
// (application code) can be wired separately.
|
|
void set_handler(std::string rcmd, Handler h) {
|
|
handlers_.insert_or_assign(std::move(rcmd), std::move(h));
|
|
}
|
|
bool has(const std::string& rcmd) const { return by_rcmd_.count(rcmd) > 0; }
|
|
bool has_handler(const std::string& rcmd) const { return handlers_.count(rcmd) > 0; }
|
|
|
|
// Enumeration + spec lookup, so a front-end (e.g. the gRPC daemon) can
|
|
// attach behaviour to every declared command and fall back to the
|
|
// declarative ack when its tool client isn't connected.
|
|
std::vector<std::string> names() const {
|
|
std::vector<std::string> out;
|
|
out.reserve(by_rcmd_.size());
|
|
for (const auto& [rcmd, _] : by_rcmd_) out.push_back(rcmd);
|
|
return out;
|
|
}
|
|
std::optional<Spec> spec(const std::string& rcmd) const {
|
|
auto it = by_rcmd_.find(rcmd);
|
|
if (it == by_rcmd_.end()) return std::nullopt;
|
|
return it->second;
|
|
}
|
|
Result dispatch(const std::string& rcmd,
|
|
const std::vector<CommandParameter>& params) const {
|
|
auto it = by_rcmd_.find(rcmd);
|
|
if (it == by_rcmd_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt, std::nullopt};
|
|
HostCmdAck ack = it->second.ack;
|
|
if (auto hit = handlers_.find(rcmd); hit != handlers_.end() && hit->second)
|
|
ack = hit->second(rcmd, params); // vendor behaviour decides the outcome
|
|
return {ack, it->second.emit_ceid, it->second.set_alarm, it->second.force_spool};
|
|
}
|
|
|
|
private:
|
|
std::map<std::string, Spec> by_rcmd_;
|
|
std::map<std::string, Handler> handlers_;
|
|
};
|
|
|
|
} // namespace secsgem::gem
|