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>
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>
Per-substrate transition history now survives restart. Each entry's
steady_clock timestamp is written as a system_clock-millis snapshot;
on replay the steady_clock time_point is reconstructed relative to
the current (steady_now, system_now) pair, so inter-event spacing
is preserved across restarts even if the FSM is in a different
process. Absolute wall-clock accuracy degrades by any NTP step
that happened between write and read; that's a documented caveat.
Record format goes v1 → v2. v1 (history-less) records still load,
just with empty history.
Test updates:
- the old "history is NOT journaled" test is REPLACED with one
that asserts every axis + event + label round-trips.
- hand-crafted v1 record on disk still loads (proves backwards
compat).
- 15 ms-spaced events restore with their spacing intact (±slop
for scheduler jitter).
Closes the "substrate history persistence" caveat from the post-#1-13
status writeup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the v1 caveat: the optional E40-0705 trailers on S16F11 —
recipe variables (RcpVar) and process parameters (ProcessParam),
each carrying a secs2::Item value of arbitrary type — now survive
restart.
Record format bumps to v2:
v2 header = v1 header
+ [u16 rcpvar_count][repeat: u16 name_len, name, u32 enc_len,
secs2::encode(value)]
+ [u16 ppparam_count][...same shape]
v1 records are still accepted by load_record_ (no extras come back).
Two new tests:
- round-trip mixed F4 / ASCII / U4 / nested-list values through
rcpvars + prprocessparams
- hand-crafted v1 record on disk still loads cleanly, just with
empty extras (proves backwards compat)
Closes the "PJ rcpvars / prprocessparams persistence" caveat from
the post-#1-13 status writeup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Real GEM sessions don't serialize requests — the host can have many
primaries outstanding, replies may arrive in any order, and both
peers can talk at once. Connection demuxes via system_bytes per
E37 §8.3; this commit pins the behaviour with four wire tests:
- 5 in-flight requests; equipment buffers all primaries before
replying — proves Connection holds the pending map correctly
even when no replies are coming.
- 7 pipelined primaries with synchronous in-handler replies;
every host callback fires with the correct function and stream.
- Bidirectional in-flight: host issues 3 primaries while equipment
issues 3 of its own; all 6 callbacks resolve with the right
replies.
- 100-burst sequential cycle; confirms the pending_requests_ map
doesn't leak entries (every reply delivered ⇒ map drained).
Closes#13 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SEMI E5 allows identifier fields (DATAID, RPTID, VID, CEID, ALID,
EXID, OBJID, …) to be encoded as U1, U2, U4, or U8. Our parsers
route through any_unsigned_first<T> in messages_helpers.hpp. The
existing per-message round-trip tests prove the U4 path; this
commit adds the cross-width matrix that the interop incident with
secsgem-py demanded:
- as_u4_scalar accepts U1/U2/U4/U8 inputs for the same value
- as_u8_scalar accepts every narrower width
- as_u1_scalar accepts wider widths when the value fits
- as_u1_scalar / as_u2_scalar REJECT out-of-range values rather
than silently truncating
- codec round-trip preserves the format byte AND the value
- signed counterparts (as_i4_scalar) follow the same rule for I1/I2
If a future code-gen change hard-codes a single width on any
identifier field, the rejection case here breaks loudly.
Closes#12 in the test-gap backlog (renumbered: this is gap entry
"identifier wildcard matrix").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four new test cases:
* S3F19 verify with matching map → SlotMapVerifyAck::Accept and
CSMS lands in Read on the equipment side.
* S3F19 verify with disagreeing map → Mismatch ack and CSMS lands
in Mismatched.
* 4 LPs + 4 carriers, host verifies CAR-1 (mismatch) and CAR-3
(match) — only those two carriers move on the CSMS axis;
CAR-2/CAR-4 stay NotRead. Confirms per-carrier independence.
* Multi-LP E84 handshake sequencing (load then unload) round-trips
through Idle. Documents that the current E84StateMachine is
per-equipment, not per-port — a future per-port FSM would
update this test alongside.
Closes#11 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test_gem300_scenario.cpp drives EquipmentDataModel in-memory. This
companion test does the same lifecycle through actual hsms::Connection
frames on a loopback socket pair:
S1F13/F14 establish comm
S3F17/F18 carrier action ProceedWithCarrier (E87)
S16F11/F12 process job create (E40)
S14F9/F10 control job create (E94)
S16F27/F28 CJSTART → CJ → Executing
S6F11 ControlJobExecuting CEID auto-emitted on transition
CJ → Completed via internal AllJobsComplete
EquipmentEmulator owns the data model + a passive Connection,
registers state-change handlers that synthesize S6F11/S16F9 on
transitions, and dispatches the inbound primaries above. HostEmulator
wraps the active Connection and captures everything the equipment
sends unsolicited.
This is the wire-level equivalent of the existing in-memory scenario,
which closes the gap between "FSM works" and "full GEM 300 stack
works on a wire".
Closes#10 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FSM unit tests already verified state transitions fire the change
handler — but they don't prove the frame leaves the socket with the
right CEID and linked report payload. This commit wires a passive
equipment Connection to an EquipmentDataModel via a small emitter,
drives transitions, and asserts on what the host peer receives.
Six new tests:
EPT → Productive ⇒ S6F11(kCeidProductive) with the linked report
EPT (no subscription) ⇒ no S6F11 (proves disable gate)
PJ Queued→SettingUp ⇒ S16F9 PRJobAlert with PRJOBID + state byte
PJ alert_enabled=false ⇒ no S16F9 (per-PJ gate works)
CJ → Executing ⇒ S6F11(ControlJobExecuting) on the wire
Substrate StartProcessing ⇒ S6F11(SubstrateInProcess) on the wire
All use the generated parse_s6f11 / parse_s16f9 to decode the
incoming frame and assert against typed fields (CEID, PRJOBID, etc.)
rather than poking variant internals — that ties the test to the
schema-as-data rather than to wire byte offsets.
Closes#9 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deterministic-seed fuzz coverage of the byte-decoding surface:
- secs2::decode on 2000 random buffers
- secs2::decode on every truncation of a real encoding + 500
one-byte flips of the full encoding
- hsms::Frame::decode on 1000 random payloads
- hsms::Header::decode on 2000 random 10-byte buffers
- secsi::Block::decode on 2000 random buffers
- secs2 encode/decode round-trip identity across a battery of every
Item factory (List, ASCII, Binary, Boolean, U1..U8, I1..I8, F4/F8,
nested List)
- oversize <A 3 length-bytes> length-prefix doesn't allocate GBs
- 64-level nested List round-trip doesn't blow the stack
Contract is binary: no crash, no UB. Each decoder is allowed to throw
or return whatever; we deliberately don't assert *what* result comes
back, only that control returns. Fixed PRNG seeds make any failure
reproducible from the CI log alone.
Closes#8 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test_secsi.cpp covered T2 on the send side (retry) and a tick-based
back-to-back exchange. This commit fills in the rest of the timer
matrix at FSM level:
T1 in RecvBlock → abort, reason mentions "T1"
T1 outside RecvBlock → ignored
T2 in RecvEotSent → abort
T2 in RecvBlock → abort (mid-block stall)
T3 / T4 → FSM-level no-op (documented as upper-layer driven)
T2 contrast → send-side retries, recv-side aborts (same timer,
different recovery, both demonstrated in one test)
If a future commit moves T3 or T4 enforcement into the FSM, the
no-op test breaks loudly so protocol.hpp can be updated alongside.
Closes#7 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
S9F3/F5 are covered by test_s9_fallback (router path); S9F9/F11 by
test_hsms_timers (timer/over-length). This commit adds S9F7 wire-level
tests for the third path — a primary whose body fails secs2::decode.
Three new cases:
- hand-built primary with truncated <B> body provokes S9F7
carrying the original 10-byte MHEAD (sys + stream + function)
- emission is non-fatal: the next well-formed primary still routes
to the registered handler
- data-while-NOT-SELECTED still echoes Reject(EntityNotSelected)
(sanity copy of the test_hsms_connection case so the "what does
the equipment say when a peer sends garbage" family lives together)
Closes#6 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Real-socket tests for the timer family in E37 §10 — these replace
the "the timer fires somewhere" implicit assumption with
end-to-end observations on a loopback pair:
T3: send_request that gets no reply emits S9F9 with the original
MHEAD echoed in the body and surfaces Timeout to the caller.
T6: active mode whose Select.req goes unanswered self-closes
with a "T6 timeout on Select" reason.
T7: passive mode that never receives Select.req self-closes
with a "T7 not-selected timeout" reason.
T8: peer sends only the 4-byte length prefix; T8 expires mid-read
and closes with "T8 intercharacter timeout".
Plus S9F11 emission for an over-length frame (length prefix of
1 GiB+1) — body's <B 10> echoes the offending bytes verbatim.
Per-test timer profiles (only the timer under test is short, the
rest are 5s) so the FSM isn't racing against unrelated timers.
Closes#5 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-EXID binary record (.ex), magic + version + atomic .tmp+rename.
Records full E5 §9 lifecycle: state, EXID, EXTYPE, EXMESSAGE, and
the candidate EXRECVRA list.
Cleared exceptions are terminal — the FSM transitions through
Cleared remove the in-memory entry AND delete the journal file
(matching the existing in-memory semantics). Recovering /
RecoverFailed states survive restart: the application can decide
on replay whether to retry recovery or abort.
Five new tests cover post+replay, Recovering-survives-restart,
autonomous-clear cleanup, RecoverFailed retry post-restart, and
corrupt-record drop.
This completes #12 in the test-gap backlog (persistence for the four
in-memory stores beyond Spool).
Closes#4 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-job binary record (.pj / .cj) with magic+version, atomic
.tmp+rename. PJ store additionally writes an order.idx index file
that preserves HOQ-aware queue position across restarts.
Rcpvars / prprocessparams (secs2::Item variants) are intentionally
out of scope for v1 — they're optional E40 trailers and need a body
codec round-trip; callers re-populate via set_e40_extras() after
restart.
Five new tests cover full lifecycle replay (Processing mid-run +
HOQ-reordered queue), dequeue-deletes-file, corrupt-record drop,
CJ state + PJ-list replay, and CJ remove cleanup.
Closes#3 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same pattern as carriers: per-substrate binary record (.sub) with
atomic .tmp+rename, replay on enable, delete on remove. Records
current state across all three E90 axes (location / processing /
ID-status), plus substid / carrierid / slot / free-form location
label. History is deliberately NOT journaled — it's an in-memory
ring buffer and rebuilding from replayed state would mislead.
Five new tests cover full-axis replay, every terminal processing
state, remove-deletes-journal, corrupt-record drop, and the
history-is-transient invariant.
Closes#2 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors SpoolStore: per-record file with atomic .tmp+rename, magic+
version-prefixed binary layout, replay on enable, delete on remove.
FSMs gain a restore_state() that bypasses the transition table and
handlers since a replay isn't a transition.
Six new tests cover write+restart+replay across every CIDS/CSMS/CAS
axis, remove-deletes-journal, malformed-record drop-not-poison, and
the persistence-disabled no-op path.
Closes#1 in the test-gap backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A host couldn't drive the new messages through the HostHandler class —
only the server side knew how to dispatch them. Adds six new senders
plus a unit test that walks each through a real loopback connection:
* send_legacy_remote_command -> S2F21
* send_event_report_request -> S6F15
* send_individual_report_request -> S6F19
* send_annotated_report_request -> S6F21
* send_pp_load_inquire -> S7F1
* send_delete_pp -> S7F17
Suite: 296 cases / 1571 assertions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the test gap for messages I added but whose reply parsers were
only generated, never exercised:
* S6F8 — full nested DATAID/CEID/DS/DV structure.
* S12F14 — row-format map reply (RSINF tuples).
* S12F16 — array-format map reply.
* S12F18 — coordinate-format map reply.
Suite: 295 cases / 1545 assertions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds opt-in disk persistence to SpoolStore. `enable_persistence(dir)`
turns every enqueue into a single `<seq>.spool` file alongside the
in-memory queue; drain and clear delete the matching files; restart
replays the directory sorted by seq.
Writes are atomic: serialize the message via the SECS-II codec, write
to `.tmp`, then `std::filesystem::rename` to the final name. Malformed
records are dropped silently so a single bad file can't poison the
whole spool.
`secs_server --spool-dir <path>` enables persistence at startup.
Without the flag the behaviour is identical to before (in-memory only).
Two new tests: enqueue → restart → replay → drain restores the wire
order, and clear deletes the journal files.
Test suite: 291 cases / 1515 assertions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the simplified <L,3 PRJOBID PPID MTRLOUTSPEC> demo body with
the full SEMI E40-0705 §10.2 shape:
<L,5 PRJOBID MF PRRECIPEMETHOD
<L,2 PPID <L,n <L,2 RCPPARNM RCPPARVAL>>>
<L,n MTRLOUTSPEC>
<L,n <L,2 PARAMNAME PARAMVAL>>>
ProcessJob now carries the extra fields (MaterialFlag, ProcessRecipeMethod,
RcpVar[], ProcessParam[]) so a tool's recipe engine can later consume
the recipe-variable overrides and per-job process parameters. Server
S16F11 dispatch populates them via the new ProcessJobStore::set_e40_extras
helper after a successful create.
MaterialFlag + ProcessRecipeMethod enums live in their own tiny header
(`e40_constants.hpp`) so process_jobs.hpp (the store) can use them
without dragging in messages_helpers.hpp (which would create a circular
include via data_model.hpp).
The simplified 3-arg HostHandler::send_create_process_job convenience
remains; it constructs a sensible-default PRJobCreateRequest internally.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the SECS-II messages secsgem-py 0.3.0 ships but our C++ catalog
didn't have, plus the alternative wafer-map formats from E5 §13.
None of these were strictly required for GEM core compliance, but
they're the messages a host might send to a conformant equipment.
* S7F1/F2 — Process Program Load Inquire / Grant. Equipment-side
space-and-policy check before a host commits to S7F3.
* S7F17/F18 — Delete Process Program. Empty list = delete-all.
* S6F5/F6 — Multi-block Data Send Inquire / Grant (with MultiBlockGrant
enum: Ok/Busy/NoSpace/DuplicateMsg/BadMsg).
* S6F7/F8 — Data Transfer Request / Send. Host pulls a DATAID;
equipment replies with the nested DS/DV structure.
* S6F15/F16 — Event Report Request (host-initiated). Reply mirrors
the unsolicited S6F11.
* S6F19/F20 — Individual Report Request (RPTID -> values).
* S6F21/F22 — Annotated Individual Report Request (RPTID -> (VID, value)).
* S2F21/F22 — Legacy Remote Command (no parameter list). Delegates
to the same HostCommandRegistry as S2F41.
* S12F9/F10 — Map Data Send (array format, MAPFT=1).
* S12F11/F12 — Map Data Send (coordinate format, MAPFT=2).
* S12F13/F14, F15/F16, F17/F18 — Map Data Request variants for the
row, array, and coordinate formats.
11 new round-trip tests; suite at 289 cases / 1495 assertions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The biggest single gap I called out in the GEM300 audit — closed.
E84 is the digital handshake between AMHS (Automated Material
Handling System) and the equipment for carrier load/unload. Unlike
the rest of GEM300, this isn't SECS messaging; it's a fixed set of
ten parallel boolean wires that follow a strict sequencing protocol
(E84-0710 §6.3).
Adds:
E84Signal enum CS_0/CS_1/VALID/TR_REQ/BUSY/COMPT/L_REQ/U_REQ/
READY/ES
E84SignalSet 10-bit bitmap with bool get/set
E84State Idle / CarrierPresent / ValidAsserted /
LoadReady / UnloadReady / Transferring /
Complete / EmergencyStop
E84StateMachine re-evaluates state on every signal change,
observable via set_state_change_handler
Joins EquipmentDataModel as `e84` (top-level — there's one per tool,
not per port). ES (emergency stop) dominates regardless of other
signals; COMPT and BUSY override the VALID-handshake states. Same
FSM drives real opto-isolated I/O lines (when wired through an
asio digital input adapter) and the back-to-back test simulation.
Six test cases cover the full load handshake trace (six transitions,
including the transient LoadReady-after-BUSY-drops state), the
unload variant via U_REQ, ES dominance + recovery, reset(), and
no-op suppression for idempotent signal writes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends the existing Clock with the metrics a host needs to gate
time-sensitive data against the equipment's sync state (E148 §6.3):
offset_seconds() current applied offset vs system clock
last_drift_seconds() signed drift observed at the most recent sync
sync_count() how many successful syncs have happened
sync_quality() Synchronized (|drift|<=1s) /
Drifting (<=60s) / Unsynchronized (>60s or
never synced)
The thresholds are tuneable per call; the defaults match typical fab
practice but the application can pass tighter bounds for tracelog-
sensitive flows. set_time_string() now snapshots the apparent delta
between the previously-applied offset and the new one as
last_drift_seconds_ at the moment of resync; no background timer.
Three new test cases cover the initial Unsynchronized state, a large
forward drift registering as Unsynchronized, and a same-value resync
landing as Synchronized.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The catalog had S14F9/F10 / F11/F12 specialized for E94 ControlJob;
this commit adds the generic E14 attribute access pair, the most-
queried half of the E39 surface area, backed by the CemObjectStore.
S14F1 / F2 GetAttr — OBJSPEC + OBJTYPE + ATTRID list ->
(ATTRID, VALUE) pairs + OBJACK
S14F3 / F4 SetAttr — same addressing, applies ATTRID/VALUE pairs,
reply echoes stored values + OBJACK
Server dispatches both into the CemObjectStore added in tranche G.
OBJTYPE validation is case-sensitive against the CemObjectType name
(Equipment / Subsystem / IODevice / Module / MaterialLocation).
Unknown objects return Denied_UnknownObject; type mismatches return
Denied_InvalidAttribute.
The shared AttrValue struct is declared external_struct: true on
F3/F4 so both directions share the same C++ type.
Two round-trip tests cover both pairs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the two E40 bulk/control gaps the COMPLIANCE doc had flagged
as out-of-scope:
S16F7 / F8 PRJobMonitor — host enables/disables S16F9 alerts
per PJ. PRALERT bit 7 is the enable flag (matches the
ALED convention from S5F3). Server dispatches into the
existing set_alert() store API.
S16F15 / F16 PRJobCreateMultiple — bulk create variant. Host posts
a list of (PRJOBID, PPID, MTRLOUTSPEC) entries; the
equipment processes them in order and returns a
per-PJ HCACK list so the host can identify which
subset failed. Same validators as S16F11.
Catalog now has three new structs: PRJobMonitorEntry,
PRJobCreateEntry, PRJobCreateMultiResult. Two round-trip tests cover
the new wire shapes; server-side correctness is exercised through the
existing PJ store invariants (dedup, validator) which both new paths
delegate to.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the slot-map verification gap I called out:
S3F19 / F20 host -> equip: verify expected slot map against what
the equipment has scanned. Equipment compares element-
wise; on match drives CSMS NotRead -> Read and replies
SVACK=Accept; on mismatch drives CSMS -> Mismatched and
replies SVACK=Mismatch.
S3F21 / F22 equip -> host: equipment-initiated slot map report
(typically pushed after CARRIERID is confirmed).
New SVACK enum: SlotMapVerifyAck { Accept, Mismatch, CarrierUnknown,
Error }. Server dispatch on S3F19 wires the actual CSMS transition
through the CarrierStore from D3.
Two round-trip tests cover both pairs; the FSM-driving behaviour is
exercised through the in-process tests because secs_server.cpp is
the dispatch entry point (no separate integration test needed beyond
the wire round-trip).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the substrate-ID verification FSM that E90 §6.4.6 calls for:
NotConfirmed initial; equipment hasn't read the ID yet
WaitingForHost ID has been read; awaiting host accept/reject
Confirmed host confirmed (or force-bound)
Mismatched host rejected — recoverable via Bind
Events:
Read NotConfirmed -> WaitingForHost
Confirm WaitingForHost -> Confirmed
Mismatch WaitingForHost -> Mismatched
Bind any -> Confirmed (force-bind)
Reset any -> NotConfirmed
Wire-byte values pinned via static_assert. The third axis is now
exposed on SubstrateStateMachine alongside location_state() and
processing_state(); set_id_handler() observes transitions. Existing
two-axis API is unchanged.
4 new test cases cover the happy path, Mismatch+Bind recovery, Reset
from any state, and same-state event handler suppression.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each Substrate now retains an append-only history of state transitions
(both location and processing axes), the triggering event captured as
a std::variant<SubstrateEvent, SubstrateProcessingEvent>, the location
label at the time, and a steady_clock timestamp.
E90 §6.6 requires the equipment to be able to report a wafer's
processing history — typically queried via S6F11 batched reports or
SVID reads. This commit lays the runtime substrate; wire query
plumbing is the natural follow-up.
set_history_limit(n) caps per-substrate retention (default 256, 0 =
unbounded). Oldest entries are dropped when the cap is reached;
vector-erase is fine at this scale (typical wafer lifecycle is a few
dozen transitions).
Two new test cases cover the recording invariants (every fire results
in one history entry on the right axis) and history_limit eviction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EptStateMachine now retains per-state cumulative dwell time so the
host can read it as SVIDs. The implementation is zero-overhead while
the FSM is idle (no timers, no background work) — on every transition
we add the prior state's dwell to its bucket and reset the entered_
timestamp. Live dwell in the current state is included in
accumulated() via a now-vs-entered_ delta at read time.
New public API:
accumulated(EptState) per-state cumulative ms (incl. live dwell)
total_elapsed() denominator for utilization ratios
reset_history() S2F43-style history clear
This closes the gap I called out: previously we emitted CEIDs on
transition but didn't accumulate the bucket the host actually queries
for utilization metrics. Wiring these into specific SVIDs is the
application's job (equipment.yaml declares SVIDs against any read
callable); the runtime data is now there.
4 new test cases cover accumulation, live-dwell inclusion, and reset.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The S9F3/F5 fallback was previously inlined in apps/secs_server.cpp;
this commit lifts it onto Router as a template helper and adds two
focused tests asserting the wire behaviour against a real back-to-
back HSMS Connection pair.
template <typename EmitFn, typename HeaderProvider>
std::optional<Message> dispatch_with_s9(emit, header, msg);
The helper does the has_handler / has_handler_for_stream check and
calls the supplied emit function with S9F3 (unknown stream) or S9F5
(unknown function in known stream). The header_provider returns the
optional MHEAD bytes — keeping the helper free of any direct
Connection coupling.
Tests:
- SUT registered only for S1F1; peer sends S1F5 -> SUT replies
S9F5 to the peer.
- SUT registered only for S1F1; peer sends S7F19 -> SUT replies
S9F3 to the peer.
Closes Tranche I — SML parser and the auto-S9F* fallback closeout
both verified end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds parse_sml(text) -> Item / try_parse_sml(text) -> optional<Item>
in secs2/sml.hpp. Round-trips with the existing to_sml() emitter for
every Item shape the codec produces: lists with nesting, ASCII / JIS8,
Binary (decimal and 0xHH literals), Boolean (T/F or 1/0, scalar and
multi-element), U1-U8 / I1-I8 / F4 / F8 vectors, and the optional
`[n]` count syntax (accepted but not enforced).
The parser is whitespace-insensitive outside quoted strings and uses
a small Cursor type for read_word / read_quoted / skip_ws. Numeric
literals go through strtoul/strtoll/strtod so SML can carry hex,
octal, and decimal interchangeably (the emitter writes hex for Binary
and decimal everywhere else).
11 test cases cover the full round-trip surface, the whitespace
invariant, unknown-tag rejection, the try_parse error-swallowing
variant, and the optional `[n]` count.
secsgem-py has secs/sml.py for the same purpose; this brings the C++
port to parity on the tooling side.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-module process-tracking state machine. An E157 instance models a
single recipe step at a single module, with the canonical lifecycle:
NotExecuting -> GeneralExecuting (StartGeneral)
-> StepExecuting (StartStep)
-> StepCompleted (CompleteStep)
Plus universal escape hatches: Reset returns any state to
NotExecuting; Abort terminates from any state to StepCompleted.
ModuleStore wraps the FSM with the now-standard pattern:
- non-movable (this-capture lambdas)
- per-module bind() carries current_substid + recipe_step
- fire(module_id, event) delegates to the FSM
- set_state_change_handler observes every transition with module_id
Joins EquipmentDataModel. 5 test cases cover happy path, Reset from
each interior state, Abort, store-level create dedup + bind, and the
multi-module change handler keying.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hierarchical object tree for equipment self-description. Each object
carries a CemObjectType (Equipment / Subsystem / IODevice / Module /
MaterialLocation / Other), an optional parent_objid, and a flat
attribute map keyed by name (the wire shape S14F1 / F3 returns).
Operations covered:
add(CemObject) - dedup'd, validates parent exists
get / has - lookup by objid
get_attr / set_attr - E14 GetAttr / SetAttr semantics
children(parent) - tree traversal; empty parent = roots
The flat-map representation matches how E14 ObjectService traffic
addresses nodes (by OBJSPEC string). Wiring S14F1/F2 GetAttr and
S14F3/F4 SetAttr to this store is a downstream commit; the data model
is what was missing.
Joins EquipmentDataModel alongside the other top-level stores. Three
test cases cover hierarchical add+dedup, children() traversal, and
get/set/missing attribute semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the six E116-0712 §6.2 buckets for classifying equipment time:
NonScheduledTime (0) not scheduled to operate
ScheduledDowntime (1) planned maintenance window
UnscheduledDowntime (2) faults / unplanned stoppage
Engineering (3) engineering / qualification time
Standby (4) idle but available
Productive (5) actively producing
Wire-byte values pinned via static_assert to E116 §10.3.
The FSM is a classifier rather than a strict lifecycle — every
(state, event) pair is legal — but it remains data-driven through the
shared CarrierTransitionTable template so the default cross-product is
expressible declaratively.
The state-change handler also surfaces dwell time (how long the
previous state was held) computed off std::chrono::steady_clock, so
accounting code can compute MTBF / availability / utilization from a
single source without maintaining a parallel timestamp log.
4 test cases cover the initial state, every event firing, dwell-time
reporting, and the no-op same-state event (no handler call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the Carrier Management wire surface that drives the FSMs from D1:
S3F17 / S3F18 CarrierAction (host issues ProceedWithCarrier /
CancelCarrier / BindCarrierID / etc.; CommandParameter
list is reused from S2F41).
S3F23 / S3F24 PortGroupChangeReport (equipment notifies host of port
group composition changes).
S3F25 / S3F26 CarrierTransfer (host instructs source -> target port
transfer).
S3F27 / S3F28 CancelCarrier (host cancels an outstanding carrier op).
Two new ack enums in messages_helpers.hpp:
CarrierActionAck — CAACK byte; covers the common error responses
(CarrierIDUnknown, Inaccessible, ActionInProgress).
PortGroupAck — PGACK byte (Accept / Error).
Round-trip tests for all four pairs. Server-side dispatch + the
CarrierStore + LoadPortStore that the FSMs key into land in D3/D4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-carrier triple FSM: CIDS (id verification), CSMS (slot-map), CAS
(access). Per-port triple FSM: LPTS (transfer), LRS (reservation), LAS
(association). Wire-byte enum values pinned via static_assert to match
E87-0716 §10.3.
CarrierStateMachine combines the three carrier-side FSMs because they
are independent but always observed together; same for LoadPortState-
Machine. Generic CarrierTransitionTable<State, Event> template is
reused across all six tables — same row shape as the PJ/CJ/Exception
tables that already exist.
Default tables cover the spec's documented transitions:
CIDS: NotConfirmed <-> Confirmed/Mismatched/Unknown, Cancel returns
to NotConfirmed from any state, Bind force-confirms.
CSMS: NotRead -> Read -> {Mismatched, Reset}.
CAS: NotAccessed -> InAccess -> Complete (terminal).
LPTS: OutOfService <-> InService <-> Loading/Unloading.
LRS / LAS: simple boolean toggle pairs.
15 test cases assert the happy-path lifecycles, cross-state cancels,
and that change handlers fire only on real transitions (Read in
NotConfirmed is a no-op, not a handler call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ALCD's lower 7 bits are a bitmap of category flags per E5 §10.3 and
E30 §6.13; a single alarm may carry multiple categories at once
(e.g. an irrecoverable equipment-safety fault is 0x10 | 0x02).
Adds:
enum class AlarmSeverity : uint8_t
PersonalSafety EquipmentSafety ParameterError ParameterWarning
Irrecoverable EquipmentStatus Attention
has_severity(alcd, bit), severity_bits(alcd)
Alarm::has(bit), Alarm::is_safety()
constexpr severity_mask = 0x7F
Tests cover single-category alarms, multi-category combos, and that
the bit-7 SET/CLEAR flag is correctly excluded from category bits.
Closes Tranche C (E5 alarm/exception state model).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-EXID exception lifecycle for E5 §9. States mirror the wire flow:
Posted equipment sent S5F9, awaiting host or autonomous clear
Recovering host's S5F13 accepted; equipment running recovery
RecoverFailed S5F15 reported a failed result; host may retry
Cleared terminal — store removes the row
Events:
Created synthetic NoState->Posted observer signal
Recover host's S5F13 (Posted/RecoverFailed -> Recovering)
RecoveryComplete equipment internal (Recovering -> Cleared)
RecoveryFailed equipment internal (Recovering -> RecoverFailed)
RecoveryAbort host's S5F17 (Recovering -> Posted)
Clear equipment internal (Posted/RecoverFailed -> Cleared)
ExceptionStore mirrors ProcessJobStore: per-EXID FSMs heap-allocated via
unique_ptr, non-movable to keep `this`-captures safe, synthetic Created
fires after the row lands so observers can decide whether to emit S5F9
out of band. on_recover validates EXRECVRA against the candidates the
post advertised.
The store joins EquipmentDataModel alongside process_jobs / control_jobs.
S5F9-F18 server-side dispatch lands in C2.
Tests (12 cases) cover FSM transitions including retry, abort, and
autonomous clear, plus store-level duplicate-rejection, EXRECVRA
validation, and Cleared-removes-the-row semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five end-to-end tests wire a real HostHandler against a real passive
HSMS Connection over a TCP loopback pair and assert wire-level
behaviour matches expectations:
- establish_communication + go_remote sequence S1F13 then S1F17
- send_remote_command produces a wire-correct S2F41 the equipment
can re-parse with parse_s2f41 and recover CPNAME/CPVAL
- send_terminal_display round-trips through S10F1/F2
- E40/E94 create+command sequence (S16F11, S14F9, S16F5)
- Inbound S5F1 alarm fires the host's alarm observer + auto-acks
Each test uses the existing pump_until / SocketPair harness pattern
from test_hsms_connection.cpp. The recorder pattern keeps the
equipment-side dispatch table small — every test installs the same
canned reply handler.
This closes Tranche B (host mode). HostHandler now has the inbound
+ outbound surface secsgem-py's GemHostHandler exposes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>