Commit Graph

56 Commits

Author SHA1 Message Date
raphael 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>
2026-06-09 11:58:03 +02:00
raphael 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>
2026-06-09 11:52:43 +02:00
raphael 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>
2026-06-09 11:50:18 +02:00
raphael 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>
2026-06-09 11:40:38 +02:00
raphael 998e81b3d8 persistence: substrate history journaling in v2 record
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>
2026-06-09 11:34:54 +02:00
raphael d9f23d6db8 persistence: PJ rcpvars + prprocessparams in v2 record format
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>
2026-06-09 11:31:58 +02:00
raphael b3bde7f087 persistence: ExceptionStore enable_persistence(dir)
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>
2026-06-09 10:37:36 +02:00
raphael 1189ffc994 persistence: ProcessJobStore + ControlJobStore enable_persistence(dir)
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>
2026-06-09 10:35:26 +02:00
raphael 1548b49afd persistence: SubstrateStore enable_persistence(dir)
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>
2026-06-09 10:31:54 +02:00
raphael f56639ba17 persistence: CarrierStore + LoadPortStore enable_persistence(dir)
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>
2026-06-09 10:25:50 +02:00
raphael 29f646c7ca HostHandler: senders for the AA tranche messages
tests / build-and-test (push) Failing after 34s
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>
2026-06-09 01:20:23 +02:00
raphael a1dc7937d4 test: live persistent-spool restart end-to-end
Adds a docker-compose service `server-spool` that runs secs_server
with --spool-dir pointed at a named volume.  Two-phase Python
harness (interop/spool_persistence_test.py):

  1. Enqueue phase: force-spool one S6F11(CEID=300) via the
     SPOOL_ON / START / SPOOL_OFF RCMD trio, then disconnect.
  2. Driver runs `docker compose restart server-spool` between
     the phases — the named volume preserves the journal files.
  3. Drain phase: reconnect, send S6F23(Transmit), verify the
     replayed S6F11 carries CEID 300.

Surfaces a real interop bug along the way: secsgem-py 0.3.0 encodes
RSDC (and other "single-byte status" fields) as <U1>, while SEMI E5
spells them as <B>.  Our `as_binary_first` was strict on Binary; now
accepts either (the byte semantics are identical, and the leniency is
symmetric with the U-type widening from the first interop commit).

Result: enqueue → docker restart → drain returns CEID 300 cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 01:04:49 +02:00
raphael d69f26b415 CC1: persistent file-backed spool
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>
2026-06-08 23:51:46 +02:00
raphael cfa2d1e531 BB1: full E40 S16F11 body — MF, PRRECIPEMETHOD, RCPVARS, PRPROCESSPARAMS
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>
2026-06-08 23:44:05 +02:00
raphael 4197cdfb25 AA: catalog growth — S7F1/F17, S6F5-F22, S2F21, S12F9-F18
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>
2026-06-08 23:30:51 +02:00
raphael 2d60571a9c interop: secsgem-py cross-validation harness + lenient identifier parsing
Adds a Docker-based interop harness that drives the C++ server with
secsgem-py 0.3.0 as the active host and probes a secsgem-py-passive
equipment from a minimal C++ active client.  Surfaces and fixes four
interoperability bugs uncovered by cross-testing:

  * SEMI E5 identifier formatcodes are a U1|U2|U4|U8 wildcard;
    secsgem-py picks the narrowest fitting width while our parsers
    only accepted U4.  `as_uN_scalar` / `as_iN_scalar` now accept
    any unsigned/signed width and range-check the downcast.
  * PPBODY (S7F3/F6) is "ASCII | Binary | List" per the spec;
    secsgem-py defaults to ASCII.  Added BINARY_OR_ASCII codegen
    item type with `as_text_or_binary` accessor.
  * S1F23/F24 Collection Event Namelist was unimplemented; added
    schema + `vids_for(ceid)` accessor on EventReportSubscriptions
    plus the dispatch handler.
  * S10F1 was registered as a host->equipment handler, but per
    SEMI E5 §12 S10F1 is equipment->host; S10F3 is the actual
    host->equipment Terminal Display Single.  Added an S10F3
    handler alongside (we keep S10F1 too for backward compat).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 23:17:18 +02:00
