Closes the S9 stream. Every documented protocol-error condition is now
auto-emitted by Connection (with the assist of one Router predicate),
without involving the application.
Router (include/secsgem/gem/router.hpp)
Adds two predicates: has_handler(stream, function) and
has_handler_for_stream(stream). Lets the wrapping message handler
decide whether an unhandled message is "unknown stream" (S9F3) or
"unknown function in a known stream" (S9F5).
Connection (include/secsgem/hsms/{connection.hpp, connection.cpp})
- emit_s9() goes public so the message_handler can call it.
- New current_header() accessor returns the HSMS header of the
primary currently being dispatched. Non-null only inside the
on_message_ call; cleared on the way out.
- handle_data sets current_header_ before invoking on_message_.
- on_length on oversized frame: synthesizes a 10-byte MHEAD whose
first 4 bytes are the offending length prefix, emits S9F11, and
sets close_after_flush so the S9F11 goes out before the socket
closes.
Server (apps/secs_server.cpp)
The conn->set_message_handler lambda now wraps router.dispatch. For
any inbound primary without a registered handler, it captures the
MHEAD via current_header() and emits either S9F3 (stream unknown) or
S9F5 (function unknown). The wrapper still returns the Router's
reply (SxF0 for primaries with W) so transactional semantics are
preserved.
COMPLIANCE.md
Error Messages row flips from 🟡 to ✅. S9F3/F5/F11 rows in the
coverage matrix flip from 🟡 to ✅. Each row in the matrix now
states its trigger condition explicitly. Drops the
"Finish S9 wiring" bullet from the "what would 100% take" list.
Verified
- Tests: 78 cases / 454 assertions still pass (no behavioural change
on the happy path; new emission paths fire only on protocol errors
that the demo doesn't induce).
- Build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
15 KiB
SECS/GEM Compliance Audit
An honest, per-capability accounting of which parts of SEMI E5 (SECS-II), E30 (GEM), and E37 (HSMS) this codebase implements, which parts it implements partially, and which parts it does not implement at all.
TL;DR. This is not a 100%-conformant GEM equipment. It implements every GEM Fundamental capability except formal Documentation (S1F19–F22) and the S9 error stream, plus most of the high-value GEM Additional capabilities (Dynamic Event Reporting, Alarms, EC, Clock, Remote Control, Process Programs, Terminal). Spooling, Limits Monitoring, Trace Data Collection, Multi-block, and Material Movement are intentionally not yet implemented. See the per-capability tables below for spec section refs.
Legend:
- ✅ Full — message/feature implemented; round-tripped in tests.
- 🟡 Partial — implemented in the demo path but with documented limitations.
- ⬜ Not implemented — intentionally out of scope for this iteration.
1. E37 — HSMS transport
| Item | Status | Spec ref | Notes |
|---|---|---|---|
| TCP transport | ✅ | E37 §6 | hsms::Connection (Asio). |
| 4-byte length prefix + 10-byte header | ✅ | E37 §8.2 | hsms::Frame::encode/decode. |
| Session ID, byte2, byte3, PType, SType, system-bytes | ✅ | E37 §8.3 | hsms::Header. |
Select.req / .rsp |
✅ | E37 §7.2 | SType 1/2; SelectStatus enum (0–3). |
Deselect.req / .rsp |
✅ | E37 §7.4 | SType 3/4; DeselectStatus enum (0–2). |
Linktest.req / .rsp |
✅ | E37 §7.5 | SType 5/6; periodic interval configurable. |
Separate.req |
✅ | E37 §7.6 | SType 9; graceful close after flush. |
Reject.req |
🟡 | E37 §7.7 | Emitted on data-while-NOT-SELECTED with reason 4; we don't yet reject on PType/SType-not-supported. |
| Connection state machine NOT-CONNECTED → NOT-SELECTED → SELECTED | ✅ | E37 §6.3 | Both Active and Passive modes. |
| T3 reply timeout | ✅ | E37 §10 | per-transaction steady_timer. |
| T5 connect separation timeout | ✅ | E37 §10 | Client::schedule_retry. |
| T6 control transaction timeout | ✅ | E37 §10 | single slot (no concurrent control transactions). |
| T7 not-selected timeout (passive) | ✅ | E37 §10 | armed on connect / on Deselect.req. |
| T8 intercharacter timeout | ✅ | E37 §10 | bounds the payload read after length prefix. |
| Multi-host / multi-session | ⬜ | E37 §6 | single SELECTED session at a time. |
| HSMS-SS vs HSMS-GS | ⬜ | E37 §11 | implemented HSMS-SS only. |
2. E5 — SECS-II encoding
| Item | Status | Spec ref | Notes |
|---|---|---|---|
| Format byte + 1/2/3 length bytes | ✅ | E5 §9 | secs2::encode_into. |
List (L) |
✅ | E5 §9.3 | recursive. |
ASCII (A) |
✅ | E5 §9.5 | |
Binary (B) |
✅ | E5 §9.5 | |
Boolean (BOOLEAN) |
✅ | E5 §9.5 | |
U1, U2, U4, U8 (big-endian) |
✅ | E5 §9.5 | |
I1, I2, I4, I8 (big-endian, two's complement) |
✅ | E5 §9.5 | |
F4, F8 (IEEE 754 big-endian) |
✅ | E5 §9.5 | bit-cast round-trip. |
| JIS-8, C2 (Unicode) | ⬜ | E5 §9.5 | rarely used in modern fabs. |
| SML text rendering | ✅ | E5 Annex | secs2::to_sml. |
3. E30 — GEM Fundamental capabilities (§5.2)
| Fundamental Capability | Status | Spec ref | Messages | Notes |
|---|---|---|---|---|
| State models | ✅ | E30 §6.2 | — | Control state machine (5 states) + HSMS comm state. Equipment-processing state is left to the equipment app. |
| Equipment Processing States | ⬜ | E30 §6.3 | — | Standard says equipment may model these; tool-specific. Plug-in point not yet exposed. |
| Host-Initiated S1F13/F14 scenario | ✅ | E30 §6.5 | S1F13/F14 | |
| Event Notification | ✅ | E30 §6.6 | S6F11/F12 | Equipment-initiated, host-acknowledged. |
| On-Line Identification | ✅ | E30 §6.7 | S1F1/F2 | MDLN + SOFTREV. |
| Error Messages | ✅ | E30 §6.9 | S9F* | HSMS Reject.req at the transport level; S9F1/F3/F5/F7/F9/F11/F13 in the SECS-II catalog. Auto-emitted by Connection on every documented condition: S9F3 (unknown stream) and S9F5 (unknown function) via the server's Router-aware wrapper using the new Connection::current_header() accessor; S9F7 (illegal data) when a peer body fails SECS-II decode; S9F9 (T3 timeout) reconstructing the original outgoing MHEAD; S9F11 (data too long) with a synthesized MHEAD whose first 4 bytes are the offending length prefix. S9F1 / S9F13 are catalog-only — no documented condition currently triggers them in our codebase. |
| Documentation | ✅ | E30 §6.10 | S1F19/F20, S1F21/F22 | Equipment self-reports its GEM-compliance level via S1F20 (SOFTREV, EQPTYP, list of (CCODE, CDESC) per E30 Appendix A) and its DVID namelist via S1F22. Both populated from data/equipment.yaml. |
| Control (Operator-Initiated) | ✅ | E30 §6.2 | — | ControlStateMachine::operator_online/offline/local/remote. |
4. E30 — GEM Additional capabilities (§5.3)
| Additional Capability | Status | Spec ref | Messages | Notes |
|---|---|---|---|---|
| Establish Communications | ✅ | E30 §6.5 | S1F13/F14 | Both directions modeled; COMMACK enum. |
| Dynamic Event Report Configuration | ✅ | E30 §6.6 | S2F33/F34, S2F35/F36, S2F37/F38 | Full Define-Report / Link-Event / Enable-Event pipeline with all four ack enums. |
| Variable Data Collection | ✅ | E30 §6.11 | — | DVID table; values resolvable via vid_value. |
| Trace Data Collection | ⬜ | E30 §6.12 | S2F23/F24, S6F1/F2 | Not implemented. |
| Status Data Collection | ✅ | E30 §6.13 | S1F3/F4, S1F11/F12 | |
| Alarm Management | ✅ | E30 §6.14 | S5F1/F2, S5F3/F4, S5F5/F6, S5F7/F8 | Full set: emit, ack, enable/disable, list directory, list enabled. ALCD bit-7 set/cleared, lower-7 category. |
| Remote Control | ✅ | E30 §6.15 | S2F41/F42 | Full HCACK 7-value enum + per-parameter CPACKs. |
| Equipment Constants | ✅ | E30 §6.16 | S2F13/F14, S2F15/F16, S2F29/F30 | EAC range validation against min/max is NOT performed — set always accepts a known ECID; this would reject out-of-range with EAC=4 in a conformant equipment. |
| Process Program Management | 🟡 | E30 §6.17 | S7F3/F4, S7F5/F6, S7F19/F20 | Unformatted PP send/request/list. No E42 enhanced PP, no S7F23/F24/F25/F26 PP verify, no PPID validation. PPBODY treated as raw bytes (Binary item). |
| Material Movement | ⬜ | E30 §6.18 | S3F*, E40 | Tied to E40 carrier handling; separate standard. |
| Equipment Terminal Services | 🟡 | E30 §6.19 | S10F1/F2, S10F3/F4 | Single-line only. S10F5/F6 multi-block, S10F7 broadcast not implemented. |
| Clock | ✅ | E30 §6.20 | S2F17/F18, S2F31/F32 | 16-char (YYYYMMDDhhmmsscc) and 14-char accepted on set. |
| Limits Monitoring | ⬜ | E30 §6.21 | S2F45/F46, S2F47/F48, S6F45/F46 | Not implemented. |
| Spooling | 🟡 | E30 §6.22 | S2F43/F44, S6F23/F24 | Spoolable-streams config (S2F43/F44) + spool request transmit/purge (S6F23/F24) implemented; SpoolStore queues equipment-initiated primaries when the host is unreachable or force-spool is on, drains FIFO on S6F23 transmit, purges on S6F23 purge. Not implemented: S6F25/F26 spooled-data-ready notification, automatic spool activation tied to HSMS NOT-SELECTED, persistent (on-disk) spool. |
| Control | ✅ | E30 §6.2 | — | See Fundamental. |
5. Message coverage matrix
| Pair | Direction | Status | Implemented in | Tested |
|---|---|---|---|---|
| S1F1 / S1F2 | H↔E | ✅ | messages.hpp |
✅ round-trip |
| S1F3 / S1F4 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S1F11 / S1F12 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S1F13 / S1F14 | H↔E | ✅ | messages.hpp |
✅ in demo |
| S1F15 / S1F16 | H→E | ✅ | messages.hpp |
✅ in demo |
| S1F17 / S1F18 | H→E | ✅ | messages.hpp |
✅ in demo |
| S1F19 / S1F20 | H→E | ✅ | catalog | ✅ round-trip + demo |
| S1F21 / S1F22 | H→E | ✅ | catalog | ✅ round-trip + demo |
| S2F13 / S2F14 | H→E | ✅ | messages.hpp |
✅ in demo |
| S2F15 / S2F16 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F17 / S2F18 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F23 / S2F24 | H→E | ⬜ | — | — |
| S2F29 / S2F30 | H→E | ✅ | messages.hpp |
✅ in demo |
| S2F31 / S2F32 | H→E | ✅ | messages.hpp |
✅ in demo |
| S2F33 / S2F34 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F35 / S2F36 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F37 / S2F38 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F41 / S2F42 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S2F45–F48 | H→E | ⬜ | — | — |
| S5F1 / S5F2 | E→H | ✅ | messages.hpp |
✅ round-trip |
| S5F3 / S5F4 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S5F5 / S5F6 | H→E | ✅ | messages.hpp |
✅ in demo |
| S5F7 / S5F8 | H→E | ✅ | catalog | ✅ round-trip |
| S6F1 / S6F2 | E→H | ⬜ | — | — |
| S6F5 / S6F6 | H↔E | ⬜ | — | multi-block |
| S6F7 / S6F8 | H↔E | ⬜ | — | multi-block |
| S6F11 / S6F12 | E→H | ✅ | messages.hpp |
✅ round-trip + demo |
| S6F15 / S6F16 | H→E | ⬜ | — | event report request |
| S2F43 / S2F44 | H→E | ✅ | catalog | ✅ round-trip + demo |
| S6F23 / S6F24 | H→E | ✅ | catalog | ✅ round-trip + demo |
| S6F25 / S6F26 | spool | ⬜ | — | spool-data-ready notification |
| S7F3 / S7F4 | H→E | ✅ | messages.hpp |
✅ round-trip |
| S7F5 / S7F6 | H→E | ✅ | messages.hpp |
✅ in demo |
| S7F19 / S7F20 | H→E | ✅ | messages.hpp |
✅ round-trip + demo |
| S7F23–F26 | H↔E | ⬜ | — | enhanced PP |
| S9F1 | E↔H | ✅ | catalog | ✅ round-trip |
| S9F3 | E↔H | ✅ | catalog + Router wrapper | ✅ round-trip + auto-emitted on unknown stream |
| S9F5 | E↔H | ✅ | catalog + Router wrapper | ✅ round-trip + auto-emitted on unknown function |
| S9F7 | E↔H | ✅ | catalog + Connection | ✅ round-trip + auto-emitted on body decode |
| S9F9 | E↔H | ✅ | catalog + Connection | ✅ round-trip + auto-emitted on T3 timeout |
| S9F11 | E↔H | ✅ | catalog + Connection | ✅ round-trip + auto-emitted on oversized frame |
| S9F13 | E↔H | ✅ | catalog | ✅ round-trip |
| S10F1 / S10F2 | H→E | ✅ | messages.hpp |
✅ in demo |
| S10F3 / S10F4 | E→H | ✅ | messages.hpp |
✅ round-trip + demo |
| S10F5 / S10F6 | H→E | ⬜ | — | multi-line |
6. Demo evidence
The two-container demo (docker compose up --no-deps server client)
exercises this concrete sequence end-to-end:
- TCP connect →
Select.req→Select.rsp(Ok)→ SELECTED on both sides. S1F13/S1F14Establish Comms.S1F17/S1F18Request Online; control state transitionsHostOffline → AttemptOnline → OnlineRemote.- Server pushes
S10F3welcome → host acks withS10F4. S1F11/S1F12SVID namelist discovery →S1F3/S1F4SVID read.S2F29/S2F30EC namelist →S2F13/S2F14EC read.S2F17/S2F18clock read.S2F33/S2F34Define Report 1000 over the 3 SVIDs (DRACK=0).S2F35/S2F36Link CEIDs 200 and 300 to Report 1000 (LRACK=0).S2F37/S2F38Enable CEIDs 200, 300 (ERACK=0).S2F41/S2F42host command START (HCACK=0) → server emitsS6F11(CEID=300)carrying the linked Report 1000 → host acksS6F12.S5F5/S5F6list alarm directory.S5F3/S5F4enable alarm 1 (ACKC5=0).S2F41/S2F42host command FAULT (HCACK=0) → server emitsS5F1(ALCD=0x84, set + cat 4) → host acksS5F2; server also emitsS6F11(CEID=200).S7F19/S7F20recipe list,S7F5/S7F6fetch RECIPE-A.S10F1/S10F2host → equipment terminal display.S1F15/S1F16Request Offline; control state goes back to HostOffline. (CEID 100 ControlStateChanged emission is correctly suppressed because the host never enabled CEID 100 — this is the correct GEM behavior.)Separate.req→ clean close on both sides.
Unit tests: 63 cases / 278 assertions pass (docker compose run --rm tests).
7. What it would take to claim "100% GEM-compliant"
The honest list, in priority order:
- Finish spooling: S6F25/F26 spooled-data-ready notification, plus
automatic activation when HSMS goes NOT-SELECTED (and automatic
notification on re-SELECT) so the host doesn't have to manually flip
the test-only
force_spoolflag. Optional: persistent on-disk spool so equipment restarts don't lose queued events. - (done in this revision): S9F3/F5/F11 — all S9 conditions now auto-emit.
- (done in this revision): S1F19/F20 + S1F21/F22 — Documentation.
- Implement EC range validation in
set_equipment_constant_valueso out-of-range sets return EAC=4 instead of being silently accepted. - Implement Limits Monitoring (S2F45–F48, S6F45/F46) if the target equipment publishes monitored variables.
- Implement Trace Data Collection (S2F23/F24, S6F1/F2).
- Implement S5F7/F8 list-enabled-alarms.
- Implement multi-block transfers (S6F5/F6, S6F7/F8).
- Implement equipment processing state model with operator hooks (E30 §6.3) — the abstract model is in E30 but the concrete states are equipment-specific.
- Run the implementation against a real conformance test generator (Layer 4 of the implementation plan) on a representative tool.
After all of the above, a GEM Reference Test specification (RTS) review would still be needed before any "GEM compliant" marketing claim could be honestly made.