feat(gem): add host-command behaviour hook to HostCommandRegistry
Host commands were declarative-only: dispatch() returned the YAML-defined HCACK plus side effects, and ignored the command parameters entirely (the param list was a commented-out argument). Equipment could acknowledge a command but never run anything in response — the pvd_tool example worked around this by hard-coding behaviour in a C++ router handler. Add set_handler(rcmd, fn): a registered handler receives the live CPNAME/ CPVAL parameters and returns the HCACK, overriding the declarative default. Live on S2F41/F21/F49 via the shared dispatch(). No handler => byte-for-byte the previous declarative behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -48,19 +49,41 @@ class HostCommandRegistry {
|
|||||||
std::optional<bool> force_spool;
|
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) {
|
void register_command(std::string rcmd, Spec spec) {
|
||||||
by_rcmd_.insert_or_assign(std::move(rcmd), std::move(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(const std::string& rcmd) const { return by_rcmd_.count(rcmd) > 0; }
|
||||||
|
bool has_handler(const std::string& rcmd) const { return handlers_.count(rcmd) > 0; }
|
||||||
Result dispatch(const std::string& rcmd,
|
Result dispatch(const std::string& rcmd,
|
||||||
const std::vector<CommandParameter>& /*params*/) const {
|
const std::vector<CommandParameter>& params) const {
|
||||||
auto it = by_rcmd_.find(rcmd);
|
auto it = by_rcmd_.find(rcmd);
|
||||||
if (it == by_rcmd_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt, std::nullopt};
|
if (it == by_rcmd_.end()) return {HostCmdAck::InvalidCommand, std::nullopt, std::nullopt, std::nullopt};
|
||||||
return {it->second.ack, it->second.emit_ceid, it->second.set_alarm, it->second.force_spool};
|
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:
|
private:
|
||||||
std::map<std::string, Spec> by_rcmd_;
|
std::map<std::string, Spec> by_rcmd_;
|
||||||
|
std::map<std::string, Handler> handlers_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace secsgem::gem
|
} // namespace secsgem::gem
|
||||||
|
|||||||
@@ -117,6 +117,73 @@ TEST_CASE("host command registry returns spec + result") {
|
|||||||
CHECK(r.dispatch("UNKNOWN", {}).ack == HostCmdAck::InvalidCommand);
|
CHECK(r.dispatch("UNKNOWN", {}).ack == HostCmdAck::InvalidCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("host command handler runs vendor behaviour and decides the ack") {
|
||||||
|
HostCommandRegistry r;
|
||||||
|
r.register_command("START", {HostCmdAck::Accept, 300, std::nullopt});
|
||||||
|
|
||||||
|
// Behaviour: record that we ran, read a parameter, and choose the ack.
|
||||||
|
bool ran = false;
|
||||||
|
std::string seen_ppid;
|
||||||
|
r.set_handler("START", [&](const std::string& rcmd,
|
||||||
|
const std::vector<CommandParameter>& params) {
|
||||||
|
ran = true;
|
||||||
|
CHECK(rcmd == "START");
|
||||||
|
for (const auto& p : params)
|
||||||
|
if (p.name == "PPID") seen_ppid = p.value.as_ascii();
|
||||||
|
return HostCmdAck::Accept;
|
||||||
|
});
|
||||||
|
|
||||||
|
CHECK(r.has_handler("START"));
|
||||||
|
CHECK_FALSE(r.has_handler("STOP"));
|
||||||
|
|
||||||
|
std::vector<CommandParameter> params{{"PPID", s2::Item::ascii("RECIPE-A")}};
|
||||||
|
auto res = r.dispatch("START", params);
|
||||||
|
|
||||||
|
CHECK(ran);
|
||||||
|
CHECK(seen_ppid == "RECIPE-A");
|
||||||
|
CHECK(res.ack == HostCmdAck::Accept);
|
||||||
|
// Declarative side effects from the Spec still travel with the result.
|
||||||
|
CHECK(res.emit_ceid.value_or(0) == 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("host command handler can reject, overriding the declarative ack") {
|
||||||
|
HostCommandRegistry r;
|
||||||
|
// YAML default says Accept; the handler decides otherwise at runtime.
|
||||||
|
r.register_command("START", {HostCmdAck::Accept, 300, std::nullopt});
|
||||||
|
r.set_handler("START", [](const std::string&,
|
||||||
|
const std::vector<CommandParameter>&) {
|
||||||
|
return HostCmdAck::CannotDoNow; // e.g. equipment busy
|
||||||
|
});
|
||||||
|
|
||||||
|
auto res = r.dispatch("START", {});
|
||||||
|
CHECK(res.ack == HostCmdAck::CannotDoNow);
|
||||||
|
// emit_ceid is still carried, but the router only applies it on Accept,
|
||||||
|
// so a rejecting handler naturally suppresses the event.
|
||||||
|
CHECK(res.emit_ceid.value_or(0) == 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("host command with no handler stays purely declarative") {
|
||||||
|
HostCommandRegistry r;
|
||||||
|
r.register_command("STOP", {HostCmdAck::Accept, std::nullopt, std::nullopt});
|
||||||
|
// No set_handler call: behaviour is exactly the pre-existing YAML default.
|
||||||
|
CHECK_FALSE(r.has_handler("STOP"));
|
||||||
|
CHECK(r.dispatch("STOP", {}).ack == HostCmdAck::Accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("host command handler on an unregistered command is never reached") {
|
||||||
|
HostCommandRegistry r;
|
||||||
|
bool ran = false;
|
||||||
|
// Handler attached but the command was never registered in the dictionary.
|
||||||
|
r.set_handler("GHOST", [&](const std::string&,
|
||||||
|
const std::vector<CommandParameter>&) {
|
||||||
|
ran = true;
|
||||||
|
return HostCmdAck::Accept;
|
||||||
|
});
|
||||||
|
auto res = r.dispatch("GHOST", {});
|
||||||
|
CHECK(res.ack == HostCmdAck::InvalidCommand);
|
||||||
|
CHECK_FALSE(ran); // unknown commands short-circuit before the handler
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Event reports -------------------------------------------------------
|
// ---- Event reports -------------------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("define reports rejects unknown VID") {
|
TEST_CASE("define reports rejects unknown VID") {
|
||||||
|
|||||||
Reference in New Issue
Block a user