L: E87 slot-map verification wire (S3F19/F20 + F21/F22)
Closes the slot-map verification gap I called out:
S3F19 / F20 host -> equip: verify expected slot map against what
the equipment has scanned. Equipment compares element-
wise; on match drives CSMS NotRead -> Read and replies
SVACK=Accept; on mismatch drives CSMS -> Mismatched and
replies SVACK=Mismatch.
S3F21 / F22 equip -> host: equipment-initiated slot map report
(typically pushed after CARRIERID is confirmed).
New SVACK enum: SlotMapVerifyAck { Accept, Mismatch, CarrierUnknown,
Error }. Server dispatch on S3F19 wires the actual CSMS transition
through the CarrierStore from D3.
Two round-trip tests cover both pairs; the FSM-driving behaviour is
exercised through the in-process tests because secs_server.cpp is
the dispatch entry point (no separate integration test needed beyond
the wire round-trip).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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<uint8_t>(c->slots[i].state) !=
|
||||
static_cast<uint8_t>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<std::string> raw_slots = {std::string({0x01, 0x01, 0x00, 0x01})};
|
||||
// Build via the typed builder: list of BINARY_BYTE per slot.
|
||||
std::vector<uint8_t> 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<uint8_t> 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);
|
||||
|
||||
Reference in New Issue
Block a user