raphael 0e832d6ff7 P: E84 Parallel I/O handoff signaling
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>
2026-06-08 09:17:17 +02:00
raphael 564bd47132 O: E148 time-sync drift tracking + quality metric
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>
2026-06-08 09:13:16 +02:00
raphael 5a3f5ca6da L: E87 slot-map verification wire (S3F19/F20 + F21/F22)
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>
2026-06-08 08:48:56 +02:00
raphael a52d44ade5 K2: SubstrateIDStatus (third E90 axis)
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>
2026-06-08 08:46:10 +02:00
raphael a28a8b5982 K1: SubstrateHistory ring buffer per substrate
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>
2026-06-08 08:43:24 +02:00
raphael 06f664dfab J: E116 time-bucket accounting
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>
2026-06-08 08:41:09 +02:00
raphael 28dac8e9c8 I2: Router::dispatch_with_s9 helper + end-to-end S9F3/F5 tests
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>
2026-06-08 03:58:03 +02:00
raphael e2348db082 I1: SML parser — inverse of to_sml()
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>
2026-06-08 03:56:13 +02:00
raphael 90fdf45df5 H2: E157 module CEIDs + server-side emission
Adds the canonical E157 collection-event identifiers in the 1570+ block:

  1570 ModuleProcessStateChange  (generic; fired on any transition)
  1571 ModuleNotExecuting
  1572 ModuleGeneralExecuting
  1573 ModuleStepExecuting
  1574 ModuleStepCompleted

Server installs a state-change handler that fires both the generic
CEID and the state-specific one for each transition.  Hosts that
prefer "wake me on any module change and I'll fan it out myself" can
subscribe to only the generic CEID; hosts that want narrower
notifications subscribe to specific states.

Closes Tranche H — E157 Module Process Tracking end-to-end (FSM +
Store + CEIDs + server emission).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 03:38:11 +02:00
raphael 82fac6fd17 H1: ModuleStateMachine + ModuleStore (E157 §6)
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>
2026-06-08 03:37:20 +02:00
raphael d159bd39d7 G: CemObjectStore (E120 Common Equipment Model)
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>
2026-06-08 03:35:05 +02:00
raphael cdf4049016 F2: EPT joined to EquipmentDataModel + server-side CEID emission
EquipmentDataModel now carries an EptStateMachine as a value member
alongside the other top-level state machines.  Server installs a
state-change handler that maps every EPT transition to a CEID emission
through the existing emit_event path:

  1100 NonScheduledTime    1103 Engineering
  1101 ScheduledDowntime   1104 Standby
  1102 UnscheduledDowntime 1105 Productive

CEIDs land in the 1100+ block to keep clear of the demo equipment.yaml
(100s/200s/400s) and E90 (900s).  Log lines include the dwell time of
the previous state so trace-level diagnostics show utilization without
extra tooling.

Application code drives transitions by calling model->ept.on_event(...);
the existing event-report machinery (subscription state, S6F11
batching, spool) gates wire emission so EPT events spool on offline
hosts like every other CEID.

Closes Tranche F — E116 Equipment Performance Tracking end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 03:16:56 +02:00
raphael 7bff01c363 F1: EptStateMachine (E116 Equipment Performance Tracking)
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>
2026-06-08 02:17:16 +02:00
raphael 777fa5e9f9 E3: Host-side E90 substrate event observer
Adds a typed substrate-event callback to HostHandler that decodes the
canonical E90 CEIDs from incoming S6F11 messages into the matching
SubstrateState / SubstrateProcessingState enum values.  Host
applications now get strongly-typed substrate observability without
having to maintain their own CEID-to-state lookup.

  using SubstrateEventHandler =
      std::function<void(uint32_t ceid, SubstrateState location,
                         SubstrateProcessingState processing)>;

  void set_substrate_event_handler(SubstrateEventHandler);

