From 5f1444c7561739e6251be9133d8d6ef7fe49e096 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 00:11:35 +0200 Subject: [PATCH] D2: E87 wire messages (S3F17/F18 + F23/F24 + F25/F26 + F27/F28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the Carrier Management wire surface that drives the FSMs from D1: S3F17 / S3F18 CarrierAction (host issues ProceedWithCarrier / CancelCarrier / BindCarrierID / etc.; CommandParameter list is reused from S2F41). S3F23 / S3F24 PortGroupChangeReport (equipment notifies host of port group composition changes). S3F25 / S3F26 CarrierTransfer (host instructs source -> target port transfer). S3F27 / S3F28 CancelCarrier (host cancels an outstanding carrier op). Two new ack enums in messages_helpers.hpp: CarrierActionAck — CAACK byte; covers the common error responses (CarrierIDUnknown, Inaccessible, ActionInProgress). PortGroupAck — PGACK byte (Accept / Error). Round-trip tests for all four pairs. Server-side dispatch + the CarrierStore + LoadPortStore that the FSMs key into land in D3/D4. Co-Authored-By: Claude Opus 4.7 --- data/messages.yaml | 104 +++++++++++++++++++++++ include/secsgem/gem/messages_helpers.hpp | 18 ++++ tests/test_messages.cpp | 52 ++++++++++++ 3 files changed, 174 insertions(+) diff --git a/data/messages.yaml b/data/messages.yaml index ac67722..5ac1dc0 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -1246,6 +1246,110 @@ messages: - {name: mapercd, shape: {kind: scalar, item_type: BINARY_BYTE}} - {name: errtext, shape: {kind: scalar, item_type: ASCII}} + # ===================================================================== + # S3 — E87 Carrier Management. + # + # CARRIERID is an ASCII identifier (per E87-0716 §10.2 typically up to + # 40 chars). CAACK is the host/equipment ack byte for carrier-action + # requests, modelled here as CarrierActionAck. PORTID is U1, SLOTID + # is U1. We carry only the most common subset of S3 here; richer + # forms (E14 generic ObjectService) are YAML extension points. + # ===================================================================== + + # S3F17 / S3F18 — CarrierAction. Host instructs equipment to perform + # a carrier-level operation (ProceedWithCarrier, CancelCarrier, + # CancelCarrierAtPort, BindCarrierID, ResetCarrier, ...). The list of + # parameters is action-specific; we carry it as a parallel name/value + # list mirroring S2F41's CommandParameter shape. + - id: S3F17 + stream: 3 + function: 17 + w: true + builder: s3f17_carrier_action + parser: parse_s3f17 + body: + kind: list + struct_name: CarrierActionRequest + fields: + - {name: dataid, shape: {kind: scalar, item_type: U4}} + - {name: carrieraction, shape: {kind: scalar, item_type: ASCII}} + - {name: carrierid, 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: S3F18 + stream: 3 + function: 18 + builder: s3f18_carrier_action_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: CarrierActionAck, param: caack} + + # S3F23 / S3F24 — Port Group Change Report (equipment notifies host of + # a port-grouping change; E87-0716 §10.4). + - id: S3F23 + stream: 3 + function: 23 + w: true + builder: s3f23_port_group_change_report + parser: parse_s3f23 + body: + kind: list + struct_name: PortGroupChangeReport + fields: + - {name: ctngrpid, shape: {kind: scalar, item_type: ASCII}} + - name: portids + shape: {kind: list_of, element: {kind: scalar, item_type: U1}} + + - id: S3F24 + stream: 3 + function: 24 + builder: s3f24_port_group_change_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: PortGroupAck, param: pgack} + + # S3F25 / S3F26 — Carrier Transfer (host instructs equipment to move + # a carrier between source and target ports). + - id: S3F25 + stream: 3 + function: 25 + w: true + builder: s3f25_carrier_transfer + parser: parse_s3f25 + body: + kind: list + struct_name: CarrierTransferRequest + fields: + - {name: carrierid, shape: {kind: scalar, item_type: ASCII}} + - {name: source_portid, shape: {kind: scalar, item_type: U1}} + - {name: target_portid, shape: {kind: scalar, item_type: U1}} + + - id: S3F26 + stream: 3 + function: 26 + builder: s3f26_carrier_transfer_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: CarrierActionAck, param: caack} + + # S3F27 / S3F28 — Cancel Carrier (host cancels a carrier operation). + - id: S3F27 + stream: 3 + function: 27 + w: true + builder: s3f27_cancel_carrier + parser: parse_s3f27 + body: {kind: scalar, item_type: ASCII, param: carrierid} + + - id: S3F28 + stream: 3 + function: 28 + builder: s3f28_cancel_carrier_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: CarrierActionAck, param: caack} + # ===================================================================== # S14 — Object services (E94 ControlJob create/delete). # diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp index 4803bcb..266a96f 100644 --- a/include/secsgem/gem/messages_helpers.hpp +++ b/include/secsgem/gem/messages_helpers.hpp @@ -171,4 +171,22 @@ enum class MapDataAck : uint8_t { // S12F8 MAPER AbortNoMap = 3, }; +// E87 §10.2 carrier-action ack code (CAACK). Same byte used by S3F18, +// S3F26 (carrier transfer), and S3F28 (cancel carrier). +enum class CarrierActionAck : uint8_t { + Accept = 0, + Invalid = 1, + ParameterInvalid = 2, + CarrierIDUnknown = 3, + CarrierActionInvalid = 4, + CarrierInaccessible = 5, + CarrierActionInProgress = 6, +}; + +// E87 §10.4 port-group change ack code. +enum class PortGroupAck : uint8_t { + Accept = 0, + Error = 1, +}; + } // namespace secsgem::gem diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 699d6d5..e04dd30 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -737,3 +737,55 @@ TEST_CASE("S12F19 Map Error Send round-trip") { CHECK(parsed->errtext == "unrecognized MID"); CHECK_FALSE(m.reply_expected); } + +// ---- S3 E87 Carrier Management ----------------------------------------- + +TEST_CASE("S3F17 / S3F18 CarrierAction round-trip") { + std::vector params{ + {"PORTID", s2::Item::u1(uint8_t{1})}, + }; + auto m = s3f17_carrier_action(/*dataid=*/3, "ProceedWithCarrier", + "CAR-001", params); + CHECK(m.stream == 3); + CHECK(m.function == 17); + CHECK(m.reply_expected); + + auto parsed = parse_s3f17(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->dataid == 3); + CHECK(parsed->carrieraction == "ProceedWithCarrier"); + CHECK(parsed->carrierid == "CAR-001"); + REQUIRE(parsed->params.size() == 1); + CHECK(parsed->params[0].name == "PORTID"); + + CHECK(*ack_byte(s3f18_carrier_action_ack(CarrierActionAck::Accept)) == 0); + CHECK(*ack_byte(s3f18_carrier_action_ack(CarrierActionAck::CarrierIDUnknown)) == 3); +} + +TEST_CASE("S3F23 / S3F24 PortGroupChangeReport round-trip") { + auto m = s3f23_port_group_change_report("GROUP-A", {1, 2, 3}); + auto parsed = parse_s3f23(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->ctngrpid == "GROUP-A"); + REQUIRE(parsed->portids.size() == 3); + CHECK(parsed->portids[1] == 2); + CHECK(*ack_byte(s3f24_port_group_change_ack(PortGroupAck::Accept)) == 0); +} + +TEST_CASE("S3F25 / S3F26 CarrierTransfer round-trip") { + auto m = s3f25_carrier_transfer("CAR-001", /*source=*/1, /*target=*/2); + auto parsed = parse_s3f25(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->carrierid == "CAR-001"); + CHECK(parsed->source_portid == 1); + CHECK(parsed->target_portid == 2); + CHECK(*ack_byte(s3f26_carrier_transfer_ack(CarrierActionAck::Accept)) == 0); +} + +TEST_CASE("S3F27 / S3F28 CancelCarrier round-trip") { + auto m = s3f27_cancel_carrier("CAR-001"); + auto parsed = parse_s3f27(m); + REQUIRE(parsed.has_value()); + CHECK(*parsed == "CAR-001"); + CHECK(*ack_byte(s3f28_cancel_carrier_ack(CarrierActionAck::Accept)) == 0); +}