diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index 23d8a3f..ee65f66 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -766,6 +766,33 @@ int main(int argc, char** argv) { return gem::s3f26_carrier_transfer_ack(gem::CarrierActionAck::Accept); }); + // S3F19 — Slot Map Verify. Host sends its expected slot map for + // CARRIERID; equipment compares against locally-stored slots and + // drives CSMS (NotRead -> Read on Accept, -> Mismatched on Mismatch). + router.on(3, 19, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s3f19(msg); + if (!req) + return gem::s3f20_slot_map_verify_ack(gem::SlotMapVerifyAck::Error); + auto* c = model->carriers.get(req->carrierid); + if (!c) + return gem::s3f20_slot_map_verify_ack(gem::SlotMapVerifyAck::CarrierUnknown); + bool match = c->slots.size() == req->slots.size(); + if (match) { + for (std::size_t i = 0; i < req->slots.size(); ++i) { + if (static_cast(c->slots[i].state) != + static_cast(req->slots[i])) { match = false; break; } + } + } + if (match) { + model->carriers.fire_slot_map_event(req->carrierid, gem::SlotMapEvent::Read); + logfn("S3F19 CARRIER=" + req->carrierid + " -> S3F20 Accept"); + return gem::s3f20_slot_map_verify_ack(gem::SlotMapVerifyAck::Accept); + } + model->carriers.fire_slot_map_event(req->carrierid, gem::SlotMapEvent::Mismatch); + logfn("S3F19 CARRIER=" + req->carrierid + " -> S3F20 Mismatch"); + return gem::s3f20_slot_map_verify_ack(gem::SlotMapVerifyAck::Mismatch); + }); + // S3F27 — Cancel Carrier (single-EXID form). router.on(3, 27, [model, logfn](const s2::Message& msg) { auto cid = gem::parse_s3f27(msg); diff --git a/data/messages.yaml b/data/messages.yaml index 5ac1dc0..4fbba27 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -1335,6 +1335,53 @@ messages: builder: s3f26_carrier_transfer_ack body: {kind: scalar, item_type: BINARY_BYTE, enum: CarrierActionAck, param: caack} + # S3F19 / S3F20 — Slot Map Verify. Host sends its expected slot map + # for the carrier; equipment compares against its locally-read map + # and replies SVACK (Accept/Mismatch). On Accept, CSMS transitions + # to Read; on Mismatch, CSMS goes to Mismatched. + - id: S3F19 + stream: 3 + function: 19 + w: true + builder: s3f19_slot_map_verify + parser: parse_s3f19 + body: + kind: list + struct_name: SlotMapVerifyRequest + fields: + - {name: carrierid, shape: {kind: scalar, item_type: ASCII}} + - name: slots + shape: {kind: list_of, element: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S3F20 + stream: 3 + function: 20 + builder: s3f20_slot_map_verify_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: SlotMapVerifyAck, param: svack} + + # S3F21 / S3F22 — Slot Map Report. Equipment-initiated report of + # the slot map it sees (typically pushed after a CARRIERID is read + # so the host can decide whether to ProceedWithCarrier). + - id: S3F21 + stream: 3 + function: 21 + w: true + builder: s3f21_slot_map_report + parser: parse_s3f21 + body: + kind: list + struct_name: SlotMapReport + fields: + - {name: carrierid, shape: {kind: scalar, item_type: ASCII}} + - name: slots + shape: {kind: list_of, element: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S3F22 + stream: 3 + function: 22 + builder: s3f22_slot_map_report_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: SlotMapVerifyAck, param: svack} + # S3F27 / S3F28 — Cancel Carrier (host cancels a carrier operation). - id: S3F27 stream: 3 diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp index 266a96f..245943f 100644 --- a/include/secsgem/gem/messages_helpers.hpp +++ b/include/secsgem/gem/messages_helpers.hpp @@ -189,4 +189,13 @@ enum class PortGroupAck : uint8_t { Error = 1, }; +// E87 slot-map verify ack code (SVACK). Same byte used by S3F20 +// (verify reply) and S3F22 (host's ack of equipment's slot-map report). +enum class SlotMapVerifyAck : uint8_t { + Accept = 0, + Mismatch = 1, + CarrierUnknown = 2, + Error = 3, +}; + } // namespace secsgem::gem diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index e04dd30..879bf0e 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -782,6 +782,34 @@ TEST_CASE("S3F25 / S3F26 CarrierTransfer round-trip") { CHECK(*ack_byte(s3f26_carrier_transfer_ack(CarrierActionAck::Accept)) == 0); } +TEST_CASE("S3F19 / S3F20 SlotMapVerify round-trip") { + std::vector raw_slots = {std::string({0x01, 0x01, 0x00, 0x01})}; + // Build via the typed builder: list of BINARY_BYTE per slot. + std::vector slots = {0x01, 0x01, 0x00, 0x01}; + auto m = s3f19_slot_map_verify("CAR-001", slots); + CHECK(m.stream == 3); + CHECK(m.function == 19); + CHECK(m.reply_expected); + + auto parsed = parse_s3f19(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->carrierid == "CAR-001"); + CHECK(parsed->slots == slots); + + CHECK(*ack_byte(s3f20_slot_map_verify_ack(SlotMapVerifyAck::Accept)) == 0); + CHECK(*ack_byte(s3f20_slot_map_verify_ack(SlotMapVerifyAck::Mismatch)) == 1); +} + +TEST_CASE("S3F21 / S3F22 SlotMapReport round-trip") { + std::vector slots = {0x01, 0x00}; + auto m = s3f21_slot_map_report("CAR-002", slots); + auto parsed = parse_s3f21(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->carrierid == "CAR-002"); + CHECK(parsed->slots == slots); + CHECK(*ack_byte(s3f22_slot_map_report_ack(SlotMapVerifyAck::Accept)) == 0); +} + TEST_CASE("S3F27 / S3F28 CancelCarrier round-trip") { auto m = s3f27_cancel_carrier("CAR-001"); auto parsed = parse_s3f27(m);