912304966f
register_default_handlers was a relocated app main(): one 1086-line function, all-or-nothing. It is now 15 per-capability registration functions along the lines GEM itself defines (S1F19): identification, equipment constants, clock, event reports, remote commands, trace/limits, spooling, alarms, exceptions, material tracking (E90/E116/E157), carriers (E87), recipes, object services (E39), jobs (E40/E94), terminal services. A sensor-class tool registers three functions instead of carrying carrier/job handlers it doesn't have; register_default_handlers composes all 15. Each function derives exactly the runtime aliases its handlers use (generated programmatically from the moved bodies with boundary/substitution guards — zero hand-retyping). Magic constants are gone: the control-state/clock SVIDs (were hardcoded 1/2) and the CJ Executing/Completed CEIDs (were 400/401) now come from a "roles:" block in equipment.yaml via EquipmentDescriptor, with historical defaults when absent, loader parsing, and validation (CEID roles must name declared events). The coupling is now visible in ONE file instead of silently split between YAML and C++ — the exact drift class this repo's spec-as-data philosophy exists to kill. Tests: capability subsetting, role-driven SVID refresh via S1F3, roles loader (shipped/custom/absent). Battery: core 473/3087 incl. the 53-handler conformance sweep, daemon 125/125, live GEM300 demo (client exit 0), daemon interop 20/20 vs secsgem-py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.6 KiB
C++
102 lines
3.6 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/default_handlers.hpp"
|
|
#include "secsgem/gem/messages.hpp"
|
|
#include "secsgem/gem/runtime.hpp"
|
|
|
|
using namespace secsgem;
|
|
namespace gem = secsgem::gem;
|
|
namespace s2 = secsgem::secs2;
|
|
|
|
#ifndef SECSGEM_DATA_DIR
|
|
#error "SECSGEM_DATA_DIR not defined; see CMakeLists.txt"
|
|
#endif
|
|
|
|
static gem::EquipmentRuntime::Config test_config() {
|
|
gem::EquipmentRuntime::Config c;
|
|
c.equipment_yaml = SECSGEM_DATA_DIR "/equipment.yaml";
|
|
c.control_state_yaml = SECSGEM_DATA_DIR "/control_state.yaml";
|
|
c.process_job_yaml = SECSGEM_DATA_DIR "/process_job_state.yaml";
|
|
c.control_job_yaml = SECSGEM_DATA_DIR "/control_job_state.yaml";
|
|
c.port = 0;
|
|
return c;
|
|
}
|
|
|
|
TEST_CASE("register_default_handlers populates the runtime's Router") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
CHECK(rt.router().size() == 0); // nothing registered yet
|
|
gem::register_default_handlers(rt);
|
|
CHECK(rt.router().size() > 40); // the full GEM handler set (~56)
|
|
}
|
|
|
|
TEST_CASE("register_default_handlers: S1F1 dispatches to S1F2 On-Line Data") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
gem::register_default_handlers(rt);
|
|
|
|
auto reply = rt.router().dispatch(s2::Message(1, 1, true)); // Are You There
|
|
REQUIRE(reply.has_value());
|
|
CHECK(reply->stream == 1);
|
|
CHECK(reply->function == 2);
|
|
}
|
|
|
|
TEST_CASE("register_default_handlers: S2F41 reaches the on_command behaviour hook") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
gem::register_default_handlers(rt);
|
|
|
|
bool ran = false;
|
|
std::string seen;
|
|
rt.on_command("START", [&](const std::string& rcmd,
|
|
const std::vector<gem::CommandParameter>&) {
|
|
ran = true;
|
|
seen = rcmd;
|
|
return gem::HostCmdAck::Accept;
|
|
});
|
|
|
|
auto reply = rt.router().dispatch(gem::s2f41_host_command("START", {}));
|
|
REQUIRE(reply.has_value());
|
|
CHECK(reply->stream == 2);
|
|
CHECK(reply->function == 42); // S2F42 Host Command Ack
|
|
CHECK(ran); // the handler ran inside the S2F41 dispatch
|
|
CHECK(seen == "START");
|
|
}
|
|
|
|
TEST_CASE("register_default_handlers: unknown command is rejected, hook not invoked") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
gem::register_default_handlers(rt);
|
|
|
|
auto reply = rt.router().dispatch(gem::s2f41_host_command("NO_SUCH_CMD", {}));
|
|
REQUIRE(reply.has_value());
|
|
CHECK(reply->stream == 2);
|
|
CHECK(reply->function == 42); // still an S2F42, carrying an error HCACK
|
|
}
|
|
|
|
TEST_CASE("capability registration is subsettable (the point of the decomposition)") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
gem::register_identification(rt);
|
|
gem::register_terminal_services(rt);
|
|
|
|
// What we registered answers...
|
|
CHECK(rt.router().has_handler(1, 1)); // S1F1
|
|
CHECK(rt.router().has_handler(1, 13)); // S1F13
|
|
CHECK(rt.router().has_handler(10, 1)); // S10F1
|
|
// ...and what we didn't, doesn't: a sensor-class tool with no remote
|
|
// commands, jobs, or carriers simply never registers them.
|
|
CHECK_FALSE(rt.router().has_handler(2, 41)); // remote commands
|
|
CHECK_FALSE(rt.router().has_handler(16, 11)); // E40 jobs
|
|
CHECK_FALSE(rt.router().has_handler(3, 17)); // E87 carriers
|
|
}
|
|
|
|
TEST_CASE("role bindings drive the CJ state-change CEIDs and SVID refresh") {
|
|
gem::EquipmentRuntime rt(test_config());
|
|
gem::register_default_handlers(rt);
|
|
|
|
// The shipped roles block binds control_state_svid: 1 — S1F3 refreshes it.
|
|
auto reply = rt.router().dispatch(gem::s1f3_selected_status_request({1}));
|
|
REQUIRE(reply.has_value());
|
|
auto sv = rt.model().svids.value(1);
|
|
REQUIRE(sv.has_value());
|
|
CHECK(sv->as_ascii() == std::string("HostOffline")); // initial control state
|
|
}
|