Axes not addressed by a given CEID stay at NoState — the handler
distinguishes "this CEID updates the location axis" from "this CEID
updates the processing axis" so the host can keep its own per-
substrate FSM in sync.

Closes Tranche E — E90 Substrate Tracking end-to-end (FSM + Store +
CEIDs + server emission + host observer).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 01:23:11 +02:00
raphael 511950aa4a E2: E90 standard CEIDs + server-side substrate event emission
Adds the canonical E90-0716 §6.5 collection-event identifiers as a
single header of inline constants (gem/e90_constants.hpp) keyed off
SubstrateState / SubstrateProcessingState transitions:

  901 AtSource           910 NeedsProcessing
  902 AtWork             911 InProcess
  903 AtDestination      912 Processed
                         913 Aborted
                         914 Stopped
                         915 Rejected
                         916 Lost
                         917 Skipped

Values use the 901+ block to avoid collision with the demo CEIDs in
data/equipment.yaml (100s/200s/400s).

Server installs location + processing change handlers on
model->substrates that map every transition to emit_event() with the
matching CEID.  The existing event-report machinery (subscription
state, S6F11 batching, spool) gates the actual wire emission, so this
plays nicely with hosts that subscribe to only a subset of substrate
events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 00:50:53 +02:00
raphael 7c726ed9ba E1: SubstrateStateMachine + SubstrateStore (E90 §6)
Per-substrate dual FSM with two orthogonal axes:

  Location (STS):
    AtSource -> AtWork (Acquire) -> AtDestination (Release)
    AtWork  -> AtSource (Return; processing aborted before completion)

  Processing:
    NeedsProcessing -> InProcess (Start) -> Processed (End)
    InProcess -> {Aborted, Stopped, Rejected, Lost} terminal
    NeedsProcessing -> {Skipped, Lost} terminal

Wire-byte values pinned via static_assert to E90-0716 §10.3.

SubstrateStore mirrors the CarrierStore pattern: non-movable, per-row
SubstrateStateMachine heap-allocated with handlers dispatching through
the store's location/processing callbacks; fire_location_event accepts
an optional new_location string so the application can carry
equipment-specific module names alongside the FSM state.

Joins EquipmentDataModel alongside carriers / load_ports.  9 test
cases cover initial state, full location lifecycle, all five
processing exits, and store-level dual-axis observer firing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 00:49:40 +02:00
raphael 7668ceaae4 D3: CarrierStore + LoadPortStore
Per-CARRIERID and per-PORTID stores wrap the D1 FSMs, mirroring the
ProcessJobStore / ExceptionStore pattern: heap-allocated state machines
keyed in a std::map, non-movable to keep this-capture lambdas safe,
synthetic create() that wires per-row change handlers into the store's
top-level callbacks.

CarrierStore:
  create(carrierid, port_id, capacity)  — default 25-slot map
  fire_id_event / fire_slot_map_event / fire_access_event
  set_id_handler / set_slot_map_handler / set_access_handler

LoadPortStore:
  create(port_id)
  associate(pid, carrierid) / disassociate(pid)
  fire_transfer_event / fire_reservation_event
  set_transfer_handler / set_reservation_handler / set_association_handler

Both join EquipmentDataModel alongside process_jobs / control_jobs /
exceptions.  Six test cases cover create-dedup, ID-status change
observation, slot-map / access independence, port association,
transfer lifecycle, and reservation handler firing.

