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, {});
|
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
|
// S2F49 — Enhanced Remote Command. OBJSPEC scopes the command at a
|
||||||
// specific object instance (e.g. a CJ or PJ id); for now we delegate
|
// 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
|
// 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);
|
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) {
|
router.on(6, 23, [&io, active_conn, model, logfn](const s2::Message& msg) {
|
||||||
auto rsdc = gem::parse_s6f23(msg);
|
auto rsdc = gem::parse_s6f23(msg);
|
||||||
if (!rsdc) return gem::s6f24_request_spool_data_ack(gem::SpoolRequestAck::Denied);
|
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);
|
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) {
|
router.on(7, 3, [model, logfn](const s2::Message& msg) {
|
||||||
auto pp = gem::parse_s7f3(msg);
|
auto pp = gem::parse_s7f3(msg);
|
||||||
if (!pp) return gem::s7f4_process_program_ack(gem::ProcessProgramAck::LengthError);
|
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)");
|
logfn("S7F3 PPID=" + pp->ppid + " -> S7F4 (Accept)");
|
||||||
return gem::s7f4_process_program_ack(gem::ProcessProgramAck::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) {
|
router.on(7, 5, [model, logfn](const s2::Message& msg) {
|
||||||
auto ppid = gem::parse_s7f5(msg);
|
auto ppid = gem::parse_s7f5(msg);
|
||||||
if (!ppid) return gem::s7f6_process_program_data("", "");
|
if (!ppid) return gem::s7f6_process_program_data("", "");
|
||||||
|
|||||||
@@ -520,6 +520,26 @@ messages:
|
|||||||
# values; every REPGSZ samples it emits an S6F1 batch; after TOTSMP
|
# values; every REPGSZ samples it emits an S6F1 batch; after TOTSMP
|
||||||
# total samples it stops. Periodic execution is the application's
|
# total samples it stops. Periodic execution is the application's
|
||||||
# responsibility; this codebase only stores the request.
|
# 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
|
- id: S2F23
|
||||||
stream: 2
|
stream: 2
|
||||||
function: 23
|
function: 23
|
||||||
@@ -905,6 +925,151 @@ messages:
|
|||||||
builder: s6f12_event_report_ack
|
builder: s6f12_event_report_ack
|
||||||
body: {kind: scalar, item_type: BINARY_BYTE, enum: EventReportAck, param: 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).
|
# S6F23 / S6F24 — Request Spooled Data (E30 §6.22; host transmits or purges).
|
||||||
- id: S6F23
|
- id: S6F23
|
||||||
stream: 6
|
stream: 6
|
||||||
@@ -941,6 +1106,29 @@ messages:
|
|||||||
# S7 — Process programs
|
# 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
|
- id: S7F3
|
||||||
stream: 7
|
stream: 7
|
||||||
function: 3
|
function: 3
|
||||||
@@ -996,6 +1184,26 @@ messages:
|
|||||||
element: {kind: scalar, item_type: ASCII}
|
element: {kind: scalar, item_type: ASCII}
|
||||||
name: ppids
|
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
|
# S9 — Protocol error notifications (E5). All carry the 10-byte
|
||||||
# offending message header (MHEAD) as a single BINARY item, except
|
# offending message header (MHEAD) as a single BINARY item, except
|
||||||
@@ -1264,6 +1472,201 @@ messages:
|
|||||||
builder: s12f8_map_data_send_row_ack
|
builder: s12f8_map_data_send_row_ack
|
||||||
body: {kind: scalar, item_type: BINARY_BYTE, enum: MapDataAck, param: maper}
|
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
|
# S12F19 — Map Error Send. Equipment reports a non-recoverable map
|
||||||
# error (e.g. format mismatch, oversize payload). One-way (no reply).
|
# error (e.g. format mismatch, oversize payload). One-way (no reply).
|
||||||
- id: S12F19
|
- id: S12F19
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
@@ -233,6 +232,17 @@ enum class EventReportAck : uint8_t { // S6F12
|
|||||||
Denied = 1,
|
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
|
enum class TerminalAck : uint8_t { // S10F2, S10F4
|
||||||
Accepted = 0,
|
Accepted = 0,
|
||||||
WillNotDisplay = 1,
|
WillNotDisplay = 1,
|
||||||
|
|||||||
@@ -484,6 +484,139 @@ TEST_CASE("S7F3 process-program send round-trip") {
|
|||||||
CHECK(parsed->ppbody == "step1\nstep2\n");
|
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<AnnotatedValue> 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") {
|
TEST_CASE("S2F25 / S2F26 loopback diagnostic round-trip") {
|
||||||
// Arbitrary binary payload — host sends, equipment echoes back.
|
// Arbitrary binary payload — host sends, equipment echoes back.
|
||||||
const std::string payload("\x00\x01\x02\xFE\xFF some text", 14);
|
const std::string payload("\x00\x01\x02\xFE\xFF some text", 14);
|
||||||
|
|||||||
Reference in New Issue
Block a user