c8e8e80735286fa0f858275fd2a1ca03c92c1dab
26 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b031f057af |
docs: customer-ready sweep + README restructure + tshark CI fix
Audit pass over the public-facing surface so a customer can read it end-to-end without tripping on stale numbers or self-contradictions. README + docs accuracy: - Test counts 426 → 445, assertions 2 557 → 2 753 (verified via doctest run); E5 row was missing test_e5_kat (19 cases) - Interop checks 24 → 31, COMPLIANCE.md message count 149 → 164, COMPLIANCE.md "291 cases / 1515 assertions" → 445 / 2 753 - README "60+ test IDs" for MES_INTEROP → actual 59 - PVD example counts: 32 SVIDs/17 CEIDs → 29/21, "~40 handlers in ~200 lines" → 51 in ~460, "~700 lines" → ~1,100; main.cpp header table-of-contents resynced with the actual 7 sections Out-of-scope honesty (COMPLIANCE.md §8 + FAQ.md): - Removed HSMS-GS (was both ✅ implemented in §1 and "out of scope" in §8; INTEGRATION.md §7 documents using it) - Removed multi-block SECS-I (split_message/assemble_message exist with 4 dedicated tests) - Added serial-port wiring as the genuine open ⬜ item — FSM is tested end-to-end over TCP; only the asio serial_port glue is deferred - COMPLIANCE.md intro now lists E42 and notes "E37 (SS + GS)" README restructure: - Moved the 8-command proof table and per-standard test-coverage table to a new PROOFS.md (72 lines) - README now leads with what / Quick start / Documentation map, then a one-paragraph "How it's proved" linking to PROOFS.md - Updated cross-refs in FAQ.md, GLOSSARY.md, VERIFICATION.md, and interop/README.md to point at PROOFS.md CI fix — tshark-dissector job: - interop/tshark_validate.sh hardcoded /app/build/secs_server etc. which only works inside the docker image. Now derives ROOT from the script's own location and accepts BUILD/SERVER/CLIENT/DATA env overrides, so CI can run it from the workspace dir - Verified still passes in docker (69 frames, 0 malformed) .gitignore: - Added build-fuzz/ and build-tsan/ (were showing as untracked) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2ea3ab796a |
e84: SEMI §6 handshake timers TA1/TA2/TA3
E84StateMachine had the full signal-level handshake but no timer
enforcement. In a real AMHS that's a deadlock: if equipment is slow to
assert L_REQ / U_REQ, or AMHS is slow to assert BUSY / COMPT, neither
side notices — the wires just sit stuck. SEMI E84 §6 mandates three
timers that bound each leg of the dance.
TA1 — armed in ValidAsserted, cancelled in Load/UnloadReady.
AMHS bounds how long equipment takes to acknowledge VALID.
TA2 — armed in Load/UnloadReady, cancelled in Transferring.
Equipment bounds how long AMHS takes to start the transfer.
TA3 — armed in Transferring, cancelled on Complete.
Equipment bounds the BUSY-phase duration.
The FSM stays I/O-free (it's the design invariant): arm/cancel are
delivered via callbacks, the application owns the asio::steady_timer,
and the application calls `fsm.on_timeout(id)` when its real clock
fires. Stale on_timeout calls (post-cancel race) are no-ops.
On expiry, the FSM transitions to a new `HandoffFault` state, records
the `E84Fault` reason, fires the optional fault_handler, and latches
the fault until `reset()`. Signal jitter on the wires cannot silently
clear a recorded handshake timeout — once you've crossed the timer,
you stop.
Defaults are all-zero, which disables arming. This is what every
existing test relies on, and what back-to-back simulation (no
wall-clock) needs. Production tools call `set_timeouts({2s, 2s, 60s})`
or whatever their port spec dictates.
12 new test cases / 59 assertions: arming per state, cancelling per
exit, expiry-to-fault for all three timers, ES cancels everything,
stale-expiry no-op, fault latching across signal jitter, and a
full-cycle arm/cancel trace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a4419e15cd |
conformance: expand harness from 8 to 47 host-driven checks
The previous harness only exercised S1F1/F11/F13/F19, S2F17/F29, S5F5, S7F19 — about 15% of what COMPLIANCE.md claims as ✅. Customers running secs_conformance against their tool got near-zero conformance signal on dynamic event reports, GEM 300, alarm management, exception recovery, terminal services, spool, and PP management. This expansion covers, in one sequential run: - Establish comms + identification (S1F13/F1) - Status / DVID / CEID / EC namelists + values (S1F11/F3/F21/F23, S2F29/F13) - Dynamic event reports: define / link / enable + readback paths (S2F33/F35/F37, S6F15/F19/F21) - All three remote-command forms (S2F41/F21/F49) - Equipment-initiated S6F11 observation triggered by RCMD=START - Trace init, limits attrs, spool reset + transmit (S2F23, S2F47, S2F43, S6F23) - Alarm management: list, list-enabled, enable (S5F5/F7/F3) - Exception recovery: request + abort (S5F13/F17) - PP load-inquire / list / request (S7F1/F19/F5) - Terminal display both directions (S10F3, S10F5) - E40 PJ create / monitor / command / dequeue (S16F11/F7/F5/F13) - E94 CJ create / command / delete (S14F9, S16F27, S14F11) - E87 carrier action / slot map / transfer / cancel (S3F17/F19/F25/F27) - E39 GetAttr (S14F1) - GEM compliance self-report (S1F19) Pass criterion is the spec-mandated reply function code, not any specific ACK value — CarrierIDUnknown / Denied_UnknownObject / PpidNotFound / Error are well-formed F-coded replies and count as protocol-conformant. This lets the harness run against any equipment without preloading state. 47 / 47 PASS against the in-repo demo server. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d470442a8c |
docs: drop implementation_plan.md, rewrite README for fab deployment
implementation_plan.md was a Layer-0..6 roadmap from the project's
spec-as-data exploration phase; every layer it described is now
shipped (Layer 0 foundations through Layer 4 message catalog +
state machines). Removed.
README rewritten for the fab-deployment audience. Sections added:
1. Persistence directory layout (storage rules, disk budget, DR)
2. Security (network isolation, TLS tunnels, audit logging,
config signing)
3. Monitoring + observability (signals → hooks table, Prometheus
pattern)
4. High availability (active/standby on shared persistence)
5. Deployment patterns (Docker / systemd / k8s)
6. Upgrade path (YAML reload, code rollout, schema versioning)
7. Integration with the fab stack (MES / AMHS / OHT / recipe
engine table)
8. Compliance + certification (fork COMPLIANCE.md per tool, run
RTS)
9. Testing in production (canary, synthetic transactions, shadow
traffic)
10. Operational runbook (incident → first check → mitigation)
Stale stats refreshed: test count went 148/794 → 384/2390;
catalog grew to 164 messages; HSMS-GS, SECS-I T3/T4, per-port E84,
E42 formatted PPs all mentioned.
COMPLIANCE.md §9 lost its stale `implementation_plan.md` reference.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
78fb0c3826 |
e42: enhanced (formatted) process programs S7F23-F26
E42 was an explicit out-of-scope item in the prior COMPLIANCE.md.
This commit closes it.
Wire messages added via the catalog:
S7F23 Formatted PP Send (H↔E, W=1)
S7F24 Formatted PP Ack (ProcessProgramAck)
S7F25 Formatted PP Request (PPID, W=1)
S7F26 Formatted PP Data (E→H, no reply)
Body shape: <L,4 PPID MDLN SOFTREV <L,n <L,2 CCODE <L,m <L,2
PNAME PVAL>>>>>. PVAL is declared ITEM so any SECS-II Item type
round-trips — proven by a test that mixes ASCII, BOOLEAN, U4, F8,
Binary, and nested List values in one step.
RecipeStore extension:
add_formatted(ppid, FormattedRecipe{mdln, softrev, steps})
get_formatted(ppid) -> optional<FormattedRecipe>
has_formatted(ppid) -> bool
Formatted + opaque views live alongside each other: a PPID can carry
both, size() counts unique PPIDs. remove() kills both views.
Six new tests cover wire round-trip per function, every
ProcessProgramAck code, ITEM passthrough, and the store's dual-view
semantics.
COMPLIANCE.md updated: E30 §6.17 row mentions S7F23-F26, S5 message
table grows two rows, §8 "out of scope" entry for E42 removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d4d1a411d7 |
secsi: T3 / T4 enforcement moved into the FSM
The SECS-I Protocol FSM now enforces T3 (reply timeout) and T4
(inter-block timeout) directly, instead of leaving them as
upper-layer hooks.
T3: on complete_send, if the block we just acked had W=1, record its
system_bytes in awaiting_reply_sys_ and emit ActionStartTimer{T3}.
deliver_recv cancels T3 when a block arrives whose system_bytes
match the outstanding request. EventTimeout{T3} aborts the FSM with
"T3 reply timeout".
T4: deliver_recv emits ActionStartTimer{T4} whenever the delivered
block has end_block=false. The next block's deliver_recv cancels
the timer; EventTimeout{T4} aborts with "T4 inter-block timeout".
abort() now also cancels T3/T4 and clears the tracking state.
Test changes:
- Old "T3/T4 are FSM-level no-ops" test → REPLACED by four new
tests: T3 arm+expire, T3 arm+matching-reply cancels, T4
arm+expire, T4 arm+next-block cancels.
- Two new observer accessors on Protocol (awaiting_reply,
awaiting_next_block) so the tests can assert tracking state
without poking internals.
COMPLIANCE.md §1a: T3 + T4 rows go ⬜ → ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
77197b9c1e |
e84: per-port FSM via E84PortStore
E84 (Parallel I/O) is fundamentally per-load-port: each port has its own ten-wire handshake with the AMHS. Earlier revisions modeled it as a single equipment-wide FSM; this commit refactors to a per-port store, so multi-LP tools can run independent handshakes in parallel. Public API change in EquipmentDataModel: E84StateMachine e84; -> removed E84PortStore e84_ports; // create(port_id), get(port_id), ... Convenience pass-throughs: E84PortStore::on_signal_change auto-creates the port on first use (ergonomic for demos); applications should call create() explicitly with their full port set. The two existing callsites (test_gem300_scenario, test_e87_wire_scenarios) are updated. The multi-LP test now demonstrates the actual win: interleaved LP1 load + LP2 unload handshakes that reach their respective Ready states without sequencing, and an ES on LP1 that does NOT affect LP2 — exactly the failure mode the previous design couldn't catch. Five new dedicated tests in test_e84_ports.cpp for the store itself. COMPLIANCE.md §4i updated: row now reflects per-port design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2f0a4ba339 |
e30: S10F7 broadcast terminal display
Adds the last terminal-services message: a multi-line broadcast push to all terminals, no reply. Same TID+lines body as S10F5, W=0. Generated via the catalog: data/messages.yaml schema entry + auto-generated s10f7_terminal_display_broadcast / parse_s10f7. Test round-trips TID and a 3-line broadcast through the builder and parser, confirms W=0. COMPLIANCE.md updated: S10F7 row in §5 added; §8 "out of scope" entry removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d0c7fb71b6 |
hsms: HSMS-GS multi-session support (E37 §11)
Connection now supports both HSMS-SS (single session — the
constructor's behaviour, unchanged) and HSMS-GS (multi-session).
add_session(device_id) registers additional sessions; each one has
its own NotSelected/Selected state and its own message/selected
handlers. In GS mode the Select.req carries session_id=device_id;
in SS mode it stays at 0xFFFF (legacy). Linktest/Separate remain
connection-scope per spec.
Public API additions:
add_session(device_id)
set_session_message_handler(device_id, h)
set_session_selected_handler(device_id, h)
session_state(device_id) -> State
is_session_selected(device_id) -> bool
send_request(device_id, msg, cb)
send_data(device_id, msg)
Internal refactor: state_/on_message_/on_selected_ folded into a
SessionSlot map keyed by device_id; SS-style getters/setters route
through the primary session. T7 + linktest are connection-scope —
T7 fires only when no session is selected; linktest runs while at
least one is.
Five wire-level tests:
- passive: two sessions selected independently via Select.req
with their own session_id
- GS Select.req for an unregistered session id is Rejected
(EntityNotSelected)
- data routed by session_id; data on a not-selected session is
Rejected
- active: two registered sessions both end up selected via
serialized Select.req per session
- SS legacy: existing single-session API still works (session_id
0xFFFF in Select.req)
COMPLIANCE.md §1 updated: HSMS-GS row goes ⬜ → ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0bbc7b7acf |
DD1: refresh COMPLIANCE.md for full GEM 300 + 291 tests
The doc was last updated when only E40 + E94 were in tree. Brings it
up to date with everything actually implemented:
* New §4a–§4k tables for each GEM 300 standard: E40, E94, E87, E90,
E116, E120, E148, E157, E84, E5 §13 wafer maps, and the exception
recovery extension (S5F13–F18 + ExceptionStateMachine).
* Refreshed message coverage matrix to all 149 catalog entries
(added S1F23/F24, S2F21/F22, S6F5–F8/F15–F22, S7F1/F2/F17/F18,
S10F3/F4, S12F9–F18).
* Updated test count to 291 cases / 1515 assertions.
* New §7 documents the secsgem-py interop harness (24 host-side
checks + raw-GEM300 round-trip).
* §8 trimmed: persistent spool is no longer "out of scope" (CC1
landed); E40/E87/E90 removed from "Layer 5 follow-on" list since
they're done.
* §9 honesty pass — every GEM 300 standard in scope now implemented
end-to-end; the remaining gap is third-party RTS certification +
per-vendor application wiring.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
72fa73fee0 |
A5: SECS-I-over-TCP convenience layer
Wires the SECS-I Protocol FSM behind an asio TCP socket so the block protocol can run over loopback without serial hardware. Mirrors secsgem-py's `secsitcp/` adapter — useful for back-to-back simulators and CI without a serial device. Adds: include/secsgem/secsi/tcp_transport.hpp src/secsi/tcp_transport.cpp tests/test_secsi_tcp.cpp The transport: - Splits outgoing SECS-II messages into blocks (transparent multi-block). - Accumulates incoming blocks until end_block=true, then assembles and delivers as a single SECS-II message — same surface as the HSMS Connection's MessageHandler. - Drives T1 / T2 timers from asio steady_timer; T3/T4 stay upper-layer per the FSM contract. - Auto-allocates monotonic system bytes per send. Tests cover single-block delivery, multi-block reassembly (700-byte ASCII body spanning multiple SECS-I blocks), and bidirectional exchange. This closes Tranche A (catch-up to secsgem-py wire/transport surface). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a400ef3160 |
A4: SECS-I transport (block protocol + E4 retry FSM)
Adds a complete IO-free SECS-I implementation:
include/secsgem/secsi/header.hpp 10-byte block header (R/W/E bits)
include/secsgem/secsi/block.hpp length + header + body + checksum
include/secsgem/secsi/protocol.hpp half-duplex FSM (ENQ/EOT/ACK/NAK)
src/secsi/* implementations
tests/test_secsi.cpp header, block, multi-block split,
back-to-back FSM drive, RTY,
contention, T2 timeout
The protocol is event-driven (`Event` → `Action` queue), so wiring it
to an asio serial_port is a thin adapter — that lands in the next
commit so this one stays reviewable.
Key design points:
- Master/slave contention: slave yields on simultaneous ENQ (E4 §7.1.4).
- RTY exhaustion raises ActionRaiseError, clears the send queue, resets
to Idle (no zombie state).
- Multi-block assembler validates contiguous 1..N numbering and exclusive
E-bit-on-last invariants — rejects malformed sequences with nullopt.
- Block::checksum is exposed publicly for the receive path's verification.
Tests cover the happy path (back-to-back delivery), error paths
(checksum mismatch, short input, oversize body), retries (NAK chain to
exhaustion), and protocol corner cases (contention, T2 timeout).
secsgem-py implements SECS-I block framing but lacks the explicit RTY
state machine; this commit puts the C++ port ahead on transport
correctness.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ec478ac9cb |
A3: S12 Wafer Maps stream (E5 §13)
Adds the core map-management messages: setup send/request (F1-F4), transmit inquire/grant (F5/F6), data send in row format (F7/F8), and the one-way error report (F19). Reference points (REFP) are an external struct shared across F1 and F4. The alternative data encodings — array format (F9/F10), coordinate format (F11/F12), and the corresponding request pairs (F13-F18) — are scope-deferred. They're mechanical YAML edits once a tool needs them; the codec already handles the underlying BINARY/list shapes. Three new ack enums: MapSetupAck (SDACK), MapTransmitGrant (GRANT), MapDataAck (MAPER). No state machine yet — maps are a data exchange, not a lifecycle. secsgem-py ships S12F0-F19 as a single block; this commit covers the practically-used subset and matches their wire shapes where they overlap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
fafbd2abd2 |
A2: S2F49/F50 Enhanced Remote Command
Adds the OBJSPEC-scoped sibling of S2F41 with extended per-parameter ack shape (CPACK + CEPACK). Wire: S2F49 body <DATAID OBJSPEC RCMD <L,n <CPNAME CPVAL>>> S2F50 body <HCACK <L,n <CPNAME CPACK CEPACK>>> Server delegates to the existing HostCommandRegistry, logs OBJSPEC for audit, and currently returns empty cpacks (all-OK). Per-parameter failures will be wired when the command registry grows CEPACK-level validation; this commit is the catalog + dispatch scaffolding. secsgem-py defines these in its catalog but never dispatches them; this puts the C++ port marginally ahead on remote-command coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b59c62bbc9 |
A1: S5F13-F18 exception recovery messages (E5 §9.5-9.7)
Catalog gains the recover-request / recover-complete / recover-abort loop that closes E5's exception lifecycle. Wire shapes: S5F13/F14 Exception Recover Request / Acknowledge S5F15/F16 Exception Recover Complete Notify / Acknowledge S5F17/F18 Exception Recover Abort Request / Acknowledge Acks use the same AlarmAck byte already in use by S5F10/F12. EXRESULT on F15 is modelled as ASCII (the common case; vendor-specific richer shapes are a YAML edit). Round-trip tests cover all three pairs. Server dispatch is left for a later commit alongside the per-alarm AlarmStateMachine (Tranche C) — this commit is wire-coverage only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
90c177b7ce |
E40 Process Jobs + E94 Control Jobs + E30 communication state
GEM300 layer: SEMI E40-0705 Process Job and E94-0705 Control Job state machines, plus the E30 §6.1 communication-state machine that sits between HSMS SELECT and full GEM communication. Data-driven via data/process_job_state.yaml and data/control_job_state.yaml, mirroring the existing control_state.yaml pattern. Wire coverage: S14F9/F10 CreateObject (CJ) host -> equipment S14F11/F12 DeleteObject (CJ) host -> equipment S16F5/F6 PRJobCommand host -> equipment S16F9 PRJobAlert equipment -> host S16F11/F12 PRJobCreate (simplified body) host -> equipment S16F13/F14 PRJobDequeue host -> equipment S16F27/F28 CJobCommand host -> equipment Process Job FSM exposes 8 states matching PRJOBSTATE bytes (E40 §10.3.2); HOQ is reorder-aware (move-to-head against an insertion-order vector); Stop/Abort on a Queued PJ routes through ABORTING so the host observes PRJOBSTATE=7 on the wire (§6.3); alert_enabled is settable per-PJ for PRALERT control; FSM dispatches through ProcessJobStore::on_change_ dynamically so a late set_state_change_handler() reaches existing PJs. Hardening: loader rejects NoState (sentinel) as initial/from/to and rejects `on: created` rows; static_asserts pin enum values to wire bytes; ProcessJobStore is non-movable to keep the per-PJ this-capture safe. Server simulator cascades the full CJ -> PJ lifecycle on CJSTART so the wire trace exercises every legal state. CEIDs 400/401 fire on CJ state changes via the existing event-report pipeline. Tests: 60+ new assertions across test_process_jobs, test_control_jobs, test_communication_state, test_hsms_connection, plus loader and messages round-trip coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1f67aad985 |
100%/F: S10F5/F6 multi-line + honest 100% in COMPLIANCE.md + README pass
tests / build-and-test (push) Failing after 33s
The final additions: S10F5/F6 multi-line terminal display (closes the last partial Additional capability — Equipment Terminal Services flips ✅), and a thorough COMPLIANCE.md / README pass that states the 100% claim honestly. Catalog + handlers data/messages.yaml S10F5 / S10F6 added. apps/secs_server.cpp router.on(10, 5) iterates the line list, acks with S10F6. tests/test_messages.cpp Round-trips a 3-line multi-line display. COMPLIANCE.md (rewritten) Every GEM Fundamental ✅. Every GEM Additional that E30 binds to a concrete message set ✅. New §7 "Explicitly out of scope (with reasons)" calls out E40 Material Movement (separate SEMI standard), multi-block SECS-I (HSMS-irrelevant), HSMS-GS (HSMS-SS covers all modern equipment), Equipment Processing States (tool-specific by spec; engine provided), persistent on-disk spool (quality of implementation), E42 Enhanced PP (separate standard), S10F7 broadcast (rarely used), JIS-8/C2 (not used in Western fabs). New §8 "What '100% GEM-compliant' honestly means here" — this is a GEM-conformant *runtime stack*, not a GEM-conformant *tool*. Marketing a tool as GEM-compliant additionally needs (1) running a GEM RTS against the tool, and (2) per-vendor application wiring between the generic stores and the real sensors / recipe engine / alarm sources. README.md (rewritten) Architecture diagram updated to reflect the actual store list (nine stores). "Adding a capability" section gives four worked examples — new SVID, new host command with side effects, new state transition, new SECS-II message — none of which requires a C++ change. Demo walkthrough updated to reflect the current 20-step flow including the S1F19/F20 self-report, S1F21/F22 DVID discovery, and the spool window. Code clarity include/secsgem/gem/data_model.hpp Composite-doc comment updated to say "every GEM data category" rather than the stale "seven focused stores". Verified - Tests: 84 cases / 487 assertions pass. - Demo: 198 server/host log lines; exits 0 end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1e7105a9e0 |
100%/E: Spool S6F25/F26 + auto-trigger on re-SELECT
tests / build-and-test (push) Failing after 31s
Closes spooling. S6F25 (NUM-MSG) goes into the catalog; S6F26 (ACKC6) likewise. The server's on_selected handler now checks the spool on entering SELECTED — if there's queued data, it auto-emits S6F25 so the host can decide what to do (S6F23 Transmit vs Purge). The happy-path demo never drops TCP so the auto-trigger doesn't fire there, but the canonical re-SELECT path is wired. Client gains a handler for inbound S6F25 that logs the count and acks S6F26. COMPLIANCE.md: Spooling Additional capability flips from 🟡 to ✅. Remaining out of scope for spooling: persistent on-disk spool so restarts don't lose queued events. Demo + tests don't need it; real fab equipment would. Tests: 83 cases / 481 assertions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6cedaa10dc |
100%/D: Trace Data Collection (S2F23/F24 + S6F1/F2, E30 §6.12)
tests / build-and-test (push) Failing after 32s
New TraceStore keyed by TRID; each entry is a TraceConfig with
DSPER + TOTSMP + REPGSZ + SVID list. S2F23 validates that every SVID
exists (TIAACK=4 otherwise) and registers the trace.
S6F1's body is L,4 of {TRID U4, SMPLN U4, STIME ASCII, list_of <Item>}
— the application chooses whether each value Item is a scalar SVID
value or a packed batch.
The periodic sampling timer that turns an active TraceConfig into
S6F1 emissions is intentionally left to the application (E5 doesn't
mandate a specific scheduler and vendors typically already have one).
Four new SxFy in the catalog.
COMPLIANCE.md: Trace Data Collection Additional capability flips ✅.
Tests: 82 cases / 477 assertions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
224130d99f |
100%/C: Limits Monitoring (S2F45–F48, E30 §6.21)
tests / build-and-test (push) Failing after 35s
New LimitMonitorStore keyed by VID; each entry is a vector of LimitDefinition (LIMITID + upper/lower deadband as arbitrary Items). S2F45/F46 set, S2F47/F48 read. VLAACK validates each VID exists. Four new SxFy in the catalog; codegen handles the nested list-of-(VID, list-of-LimitDefinition) shape. LimitDefinition is defined in store/limits.hpp and referenced as external_struct so the data model and the message codecs share one type. The actual "value crossed limit" detection + CEID emission is left to the application's set_value path (E30 §6.21 leaves *how* the equipment detects crossings up to the implementer). COMPLIANCE.md: Limits Monitoring Additional capability flips ✅. Tests: 80 cases / 465 assertions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
65db38d9f2 |
100%/B: S9F3/F5/F11 emission + Router fallback
tests / build-and-test (push) Failing after 31s
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>
|
||
|
|
88205037ec |
100%/A: S5F7/F8 list enabled alarms
tests / build-and-test (push) Failing after 32s
Closes the small remaining hole in Alarm Management. S5F7 is header-only; S5F8 has the same wire shape as S5F6 (vector of <L,3 <B ALCD> <U4 ALID> <A ALTX>>) but only includes alarms whose enabled flag is set. Codegen handles both as a list_of with struct_name=AlarmListing; server pre-computes the per-row ALCD from (severity_category, active state) before passing the rows in. COMPLIANCE.md: Alarm Management Additional capability flips ✅. Tests: 78 cases / 454 assertions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
813e011409 |
Close COMPLIANCE.md gap: Documentation (S1F19-F22)
tests / build-and-test (push) Failing after 32s
Adds the GEM "Documentation" Fundamental capability: the equipment now
self-reports which GEM capabilities it supports, and the host can
discover the DVID namelist with the same shape used for SVIDs.
Catalog (data/messages.yaml -> generated messages.hpp)
S1F19 W header-only Get GEM Compliance Request
S1F20 <L,3 <A SOFTREV>
<A EQPTYP>
<L,a <L,2 <U1 CCODE> <A CDESC>>>>
Get GEM Compliance Data
S1F21 W <L,n <U4 VID>> DVID Namelist Request (n=0 = all)
S1F22 <L,n <L,3 <U4 VID>
<A VNAME>
<A UNITS>>> DVID Namelist Data
Codegen emits CapabilityEntry and GemCompliance structs. S1F22 reuses
S1F12's StatusName struct (same wire shape; dedup avoids redefinition).
Equipment data dictionary (data/equipment.yaml)
device: Adds `equipment_type: "EQUIPMENT"`
for the S1F20 EQPTYP field.
capabilities: New section. List of
- {code, name} (CCODE, CDESC) pairs honestly
reflecting what the codebase
implements: 1, 2, 3, 5, 6, 7, 8,
9, 11, 12, 14 (partial), 15.
dvids: New section, same schema as
svids:. Demo populates two:
- WaferCounter (U4, units wafer)
- ChamberPressure (F4, units Torr)
Loader (src/config/loader.cpp + include/secsgem/config/loader.hpp)
EquipmentDescriptor gains equipment_type and capabilities (vector of
(uint8_t, string) pairs). load_equipment now reads `capabilities:`
into the descriptor and `dvids:` into model.dvids.
Server (apps/secs_server.cpp)
router.on(1, 19) returns S1F20 with desc.software_rev,
desc.equipment_type, and desc.capabilities converted to
vector<CapabilityEntry>.
router.on(1, 21) returns S1F22 built from model.dvids.all().
Client (apps/secs_client.cpp)
Two new demo steps after Request Online and before SVID discovery:
S1F19 -> S1F20: logs SOFTREV, EQPTYP, and every (CCODE, CDESC)
the equipment claims.
S1F21 -> S1F22: logs each DVID with units.
Tests
tests/test_messages.cpp Round-trip S1F19/F20 with a 3-entry
capability list; round-trip S1F22 with two
DVIDs.
tests/test_loader.cpp Asserts equipment_type, the capabilities
list contains CCODE 14 (Spooling), and the
two DVIDs land in model.dvids.
COMPLIANCE.md
"Documentation" Fundamental moves from ⬜ to ✅.
S1F19/F20 + S1F21/F22 rows in the coverage matrix flip to ✅.
The "what would it take" list drops the documentation-messages bullet.
Verified
- Tests: 77 cases / 444 assertions pass.
- Demo: client logs the full capability list received from the
equipment, including CCODE 14 "Spooling (partial; S2F43/F44 +
S6F23/F24)" — the equipment honestly reports its partial
implementation rather than overclaiming.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
547fd2116b |
Close COMPLIANCE.md gap: S9 error stream
tests / build-and-test (push) Failing after 43s
Adds S9F1, F3, F5, F7, F9, F11, F13 to the message catalog and wires
the two emission paths that the Connection layer can drive without help
from the Router or the application: S9F7 on a body-decode failure and
S9F9 on a T3 transaction-timer timeout.
Catalog (data/messages.yaml -> generated messages.hpp)
All six MHEAD-carrying messages (F1/F3/F5/F7/F9/F11) use the same
shape — a single <B 10> body with the offending 10-byte HSMS header.
S9F13 (conversation timeout) carries <L,2 <A MEXP> <A EDID>>.
Connection-side emissions (src/hsms/connection.cpp)
emit_s9(function, mhead) New private helper. Builds a 9/function/W=0
data message whose body is <B 10> with the
MHEAD bytes, allocates a fresh sys_bytes,
and queues it onto the write path. No
reply is tracked.
S9F7 on body decode handle_data wraps Message::from_body in a
try/catch. Previously any decode error
closed the connection; now it emits S9F7
with the offending header and continues
reading. Reply-side decode failure also
emits S9F7 and surfaces the new
Error::IllegalData to the waiting
ReplyHandler (rather than making the
caller wait out T3).
S9F9 on T3 timeout The send_request T3 callback rebuilds the
original outgoing MHEAD from
(device_id, expected_stream,
expected_function-1, sys, W=1) and emits
S9F9 before invoking the callback with
Error::Timeout (unchanged).
What's intentionally not yet wired (logged in COMPLIANCE.md)
- S9F3 / S9F5 — "unknown stream / function". These need to live in
the Router's fallback path, which would require either the Router
knowing about a Connection-shaped sender or the Connection's
message wrapper learning which streams the Router has handlers
for. Deferred — today the fallback returns SxF0 only.
- S9F11 — "Data Too Long". Currently we close on oversized frames;
we'd need to also build a synthetic 10-byte MHEAD substitute (the
real header isn't yet available at the point of detection) and
flush it through close_after_flush.
Tests + docs
tests/test_messages.cpp Round-trip every S9F* using a representative
10-byte MHEAD literal; check S9F13 carries
MEXP + EDID. +2 cases / +37 assertions.
COMPLIANCE.md Error Messages row moved from "no S9 stream"
to a detailed status describing what's
emitted vs catalog-only. Coverage matrix
expanded per-message (F1/F7/F9/F13 ✅;
F3/F5/F11 🟡 catalog-only).
Build/demo unaffected: 75 cases / 420 assertions pass; the happy-path
demo never trips a decode error or T3, so the S9 path isn't exercised
end-to-end (but unit tests prove the wire shape).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0721db9542 |
Close COMPLIANCE.md gap: spooling (E30 §6.22)
tests / build-and-test (push) Failing after 42s
Implements the largest functional gap from the compliance audit. The
equipment now queues events the host can't immediately receive (either
because there's no SELECTED session or because the demo's force-spool
flag is on) and transmits the queue on host request.
What's new
include/secsgem/gem/store/spool.hpp
SpoolStore: a deque queue with a configurable per-stream whitelist
(so only streams 5+6 spool by default), a max_size cap with FIFO
eviction on overflow, and a `force_spool` test flag. Enqueue
returns one of Queued / Dropped_NotSpoolable / Dropped_Full so the
caller can fall back to live delivery when appropriate. Drain
pops the entire queue in FIFO order. Two new ack enums:
ResetSpoolAck (S2F44 RSPACK) and SpoolRequestAck (S6F24 RSDA), plus
SpoolRequestCode (S6F23 RSDC, Transmit/Purge).
data/messages.yaml + auto-regenerated messages.hpp
S2F43 W <L,n <B stream>> Reset Spooling
S2F44 <L,2 <B RSPACK> <L,a ...>> Reset Spooling Ack
S6F23 W <B RSDC> Request Spooled Data
S6F24 <B RSDA> Request Spooled Data Ack
data/equipment.yaml
`spool:` section: max_size + spoolable_streams list. Two new host
commands SPOOL_ON / SPOOL_OFF that flip the force-spool flag (these
stand in for "host link down" in the demo without dropping TCP).
include/secsgem/gem/store/host_commands.hpp
Spec/Result gain an optional<bool> force_spool field. S2F41
dispatch returns the result, the server applies it after S2F42 is
queued.
src/config/loader.cpp
Reads `spool:` from equipment.yaml; reads `force_spool` from each
host_commands entry; populates SpoolStore + CommandSpec.
apps/secs_server.cpp
New `deliver_or_spool(msg, what)` helper. emit_event and
emit_alarm_set funnel through it: if force_spool is on (or there's
no active session), msg.stream is checked against the spoolable
list and the message is enqueued; otherwise it's sent live.
Two new handlers:
S2F43 parses the stream list, updates SpoolStore, replies S2F44
S6F23 RSDC=Transmit drains and re-sends each as a fresh primary
(posted on the executor so the S6F24 ack flushes first);
RSDC=Purge clears the queue and acks.
The S2F41 handler now also propagates result.force_spool into the
SpoolStore.
apps/secs_client.cpp
Demo extended with 4 new steps after the FAULT branch:
SPOOL_ON -> S2F42 Accept
START -> S2F42 Accept; CEID 300 emission spooled (no live S6F11)
SPOOL_OFF -> S2F42 Accept; queue still has the message
S6F23(Transmit) -> S6F24 Accept; spooled S6F11 arrives next
Then the existing S7F19/S7F5/S10F1/S1F15/Separate flow continues.
tests/test_data_model.cpp
Four new TEST_CASEs for SpoolStore (whitelist, FIFO eviction at
max_size, drain ordering, force flag).
tests/test_loader.cpp
Confirms equipment.yaml's `spool:` section populates the store and
`force_spool: true/false` flows through to dispatch results.
COMPLIANCE.md
Spooling moves from ⬜ to 🟡. Adds S2F43/F44 + S6F23/F24 as ✅ in
the message coverage matrix; calls out what's still missing
(S6F25/F26 notification, automatic activation on HSMS NOT-SELECTED,
persistent on-disk spool).
Verified
- Tests: 73 cases / 383 assertions pass (+4 spool cases).
- Demo (docker compose up server client) walks the full happy path
and the spool path, observed in the server log as:
spool: force_spool=true (depth=0)
spool: S6F11 CEID=300 queued (depth=1)
spool: force_spool=false (depth=1)
S6F23 transmit: draining 1 messages
and on the host side as the queued S6F11 arriving in the correct
order after S6F24.
Known limitations (logged in COMPLIANCE.md)
- Spool activation is manual via SPOOL_ON/OFF rather than
automatically triggered by HSMS NOT-SELECTED.
- No S6F25/F26 spooled-data-ready notification on re-SELECT.
- In-memory only; an equipment restart loses queued events.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
96b02f8b50 |
Initial commit: C++20 SECS-II / HSMS / GEM client + server
A fully containerised SECS/GEM toolchain. Single docker compose project,
no host build tools. 63 unit-test cases / 278 assertions, two demo
executables, end-to-end two-container demo exercising every implemented
capability.
Architecture (bottom-up):
secs2/ E5 SECS-II codec
Item variant over L/A/B/BOOLEAN/I1-8/U1-8/F4/F8
encode/decode big-endian, 1/2/3-byte length encoding
Message SxFy + W-bit + optional root item
to_sml human-readable text rendering
hsms/ E37 HSMS transport (TCP)
Header 10-byte header + SType enum (Data/Select/Deselect/
Linktest/Reject/Separate)
Frame 4-byte length prefix + payload encode/decode
Connection async Asio TCP, NOT-SELECTED -> SELECTED state machine,
T3/T5/T6/T7/T8 timers, system-bytes reply correlation,
graceful close-after-flush separation
endpoint active Client (connect with T5 retry) and passive Server
(accept loop) wrappers over Connection
gem/ E30 GEM logic
ControlStateMachine 5-state E30 control model with operator
actions, host requests, SEMI-mandated ack
codes (OnlineAck, OfflineAck, CommAck), and
a state-change handler
EquipmentDataModel in-memory dictionary: SVIDs, DVIDs, ECIDs
(with EAC), CEIDs, report defs, CEID->report
links, enabled-events set, alarm table
(ALCD, enabled, active), process programs,
host command registry, clock (16-char
YYYYMMDDhhmmsscc with offset)
messages.hpp builders + parsers for every SxFy below
GEM message coverage (full list):
S1F1/F2 Are You There / On Line Data
S1F3/F4 Selected Equipment Status Request / Data
S1F11/F12 Status Variable Namelist Request / Data
S1F13/F14 Establish Communications (+ CommAck)
S1F15/F16 Request OFFLINE (+ OfflineAck)
S1F17/F18 Request ONLINE (+ OnlineAck)
S2F13/F14 Equipment Constant Request / Data
S2F15/F16 EC Send + EquipmentAck (Accept/UnknownEcid/Busy/OutOfRange)
S2F17/F18 Date and Time Request / Data
S2F29/F30 Equipment Constant Namelist Request / Data
S2F31/F32 Date and Time Set Request / TimeAck
S2F33/F34 Define Report + DefineReportAck (5 enum values)
S2F35/F36 Link Event Report + LinkEventAck
S2F37/F38 Enable / Disable Event Report + EnableEventAck
S2F41/F42 Host Command + HostCmdAck (7 values) + per-param CPACKs
S5F1/F2 Alarm Report Send + AlarmAck (ALCD bit-7 set/cleared
+ lower-7 category)
S5F3/F4 Enable/Disable Alarm Send + AlarmAck
S5F5/F6 List Alarms Request / Data (active alarms tagged in ALCD)
S6F11/F12 Event Report Send (equipment-initiated CEID emission
with full report data) + EventReportAck
S7F3/F4 Process Program Send + ProcessProgramAck (7 values)
S7F5/F6 Process Program Request / Data
S7F19/F20 Current EPPD List Request / Data
S10F1/F2 Terminal Display Single (host->equipment) + TerminalAck
S10F3/F4 Terminal Display Single (equipment->host)
Demo apps:
apps/secs_server.cpp passive equipment. Populates the data model
with 3 SVIDs (ControlState, Clock,
EventsEnabled), 2 ECIDs, 3 CEIDs
(ControlStateChanged, AlarmSetEvent,
ProcessStarted), 2 alarms (Chiller Temp High
cat 4, Door Open cat 1), 2 recipes
(RECIPE-A, RECIPE-B), and 4 host commands
(START, STOP, PAUSE, FAULT). Emits S6F11 on
every control state transition + on START;
emits S5F1 + the AlarmSetEvent CEID on FAULT.
Pushes an S10F3 welcome message when the host
comes online.
apps/secs_client.cpp active host. Walks 17 steps: Establish ->
Online -> S1F11 SVID namelist -> S1F3 read ->
S2F29 EC namelist -> S2F13 read ->
S2F17 clock -> S2F33/S2F35/S2F37 dynamic
event subscription -> S2F41 START
(-> receives S6F11) -> S5F5 alarm list ->
S5F3 enable alarm 1 -> S2F41 FAULT
(-> receives S5F1 + S6F11) -> S7F19/S7F5
recipe list + body -> S10F1 terminal ->
S1F15 Offline -> Separate. Handles inbound
S6F11, S5F1, S10F3 primaries.
Testing:
tests/test_secs2.cpp codec round-trip for every format,
byte-layout assertions for known values,
truncation/trailing-byte rejection,
nested list round-trip, SML rendering
tests/test_hsms.cpp header byte layout, data + control
header round-trip, full frame round-
trip with length prefix, short-payload
rejection
tests/test_control_state.cpp every (state, event) pair in the E30
control state machine, including
AlreadyOnline / NotAccept rejections
and idempotent offline-while-offline
tests/test_data_model.cpp SVID/ECID/Alarm/Recipe CRUD, clock
format + parse, host command registry,
full event-report pipeline (define ->
link -> enable -> compose) with
every error path (InvalidVid,
UnknownCeid, UnknownRptid), alarm
set/clear with ALCD bit-7 semantics
tests/test_messages.cpp round-trip + byte-layout for every
builder/parser pair, including S6F11
event reports with mixed item types
Toolchain:
Dockerfile Ubuntu 24.04, g++-13, CMake, Ninja, libasio-dev
docker-compose.yml builder / tests / server / client services,
source bind-mounted, build artifacts in a
named volume so the host tree stays clean
CMakeLists.txt C++20, -Wall -Wextra -Wpedantic, standalone
Asio (ASIO_STANDALONE), doctest via FetchContent
Documentation:
README.md architecture, quick start, demo log
COMPLIANCE.md honest per-capability E5/E30/E37 audit with
spec section refs. Calls out what's implemented,
what's partial (Reject.req, Alarms missing F7/F8,
EC range validation, PP without verify, terminal
single-line only), and what's intentionally not
yet implemented (spooling, S9 error stream,
Documentation S1F19/F20+F21/F22, limits monitoring,
trace data collection, multi-block, material
movement). Does NOT claim "100% GEM-compliant" and
lists the work required to honestly make that claim.
This is Layer 0 + the start of Layer 1 from implementation_plan.md.
The transition-table-driven "spec-as-data" architecture (Layer 1
proper) is not yet implemented; the current code uses imperative
state machines that are structurally ready to be refactored onto
tables.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|