// Cross-validate our C++ secs_server against secs4java8 (Apache 2.0, // kenta-shimizu). Independent SECS/HSMS implementation by a different // author + language ecosystem = independent spec interpretation. If // secs4java8 and our C++ codec both parse each other's bytes cleanly, // spec-correctness has corroboration that secsgem-py alone can't give. // // Targets the wire surface the secsgem-py interop deliberately skipped: // GEM 300 (S3 E87, S14 E94, S16 E40) with full-body messages, S2F49 // enhanced commands, S5F13 exception recovery, S2F23 trace init. // // Build (via Dockerfile in this directory): // javac -cp /opt/secs4java8/Export.jar Secs4jHostHarness.java // java -cp /opt/secs4java8/Export.jar:. Secs4jHostHarness import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.shimizukenta.secs.SecsMessage; import com.shimizukenta.secs.hsms.HsmsConnectionMode; import com.shimizukenta.secs.hsmsss.HsmsSsCommunicator; import com.shimizukenta.secs.hsmsss.HsmsSsCommunicatorConfig; import com.shimizukenta.secs.secs2.Secs2; public class Secs4jHostHarness { static final AtomicInteger pass = new AtomicInteger(); static final AtomicInteger fail = new AtomicInteger(); static final List failures = new ArrayList<>(); static void check(String label, boolean ok, String detail) { String tag = ok ? "[OK ]" : "[FAIL]"; System.out.println(tag + " " + label + (detail.isEmpty() ? "" : " — " + detail)); if (ok) pass.incrementAndGet(); else { fail.incrementAndGet(); failures.add(label + ": " + detail); } } // Run a request and check the reply's S/F matches the expected pair. // We deliberately don't assert specific ACK values — secs4java8 and // our codec must agree on which S/F pair the equipment replies with, // and that the body parses; the specific ACK ("Carrier ID Unknown" // vs "Accept") depends on equipment state, which we don't pre-load. static void roundtrip(HsmsSsCommunicator comm, String label, int stream, int function, boolean w, Secs2 body, int expectedReplyStream, int expectedReplyFunction) { try { Optional reply = comm.send(stream, function, w, body); if (!reply.isPresent()) { check(label, false, "no reply (W=" + w + ")"); return; } int rs = reply.get().getStream(); int rf = reply.get().getFunction(); boolean ok = rs == expectedReplyStream && rf == expectedReplyFunction; check(label, ok, ok ? ("got S" + rs + "F" + rf) : ("expected S" + expectedReplyStream + "F" + expectedReplyFunction + " got S" + rs + "F" + rf)); } catch (Exception e) { check(label, false, "threw " + e.getClass().getSimpleName() + ": " + e.getMessage()); } } public static void main(String[] args) throws Exception { String host = args.length > 0 ? args[0] : "127.0.0.1"; int port = args.length > 1 ? Integer.parseInt(args[1]) : 5099; HsmsSsCommunicatorConfig config = new HsmsSsCommunicatorConfig(); config.socketAddress(new InetSocketAddress(host, port)); config.connectionMode(HsmsConnectionMode.ACTIVE); config.sessionId(0); config.isEquip(false); config.notLinktest(); config.timeout().t3(5.0F); config.timeout().t5(2.0F); config.timeout().t6(5.0F); config.timeout().t8(5.0F); config.gem().mdln("SECS4J"); config.gem().softrev("1.0"); System.out.println("connecting active to " + host + ":" + port); try (HsmsSsCommunicator comm = HsmsSsCommunicator.newInstance(config)) { CountDownLatch selected = new CountDownLatch(1); comm.addSecsCommunicatableStateChangeListener(s -> { if (s) selected.countDown(); }); comm.open(); if (!selected.await(15, TimeUnit.SECONDS)) { System.err.println("FAIL: never reached SELECTED"); System.exit(2); } System.out.println("HSMS SELECTED."); // -------- Basic round-trips (overlap with secsgem-py) -------- // S1F1 → S1F2 (Are You There) roundtrip(comm, "S1F1 → S1F2 (Are You There)", 1, 1, true, Secs2.empty(), 1, 2); // S1F13 → S1F14 (Establish Communications) // body: > roundtrip(comm, "S1F13 → S1F14 (Establish Comms)", 1, 13, true, Secs2.list(Secs2.ascii("SECS4J"), Secs2.ascii("1.0")), 1, 14); // S1F17 → S1F18 (Request Online) roundtrip(comm, "S1F17 → S1F18 (Online)", 1, 17, true, Secs2.empty(), 1, 18); // S1F19 → S1F20 (GEM Compliance) — empty body, list reply roundtrip(comm, "S1F19 → S1F20 (GEM Compliance)", 1, 19, true, Secs2.empty(), 1, 20); // S1F21 → S1F22 (DVID namelist) — empty list = all DVIDs roundtrip(comm, "S1F21 → S1F22 (DVID namelist)", 1, 21, true, Secs2.list(), 1, 22); // S1F23 → S1F24 (CEID namelist) — empty list = all CEIDs roundtrip(comm, "S1F23 → S1F24 (CEID namelist)", 1, 23, true, Secs2.list(), 1, 24); // S2F17 → S2F18 (Clock) — empty body roundtrip(comm, "S2F17 → S2F18 (Clock)", 2, 17, true, Secs2.empty(), 2, 18); // -------- GEM 300 streams: the gap secsgem-py couldn't fill ---- // S3F17 → S3F18 (E87 carrier action): // body: > roundtrip(comm, "S3F17 → S3F18 (E87 carrier action, full body)", 3, 17, true, Secs2.list( Secs2.uint4(1L), Secs2.ascii("ProceedWithCarrier"), Secs2.ascii("CAR-SECS4J-1"), Secs2.list()), 3, 18); // S3F19 → S3F20 (Slot map verify): // body: >> roundtrip(comm, "S3F19 → S3F20 (slot map verify)", 3, 19, true, Secs2.list( Secs2.ascii("CAR-SECS4J-1"), Secs2.list()), 3, 20); // S3F25 → S3F26 (Carrier transfer): // body: > roundtrip(comm, "S3F25 → S3F26 (carrier transfer)", 3, 25, true, Secs2.list( Secs2.ascii("CAR-SECS4J-1"), Secs2.uint1(1), Secs2.uint1(2)), 3, 26); // S3F27 → S3F28 (Cancel carrier): body: roundtrip(comm, "S3F27 → S3F28 (cancel carrier)", 3, 27, true, Secs2.ascii("CAR-SECS4J-1"), 3, 28); // S14F9 → S14F10 (E94 ControlJob create): // body: >> roundtrip(comm, "S14F9 → S14F10 (E94 CJ create, full body)", 14, 9, true, Secs2.list( Secs2.ascii("CJ-SECS4J-1"), Secs2.list(Secs2.ascii("PJ-SECS4J-1"))), 14, 10); // S16F5 → S16F6 (E40 PRJobCommand): // body: > roundtrip(comm, "S16F5 → S16F6 (E40 PJ command)", 16, 5, true, Secs2.list( Secs2.ascii("PJ-NONEXISTENT"), Secs2.ascii("PJABORT")), 16, 6); // S16F27 → S16F28 (E94 CJobCommand): // body: > roundtrip(comm, "S16F27 → S16F28 (E94 CJ command)", 16, 27, true, Secs2.list( Secs2.ascii("CJ-SECS4J-1"), Secs2.ascii("CJSTOP")), 16, 28); // S14F11 → S14F12 (E94 DeleteControlJob): body: roundtrip(comm, "S14F11 → S14F12 (E94 CJ delete)", 14, 11, true, Secs2.ascii("CJ-SECS4J-1"), 14, 12); // -------- Other streams secsgem-py interop skipped -------- // S2F49 → S2F50 (Enhanced remote command): // body: > roundtrip(comm, "S2F49 → S2F50 (enhanced remote command)", 2, 49, true, Secs2.list( Secs2.uint4(1L), Secs2.ascii("EQUIPMENT"), Secs2.ascii("STOP"), Secs2.list()), 2, 50); // S2F23 → S2F24 (Trace init): // body: > roundtrip(comm, "S2F23 → S2F24 (trace init)", 2, 23, true, Secs2.list( Secs2.uint4(8001L), Secs2.ascii("000001"), Secs2.uint4(0L), Secs2.uint4(1L), Secs2.list(Secs2.uint4(1L))), 2, 24); // S5F13 → S5F14 (Exception recover): // body: > roundtrip(comm, "S5F13 → S5F14 (exception recover)", 5, 13, true, Secs2.list(Secs2.uint4(999L), Secs2.ascii("NOP")), 5, 14); // S5F17 → S5F18 (Exception recover abort): body: roundtrip(comm, "S5F17 → S5F18 (exception recover abort)", 5, 17, true, Secs2.uint4(999L), 5, 18); // S1F15 → S1F16 (Request Offline) — cleanup roundtrip(comm, "S1F15 → S1F16 (offline)", 1, 15, true, Secs2.empty(), 1, 16); System.out.println(); System.out.println("=== secs4j interop summary ==="); System.out.println(" passed: " + pass.get()); System.out.println(" failed: " + fail.get()); if (!failures.isEmpty()) { System.out.println(); System.out.println("FAILURES:"); for (String s : failures) System.out.println(" - " + s); } } System.exit(fail.get() == 0 ? 0 : 1); } }