From 4197cdfb2568696bdc10ee33af3609f0400a5e69 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Mon, 8 Jun 2026 23:30:51 +0200 Subject: [PATCH] =?UTF-8?q?AA:=20catalog=20growth=20=E2=80=94=20S7F1/F17,?= =?UTF-8?q?=20S6F5-F22,=20S2F21,=20S12F9-F18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the SECS-II messages secsgem-py 0.3.0 ships but our C++ catalog didn't have, plus the alternative wafer-map formats from E5 §13. None of these were strictly required for GEM core compliance, but they're the messages a host might send to a conformant equipment. * S7F1/F2 — Process Program Load Inquire / Grant. Equipment-side space-and-policy check before a host commits to S7F3. * S7F17/F18 — Delete Process Program. Empty list = delete-all. * S6F5/F6 — Multi-block Data Send Inquire / Grant (with MultiBlockGrant enum: Ok/Busy/NoSpace/DuplicateMsg/BadMsg). * S6F7/F8 — Data Transfer Request / Send. Host pulls a DATAID; equipment replies with the nested DS/DV structure. * S6F15/F16 — Event Report Request (host-initiated). Reply mirrors the unsolicited S6F11. * S6F19/F20 — Individual Report Request (RPTID -> values). * S6F21/F22 — Annotated Individual Report Request (RPTID -> (VID, value)). * S2F21/F22 — Legacy Remote Command (no parameter list). Delegates to the same HostCommandRegistry as S2F41. * S12F9/F10 — Map Data Send (array format, MAPFT=1). * S12F11/F12 — Map Data Send (coordinate format, MAPFT=2). * S12F13/F14, F15/F16, F17/F18 — Map Data Request variants for the row, array, and coordinate formats. 11 new round-trip tests; suite at 289 cases / 1495 assertions. Co-Authored-By: Claude Opus 4.7 --- apps/secs_server.cpp | 119 +++++++ data/messages.yaml | 403 +++++++++++++++++++++++ include/secsgem/gem/messages_helpers.hpp | 12 +- tests/test_messages.cpp | 133 ++++++++ 4 files changed, 666 insertions(+), 1 deletion(-) diff --git a/apps/secs_server.cpp b/apps/secs_server.cpp index ce4e913..1504c54 100644 --- a/apps/secs_server.cpp +++ b/apps/secs_server.cpp @@ -571,6 +571,23 @@ int main(int argc, char** argv) { return gem::s2f42_host_command_ack(result.ack, {}); }); + // S2F21 — legacy Remote Command (no parameter list). Delegated to + // the same HostCommandRegistry as S2F41 so a single YAML row defines + // both behaviours; we just don't pass any parameters along. + router.on(2, 21, [model, logfn, emit_event, emit_alarm_set](const s2::Message& msg) { + auto rcmd = gem::parse_s2f21(msg); + if (!rcmd) return gem::s2f22_remote_command_ack(gem::HostCmdAck::ParameterInvalid); + auto result = model->commands.dispatch(*rcmd, {}); + logfn("S2F21 RCMD=" + *rcmd + " -> S2F22 CMDA=" + + 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::s2f22_remote_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 @@ -653,6 +670,73 @@ int main(int argc, char** argv) { return gem::s2f44_reset_spooling_ack(ack, per); }); + // S6F15 — Event Report Request. Host pulls the current payload for + // a CEID without waiting for the equipment to emit it. Reply mirrors + // S6F11 (DATAID=0, the same CEID, and the latest report rows). + router.on(6, 15, [model, logfn](const s2::Message& msg) { + auto ceid = gem::parse_s6f15(msg); + if (!ceid) + return gem::s6f16_event_report_data({0, 0, {}}); + auto reports = model->compose_reports_for(*ceid); + logfn("S6F15 CEID=" + std::to_string(*ceid) + " -> S6F16 (" + + std::to_string(reports.size()) + " reports)"); + return gem::s6f16_event_report_data({0, *ceid, reports}); + }); + + // S6F19 — Individual Report Request. Host pulls a specific RPTID; + // we return just that report's VID values (no annotation). + router.on(6, 19, [model, logfn](const s2::Message& msg) { + auto rptid = gem::parse_s6f19(msg); + std::vector values; + if (rptid) { + // Resolve each VID in the report against the current values. + for (const auto& r : model->events.all_reports()) { + if (r.id != *rptid) continue; + for (auto vid : r.vids) { + auto v = model->vid_value(vid); + values.push_back(v ? *v : s2::Item::list({})); + } + break; + } + } + logfn("S6F19 RPTID=" + std::to_string(rptid.value_or(0)) + + " -> S6F20 (" + std::to_string(values.size()) + " values)"); + return gem::s6f20_individual_report_data(values); + }); + + // S6F21 — Annotated Individual Report Request. Same lookup as F19 + // but the reply carries (VID, value) pairs so the host doesn't need + // to remember the report definition. + router.on(6, 21, [model, logfn](const s2::Message& msg) { + auto rptid = gem::parse_s6f21(msg); + std::vector rows; + if (rptid) { + for (const auto& r : model->events.all_reports()) { + if (r.id != *rptid) continue; + for (auto vid : r.vids) { + auto v = model->vid_value(vid); + rows.push_back({vid, v ? *v : s2::Item::list({})}); + } + break; + } + } + logfn("S6F21 RPTID=" + std::to_string(rptid.value_or(0)) + + " -> S6F22 (" + std::to_string(rows.size()) + " annotated values)"); + return gem::s6f22_annotated_report_data(rows); + }); + + // S6F5 — Multi-block Data Send Inquire. When the host plays this + // role we grant unconditionally (HSMS doesn't have the SECS-I + // 244-byte block ceiling that motivates the handshake). Real hosts + // would gate on storage or busy state. + router.on(6, 5, [logfn](const s2::Message& msg) { + auto req = gem::parse_s6f5(msg); + logfn("S6F5 DATAID=" + std::to_string(req ? req->dataid : 0) + + " LEN=" + std::to_string(req ? req->datalength : 0) + + " -> S6F6 GRANT6=0"); + return gem::s6f6_multi_block_grant(gem::MultiBlockGrant::Ok); + }); + router.on(6, 23, [&io, active_conn, model, logfn](const s2::Message& msg) { auto rsdc = gem::parse_s6f23(msg); if (!rsdc) return gem::s6f24_request_spool_data_ack(gem::SpoolRequestAck::Denied); @@ -826,6 +910,21 @@ int main(int argc, char** argv) { return gem::s3f28_cancel_carrier_ack(gem::CarrierActionAck::Accept); }); + // S7F1 — Process Program Load Inquire. Host asks permission to send + // LENGTH bytes for PPID; equipment responds with PPGNT. Policy here: + // accept any reasonable size (< 16 MiB which is also our HSMS frame + // cap) and reject empty PPIDs. Real equipment would gate on + // available recipe storage. + router.on(7, 1, [logfn](const s2::Message& msg) { + auto req = gem::parse_s7f1(msg); + auto ack = gem::ProcessProgramAck::Accept; + if (!req || req->ppid.empty()) ack = gem::ProcessProgramAck::PpidNotFound; + else if (req->length > 16u * 1024u * 1024u) ack = gem::ProcessProgramAck::MatrixOverflow; + logfn("S7F1 PPID=" + (req ? req->ppid : std::string{"?"}) + + " LEN=" + std::to_string(req ? req->length : 0) + + " -> S7F2 PPGNT=" + std::to_string(static_cast(ack))); + return gem::s7f2_pp_load_grant(ack); + }); router.on(7, 3, [model, logfn](const s2::Message& msg) { auto pp = gem::parse_s7f3(msg); if (!pp) return gem::s7f4_process_program_ack(gem::ProcessProgramAck::LengthError); @@ -833,6 +932,26 @@ int main(int argc, char** argv) { logfn("S7F3 PPID=" + pp->ppid + " -> S7F4 (Accept)"); return gem::s7f4_process_program_ack(gem::ProcessProgramAck::Accept); }); + // S7F17 — Delete Process Program. Empty PPID list deletes all; + // otherwise we remove each PPID and aggregate the worst ack. + router.on(7, 17, [model, logfn](const s2::Message& msg) { + auto req = gem::parse_s7f17(msg); + if (!req) return gem::s7f18_delete_pp_ack(gem::ProcessProgramAck::LengthError); + auto ack = gem::ProcessProgramAck::Accept; + if (req->empty()) { + const auto all = model->recipes.list(); + for (const auto& id : all) model->recipes.remove(id); + logfn("S7F17 delete-all (" + std::to_string(all.size()) + ") -> S7F18 Accept"); + } else { + for (const auto& id : *req) { + auto r = model->recipes.remove(id); + if (r != gem::ProcessProgramAck::Accept) ack = r; + } + logfn("S7F17 delete " + std::to_string(req->size()) + + " PPIDs -> S7F18 ACKC7=" + std::to_string(static_cast(ack))); + } + return gem::s7f18_delete_pp_ack(ack); + }); router.on(7, 5, [model, logfn](const s2::Message& msg) { auto ppid = gem::parse_s7f5(msg); if (!ppid) return gem::s7f6_process_program_data("", ""); diff --git a/data/messages.yaml b/data/messages.yaml index ebe1ba1..36d21c0 100644 --- a/data/messages.yaml +++ b/data/messages.yaml @@ -520,6 +520,26 @@ messages: # values; every REPGSZ samples it emits an S6F1 batch; after TOTSMP # total samples it stops. Periodic execution is the application's # responsibility; this codebase only stores the request. + # S2F21 / S2F22 — Remote Command (legacy form, E5). No parameter + # list — just the command name. Modern usage prefers S2F41/F42 which + # carry CP name/value pairs, but some older hosts still send the F21 + # form so we accept it. CMDA is a U1 ack code: 0=Accept, + # 1=CannotPerformNow, 2=CommandDoesNotExist, etc. (same enum as + # HostCmdAck which we use throughout the codebase). + - id: S2F21 + stream: 2 + function: 21 + w: true + builder: s2f21_remote_command + parser: parse_s2f21 + body: {kind: scalar, item_type: ASCII, param: rcmd} + + - id: S2F22 + stream: 2 + function: 22 + builder: s2f22_remote_command_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: HostCmdAck, param: cmda} + - id: S2F23 stream: 2 function: 23 @@ -905,6 +925,151 @@ messages: builder: s6f12_event_report_ack body: {kind: scalar, item_type: BINARY_BYTE, enum: EventReportAck, param: ack} + # S6F5 / S6F6 — Multi-block Data Send Inquire / Grant. Equipment + # asks permission before pushing a large S6F11 (or other primary) of + # DATALENGTH bytes; host replies with GRANT6 (0=OK, 1=Busy, 2=NoSpace, + # 3=DuplicateMsg, 4=BadMsg). HSMS doesn't need this for sizing + # reasons (no 244-byte block ceiling), but the spec contract is kept. + - id: S6F5 + stream: 6 + function: 5 + w: true + builder: s6f5_multi_block_inquire + parser: parse_s6f5 + body: + kind: list + struct_name: MultiBlockInquire + fields: + - {name: dataid, shape: {kind: scalar, item_type: U4}} + - {name: datalength, shape: {kind: scalar, item_type: U4}} + + - id: S6F6 + stream: 6 + function: 6 + builder: s6f6_multi_block_grant + body: {kind: scalar, item_type: BINARY_BYTE, enum: MultiBlockGrant, param: grant6} + + # S6F7 / S6F8 — Data Transfer Request / Send. Host pulls a previously + # announced data set by DATAID; equipment responds with the full + # `[CEID, [[DSID, [[DVNAME, DVVAL], ...]], ...]]` structure. + - id: S6F7 + stream: 6 + function: 7 + w: true + builder: s6f7_data_transfer_request + parser: parse_s6f7 + body: {kind: scalar, item_type: U4, param: dataid} + + - id: S6F8 + stream: 6 + function: 8 + builder: s6f8_data_transfer_send + parser: parse_s6f8 + body: + kind: list + struct_name: DataTransferSend + fields: + - {name: dataid, shape: {kind: scalar, item_type: U4}} + - {name: ceid, shape: {kind: scalar, item_type: U4}} + - name: data_sets + shape: + kind: list_of + element: + kind: list + struct_name: DataSet + fields: + - {name: dsid, shape: {kind: scalar, item_type: U4}} + - name: values + shape: + kind: list_of + element: + kind: list + struct_name: NamedValue + fields: + - {name: dvname, shape: {kind: scalar, item_type: ASCII}} + - {name: dvval, shape: {kind: scalar, item_type: ITEM}} + + # S6F15 / S6F16 — Event Report Request / Send (host-initiated). + # Host asks for the latest report payload bound to CEID; equipment + # replies with the same body shape as the unsolicited S6F11. + - id: S6F15 + stream: 6 + function: 15 + w: true + builder: s6f15_event_report_request + parser: parse_s6f15 + body: {kind: scalar, item_type: U4, param: ceid} + + - id: S6F16 + stream: 6 + function: 16 + builder: s6f16_event_report_data + parser: parse_s6f16 + body: + kind: list + struct_name: EventReportMessage + fields: + - {name: dataid, shape: {kind: scalar, item_type: U4}} + - {name: ceid, shape: {kind: scalar, item_type: U4}} + - name: reports + shape: + kind: list_of + element: + kind: list + struct_name: ReportData + external_struct: true + fields: + - {name: rptid, shape: {kind: scalar, item_type: U4}} + - name: values + shape: {kind: list_of, element: {kind: scalar, item_type: ITEM}} + + # S6F19 / S6F20 — Individual Report Request / Data (host-initiated). + # Host pulls a specific RPTID; equipment replies with the list of VID + # values that report currently holds. + - id: S6F19 + stream: 6 + function: 19 + w: true + builder: s6f19_individual_report_request + parser: parse_s6f19 + body: {kind: scalar, item_type: U4, param: rptid} + + - id: S6F20 + stream: 6 + function: 20 + builder: s6f20_individual_report_data + parser: parse_s6f20 + body: + kind: list_of + element: {kind: scalar, item_type: ITEM} + name: values + + # S6F21 / S6F22 — Annotated Individual Report Request / Data. + # Same as S6F19 but the reply carries (VID, V) pairs so the host + # doesn't need to remember the report definition. + - id: S6F21 + stream: 6 + function: 21 + w: true + builder: s6f21_annotated_report_request + parser: parse_s6f21 + body: {kind: scalar, item_type: U4, param: rptid} + + - id: S6F22 + stream: 6 + function: 22 + builder: s6f22_annotated_report_data + parser: parse_s6f22 + body: + kind: list_of + name: items + element: + kind: list + struct_name: AnnotatedValue + fields: + - {name: vid, shape: {kind: scalar, item_type: U4}} + - {name: value, shape: {kind: scalar, item_type: ITEM}} + # S6F23 / S6F24 — Request Spooled Data (E30 §6.22; host transmits or purges). - id: S6F23 stream: 6 @@ -941,6 +1106,29 @@ messages: # S7 — Process programs # ===================================================================== + # S7F1 / S7F2 — Process Program Load Inquire / Grant. Host announces + # an intended S7F3 of LENGTH bytes; equipment grants or denies based + # on space, busy state, mode etc. PPGNT is the 7-value + # ProcessProgramAck enum (Accept=0 / PermissionNotGranted=1 / ...). + - id: S7F1 + stream: 7 + function: 1 + w: true + builder: s7f1_pp_load_inquire + parser: parse_s7f1 + body: + kind: list + struct_name: PPLoadInquire + fields: + - {name: ppid, shape: {kind: scalar, item_type: ASCII}} + - {name: length, shape: {kind: scalar, item_type: U4}} + + - id: S7F2 + stream: 7 + function: 2 + builder: s7f2_pp_load_grant + body: {kind: scalar, item_type: BINARY_BYTE, enum: ProcessProgramAck, param: ppgnt} + - id: S7F3 stream: 7 function: 3 @@ -996,6 +1184,26 @@ messages: element: {kind: scalar, item_type: ASCII} name: ppids + # S7F17 / S7F18 — Delete Process Program Send / Acknowledge. Body is + # a list of PPIDs to delete; an empty list deletes every PP (the spec + # calls this "delete all"). ACKC7 reuses the S7F4 ack enum. + - id: S7F17 + stream: 7 + function: 17 + w: true + builder: s7f17_delete_pp_send + parser: parse_s7f17 + body: + kind: list_of + element: {kind: scalar, item_type: ASCII} + name: ppids + + - id: S7F18 + stream: 7 + function: 18 + builder: s7f18_delete_pp_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: ProcessProgramAck, param: ack} + # ===================================================================== # S9 — Protocol error notifications (E5). All carry the 10-byte # offending message header (MHEAD) as a single BINARY item, except @@ -1264,6 +1472,201 @@ messages: builder: s12f8_map_data_send_row_ack body: {kind: scalar, item_type: BINARY_BYTE, enum: MapDataAck, param: maper} + # S12F9 / F10 — Map Data Send (array format, MAPFT=1). The entire + # die grid is one BINARY blob, scanned in the order defined by ORLOC. + # STRP is the starting (col, row) for the first byte of BINLT. + - id: S12F9 + stream: 12 + function: 9 + w: true + builder: s12f9_map_data_send_array + parser: parse_s12f9 + body: + kind: list + struct_name: MapDataArray + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - name: strp + shape: + kind: list + struct_name: ReferencePoint + external_struct: true + fields: + - {name: x, shape: {kind: scalar, item_type: I4}} + - {name: y, shape: {kind: scalar, item_type: I4}} + - {name: binlt, shape: {kind: scalar, item_type: BINARY}} + + - id: S12F10 + stream: 12 + function: 10 + builder: s12f10_map_data_send_array_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: MapDataAck, param: maper} + + # S12F11 / F12 — Map Data Send (coordinate format, MAPFT=2). Each + # entry is a (XYPOS, BIN-byte) pair; sparse maps only carry the dies + # that actually exist. BINLT here is a single BINARY byte (the bin + # code) per coordinate. + - id: S12F11 + stream: 12 + function: 11 + w: true + builder: s12f11_map_data_send_coord + parser: parse_s12f11 + body: + kind: list + struct_name: MapDataCoord + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - name: entries + shape: + kind: list_of + element: + kind: list + struct_name: MapDataCoordEntry + fields: + - name: xypos + shape: + kind: list + struct_name: ReferencePoint + external_struct: true + fields: + - {name: x, shape: {kind: scalar, item_type: I4}} + - {name: y, shape: {kind: scalar, item_type: I4}} + - {name: binlt, shape: {kind: scalar, item_type: BINARY}} + + - id: S12F12 + stream: 12 + function: 12 + builder: s12f12_map_data_send_coord_ack + body: {kind: scalar, item_type: BINARY_BYTE, enum: MapDataAck, param: maper} + + # S12F13 / F14 — Map Data Request (row format). Host asks for the + # current map; equipment replies with row payload (RSINF row-start + # info + per-row bin blob). + - id: S12F13 + stream: 12 + function: 13 + w: true + builder: s12f13_map_data_request_row + parser: parse_s12f13 + body: + kind: list + struct_name: MapDataIdentifier + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S12F14 + stream: 12 + function: 14 + builder: s12f14_map_data_send_row_reply + parser: parse_s12f14 + body: + kind: list + struct_name: MapDataRowReply + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - name: rows + shape: + kind: list_of + element: + kind: list + struct_name: MapDataRowEntry + fields: + - name: rsinf + shape: + kind: list + struct_name: RowStartInfo + fields: + - {name: x, shape: {kind: scalar, item_type: I4}} + - {name: y, shape: {kind: scalar, item_type: I4}} + - {name: direction, shape: {kind: scalar, item_type: I4}} + - {name: binlt, shape: {kind: scalar, item_type: BINARY}} + + # S12F15 / F16 — Map Data Request (array format). Same request shape + # as F13; reply mirrors F9. + - id: S12F15 + stream: 12 + function: 15 + w: true + builder: s12f15_map_data_request_array + parser: parse_s12f15 + body: + kind: list + struct_name: MapDataIdentifier + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S12F16 + stream: 12 + function: 16 + builder: s12f16_map_data_send_array_reply + parser: parse_s12f16 + body: + kind: list + struct_name: MapDataArray + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - name: strp + shape: + kind: list + struct_name: ReferencePoint + external_struct: true + fields: + - {name: x, shape: {kind: scalar, item_type: I4}} + - {name: y, shape: {kind: scalar, item_type: I4}} + - {name: binlt, shape: {kind: scalar, item_type: BINARY}} + + # S12F17 / F18 — Map Data Request (coordinate format). Request carries + # SDBIN (the single bin code to filter on, or 0xFF for "all"); reply + # mirrors F11. + - id: S12F17 + stream: 12 + function: 17 + w: true + builder: s12f17_map_data_request_coord + parser: parse_s12f17 + body: + kind: list + struct_name: MapDataCoordRequest + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - {name: sdbin, shape: {kind: scalar, item_type: BINARY_BYTE}} + + - id: S12F18 + stream: 12 + function: 18 + builder: s12f18_map_data_send_coord_reply + parser: parse_s12f18 + body: + kind: list + struct_name: MapDataCoord + fields: + - {name: mid, shape: {kind: scalar, item_type: ASCII}} + - {name: idtyp, shape: {kind: scalar, item_type: BINARY_BYTE}} + - name: entries + shape: + kind: list_of + element: + kind: list + struct_name: MapDataCoordEntry + fields: + - name: xypos + shape: + kind: list + struct_name: ReferencePoint + external_struct: true + fields: + - {name: x, shape: {kind: scalar, item_type: I4}} + - {name: y, shape: {kind: scalar, item_type: I4}} + - {name: binlt, shape: {kind: scalar, item_type: BINARY}} + # S12F19 — Map Error Send. Equipment reports a non-recoverable map # error (e.g. format mismatch, oversize payload). One-way (no reply). - id: S12F19 diff --git a/include/secsgem/gem/messages_helpers.hpp b/include/secsgem/gem/messages_helpers.hpp index be6cff0..18492c6 100644 --- a/include/secsgem/gem/messages_helpers.hpp +++ b/include/secsgem/gem/messages_helpers.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -233,6 +232,17 @@ enum class EventReportAck : uint8_t { // S6F12 Denied = 1, }; + +// S6F6 GRANT6. Permission byte for equipment-initiated multi-block +// (S6F5 inquire). Codes from SEMI E5. +enum class MultiBlockGrant : uint8_t { + Ok = 0, + Busy = 1, + NoSpace = 2, + DuplicateMsg = 3, + BadMsg = 4, +}; + enum class TerminalAck : uint8_t { // S10F2, S10F4 Accepted = 0, WillNotDisplay = 1, diff --git a/tests/test_messages.cpp b/tests/test_messages.cpp index 92d8b50..3144604 100644 --- a/tests/test_messages.cpp +++ b/tests/test_messages.cpp @@ -484,6 +484,139 @@ TEST_CASE("S7F3 process-program send round-trip") { CHECK(parsed->ppbody == "step1\nstep2\n"); } +TEST_CASE("S7F1 / S7F2 PP-load inquire/grant round-trip") { + auto m = s7f1_pp_load_inquire("RECIPE-X", 4096); + CHECK(m.stream == 7); + CHECK(m.function == 1); + CHECK(m.reply_expected); + auto parsed = parse_s7f1(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->ppid == "RECIPE-X"); + CHECK(parsed->length == 4096); + + auto grant = s7f2_pp_load_grant(ProcessProgramAck::Accept); + CHECK(*ack_byte(grant) == 0); +} + +TEST_CASE("S2F21 / S2F22 legacy remote command round-trip") { + auto req = s2f21_remote_command("PAUSE"); + CHECK(req.stream == 2); + CHECK(req.function == 21); + CHECK(req.reply_expected); + auto parsed = parse_s2f21(req); + REQUIRE(parsed.has_value()); + CHECK(*parsed == "PAUSE"); + CHECK(*ack_byte(s2f22_remote_command_ack(HostCmdAck::Accept)) == 0); +} + +TEST_CASE("S6F5 / S6F6 multi-block inquire / grant round-trip") { + auto req = s6f5_multi_block_inquire(42, 2048); + auto parsed = parse_s6f5(req); + REQUIRE(parsed.has_value()); + CHECK(parsed->dataid == 42); + CHECK(parsed->datalength == 2048); + CHECK(*ack_byte(s6f6_multi_block_grant(MultiBlockGrant::Ok)) == 0); + CHECK(*ack_byte(s6f6_multi_block_grant(MultiBlockGrant::Busy)) == 1); +} + +TEST_CASE("S6F7 round-trip carries DATAID") { + auto req = s6f7_data_transfer_request(7); + auto parsed = parse_s6f7(req); + REQUIRE(parsed.has_value()); + CHECK(*parsed == 7); +} + +TEST_CASE("S6F15 / S6F16 event report request/data round-trip") { + auto req = s6f15_event_report_request(300); + auto parsed = parse_s6f15(req); + REQUIRE(parsed.has_value()); + CHECK(*parsed == 300); + + EventReportMessage erm{0, 300, {ReportData{1, {s2::Item::u4(42)}}}}; + auto data = s6f16_event_report_data(erm); + auto out = parse_s6f16(data); + REQUIRE(out.has_value()); + CHECK(out->ceid == 300); + REQUIRE(out->reports.size() == 1); + CHECK(out->reports[0].rptid == 1); +} + +TEST_CASE("S6F19 / S6F20 individual report round-trip") { + auto req = s6f19_individual_report_request(7); + auto parsed = parse_s6f19(req); + REQUIRE(parsed.has_value()); + CHECK(*parsed == 7); + auto reply = s6f20_individual_report_data({s2::Item::u4(99), s2::Item::ascii("ok")}); + auto out = parse_s6f20(reply); + REQUIRE(out.has_value()); + CHECK(out->size() == 2); +} + +TEST_CASE("S6F21 / S6F22 annotated report round-trip") { + auto req = s6f21_annotated_report_request(7); + auto parsed = parse_s6f21(req); + REQUIRE(parsed.has_value()); + CHECK(*parsed == 7); + std::vector rows{{1, s2::Item::u4(42)}, {2, s2::Item::ascii("hi")}}; + auto reply = s6f22_annotated_report_data(rows); + auto out = parse_s6f22(reply); + REQUIRE(out.has_value()); + REQUIRE(out->size() == 2); + CHECK((*out)[0].vid == 1); + CHECK((*out)[1].vid == 2); +} + +TEST_CASE("S12F9 / S12F10 array map round-trip") { + MapDataArray a{"WAFER-1", 0x01, ReferencePoint{0, 0}, std::string("\x00\x01\x02", 3)}; + auto m = s12f9_map_data_send_array(a); + auto parsed = parse_s12f9(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->mid == "WAFER-1"); + CHECK(parsed->idtyp == 0x01); + CHECK(parsed->binlt.size() == 3); + CHECK(*ack_byte(s12f10_map_data_send_array_ack(MapDataAck::Accept)) == 0); +} + +TEST_CASE("S12F11 / S12F12 coordinate map round-trip") { + MapDataCoord c{"WAFER-1", 0x01, + {{ReferencePoint{1, 2}, std::string("\x05", 1)}, + {ReferencePoint{3, 4}, std::string("\x06", 1)}}}; + auto m = s12f11_map_data_send_coord(c); + auto parsed = parse_s12f11(m); + REQUIRE(parsed.has_value()); + CHECK(parsed->mid == "WAFER-1"); + REQUIRE(parsed->entries.size() == 2); + CHECK(parsed->entries[0].xypos.x == 1); + CHECK(parsed->entries[1].xypos.y == 4); + CHECK(*ack_byte(s12f12_map_data_send_coord_ack(MapDataAck::FormatError)) == 1); +} + +TEST_CASE("S12F13/F14 + F15/F16 + F17/F18 request shapes round-trip") { + MapDataIdentifier id{"WAFER-1", 0x01}; + CHECK(parse_s12f13(s12f13_map_data_request_row(id))->mid == "WAFER-1"); + CHECK(parse_s12f15(s12f15_map_data_request_array(id))->idtyp == 0x01); + MapDataCoordRequest cr{"WAFER-1", 0x01, 0xFF}; + auto parsed_cr = parse_s12f17(s12f17_map_data_request_coord(cr)); + REQUIRE(parsed_cr.has_value()); + CHECK(parsed_cr->sdbin == 0xFF); +} + +TEST_CASE("S7F17 / S7F18 delete PP round-trip") { + auto m = s7f17_delete_pp_send({"R1", "R2", "R3"}); + auto parsed = parse_s7f17(m); + REQUIRE(parsed.has_value()); + REQUIRE(parsed->size() == 3); + CHECK((*parsed)[0] == "R1"); + CHECK((*parsed)[2] == "R3"); + + auto empty = parse_s7f17(s7f17_delete_pp_send({})); + REQUIRE(empty.has_value()); + CHECK(empty->empty()); + + CHECK(*ack_byte(s7f18_delete_pp_ack(ProcessProgramAck::Accept)) == 0); + CHECK(*ack_byte(s7f18_delete_pp_ack(ProcessProgramAck::PpidNotFound)) == 4); +} + TEST_CASE("S2F25 / S2F26 loopback diagnostic round-trip") { // Arbitrary binary payload — host sends, equipment echoes back. const std::string payload("\x00\x01\x02\xFE\xFF some text", 14);