// 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.ConcurrentLinkedQueue; 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(); }); // Capture every equipment-initiated primary into a queue // so we can assert on unsolicited S6F11 / S5F1 / S16F9 // emissions after triggering events. ConcurrentLinkedQueue observed = new ConcurrentLinkedQueue<>(); comm.addSecsMessageReceiveListener(msg -> { observed.add(new int[]{msg.getStream(), msg.getFunction()}); // Reply to known equipment-initiated primaries so the // equipment's T3 doesn't fire on our watch. try { int s = msg.getStream(), f = msg.getFunction(); if (s == 5 && f == 1) { comm.send(msg, 5, 2, false, Secs2.binary((byte)0x00)); } else if (s == 6 && f == 11) { comm.send(msg, 6, 12, false, Secs2.binary((byte)0x00)); } else if (s == 16 && f == 9) { // W=0 PJ alert — no reply expected } } catch (Exception ignore) {} }); 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); // -------- Status data (E30 §6.13) ----------------------------- // S1F3 → S1F4 (SVID values) — empty list = all SVIDs roundtrip(comm, "S1F3 → S1F4 (SVID values, all)", 1, 3, true, Secs2.list(), 1, 4); // S1F11 → S1F12 (SVID namelist) — empty list = all SVIDs roundtrip(comm, "S1F11 → S1F12 (SVID namelist, all)", 1, 11, true, Secs2.list(), 1, 12); // -------- Equipment constants (E30 §6.16) --------------------- // S2F13 → S2F14 (EC values) — empty list = all ECs roundtrip(comm, "S2F13 → S2F14 (EC values, all)", 2, 13, true, Secs2.list(), 2, 14); // S2F29 → S2F30 (EC namelist) — empty list = all ECs roundtrip(comm, "S2F29 → S2F30 (EC namelist, all)", 2, 29, true, Secs2.list(), 2, 30); // S2F15 → S2F16 (set EC). ECID 10 = TimeFormat (U4), valid // range 0..1 per equipment.yaml. Set to 1 — round-trip // proves the (ecid, value) tuple body parses. // body: >> roundtrip(comm, "S2F15 → S2F16 (set EC TimeFormat=1)", 2, 15, true, Secs2.list( Secs2.list(Secs2.uint4(10L), Secs2.uint4(1L))), 2, 16); // -------- Dynamic event reports (E30 §6.6) -------------------- // Define RPTID 9001 covering SVIDs {1, 2}, link CEID 300 // (ProcessStarted) → RPTID 9001, enable CEID 300, then // query the bound report back via S6F15/F19/F21. // S2F33 → S2F34 (define report) roundtrip(comm, "S2F33 → S2F34 (define RPTID 9001 → SVID {1,2})", 2, 33, true, Secs2.list( Secs2.uint4(9000L), Secs2.list( Secs2.list( Secs2.uint4(9001L), Secs2.list(Secs2.uint4(1L), Secs2.uint4(2L))))), 2, 34); // S2F35 → S2F36 (link event report) roundtrip(comm, "S2F35 → S2F36 (link CEID 300 → RPTID 9001)", 2, 35, true, Secs2.list( Secs2.uint4(9000L), Secs2.list( Secs2.list( Secs2.uint4(300L), Secs2.list(Secs2.uint4(9001L))))), 2, 36); // S2F37 → S2F38 (enable event) roundtrip(comm, "S2F37 → S2F38 (enable CEID 300)", 2, 37, true, Secs2.list(Secs2.bool(true), Secs2.list(Secs2.uint4(300L))), 2, 38); // S6F15 → S6F16 (event report request) roundtrip(comm, "S6F15 → S6F16 (event report request CEID=300)", 6, 15, true, Secs2.uint4(300L), 6, 16); // S6F19 → S6F20 (individual report request) roundtrip(comm, "S6F19 → S6F20 (individual report RPTID=9001)", 6, 19, true, Secs2.uint4(9001L), 6, 20); // S6F21 → S6F22 (annotated individual report) roundtrip(comm, "S6F21 → S6F22 (annotated report RPTID=9001)", 6, 21, true, Secs2.uint4(9001L), 6, 22); // -------- Remote control (E30 §6.15) -------------------------- // S2F41 → S2F42 (modern host command), DATAID-less form: // body: >>> // The equipment.yaml binds START → emit_ceid=300, so this // also triggers an unsolicited S6F11 we observe below. roundtrip(comm, "S2F41 → S2F42 (RCMD=START)", 2, 41, true, Secs2.list(Secs2.ascii("START"), Secs2.list()), 2, 42); // Give the equipment 400ms to emit the linked S6F11 to us. Thread.sleep(400); boolean sawS6F11 = observed.stream() .anyMatch(p -> p[0] == 6 && p[1] == 11); check("S6F11 received after RCMD=START (unsolicited)", sawS6F11, sawS6F11 ? "observed" : "no S6F11 in 400ms"); // S2F21 → S2F22 (legacy remote command, no params) roundtrip(comm, "S2F21 → S2F22 (legacy RCMD=STOP)", 2, 21, true, Secs2.ascii("STOP"), 2, 22); // -------- Alarm management (E30 §6.14) ------------------------ // S5F5 → S5F6 (list alarms) — empty list = all roundtrip(comm, "S5F5 → S5F6 (list alarms, all)", 5, 5, true, Secs2.list(), 5, 6); // S5F7 → S5F8 (list enabled) roundtrip(comm, "S5F7 → S5F8 (list enabled alarms)", 5, 7, true, Secs2.empty(), 5, 8); // S5F3 → S5F4 (enable alarm 1). ALED bit-7 set = enable. // body: > roundtrip(comm, "S5F3 → S5F4 (enable ALID=1)", 5, 3, true, Secs2.list(Secs2.binary((byte)0x80), Secs2.uint4(1L)), 5, 4); // Trigger FAULT host command — equipment.yaml binds it to // alarm 1 → emits S5F1. roundtrip(comm, "S2F41 → S2F42 (RCMD=FAULT)", 2, 41, true, Secs2.list(Secs2.ascii("FAULT"), Secs2.list()), 2, 42); Thread.sleep(400); boolean sawS5F1 = observed.stream() .anyMatch(p -> p[0] == 5 && p[1] == 1); check("S5F1 received after RCMD=FAULT (unsolicited)", sawS5F1, sawS5F1 ? "observed" : "no S5F1 in 400ms"); // -------- Spool (E30 §6.22) ----------------------------------- // S2F43 → S2F44 (reset spoolable streams) // body: > roundtrip(comm, "S2F43 → S2F44 (reset spool to {5, 6})", 2, 43, true, Secs2.list(Secs2.uint1(5), Secs2.uint1(6)), 2, 44); // S6F23 → S6F24 (request spool data — Transmit=0) // body: roundtrip(comm, "S6F23 → S6F24 (request spool transmit)", 6, 23, true, Secs2.binary((byte)0x00), 6, 24); // -------- Process program management (E30 §6.17) -------------- // S7F19 → S7F20 (current PPID list) roundtrip(comm, "S7F19 → S7F20 (current PPIDs)", 7, 19, true, Secs2.empty(), 7, 20); // S7F5 → S7F6 (request PP RECIPE-A) roundtrip(comm, "S7F5 → S7F6 (request RECIPE-A)", 7, 5, true, Secs2.ascii("RECIPE-A"), 7, 6); // S7F1 → S7F2 (PP load inquire) // body: > roundtrip(comm, "S7F1 → S7F2 (PP load inquire RECIPE-NEW)", 7, 1, true, Secs2.list(Secs2.ascii("RECIPE-NEW"), Secs2.uint4(128L)), 7, 2); // S7F3 → S7F4 (PP send) // body: > roundtrip(comm, "S7F3 → S7F4 (send PP RECIPE-J)", 7, 3, true, Secs2.list(Secs2.ascii("RECIPE-J"), Secs2.binary("from-java".getBytes())), 7, 4); // S7F17 → S7F18 (delete PP) // body: > roundtrip(comm, "S7F17 → S7F18 (delete RECIPE-J)", 7, 17, true, Secs2.list(Secs2.ascii("RECIPE-J")), 7, 18); // -------- Limits monitoring (E30 §6.21) ----------------------- // S2F47 → S2F48 (variable limit attributes) — empty list = all roundtrip(comm, "S2F47 → S2F48 (variable limit attributes)", 2, 47, true, Secs2.list(), 2, 48); // S2F45 → S2F46 (define variable limits). Body: // >>>> // Define an empty limits set on VID 1 — proves the body parses. roundtrip(comm, "S2F45 → S2F46 (define variable limits, empty set)", 2, 45, true, Secs2.list( Secs2.list(Secs2.uint4(1L), Secs2.list())), 2, 46); // -------- Trace (E30 §6.12) ----------------------------------- // body: // >> roundtrip(comm, "S2F23 → S2F24 (trace init, 5-field body)", 2, 23, true, Secs2.list( Secs2.uint4(8002L), Secs2.ascii("000002"), Secs2.uint4(0L), Secs2.uint4(2L), Secs2.list(Secs2.uint4(1L), Secs2.uint4(2L))), 2, 24); // -------- Terminal services (E30 §6.19) ----------------------- // S10F3 → S10F4 (terminal display single, host→equipment) // body: > roundtrip(comm, "S10F3 → S10F4 (terminal display single)", 10, 3, true, Secs2.list(Secs2.binary((byte)0x00), Secs2.ascii("hello from secs4j")), 10, 4); // S10F5 → S10F6 (terminal display multi-line) // body: >> roundtrip(comm, "S10F5 → S10F6 (terminal multi-line)", 10, 5, true, Secs2.list( Secs2.binary((byte)0x00), Secs2.list(Secs2.ascii("line A"), Secs2.ascii("line B"))), 10, 6); // -------- E40 process jobs with full body -------------------- // S16F11 → S16F12 (PRJobCreate, full E40 body). This is // the body that defeated secsgem-py's SFDL grammar. // body: // // // >>> // > // >>> roundtrip(comm, "S16F11 → S16F12 (E40 PJ create, full body)", 16, 11, true, Secs2.list( Secs2.ascii("PJ-SECS4J-2"), Secs2.binary((byte)0x01), // MF = Substrate Secs2.binary((byte)0x02), // RM = RecipeOnly Secs2.list( Secs2.ascii("RECIPE-A"), Secs2.list()), // no rcpvars Secs2.list(Secs2.ascii("WFR-J-1")), Secs2.list()), // no params 16, 12); // S16F7 → S16F8 (PRJobMonitor) // body: >> roundtrip(comm, "S16F7 → S16F8 (PJ monitor)", 16, 7, true, Secs2.list( Secs2.list(Secs2.ascii("PJ-SECS4J-2"), Secs2.binary((byte)0x80))), 16, 8); // S16F13 → S16F14 (PRJobDequeue) // body: roundtrip(comm, "S16F13 → S16F14 (PJ dequeue)", 16, 13, true, Secs2.ascii("PJ-SECS4J-2"), 16, 14); // -------- E39 generic object service -------------------------- // S14F1 → S14F2 (GetAttr) — Denied_UnknownObject is a // well-formed F-coded reply for an unknown OBJSPEC. // body: >> roundtrip(comm, "S14F1 → S14F2 (E39 GetAttr unknown object)", 14, 1, true, Secs2.list( Secs2.ascii("OBJ-NONEXISTENT"), Secs2.ascii("EQUIPMENT"), Secs2.list()), 14, 2); // -------- 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); } }