D2: E87 wire messages (S3F17/F18 + F23/F24 + F25/F26 + F27/F28)

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:11:35 +02:00
parent 94c26c0771
commit 5f1444c756
3 changed files with 174 additions and 0 deletions
+104
View File
@@ -1246,6 +1246,110 @@ messages:
- {name: mapercd, shape: {kind: scalar, item_type: BINARY_BYTE}} - {name: mapercd, shape: {kind: scalar, item_type: BINARY_BYTE}}
- {name: errtext, shape: {kind: scalar, item_type: ASCII}} - {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). # S14 — Object services (E94 ControlJob create/delete).
# #
+18
View File
@@ -171,4 +171,22 @@ enum class MapDataAck : uint8_t { // S12F8 MAPER
AbortNoMap = 3, 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 } // namespace secsgem::gem
+52
View File
@@ -737,3 +737,55 @@ TEST_CASE("S12F19 Map Error Send round-trip") {
CHECK(parsed->errtext == "unrecognized MID"); CHECK(parsed->errtext == "unrecognized MID");
CHECK_FALSE(m.reply_expected); CHECK_FALSE(m.reply_expected);
} }
// ---- S3 E87 Carrier Management -----------------------------------------
TEST_CASE("S3F17 / S3F18 CarrierAction round-trip") {
std::vector<CommandParameter> 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);
}