Files
secs-gem/interop
raphael db90a21e1d
tests / build-and-test (push) Successful in 2m2s
tests / thread-sanitizer (push) Successful in 2m28s
tests / tshark-dissector (push) Failing after 2m7s
tests / secs4j-interop (push) Failing after 0s
tests / libfuzzer (push) Successful in 3m9s
verify: expand secs4j harness 20 → 55 checks
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>
2026-06-09 16:30:49 +02:00
..

secsgem-py interop harness

Cross-validates our C++ SECS-II / HSMS / GEM implementation against secsgem-py 0.3.0, the de-facto Python reference. Everything runs in Docker — no Python or secsgem-py on the host.

What it tests

Driver Peer Coverage
host_vs_cpp_server.py C++ secs_server (passive) HSMS select/separate, S1F1/F3/F11/F17/F23, S2F13/F17/F29/F33/F35/F37/F41, S5F3/F5/F7, S5F1 unsolicited, S6F11 unsolicited, S7F3/F5/F19, S10F1/F3, S1F15
secs_interop_probe (C++) passive_equipment.py (secsgem-py GemEquipmentHandler) HSMS select, S1F13/F14, S1F1/F2, S1F3/F4, clean separate
raw_gem300_harness.py C++ secs_server (passive) GEM 300 streams secsgem-py upstream doesn't ship: S3F17/F18 (E87 carrier action), S16F5/F6 (E40 PRJobCommand), S16F27/F28 (E94 CJobCommand) — built with custom SecsStreamFunction subclasses + registered custom DataItems

24 named checks on the C++-server side; 4 explicit checks on the C++-host side; 4 GEM-300 raw-frame checks. Implicit HSMS state-machine and wire-level framing validation everywhere.

Running

# Start C++ passive server, then drive it with secsgem-py host:
docker compose up -d server
docker compose run --rm interop python3 /app/interop/host_vs_cpp_server.py \
    --host server --port 5000 --session-id 0

# Start Python passive equipment, then probe it with the C++ host:
docker compose up -d equipment_py
docker compose run --rm builder /app/build/secs_interop_probe \
    --host equipment_py --port 5000 --device 0

Both exit 0 on success.

What this caught

Real bugs surfaced by interop (now fixed):

  1. Strict U4 parsing rejected U1-encoded identifiers. SEMI E5 declares DATAID, RPTID, VID, CEID, ALID, EXID, etc. as U1 | U2 | U4 | U8; secsgem-py picks the smallest width that fits. Our as_u4_scalar, as_u2_scalar, etc. were strict. Now lenient with range-checked downcasts (messages_helpers.hpp::any_unsigned_first).
  2. PPBODY rejected when sent as ASCII. SEMI lets PPBODY be ASCII | Binary | List; secsgem-py defaults to ASCII. Added the BINARY_OR_ASCII codegen item type plus a permissive as_text_or_binary accessor, used for S7F3/F6.
  3. Missing S1F23 / S1F24 (Collection Event Namelist). Added the wire schema in data/messages.yaml, a vids_for(ceid) accessor on the event-report store, and the dispatch handler in secs_server.cpp.
  4. Missing S10F3 handler (Terminal Display Single, host→equipment). Our server only registered S10F1; per SEMI E5, S10F1 is equipment→host and S10F3 is the host→equipment counterpart. Added the missing dispatch.

The C++ test suite still passes (278 cases / 1436 assertions) after each of these changes — the fixes are purely permissive widenings, no existing behaviour was broken.