db90a21e1d
Every check the user could ask for now lands. secs4j's
comm.send(stream, function, w, body) takes arbitrary S/F + arbitrary
Secs2 body, so coverage was never coverage-limited by the Java side
— the original 20 was just the minimum to fill the gaps secsgem-py
couldn't reach.
Adds:
- Status data: S1F3, S1F11
- EC management: S2F13, S2F15 (set TimeFormat), S2F29
- Event reports: S2F33, S2F35, S2F37 (full define-link-enable
sequence), S6F15, S6F19, S6F21
- Remote control: S2F41 (modern RCMD=START + observed S6F11),
S2F21 (legacy RCMD=STOP),
S2F41 RCMD=FAULT + observed S5F1
- Alarms: S5F3, S5F5, S5F7
- Spool: S2F43, S6F23
- PP management: S7F1, S7F3, S7F5, S7F17, S7F19
- Terminal: S10F3 (single), S10F5 (multi-line)
- E40 PJ: S16F11 (full E40 body — MF + PRRECIPEMETHOD +
RecipeSpec + mtrloutspec + processparams),
S16F7 (monitor), S16F13 (dequeue)
- Limits: S2F45, S2F47
- Trace: S2F23 (5-field body)
- E39: S14F1 (GetAttr)
Plus a SecsMessageReceiveListener that captures every equipment-
initiated primary into a ConcurrentLinkedQueue and replies to S5F1
(ACKC5=0), S6F11 (ACKC6=0), S16F9 (W=0 no reply) so the
equipment's T3 doesn't fire on our watch. Two checks now assert
the unsolicited path:
- After RCMD=START, an S6F11 with the linked report must arrive
within 400ms
- After RCMD=FAULT, an S5F1 with the alarm must arrive within
400ms
Both observed against the demo equipment.
Result: 55/55 PASS. Two independent implementations
(secsgem-py + secs4java8) now corroborate the wire surface in
overlapping but distinct slices. Full E40 body — the one that
defeated secsgem-py's SFDL grammar — round-trips cleanly through
secs4j.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
543 lines
25 KiB
Java
543 lines
25 KiB
Java
// 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 <host> <port>
|
|
|
|
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<String> 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<SecsMessage> 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<int[]> 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: <L,2 <A MDLN> <A SOFTREV>>
|
|
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: <L,n <L,2 <U4 ECID> <ITEM VALUE>>>
|
|
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: <L,2 <A RCMD> <L,n <L,2 <A name> <ITEM value>>>>
|
|
// 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: <L,2 <BINARY ALED> <U4 ALID>>
|
|
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: <L,n <U1 stream>>
|
|
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: <BINARY RSDC>
|
|
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: <L,2 <A PPID> <U4 LENGTH>>
|
|
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: <L,2 <A PPID> <BINARY|ASCII PPBODY>>
|
|
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: <L,n <A PPID>>
|
|
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:
|
|
// <L,n <L,2 <U4 VID> <L,n <L,3 <U4 LIMITID> <ITEM upper> <ITEM lower>>>>>
|
|
// 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: <L,5 <U4 TRID> <A DSPER> <U4 TOTSMP> <U4 REPGSZ>
|
|
// <L,n <U4 vid>>>
|
|
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: <L,2 <BINARY TID> <A TEXT>>
|
|
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: <L,2 <BINARY TID> <L,n <A line>>>
|
|
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: <L,5 <A PRJOBID>
|
|
// <BINARY MF>
|
|
// <BINARY PRRECIPEMETHOD>
|
|
// <L,2 <A PPID> <L,n <L,2 <A RCPPARNM> <ITEM RCPPARVAL>>>>
|
|
// <L,n <A MID>>
|
|
// <L,n <L,2 <A PARAMNAME> <ITEM PARAMVAL>>>>
|
|
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: <L,n <L,2 <A PRJOBID> <BINARY PRALERT>>>
|
|
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: <A PRJOBID>
|
|
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: <L,3 <A OBJSPEC> <A OBJTYPE> <L,n <A ATTRID>>>
|
|
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: <L,4 <U4 DATAID> <A CARRIERACTION> <A CARRIERID> <L PARAMS>>
|
|
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: <L,2 <A CARRIERID> <L,n <B slot.state>>>
|
|
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: <L,3 <A CARRIERID> <U1 source_port> <U1 target_port>>
|
|
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: <A CARRIERID>
|
|
roundtrip(comm, "S3F27 → S3F28 (cancel carrier)",
|
|
3, 27, true, Secs2.ascii("CAR-SECS4J-1"), 3, 28);
|
|
|
|
// S14F9 → S14F10 (E94 ControlJob create):
|
|
// body: <L,2 <A CTLJOBID> <L,n <A PRJOBID>>>
|
|
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: <L,2 <A PRJOBID> <A PRCMD>>
|
|
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: <L,2 <A CTLJOBID> <A CTLJOBCMD>>
|
|
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: <A CTLJOBID>
|
|
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: <L,4 <U4 DATAID> <A OBJSPEC> <A RCMD> <L PARAMS>>
|
|
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: <L,5 <U4 TRID> <A DSPER> <U4 TOTSMP> <U4 REPGSZ> <L svids>>
|
|
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: <L,2 <U4 EXID> <A EXRECVRA>>
|
|
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: <U4 EXID>
|
|
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);
|
|
}
|
|
}
|