// Minimal interop driver: connect, HSMS-select, S1F13, S1F1, S1F3, // separate. Exits 0 if every step succeeds, non-zero on the first // failure. Used by the interop harness to validate that our active // (host) HSMS implementation talks to a third-party passive equipment // — in particular, secsgem-py running as GemEquipmentHandler. #include #include #include #include #include #include #include #include "secsgem/endpoint.hpp" #include "secsgem/gem/messages.hpp" #include "secsgem/gem/messages_helpers.hpp" #include "secsgem/secs2/message.hpp" using namespace secsgem; namespace s2 = secsgem::secs2; namespace gem = secsgem::gem; 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; } int exit_code = 1; } // 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 = std::chrono::milliseconds(0); asio::io_context io; Client client(io, cfg); auto logfn = [](const std::string& m) { std::cout << "[probe] " << m << std::endl; }; client.on_log(logfn); // Bail out after 15s so a stuck handshake doesn't hang the test. asio::steady_timer deadline(io); deadline.expires_after(std::chrono::seconds(15)); deadline.async_wait([&](std::error_code ec) { if (!ec) { logfn("DEADLINE reached, aborting"); io.stop(); } }); client.on_connection([&](std::shared_ptr conn) { auto fail = [&io, logfn, conn](const char* where, std::error_code ec) { logfn(std::string("FAIL ") + where + ": " + ec.message()); conn->close(where); io.stop(); }; // The equipment will also send us its own S1F13 once SELECTED — we // need to reply with S1F14, otherwise the equipment's transaction // times out. Same for any unsolicited primary we don't care about. conn->set_message_handler( [logfn](const s2::Message& msg) -> std::optional { if (msg.stream == 1 && msg.function == 13) { logfn("<- equipment S1F13, replying S1F14"); return gem::s1f14_establish_comms_ack( gem::CommAck::Accept, {"INTEROP-PROBE", "0.0.1"}); } if (msg.reply_expected) return s2::Message(msg.stream, 0, false); return std::nullopt; }); // Drive the probe sequence only after SELECT completes, otherwise // our outbound S1F13 races the HSMS Select.req and the equipment // rejects it. conn->set_selected_handler([&, conn, logfn, fail]() { // Step 1: S1F13 establish communications. conn->send_request( gem::s1f13_establish_comms("INTEROP-PROBE", "0.0.1"), [&, conn, logfn, fail](std::error_code ec, const s2::Message& reply) { if (ec) { fail("S1F13", ec); return; } logfn("OK S1F13->S1F14"); // Step 2: S1F1 Are You There. conn->send_request( gem::s1f1_are_you_there(), [&, conn, logfn, fail](std::error_code ec2, const s2::Message& reply2) { if (ec2) { fail("S1F1", ec2); return; } logfn("OK S1F1->S1F2"); // Step 3: S1F3 with empty SVID list (= "all"). conn->send_request( gem::s1f3_selected_status_request({}), [&, conn, logfn, fail](std::error_code ec3, const s2::Message& reply3) { if (ec3) { fail("S1F3", ec3); return; } logfn(std::string("OK S1F3->S1F4 body=") + reply3.sml()); exit_code = 0; logfn("ALL PROBES PASSED"); conn->separate(); // Give separate a beat to flush. auto t = std::make_shared(io); t->expires_after(std::chrono::milliseconds(200)); t->async_wait([t, &io](std::error_code) { io.stop(); }); }); }); }); }); // set_selected_handler }); client.start(); io.run(); return exit_code; }