interop: secsgem-py cross-validation harness + lenient identifier parsing
Adds a Docker-based interop harness that drives the C++ server with
secsgem-py 0.3.0 as the active host and probes a secsgem-py-passive
equipment from a minimal C++ active client. Surfaces and fixes four
interoperability bugs uncovered by cross-testing:
* SEMI E5 identifier formatcodes are a U1|U2|U4|U8 wildcard;
secsgem-py picks the narrowest fitting width while our parsers
only accepted U4. `as_uN_scalar` / `as_iN_scalar` now accept
any unsigned/signed width and range-check the downcast.
* PPBODY (S7F3/F6) is "ASCII | Binary | List" per the spec;
secsgem-py defaults to ASCII. Added BINARY_OR_ASCII codegen
item type with `as_text_or_binary` accessor.
* S1F23/F24 Collection Event Namelist was unimplemented; added
schema + `vids_for(ceid)` accessor on EventReportSubscriptions
plus the dispatch handler.
* S10F1 was registered as a host->equipment handler, but per
SEMI E5 §12 S10F1 is equipment->host; S10F3 is the actual
host->equipment Terminal Display Single. Added an S10F3
handler alongside (we keep S10F1 too for backward compat).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -450,6 +450,28 @@ int main(int argc, char** argv) {
|
||||
logfn("S1F21 -> S1F22 (" + std::to_string(rows.size()) + " DVIDs)");
|
||||
return gem::s1f22_data_variable_namelist_data(rows);
|
||||
});
|
||||
// S1F23 — Collection Event Namelist Request. Empty CEID list means
|
||||
// "every CEID in the catalog"; otherwise we filter to the requested
|
||||
// set and silently skip unknown CEIDs (per SEMI E5: "all unidentified
|
||||
// are returned in S1F24 with an empty VID list").
|
||||
router.on(1, 23, [model, logfn](const s2::Message& msg) {
|
||||
auto req = gem::parse_s1f23(msg);
|
||||
std::vector<gem::CollectionEventName> rows;
|
||||
if (req && req->empty()) {
|
||||
for (const auto& e : model->events.all_events())
|
||||
rows.push_back({e.id, e.name, model->events.vids_for(e.id)});
|
||||
} else if (req) {
|
||||
for (auto id : *req) {
|
||||
if (auto info = model->events.event_info(id)) {
|
||||
rows.push_back({id, info->name, model->events.vids_for(id)});
|
||||
} else {
|
||||
rows.push_back({id, "", {}});
|
||||
}
|
||||
}
|
||||
}
|
||||
logfn("S1F23 -> S1F24 (" + std::to_string(rows.size()) + " CEIDs)");
|
||||
return gem::s1f24_collection_event_namelist_data(rows);
|
||||
});
|
||||
|
||||
router.on(2, 13, [model, logfn](const s2::Message& msg) -> std::optional<s2::Message> {
|
||||
auto ids = gem::parse_u4_list_body(msg);
|
||||
@@ -1005,6 +1027,14 @@ int main(int argc, char** argv) {
|
||||
if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text);
|
||||
return gem::s10f2_terminal_display_ack(gem::TerminalAck::Accepted);
|
||||
});
|
||||
// S10F3 is the canonical SEMI E5 host→equipment "Terminal Display Single"
|
||||
// (S10F1 is documented in the spec as equipment→host); secsgem-py and
|
||||
// other reference libraries use F3. We accept both for compatibility.
|
||||
router.on(10, 3, [logfn](const s2::Message& msg) {
|
||||
auto td = gem::parse_s10f3(msg);
|
||||
if (td) logfn("TERMINAL[" + std::to_string(td->tid) + "] " + td->text);
|
||||
return gem::s10f4_terminal_display_ack(gem::TerminalAck::Accepted);
|
||||
});
|
||||
router.on(10, 5, [logfn](const s2::Message& msg) {
|
||||
auto td = gem::parse_s10f5(msg);
|
||||
if (td) {
|
||||
|
||||
Reference in New Issue
Block a user