AA: catalog growth — S7F1/F17, S6F5-F22, S2F21, S12F9-F18
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<int>(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<s2::Item> 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<gem::AnnotatedValue> 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<int>(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<int>(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("", "");
|
||||
|
||||
Reference in New Issue
Block a user