// secs_conformance — runnable conformance harness. // // Drives a passive GEM-300 equipment through a fixed sequence of host-initiated // checks that exercise every claimed E5/E30/E37 + GEM 300 capability, // reports per-check pass/fail, and exits 0 if every check passed. Point // it at any HSMS-SS equipment to validate end-to-end conformance from // outside the codebase. // // Run: secs_conformance --host --port 5000 --device 0 // // The check list deliberately mirrors docs/COMPLIANCE.md so that anything // the audit claims as ✅ has a runnable assertion behind it here. The // pass criterion is the spec-mandated reply *function code* — we don't // require any particular ACK value, since the harness has no equipment // state to predict. CarrierIDUnknown / Denied_UnknownObject / etc. are // well-formed F-coded replies and count as protocol-conformant. // // One observation step waits for an unsolicited S6F11 after issuing an // RCMD that the demo equipment binds to a CEID emission. #include #include #include #include #include #include #include #include #include #include #include #include "secsgem/endpoint.hpp" #include "secsgem/gem/messages.hpp" #include "secsgem/gem/messages_helpers.hpp" #include "secsgem/hsms/connection.hpp" #include "secsgem/secs2/codec.hpp" #include "secsgem/secs2/message.hpp" using namespace secsgem; namespace s2 = secsgem::secs2; namespace gem = secsgem::gem; using namespace std::chrono_literals; namespace { std::string arg(int argc, char** argv, const std::string& key, const std::string& def) { for (int i = 1; i + 1 < argc; ++i) if (key == argv[i]) return argv[i + 1]; return def; } struct Check { std::string name; bool passed = false; std::string detail; }; void log(const std::string& m) { std::cout << "[conformance] " << m << std::endl; } // Shared harness state. Built on the io_context heap so async steps // can capture by raw pointer without lifetime juggling. struct Harness { std::vector checks; // Every unsolicited primary the equipment sends us, in arrival order. std::vector> observed; void record(const std::string& name, bool ok, const std::string& detail = "") { checks.push_back({name, ok, detail}); log((ok ? "PASS " : "FAIL ") + name + (detail.empty() ? "" : " — " + detail)); } }; // One sequential step: send `req`, expect a reply with (expected_stream, // expected_function), record pass/fail, advance. void request_check(asio::io_context& io, std::shared_ptr conn, Harness* h, const std::string& name, s2::Message req, uint8_t expected_stream, uint8_t expected_function, std::function next) { conn->send_request(std::move(req), [h, name, expected_stream, expected_function, next, &io] (std::error_code ec, const s2::Message& m) { if (ec) { h->record(name, false, "no reply / " + ec.message()); } else { const bool ok = m.stream == expected_stream && m.function == expected_function; h->record(name, ok, ok ? "" : "got S" + std::to_string(m.stream) + "F" + std::to_string(m.function)); } asio::post(io, next); }); } // Wait `dt` then run `next` on the io_context. void pause_then(asio::io_context& io, std::chrono::milliseconds dt, std::function next) { auto t = std::make_shared(io); t->expires_after(dt); t->async_wait([t, next](std::error_code) { next(); }); } // Sequential runner that walks a vector of steps to completion. struct Sequence : std::enable_shared_from_this { using Step = std::function)>; std::vector steps; std::size_t i = 0; std::function finish; void run() { if (i >= steps.size()) { if (finish) finish(); return; } auto self = shared_from_this(); steps[i]([self] { ++self->i; self->run(); }); } }; } // namespace int main(int argc, char** argv) { Client::Config cfg; cfg.host = arg(argc, argv, "--host", "127.0.0.1"); cfg.port = static_cast(std::stoi(arg(argc, argv, "--port", "5000"))); cfg.device_id = static_cast(std::stoi(arg(argc, argv, "--device", "0"))); cfg.timers.linktest = 0ms; cfg.timers.t3 = 3s; asio::io_context io; Client client(io, cfg); client.on_log([](const std::string& m) { log("hsms: " + m); }); auto h = std::make_shared(); asio::steady_timer deadline(io); deadline.expires_after(5min); deadline.async_wait([&](std::error_code ec) { if (!ec) { log("global 5min deadline reached, halting"); io.stop(); } }); client.on_connection([&io, h](std::shared_ptr conn) { // Capture every equipment-initiated primary into the mailbox and // reply to the ones with W=1 so the equipment's T3 doesn't fire on // our watch. S16F9 (E40 PRJobAlert) is W=0 — no reply. conn->set_message_handler( [h](const s2::Message& msg) -> std::optional { h->observed.push_back({msg.stream, msg.function}); if (msg.stream == 1 && msg.function == 13) { return gem::s1f14_establish_comms_ack( gem::CommAck::Accept, {"CONFORMANCE", "1.0"}); } if (msg.stream == 5 && msg.function == 1) { return gem::s5f2_alarm_ack(gem::AlarmAck::Accept); } if (msg.stream == 6 && msg.function == 11) { return gem::s6f12_event_report_ack(gem::EventReportAck::Accept); } if (msg.stream == 6 && msg.function == 25) { return gem::s6f26_spool_data_ready_ack(gem::EventReportAck::Accept); } if (msg.stream == 16 && msg.function == 9) { return std::nullopt; // W=0 primary } if (msg.reply_expected) return s2::Message(msg.stream, 0, false); return std::nullopt; }); conn->set_selected_handler([&io, h, conn] { h->record("E37 §7.2 SELECT handshake", true, "selected"); auto seq = std::make_shared(); // Helper closure that pushes a request/reply step. auto add_req = [&io, h, conn, seq](const std::string& name, s2::Message req, uint8_t es, uint8_t ef) { seq->steps.push_back([&io, h, conn, name, req = std::move(req), es, ef] (std::function next) mutable { request_check(io, conn, h.get(), name, std::move(req), es, ef, std::move(next)); }); }; // === E30 §6.5 / §6.7 — establish comms + identification === add_req("E30 §6.5 S1F13/F14 Establish Comms", gem::s1f13_establish_comms("HOST", "1.0"), 1, 14); add_req("E30 §6.7 S1F1/F2 Are You There", s2::Message(1, 1, true), 1, 2); // === E30 §6.13 — status data collection === add_req("E30 §6.13 S1F11/F12 SVID Namelist", gem::s1f11_status_namelist_request({}), 1, 12); add_req("E30 §6.13 S1F3/F4 SVID Values", gem::s1f3_selected_status_request({}), 1, 4); // === E30 §6.11 / §6.10 — DVID + CEID namelists === add_req("E30 §6.11 S1F21/F22 DVID Namelist", gem::s1f21_data_variable_namelist_request({}), 1, 22); add_req("E30 §6.10 S1F23/F24 CEID Namelist", gem::s1f23_collection_event_namelist_request({}), 1, 24); // === E30 §6.16 — equipment constants === add_req("E30 §6.16 S2F29/F30 ECID Namelist", gem::s2f29_ec_namelist_request({}), 2, 30); add_req("E30 §6.16 S2F13/F14 EC Values", gem::s2f13_ec_request({}), 2, 14); // === E30 §6.20 — clock === add_req("E30 §6.20 S2F17/F18 Clock", s2::Message(2, 17, true), 2, 18); // === E30 §6.6 — dynamic event reports === constexpr uint32_t kDataId = 9001; constexpr uint32_t kRptid = 9000; constexpr uint32_t kCeidStart = 300; // ProcessStarted, from equipment.yaml add_req("E30 §6.6 S2F33/F34 Define Report", gem::s2f33_define_report(kDataId, {{kRptid, {1}}}), 2, 34); add_req("E30 §6.6 S2F35/F36 Link Event Report", gem::s2f35_link_event_report(kDataId, {{kCeidStart, {kRptid}}}), 2, 36); add_req("E30 §6.6 S2F37/F38 Enable Event", gem::s2f37_enable_event(true, {kCeidStart}), 2, 38); // === E30 §6.15 — remote control === add_req("E30 §6.15 S2F41/F42 Host Command", gem::s2f41_host_command("START", {}), 2, 42); // Observe the unsolicited S6F11 that START triggers. Pause 400ms // to give the equipment's async post a chance to land, then walk // the mailbox. seq->steps.push_back([&io, h](std::function next) { pause_then(io, 400ms, [h, next] { const bool seen = std::any_of( h->observed.begin(), h->observed.end(), [](const auto& p) { return p.first == 6 && p.second == 11; }); h->record("E30 §6.6 S6F11 Unsolicited Event Report (after START)", seen, seen ? "" : "no S6F11 observed within 400ms"); next(); }); }); add_req("E30 §6.15 S2F21/F22 Legacy Remote Command", gem::s2f21_remote_command("STOP"), 2, 22); add_req("E30 §6.15 S2F49/F50 Enhanced Remote Command", gem::s2f49_enhanced_host_command(0u, "EQUIPMENT", "NOP", {}), 2, 50); // === E30 §6.12 — trace data collection === add_req("E30 §6.12 S2F23/F24 Trace Initialize", gem::s2f23_trace_initialize_send(8001, "000001", 0, 1, {1}), 2, 24); // === E30 §6.21 — limits monitoring (read-only) === add_req("E30 §6.21 S2F47/F48 Limit Attributes Request", gem::s2f47_variable_limit_attribute_request({}), 2, 48); // === E30 §6.22 — spooling === add_req("E30 §6.22 S2F43/F44 Reset Spooling", gem::s2f43_reset_spooling({5, 6}), 2, 44); add_req("E30 §6.22 S6F23/F24 Request Spool Data", gem::s6f23_request_spool_data(gem::SpoolRequestCode::Transmit), 6, 24); // === E30 §6.14 — alarm management === add_req("E30 §6.14 S5F5/F6 List Alarms", gem::s5f5_list_alarms_request({}), 5, 6); add_req("E30 §6.14 S5F7/F8 List Enabled Alarms", s2::Message(5, 7, true), 5, 8); add_req("E30 §6.14 S5F3/F4 Enable Alarm", gem::s5f3_enable_alarm(gem::kAlarmEnableByte, 1), 5, 4); // === E5 §13 — exception recovery (well-formed reply even if EXID // unknown) === add_req("E5 §13 S5F13/F14 Exception Recover Request", gem::s5f13_exception_recover_request(999, "NOP"), 5, 14); add_req("E5 §13 S5F17/F18 Exception Recover Abort", gem::s5f17_exception_recover_abort_request(999), 5, 18); // === E30 §6.6 — host-initiated event/report read-back === add_req("E30 §6.6 S6F15/F16 Event Report Request", gem::s6f15_event_report_request(kCeidStart), 6, 16); add_req("E30 §6.6 S6F19/F20 Individual Report Request", gem::s6f19_individual_report_request(kRptid), 6, 20); add_req("E30 §6.6 S6F21/F22 Annotated Report Request", gem::s6f21_annotated_report_request(kRptid), 6, 22); // === E30 §6.17 — process program management === add_req("E30 §6.17 S7F19/F20 PP List", s2::Message(7, 19, true), 7, 20); add_req("E30 §6.17 S7F5/F6 PP Request", gem::s7f5_process_program_request("RECIPE-A"), 7, 6); add_req("E30 §6.17 S7F1/F2 PP Load Inquire", gem::s7f1_pp_load_inquire("RECIPE-X", 64u), 7, 2); // === E30 §6.19 — equipment terminal services === add_req("E30 §6.19 S10F3/F4 Terminal Display Single", gem::s10f3_terminal_display_single(0, "conformance probe"), 10, 4); add_req("E30 §6.19 S10F5/F6 Terminal Display Multi", gem::s10f5_terminal_display_multi(0, {"line 1", "line 2"}), 10, 6); // === E40 — process jobs === const std::string kConfPj = "PJ-CONF-1"; gem::PRJobCreateRequest pj_req{ kConfPj, gem::MaterialFlag::Substrate, gem::ProcessRecipeMethod::RecipeOnly, gem::RecipeSpec{"RECIPE-A", {}}, {"WFR-C-1"}, {}}; add_req("E40 S16F11/F12 PRJobCreate", gem::s16f11_pr_job_create(pj_req), 16, 12); add_req("E40 S16F7/F8 PRJobMonitor", gem::s16f7_pr_job_monitor({{kConfPj, gem::kAlarmEnableByte}}), 16, 8); // === E94 — control jobs === const std::string kConfCj = "CJ-CONF-1"; add_req("E94 S14F9/F10 CreateControlJob", gem::s14f9_create_control_job(kConfCj, {kConfPj}), 14, 10); add_req("E94 S16F27/F28 CJobCommand (CJSTOP)", gem::s16f27_cj_command(kConfCj, "CJSTOP"), 16, 28); add_req("E94 S14F11/F12 DeleteControlJob", gem::s14f11_delete_control_job(kConfCj), 14, 12); add_req("E40 S16F5/F6 PRJobCommand (PJABORT)", gem::s16f5_pr_job_command(kConfPj, "PJABORT"), 16, 6); add_req("E40 S16F13/F14 PRJobDequeue", gem::s16f13_pr_job_dequeue(kConfPj), 16, 14); // === E87 — carrier management (well-formed reply even when the // carrier id is unknown to the equipment) === add_req("E87 S3F17/F18 CarrierAction", gem::s3f17_carrier_action(0u, "ProceedWithCarrier", "CAR-CONF-1", {}), 3, 18); add_req("E87 S3F19/F20 Slot Map Verify", gem::s3f19_slot_map_verify("CAR-CONF-1", {}), 3, 20); add_req("E87 S3F25/F26 Carrier Transfer", gem::s3f25_carrier_transfer("CAR-CONF-1", 1, 2), 3, 26); add_req("E87 S3F27/F28 Cancel Carrier", gem::s3f27_cancel_carrier("CAR-CONF-1"), 3, 28); // === E39 — generic object service (Denied_UnknownObject is a // well-formed F-coded reply) === add_req("E39 S14F1/F2 GetAttr", gem::s14f1_get_attr("CONF-OBJ", "EQUIPMENT", {}), 14, 2); // === E30 §6.10 — documentation (last so failures earlier are // visible against the equipment's self-report) === add_req("E30 §6.10 S1F19/F20 GEM Compliance", gem::s1f19_get_gem_compliance_request(), 1, 20); // Cleanup + summary. seq->finish = [&io, h, conn] { log("all checks done; separating"); conn->separate(); asio::post(io, [&io, h] { std::cout << "\n========================================\n"; std::cout << " conformance summary\n"; std::cout << "========================================\n"; int passed = 0; for (const auto& c : h->checks) { std::cout << (c.passed ? " [PASS] " : " [FAIL] ") << c.name; if (!c.detail.empty()) std::cout << " — " << c.detail; std::cout << "\n"; if (c.passed) ++passed; } std::cout << "\n " << passed << " / " << h->checks.size() << " checks passed\n"; io.stop(); }); }; seq->run(); }); }); client.start(); try { io.run(); } catch (const std::exception& e) { std::cerr << "conformance: " << e.what() << std::endl; return 2; } for (const auto& c : h->checks) if (!c.passed) return 1; return h->checks.empty() ? 3 : 0; }