From fafbd2abd264dafd90a7a5cf85eede69d9dde8b9 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Sun, 7 Jun 2026 21:10:40 +0200 Subject: [PATCH] A2: S2F49/F50 Enhanced Remote Command Adds the OBJSPEC-scoped sibling of S2F41 with extended per-parameter ack shape (CPACK + CEPACK). Wire: S2F49 body >> S2F50 body >> Server delegates to the existing HostCommandRegistry, logs OBJSPEC for audit, and currently returns empty cpacks (all-OK). Per-parameter failures will be wired when the command registry grows CEPACK-level validation; this commit is the catalog + dispatch scaffolding. secsgem-py defines these in its catalog but never dispatches them; this puts the C++ port marginally ahead on remote-command coverage. Co-Authored-By: Claude Opus 4.7 --- COMPLIANCE.md | 1 + apps/secs_server.cpp | 24 ++++++++++++++++++++ data/messages.yaml | 50 +++++++++++++++++++++++++++++++++++++++++ tests/test_messages.cpp | 31 +++++++++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 56753f9..96f048b 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -175,6 +175,7 @@ YAML/handler extensions, not surgery): | S5F13 / S5F14 | H→E | ✅ | catalog | ✅ round-trip (exception recover request) | | S5F15 / S5F16 | E→H | ✅ | catalog | ✅ round-trip (exception recover complete) | | S5F17 / S5F18 | H→E | ✅ | catalog | ✅ round-trip (exception recover abort) | +| S2F49 / S2F50 | H→E | ✅ | catalog + server | ✅ round-trip + dispatch (enhanced remote command, OBJSPEC + per-CP CPACK/CEPACK) | | S6F1 / S6F2 | E→H | ✅ | catalog | ✅ round-trip | | S6F11 / S6F12 | E→H | ✅ | catalog | ✅ round-trip + demo | | S6F23 / S6F24 | H→E | ✅ | catalog | ✅ round-trip + demo | diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index ca0fd3a..4b2c261 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -389,6 +389,30 @@ int main(int argc, char** argv) { return gem::s2f42_host_command_ack(result.ack, {}); }); + // S2F49 — Enhanced Remote Command. OBJSPEC scopes the command at a + // specific object instance (e.g. a CJ or PJ id); for now we delegate + // to the same command registry as S2F41 and surface OBJSPEC in the + // log so downstream tooling can audit it. The richer CPACK/CEPACK + // shape lets us return per-parameter outcomes; until a command in + // the registry produces per-CP failures we just reply with an empty + // cpacks list, matching the spec's "all OK" interpretation. + router.on(2, 49, [model, logfn, emit_event, emit_alarm_set](const s2::Message& msg) { + auto cmd = gem::parse_s2f49(msg); + if (!cmd) return gem::s2f50_enhanced_host_command_ack(gem::HostCmdAck::ParameterInvalid, {}); + auto result = model->commands.dispatch(cmd->rcmd, cmd->params); + logfn("S2F49 DATAID=" + std::to_string(cmd->dataid) + + " OBJSPEC=" + cmd->objspec + " RCMD=" + cmd->rcmd + + " -> S2F50 HCACK=" + std::to_string(static_cast(result.ack))); + if (result.ack == gem::HostCmdAck::Accept) { + if (result.emit_ceid) emit_event(*result.emit_ceid); + if (result.set_alarm) emit_alarm_set(*result.set_alarm); + if (result.force_spool) { + model->spool.set_force_spool(*result.force_spool); + } + } + return gem::s2f50_enhanced_host_command_ack(result.ack, {}); + }); + router.on(2, 23, [model, logfn](const s2::Message& msg) { auto req = gem::parse_s2f23(msg); auto ack = gem::TraceAck::Accept; diff --git a/data/messages.yaml b/data/messages.yaml index 832c3d3..bb922b5 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -433,6 +433,56 @@ messages: - {name: name, shape: {kind: scalar, item_type: ASCII}} - {name: code, shape: {kind: scalar, item_type: BINARY_BYTE}} + # S2F49 / S2F50 — Enhanced Remote Command (E30 §6.4, E5 §10.4). Same + # intent as S2F41/F42 but routes a command at a specific object via + # OBJSPEC, carries a DATAID transaction handle, and the reply uses + # extended per-CP acks (CPACK + CEPACK) so the equipment can flag both + # "name unknown" (CPACK) and "value invalid" (CEPACK) independently. + - id: S2F49 + stream: 2 + function: 49 + w: true + builder: s2f49_enhanced_host_command + parser: parse_s2f49 + body: + kind: list + struct_name: EnhancedHostCommand + fields: + - {name: dataid, shape: {kind: scalar, item_type: U4}} + - {name: objspec, shape: {kind: scalar, item_type: ASCII}} + - {name: rcmd, shape: {kind: scalar, item_type: ASCII}} + - name: params + shape: + kind: list_of + element: + kind: list + struct_name: CommandParameter + external_struct: true + fields: + - {name: name, shape: {kind: scalar, item_type: ASCII}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} + + - id: S2F50 + stream: 2 + function: 50 + builder: s2f50_enhanced_host_command_ack + parser: parse_s2f50 + body: + kind: list + struct_name: EnhancedHostCommandReply + fields: + - {name: hcack, shape: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck}} + - name: cpacks + shape: + kind: list_of + element: + kind: list + struct_name: EnhancedCommandParameterAck + fields: + - {name: name, shape: {kind: scalar, item_type: ASCII}} + - {name: cpack, shape: {kind: scalar, item_type: BINARY_BYTE}} + - {name: cepack, shape: {kind: scalar, item_type: BINARY_BYTE}} + # S2F23 / S2F24 — Trace Initialize Send (E30 §6.12). Host requests # periodic SVID sampling: every DSPER, equipment captures the SVID # values; every REPGSZ samples it emits an S6F1 batch; after TOTSMP diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 723248d..8639275 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -157,6 +157,37 @@ TEST_CASE("S2F42 round-trip with HCACK and CPACKs") { CHECK(parsed->cpacks[1].code == 3); } +TEST_CASE("S2F49 / S2F50 enhanced remote command round-trip") { + std::vector params = { + {"LOTID", s2::Item::ascii("LOT-99")}, + {"PPID", s2::Item::ascii("RECIPE-B")}, + }; + auto m = s2f49_enhanced_host_command(/*dataid=*/7, "CJ-1", "START", params); + CHECK(m.stream == 2); + CHECK(m.function == 49); + CHECK(m.reply_expected); + + auto parsed = parse_s2f49(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->dataid == 7); + CHECK(parsed->objspec == "CJ-1"); + CHECK(parsed->rcmd == "START"); + REQUIRE(parsed->params.size() == 2); + CHECK(parsed->params[0].name == "LOTID"); + CHECK(parsed->params[0].value == s2::Item::ascii("LOT-99")); + + auto ack = s2f50_enhanced_host_command_ack( + HostCmdAck::ParameterInvalid, + {{"LOTID", 0, 0}, {"PPID", 3, 1}}); + auto preply = parse_s2f50(ack); + REQUIRE(preply.has_value()); + CHECK(preply->hcack == HostCmdAck::ParameterInvalid); + REQUIRE(preply->cpacks.size() == 2); + CHECK(preply->cpacks[1].name == "PPID"); + CHECK(preply->cpacks[1].cpack == 3); + CHECK(preply->cpacks[1].cepack == 1); +} + TEST_CASE("S2F42 no-params variant") { auto m = s2f42_host_command_ack(HostCmdAck::Accept, {}); auto parsed = parse_s2f42(m);