a1dc7937d4
Adds a docker-compose service `server-spool` that runs secs_server
with --spool-dir pointed at a named volume. Two-phase Python
harness (interop/spool_persistence_test.py):
1. Enqueue phase: force-spool one S6F11(CEID=300) via the
SPOOL_ON / START / SPOOL_OFF RCMD trio, then disconnect.
2. Driver runs `docker compose restart server-spool` between
the phases — the named volume preserves the journal files.
3. Drain phase: reconnect, send S6F23(Transmit), verify the
replayed S6F11 carries CEID 300.
Surfaces a real interop bug along the way: secsgem-py 0.3.0 encodes
RSDC (and other "single-byte status" fields) as <U1>, while SEMI E5
spells them as <B>. Our `as_binary_first` was strict on Binary; now
accepts either (the byte semantics are identical, and the leniency is
symmetric with the U-type widening from the first interop commit).
Result: enqueue → docker restart → drain returns CEID 300 cleanly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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):
- 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. Ouras_u4_scalar,as_u2_scalar, etc. were strict. Now lenient with range-checked downcasts (messages_helpers.hpp::any_unsigned_first). - PPBODY rejected when sent as ASCII. SEMI lets PPBODY be
ASCII | Binary | List; secsgem-py defaults to ASCII. Added theBINARY_OR_ASCIIcodegen item type plus a permissiveas_text_or_binaryaccessor, used for S7F3/F6. - Missing S1F23 / S1F24 (Collection Event Namelist). Added the
wire schema in
data/messages.yaml, avids_for(ceid)accessor on the event-report store, and the dispatch handler insecs_server.cpp. - 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.