Server-side dispatch (S3F17 -> CarrierStore::fire_id_event, S3F25 ->
LoadPortStore transfer) lands in D4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 00:14:50 +02:00
raphael 5f1444c756 D2: E87 wire messages (S3F17/F18 + F23/F24 + F25/F26 + F27/F28)
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>
2026-06-08 00:11:35 +02:00
raphael 94c26c0771 D1: E87 carrier and load-port state machines
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>
2026-06-08 00:09:42 +02:00
raphael c163d2060f C3: AlarmSeverity bit-flag enum + classification helpers
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>
2026-06-07 23:52:09 +02:00
raphael a1f7da4a7d C1: ExceptionStateMachine FSM + ExceptionStore
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>
2026-06-07 23:41:51 +02:00
raphael 6ff3104591 B3: HostHandler RCMD/alarm/recipe/terminal/job senders
Fleshes out the host-side message surface so the demo client app no
longer has to inline message construction.  Senders added (each is a
one-line wrapper over the codegen builders + Connection::send_request):

  Remote command:    S2F41/F42, S2F49/F50
  Alarm management:  S5F3/F4 enable, S5F5/F6 list, S5F7/F8 list-enabled,
                     S5F13/F14 recover, S5F17/F18 recover-abort
  Process programs:  S7F3/F4 send, S7F5/F6 request, S7F19/F20 EPPD
  Spool:             S6F23/F24
  Terminal:          S10F1/F2 single, S10F5/F6 multi
  E40 Process Jobs:  S16F11/F12 create, S16F5/F6 command, S16F13/F14 dequeue
  E94 Control Jobs:  S14F9/F10 create, S14F11/F12 delete, S16F27/F28 command

CommandParameter is reused from store/host_commands.hpp rather than
inventing a parallel ParamPair — host and equipment talk in the same
struct now.

Closes the outbound side of the host-mode menu.  The remaining piece
is an integration test that drives this against the equipment server
end-to-end (B4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:53:17 +02:00
raphael 95ebcc3aac B2: HostHandler status + subscription senders
Adds the five GEM event-subscription primitives the host needs to drive
the equipment's data-collection lifecycle (E30 §6.11):

  S1F3/F4    selected status data
  S1F11/F12  status variable namelist
  S2F33/F34  define reports
  S2F35/F36  link event reports
  S2F37/F38  enable/disable events

Each is a one-line wrapper over the codegen builders + Connection's
send_request, surfacing the codegen-generated DefineReportEntry /
LinkEventEntry structs to callers behind a {id, [vids]} pair API.

This is the minimum surface a host needs to walk a fresh equipment
through "define report -> link CEID -> enable" and start receiving
S6F11 event reports — the same pattern the existing demo client does
inline.  B3 lands the RCMD / recipe / job / terminal senders that
build on top.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:50:37 +02:00
raphael 63bb0cf933 B1: HostHandler base class
GEM host-side counterpart to the existing equipment server: wraps an
HSMS Connection (Active mode), installs an inbound dispatch table that
auto-acks the messages a host is expected to passively accept, and
exposes the GEM workflow primitives.

Inbound dispatch:
  S5F1  Alarm Report          observe (alarm handler) + S5F2 Accept
  S6F11 Event Report          observe (event handler) + S6F12 Accept
  S6F25 Spool Data Ready      S6F26 Accept (host policy: pull on demand)
  S10F1 Terminal Display      observe + S10F2 Accepted
  S9F*  Equipment errors      observe (s9 handler); no ack (one-way)

Workflow shortcuts:
  establish_communication()   S1F13 -> S1F14
  go_remote()                 S1F17 -> S1F18
  go_offline()                S1F15 -> S1F16

Plus a low-level send_request() escape hatch so the senders coming in
B2/B3 don't have to friend the connection internals.

Drive-by: event_reports.hpp was missing `<optional>` (worked transitively
through the equipment-side include chain but not when included from the
host-side standalone).

secsgem-py has `gem/hosthandler.py`; this mirrors its surface for the
inbound-ack and lifecycle parts.  Outbound senders land in B2/B3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:49:52 +02:00
raphael 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>
2026-06-07 21:36:17 +02:00
raphael 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>
2026-06-07 21:34:09 +02:00
raphael 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>
2026-06-07 21:15:33 +02:00
raphael 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>
2026-06-07 21:00:32 +02:00
raphael 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>
2026-06-07 02:35:24 +02:00
raphael 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>
2026-06-07 02:16:50 +02:00
raphael 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>
2026-06-07 02:08:19 +02:00
raphael 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>
2026-06-07 01:57:35 +02:00
raphael 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>
2026-06-07 00:30:43 +02:00
raphael 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>
2026-06-06 19:14:41 +02:00