Commit Graph

32 Commits

Author SHA1 Message Date
raphael c8e8e80735 secs_server: relative-path defaults so the binary runs outside docker
tests / build-and-test (push) Successful in 2m7s
tests / thread-sanitizer (push) Successful in 2m33s
tests / tshark-dissector (push) Successful in 2m15s
tests / secs4j-interop (push) Failing after 1s
tests / libfuzzer (push) Successful in 3m7s
Previously --config / --state-table / --pj-state-table /
--cj-state-table defaulted to /app/data/..., which only resolves
inside the docker image.  A host build run from the repo root
errored out unless every flag was passed explicitly.

Switch to data/equipment.yaml (and siblings) relative to CWD —
docker still works because WORKDIR=/app puts /app/data/... at the
same relative location, and host builds run from the repo root
resolve to <repo>/data/....  Existing callers that pass explicit
paths (the proof commands, tshark_validate.sh, secs4j_validate.sh,
docker compose) are unaffected.

Verified --validate-config under docker still finds all four YAMLs
and the tshark proof still passes (69 frames, 0 malformed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:00:45 +02:00
raphael 4ddf8e0f48 verify: libFuzzer harness for secs2::decode + try_parse_sml
Coverage-guided structural search for crashes and undefined behaviour
on arbitrary input to our two parsers.

What's wired:
- -DSECSGEM_FUZZ=ON CMake option, clang-only.  Adds
  -fsanitize=fuzzer-no-link,address,undefined to all targets +
  -fsanitize=fuzzer to the two fuzz executables.
- apps/fuzz_secs2_decode.cpp — feeds raw bytes to secs2::decode.
  Catches secs2::CodecError (expected) but traps on anything else
  leaking (would be a hardening bug).
- apps/fuzz_sml_parse.cpp — feeds string to try_parse_sml, which is
  contractually nothrow-equivalent; traps on any exception.
- .gitea/workflows/ci.yml — `libfuzzer` job builds with clang and
  runs each fuzzer for 60s in CI.  Any crash / ASan / UBSan flag
  fails the job.
- Dockerfile gains clang + libclang-rt-18-dev so devs can run
  locally with the same toolchain.

Result on a fresh 30-second local run:
  fuzz_secs2_decode:  70 727 random inputs, 0 crashes
  fuzz_sml_parse:    284 950 random inputs, 0 crashes

The coverage-guided search found and synthesized inputs that
exercise: zero-byte, single-byte format tags, all length-byte
counts (1/2/3), nested lists, format bytes with reserved bits, the
"BOOLEAN" SML token, malformed quoted strings, etc.  libFuzzer's
recommended dictionary at the end of each run shows what bytes /
substrings the coverage feedback discovered as discriminating —
useful signals if we ever want a hand-curated corpus.

README proof table grows to 8 commands.  After this:
  - 426 unit tests (internal)
  - 47 conformance harness checks (internal)
  - 24 secsgem-py interop checks (external — Python ref impl)
  - 20 secs4j interop checks (external — independent Java impl)
  - 69 frames dissected by Wireshark HSMS dissector (external)
  - 196 SEMI E5 KAT assertions (standards body's encoding rules)
  - **~70k + ~285k random inputs, 0 crashes (external)**
  - 100k random tool ops with all invariants holding (internal)
  - YAML validation (internal)
  - TSan clean on 2 557 assertions (internal correctness aid)

Five distinct external proofs now, each covering a different angle.

Plan: VERIFICATION.md §4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 16:27:36 +02:00
raphael 9c5d67fdad bench: secs_bench harness + BENCHMARKS.md baseline
Customer SREs and capacity planners had nothing to point at.
INTEGRATION.md asked the right questions ("how many tx/sec?"
"how much memory per active CJ?") but had no numbers.

secs_bench spins up an in-process passive equipment + active host
on an OS-allocated port, runs three canned workloads, and emits a
markdown table customers can capture and diff across commits:

- S1F1/F2 header-only round-trip   — dispatch + framing baseline
- S1F3/F4 with N SVIDs             — encode + decode throughput
- S6F11 push (W=0)                  — one-way emission ceiling
- PJ + CJ pair memory footprint    — bytes per active job

Latency reports p50/p95/p99/max via std::nth_element over the
sample vector.  RSS is read from /proc/self/statm on Linux,
mach_task_basic_info on macOS.

CLI: --requests / --concurrency / --svid-count / --store-pairs.
Default 20k req @ 16 concurrent.

BENCHMARKS.md checks in a reference run (Docker on M-series
macOS): ~140k req/s S1F1, ~79k req/s S1F3 with 32-SVID list,
~572k S6F11/s push, ~450 bytes per PJ+CJ pair.  Three orders of
magnitude headroom over typical fab tool load.

The doc is explicit about what the bench does NOT measure (real
network, persistence I/O, TLS tunnel overhead, multi-session GS
dispatch) — customers should re-run on their target hardware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 14:36:50 +02:00
raphael a4599b3b9d config: multi-error YAML validator + --validate-config CLI flag
The existing loader throws ConfigError on the first problem it hits.
A customer with a tool-specific equipment.yaml that has six issues
sees one, fixes, restarts, sees the next, fixes, restarts — six
edit-restart cycles before the server even binds.  Day-1 friction
is the top support ticket source in fab integrations.

This commit adds a parallel validator that does a separate read-only
pass and surfaces *every* issue at once:

  $ secs_server --validate-config \
      --config equipment.yaml \
      --state-table control_state.yaml
  [error] equipment.yaml:5  svids[0].type — unknown SECS-II type `WTF`
  [error] equipment.yaml:7  alarms[0].category — value 200 out of range [0, 127]
  [error] equipment.yaml:9  host_commands[0].emit_ceid — CEID 999 not declared in `ceids` section
  3 error(s), 0 warning(s) across 4 files

What it catches:
- Missing required fields (device.model_name, .software_rev, …)
- Range violations (alarm category must be 0–127, spool streams 1–127,
  device.id fits u16, etc.)
- Unknown enum values (SECS-II types, HCACK values, control/PJ/CJ
  state and event names — using the right case + snake convention
  the runtime parsers enforce)
- Duplicate IDs within svids / dvids / ecids / ceids / alarms,
  duplicate PPIDs in recipes, duplicate command names in host_commands
- Referential integrity: host_commands[*].emit_ceid must exist in
  ceids; host_commands[*].set_alarm must exist in alarms;
  emit_on_control_change must exist in ceids
- PJ-table-specific: `NoState` sentinel rejected as `initial`,
  `from`, or `to` (matches loader's existing runtime check)
- yaml-cpp Mark → 1-based line numbers when available

What it doesn't catch (out of scope this round):
- JSON Schema for editor red-squigglies (future)
- Deep semantic checks across state-table reachability
- ECID min/max value parsing (would need numeric type coupling)

Tests cover: clean file passes; multi-error YAML surfaces every issue
on a single pass; line numbers populate; control_state /
process_job_state / control_job_state casing conventions;
format_issues_to renders both severities; the shipped
data/equipment.yaml etc. validate cleanly (regression tripwire if
anyone breaks the demo configs).

INTEGRATION.md §2.3 calls out the flag and suggests CI use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 14:32:09 +02:00
raphael a4419e15cd conformance: expand harness from 8 to 47 host-driven checks
The previous harness only exercised S1F1/F11/F13/F19, S2F17/F29, S5F5,
S7F19 — about 15% of what COMPLIANCE.md claims as .  Customers running
secs_conformance against their tool got near-zero conformance signal
on dynamic event reports, GEM 300, alarm management, exception
recovery, terminal services, spool, and PP management.

This expansion covers, in one sequential run:
- Establish comms + identification (S1F13/F1)
- Status / DVID / CEID / EC namelists + values
  (S1F11/F3/F21/F23, S2F29/F13)
- Dynamic event reports: define / link / enable + readback paths
  (S2F33/F35/F37, S6F15/F19/F21)
- All three remote-command forms (S2F41/F21/F49)
- Equipment-initiated S6F11 observation triggered by RCMD=START
- Trace init, limits attrs, spool reset + transmit
  (S2F23, S2F47, S2F43, S6F23)
- Alarm management: list, list-enabled, enable (S5F5/F7/F3)
- Exception recovery: request + abort (S5F13/F17)
- PP load-inquire / list / request (S7F1/F19/F5)
- Terminal display both directions (S10F3, S10F5)
- E40 PJ create / monitor / command / dequeue
  (S16F11/F7/F5/F13)
- E94 CJ create / command / delete (S14F9, S16F27, S14F11)
- E87 carrier action / slot map / transfer / cancel
  (S3F17/F19/F25/F27)
- E39 GetAttr (S14F1)
- GEM compliance self-report (S1F19)

Pass criterion is the spec-mandated reply function code, not any
specific ACK value — CarrierIDUnknown / Denied_UnknownObject /
PpidNotFound / Error are well-formed F-coded replies and count as
protocol-conformant.  This lets the harness run against any equipment
without preloading state.

47 / 47 PASS against the in-repo demo server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 13:54:28 +02:00
raphael 06f287b415 conformance: standalone secs_conformance harness binary
The closest thing to an in-repo "RTS" — a runnable executable that
points at any HSMS-SS equipment and walks through every E30
fundamental + additional capability, reporting pass/fail per check
and exiting with the right code for CI / canary use.

  build/secs_conformance --host <ip> --port 5000 --device 0

Each check sends a host-initiated primary and asserts the equipment
replies with the expected stream/function within T3.  Checks chain
forward through async callbacks (each reply handler kicks off the
next check) so the conformance run stays inside one io.run().

Initial check set (mirrors COMPLIANCE.md §3 fundamentals):
  E37 §7.2  SELECT handshake
  E30 §6.5  S1F13/F14 Establish Comms
  E30 §6.7  S1F1/F2 Are You There
  E30 §6.13 S1F11/F12 SVID Namelist
  E30 §6.16 S2F29/F30 ECID Namelist
  E30 §6.20 S2F17/F18 Clock
  E30 §6.14 S5F5/F6 List Alarms
  E30 §6.17 S7F19/F20 PP List
  E30 §6.10 S1F19/F20 GEM Compliance

Validated against the demo server: 9/9 PASS.

README.md §8 (Compliance + certification) updated to point at the
harness as the suggested first-line conformance check.  Tool
vendors fork apps/secs_conformance.cpp and add their own
capability-specific checks alongside.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 12:57:37 +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 af2c60663e N: E39 generic ObjectService — S14F1/F2 GetAttr + S14F3/F4 SetAttr
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>
2026-06-08 09:09:51 +02:00
raphael 3b45bade8f M: S16F7/F8 PRJobMonitor + S16F15/F16 PRJobCreateMultiple
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>
2026-06-08 08:51:39 +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 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 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 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 ff1a6b3726 D4: E87 server dispatch for S3F17/F25/F27
Hooks the CarrierStore + LoadPortStore from D3 onto the wire:

  S3F17 CarrierAction:
        ProceedWithCarrier -> CarrierIDEvent::ProceedWithCarrier
        CancelCarrier      -> CarrierIDEvent::CancelCarrier
        BindCarrierID      -> CarrierIDEvent::Bind
        unknown action     -> CAACK=CarrierActionInvalid
        unknown carrier    -> CAACK=CarrierIDUnknown

  S3F25 CarrierTransfer:
        fires source.StartUnloading + target.StartLoading transfer
        events, updates Carrier::port_id, returns CAACK=Accept.

  S3F27 CancelCarrier:
        fires CIDS CancelCarrier + CAS Cancel against the carrier.

The state-change handlers wired into the stores in D3 are the right
place to emit S6F11 CEIDs for carrier-events; that hookup is left for
a follow-up commit (it depends on the equipment.yaml CEID catalog,
which doesn't yet enumerate E87 events).

Closes Tranche D — E87 Carrier Management end-to-end (FSMs + wire +
stores + dispatch).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 00:16:04 +02:00
raphael af25bc8726 C2: S5F9-F18 server dispatch for exception lifecycle
Wires the ExceptionStore from C1 onto the wire:

State-change emitter (set on ExceptionStore at server startup):
  NoState   -> Posted         emit S5F9  (exception post notify)
  Recovering -> Cleared       emit S5F15 with EXRESULT="OK"
  Recovering -> RecoverFailed emit S5F15 with EXRESULT="FAILED"
  Posted    -> Cleared        emit S5F11 (autonomous clear)
  RecoverFailed -> Cleared    emit S5F11

Router additions:
  S5F13 -> ExceptionStore::on_recover(exid, exrecvra) -> S5F14 ACKC5
  S5F17 -> ExceptionStore::on_recover_abort(exid) -> S5F18 ACKC5

S5F9 emission auto-spools when the link is offline (deliver_or_spool
already handles that).  The synthetic NoState->Posted transition fires
inside ExceptionStore::post(), so any application code that calls
model->exceptions.post(...) will produce wire activity through this
handler without further plumbing.

Verified: docker-built secsgem_tests now reports 188 cases / 1026
assertions / 0 failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 23:51:53 +02:00
raphael fafbd2abd2 A2: S2F49/F50 Enhanced Remote Command
Adds the OBJSPEC-scoped sibling of S2F41 with extended per-parameter
ack shape (CPACK + CEPACK).  Wire:

  S2F49  body <DATAID OBJSPEC RCMD <L,n <CPNAME CPVAL>>>
  S2F50  body <HCACK <L,n <CPNAME CPACK CEPACK>>>

Server delegates to the existing HostCommandRegistry, logs OBJSPEC for
audit, and currently returns empty cpacks (all-OK).  Per-parameter
failures will be wired when the command registry grows CEPACK-level
validation; this commit is the catalog + dispatch scaffolding.

secsgem-py defines these in its catalog but never dispatches them; this
puts the C++ port marginally ahead on remote-command coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:10:40 +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 1e7105a9e0 100%/E: Spool S6F25/F26 + auto-trigger on re-SELECT
tests / build-and-test (push) Failing after 31s
Closes spooling.  S6F25 (NUM-MSG) goes into the catalog; S6F26
(ACKC6) likewise.  The server's on_selected handler now checks the
spool on entering SELECTED — if there's queued data, it auto-emits
S6F25 so the host can decide what to do (S6F23 Transmit vs Purge).

The happy-path demo never drops TCP so the auto-trigger doesn't fire
there, but the canonical re-SELECT path is wired.  Client gains a
handler for inbound S6F25 that logs the count and acks S6F26.

COMPLIANCE.md: Spooling Additional capability flips from 🟡 to .

Remaining out of scope for spooling: persistent on-disk spool so
restarts don't lose queued events.  Demo + tests don't need it; real
fab equipment would.

Tests: 83 cases / 481 assertions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:24:02 +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 88205037ec 100%/A: S5F7/F8 list enabled alarms
tests / build-and-test (push) Failing after 32s
Closes the small remaining hole in Alarm Management.  S5F7 is
header-only; S5F8 has the same wire shape as S5F6 (vector of
<L,3 <B ALCD> <U4 ALID> <A ALTX>>) but only includes alarms whose
enabled flag is set.  Codegen handles both as a list_of with
struct_name=AlarmListing; server pre-computes the per-row ALCD from
(severity_category, active state) before passing the rows in.

COMPLIANCE.md: Alarm Management Additional capability flips .

Tests: 78 cases / 454 assertions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 01:52:11 +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 0721db9542 Close COMPLIANCE.md gap: spooling (E30 §6.22)
tests / build-and-test (push) Failing after 42s
Implements the largest functional gap from the compliance audit. The
equipment now queues events the host can't immediately receive (either
because there's no SELECTED session or because the demo's force-spool
flag is on) and transmits the queue on host request.

What's new

  include/secsgem/gem/store/spool.hpp
    SpoolStore: a deque queue with a configurable per-stream whitelist
    (so only streams 5+6 spool by default), a max_size cap with FIFO
    eviction on overflow, and a `force_spool` test flag.  Enqueue
    returns one of Queued / Dropped_NotSpoolable / Dropped_Full so the
    caller can fall back to live delivery when appropriate.  Drain
    pops the entire queue in FIFO order.  Two new ack enums:
    ResetSpoolAck (S2F44 RSPACK) and SpoolRequestAck (S6F24 RSDA), plus
    SpoolRequestCode (S6F23 RSDC, Transmit/Purge).

  data/messages.yaml + auto-regenerated messages.hpp
    S2F43 W   <L,n <B stream>>            Reset Spooling
    S2F44     <L,2 <B RSPACK> <L,a ...>>  Reset Spooling Ack
    S6F23 W   <B RSDC>                    Request Spooled Data
    S6F24     <B RSDA>                    Request Spooled Data Ack

  data/equipment.yaml
    `spool:` section: max_size + spoolable_streams list.  Two new host
    commands SPOOL_ON / SPOOL_OFF that flip the force-spool flag (these
    stand in for "host link down" in the demo without dropping TCP).

  include/secsgem/gem/store/host_commands.hpp
    Spec/Result gain an optional<bool> force_spool field.  S2F41
    dispatch returns the result, the server applies it after S2F42 is
    queued.

  src/config/loader.cpp
    Reads `spool:` from equipment.yaml; reads `force_spool` from each
    host_commands entry; populates SpoolStore + CommandSpec.

  apps/secs_server.cpp
    New `deliver_or_spool(msg, what)` helper.  emit_event and
    emit_alarm_set funnel through it: if force_spool is on (or there's
    no active session), msg.stream is checked against the spoolable
    list and the message is enqueued; otherwise it's sent live.
    Two new handlers:
      S2F43  parses the stream list, updates SpoolStore, replies S2F44
      S6F23  RSDC=Transmit drains and re-sends each as a fresh primary
             (posted on the executor so the S6F24 ack flushes first);
             RSDC=Purge clears the queue and acks.
    The S2F41 handler now also propagates result.force_spool into the
    SpoolStore.

  apps/secs_client.cpp
    Demo extended with 4 new steps after the FAULT branch:
      SPOOL_ON  -> S2F42 Accept
      START     -> S2F42 Accept; CEID 300 emission spooled (no live S6F11)
      SPOOL_OFF -> S2F42 Accept; queue still has the message
      S6F23(Transmit) -> S6F24 Accept; spooled S6F11 arrives next
    Then the existing S7F19/S7F5/S10F1/S1F15/Separate flow continues.

  tests/test_data_model.cpp
    Four new TEST_CASEs for SpoolStore (whitelist, FIFO eviction at
    max_size, drain ordering, force flag).

  tests/test_loader.cpp
    Confirms equipment.yaml's `spool:` section populates the store and
    `force_spool: true/false` flows through to dispatch results.

  COMPLIANCE.md
    Spooling moves from  to 🟡.  Adds S2F43/F44 + S6F23/F24 as  in
    the message coverage matrix; calls out what's still missing
    (S6F25/F26 notification, automatic activation on HSMS NOT-SELECTED,
    persistent on-disk spool).

Verified

  - Tests: 73 cases / 383 assertions pass (+4 spool cases).
  - Demo (docker compose up server client) walks the full happy path
    and the spool path, observed in the server log as:
        spool: force_spool=true (depth=0)
        spool: S6F11 CEID=300 queued (depth=1)
        spool: force_spool=false (depth=1)
        S6F23 transmit: draining 1 messages
    and on the host side as the queued S6F11 arriving in the correct
    order after S6F24.

Known limitations (logged in COMPLIANCE.md)

  - Spool activation is manual via SPOOL_ON/OFF rather than
    automatically triggered by HSMS NOT-SELECTED.
  - No S6F25/F26 spooled-data-ready notification on re-SELECT.
  - In-memory only; an equipment restart loses queued events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 22:06:55 +02:00
raphael 711ee1b40f #4 Split EquipmentDataModel into focused stores
The god-class is gone.  Each capability is now its own focused store:
StatusVariableStore, DataVariableStore, EquipmentConstantStore (with EAC
range validation), EventReportSubscriptions, AlarmRegistry, RecipeStore,
Clock, HostCommandRegistry.  Each is independently testable.

EquipmentDataModel becomes a small composite that holds one of each store
as a public member, plus three convenience methods (vid_value, vid_exists,
compose_reports_for) that span SVIDs+DVIDs and inject the right callbacks
into the EventReportSubscriptions.

New under include/secsgem/gem/store/:

  status_variables.hpp   StatusVariable, StatusVariableStore,
                         DataVariable, DataVariableStore
  equipment_constants.hpp EquipmentConstant, EquipmentConstantStore,
                          EquipmentAck. set_value() now validates
                          numeric values against min_str/max_str and
                          returns EAC=4 on out-of-range — closes the
                          COMPLIANCE.md gap about EC range validation.
  event_reports.hpp      CollectionEvent, Report, ReportData,
                         EventReportSubscriptions + DefineReportAck,
                         LinkEventAck, EnableEventAck. The store is
                         pure data; VidLookup / VidExists callbacks
                         are injected at define / emit time so the
                         service doesn't back-reference the SVID
                         store.
  alarms.hpp             Alarm, AlarmAck, AlarmRegistry.
                         Encapsulates the (enabled, active) sets and
                         ALCD byte computation.
  recipes.hpp            ProcessProgramAck, RecipeStore.
  clock.hpp              TimeAck, Clock. set_time_string applies an
                         offset so subsequent reads reflect the host
                         time without mutating system clock.
  host_commands.hpp      HostCmdAck, CommandParameter,
                         HostCommandRegistry with Spec/Result types.

include/secsgem/gem/data_model.hpp shrinks to a 50-line composite:

  struct EquipmentDataModel {
    StatusVariableStore       svids;
    DataVariableStore         dvids;
    EquipmentConstantStore    ecids;
    EventReportSubscriptions  events;
    AlarmRegistry             alarms;
    RecipeStore               recipes;
    Clock                     clock;
    HostCommandRegistry       commands;
    /* + vid_value, vid_exists, compose_reports_for sugar */
  };

src/gem/data_model.cpp is gone — every store is inline header-only.

include/secsgem/gem/messages_helpers.hpp picks up EventReportAck and
TerminalAck (S6F12 / S10F2-F4 ack enums that aren't tied to any one
store).

Call-site updates:

  apps/secs_server.cpp   model->status_variable(id) -> model->svids.get(id),
                         model->equipment_constant(id) -> model->ecids.get(id),
                         model->alarm_set(id) -> model->alarms.set_active(id),
                         model->dispatch_command(...) -> model->commands.dispatch(...),
                         and similar across every handler.  Plus
                         model->current_time_string() -> model->clock....

  src/config/loader.cpp  model.add_status_variable(sv) -> model.svids.add(sv),
                         and similar.  HostCommandRegistry::Spec replaces
                         EquipmentDataModel::CommandSpec.

  apps/secs_client.cpp   std::vector<EquipmentDataModel::CommandParam> ->
                         std::vector<CommandParameter>.

  tests/test_data_model.cpp  Rewritten around the individual stores;
                         each gets its own TEST_CASE block.  Adds three
                         new cases covering EC range validation (in
                         range / out of range / non-numeric skipped).

  tests/test_loader.cpp  m.has_event(100) -> m.events.has_event(100),
                         etc.

Verified:

  - Tests: 69 cases / 370 assertions pass (was 67 / 384; -14 stale
    composite-API assertions + 16 new store-level assertions covering
    EC range validation and the per-store add/get/list/delete paths).
  - Demo: byte-identical behaviour across the full 17-step flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:51:54 +02:00
raphael 29db1caedb #6 SxFy codegen from YAML message catalog
The bulk of the per-SxFy boilerplate — ~90 hand-written builders and parsers
across 30+ message pairs — is now generated at build time from a single YAML
catalog. Adding a new SECS-II message becomes a YAML edit; the C++ code is
generated, not maintained.

What changed
------------

data/messages.yaml
  The catalog. Describes every SxFy currently supported: stream, function,
  W-bit, builder name, optional parser name, and a recursive body shape
  grammar (scalar / list / list_of).  Shapes carry SECS-II item types
  (ASCII, BINARY_BYTE, U4, F8, ITEM, ...) and optional C++ enum types for
  typed ack codes.  Inner-most fields can be marked external_struct: true
  so structs already defined elsewhere (ReportData, CommandParameter) are
  referenced rather than redefined.

tools/gen_messages.py
  Python codegen.  Reads the catalog and emits one inline header.  Handles
  nested shapes via depth-unique variable names in the generated IIFEs, so
  S6F11's three-level nesting compiles without lambda capture conflicts.
  Post-order traversal ensures inner structs are emitted before outer ones
  that reference them.  Generates positional and (where applicable) struct
  builder overloads, plus struct-returning parsers for messages with a
  `parser:` entry.

CMakeLists.txt
  Custom command runs gen_messages.py at configure/build time and emits
  ${CMAKE_BINARY_DIR}/generated/secsgem/gem/messages.hpp.  Added to the
  secsgem target's include path so `#include "secsgem/gem/messages.hpp"`
  resolves to the generated file.  Depends on the YAML + the script, so
  edits trigger regen automatically.

Dockerfile
  Added python3 + python3-yaml to the toolchain image.

include/secsgem/gem/messages_helpers.hpp  (new)
  The small set of hand-written helpers the generated header relies on:
  scalar accessors (as_ascii / as_u4_scalar / ...), parse_u4_list_body,
  u4_list_item, ack_byte, ALED byte constants, and the two special-case
  messages whose shape doesn't fit the codegen schema (S1F4 needs
  per-row std::optional<Item> semantics; S5F6 needs a per-row ALCD
  callback).

include/secsgem/gem/messages.hpp  (deleted)
  The hand-written builder/parser file is gone. Its content now flows
  through the catalog + codegen.

include/secsgem/gem/data_model.hpp
  Moved CommandParameter to namespace scope so it can be shared between
  the data model and the messages.yaml's external_struct entry.  Added
  `using CommandParam = CommandParameter` for back-compat.

apps/secs_server.cpp + apps/secs_client.cpp
  Updated the call sites that the codegen renamed or restructured:
  - parse_terminal_display() split into parse_s10f1 / parse_s10f3.
  - s1f14_establish_comms_ack now takes a McAck struct for the nested
    identity (mdln, softrev) — call site uses brace init.
  - S2F33/S2F35 parsers return strongly-typed entries (DefineReportEntry,
    LinkEventEntry); the server adapts these to the model's pair-based
    API at the call site.
  - S2F15 parser returns vector<EcSet>; iterate by .ecid/.value.
  - S5F3 parser returns EnableAlarmRequest{aled, alid}; bool comes from
    (aled & 0x80) != 0.
  - AlarmReport's is_set()/category() methods removed; callers use the
    raw alcd byte with bit math (alcd & 0x80, alcd & 0x7F).
  - s2f42_host_command_ack and s2f41_host_command always take their
    second list argument explicitly (no defaulted arg from codegen).

tests/test_messages.cpp
  Updated to construct the generated typed structs (EcSet, StatusName,
  EnableAlarmRequest, CommandParameter, CommandParameterAck) and to read
  the new field names (.ecid/.value, .rptid/.vids, .ceid/.rptids,
  .name/.code).

Coverage
--------

Generated by codegen (44 SxFy in catalog):

  S1F1, S1F2, S1F3, S1F11, S1F12, S1F13, S1F14, S1F15, S1F16, S1F17, S1F18
  S2F13, S2F14, S2F15, S2F16, S2F17, S2F18, S2F29, S2F30, S2F31, S2F32
  S2F33, S2F34, S2F35, S2F36, S2F37, S2F38, S2F41, S2F42
  S5F1, S5F2, S5F3, S5F4, S5F5
  S6F11, S6F12
  S7F3, S7F4, S7F5, S7F6, S7F19, S7F20
  S10F1, S10F2, S10F3, S10F4

Hand-written (in messages_helpers.hpp):

  S1F4   list-of-optional-items shape (nullopt -> <L,0>)
  S5F6   per-row ALCD via callback

Adding a new SxFy
-----------------

Append a single entry to data/messages.yaml describing the body shape.
The builder + parser appear in messages.hpp after the next build.  The
host command above for S2F41 (or any other added SxFy) requires no C++
changes if the body fits the recursive scalar/list/list_of grammar.

Tests: 67 cases / 384 assertions still passing.
Demo: byte-for-byte identical behaviour (Select, Establish, Online,
S1F11/F3 namelist+values, S2F29 EC namelist, S2F33/F35/F37 dynamic event
subscription, S2F41 START -> S6F11 emission, S5F5/F3 alarm directory +
enable, S2F41 FAULT -> S5F1 alarm + S6F11, S7F19/F5 recipe ops, S10F1
terminal, S1F15 offline, Separate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:43:36 +02:00
raphael b871cd9da2 Table/YAML-driven refactor (Layer 1 start)
Move equipment capabilities and the E30 control state machine out of C++
code and into YAML data files; introduce a Router for SECS dispatch;
consolidate small files.

Behavioural changes: none.  Demo identical (15 SxFy transactions +
3 equipment-initiated primaries), 67 test cases / 384 assertions still
all green.  Structural changes only.

Why
---

The previous server.cpp held the equipment data dictionary (3 SVIDs,
2 ECIDs, 3 CEIDs, 2 alarms, 2 recipes, 4 host commands) as imperative
C++ in a 50-line `populate()` function, and routed inbound messages
through a 150-line if-ladder.  Adding a new SVID required a recompile.
Adding a new state transition required editing two switch statements
(`operator_*` and `on_host_request_*`).  The control state machine's
behavioural rules were spread across imperative code in two methods.

This is exactly what implementation_plan.md calls out as the wrong
shape: behavioural rules should live in versioned data, and every
runtime/test/analyzer should read from that data rather than re-encode
it.  This commit starts that move.

What's new
----------

data/equipment.yaml
  Equipment data dictionary.  Declarative SVIDs / ECIDs / CEIDs /
  alarms / recipes / host commands.  Host commands carry their HCACK
  ack code plus optional `emit_ceid` and `set_alarm` side-effects.
  Adding a new SVID or command is a YAML edit, no recompile.

data/control_state.yaml
  The E30 §6.2 control state transition table as data.  Each row is
  (from, on) -> (to [, then] [, ack]).  `then` chains an auto-advance
  through the transient AttemptOnline state.  The previous
  imperative switch is gone.

include/secsgem/config/loader.hpp + src/config/loader.cpp
  yaml-cpp-backed loader.  `load_control_state(path)` returns a
  ControlTransitionTable + initial state; `load_equipment(path, model)`
  populates the EquipmentDataModel and returns the device descriptor
  (id, MDLN, SOFTREV, optional auto-emit CEID).  Surfaces config
  errors with file path + field name via ConfigError.

include/secsgem/gem/router.hpp  (header-only)
  Small (stream, function) -> handler map.  Server registers all
  handlers once at startup, then the Connection's message handler is
  just `router.dispatch(msg)`.  Unhandled primaries with W set get
  SxF0 by default.  Replaces the if-ladder in secs_server.cpp.

include/secsgem/gem/control_state.hpp + .cpp
  ControlTransitionTable is the new pure data type.  ControlStateMachine
  is now a thin engine over the table: `fire(event)` looks up the row,
  optionally transitions, optionally chains a `then` transition, returns
  the ack code.  Behaviour rules no longer live in C++ switches.
  The default in-code table matches data/control_state.yaml row for row;
  tests rely on it so they don't need the YAML file.

include/secsgem/gem/data_model.hpp + .cpp
  `register_command(rcmd, CommandSpec)` replaces the function-handler
  signature.  CommandSpec = (HostCmdAck, optional emit_ceid, optional
  set_alarm).  `dispatch_command` returns a CommandResult so the server
  can fire the side-effects after S2F42 is sent.

apps/secs_server.cpp
  No populate(), no if-ladder.  Loads equipment.yaml + control_state.yaml
  at startup (clean error on bad config), wires the Router once,
  delegates dispatch.  Sm change handler reads emit_on_control_change
  from the YAML.  Welcome S10F3 removed for parity with config (a future
  YAML rule could re-introduce it declaratively).

tests/test_loader.cpp  (new)
  Verifies the YAML loader produces the same shape as the in-code
  default table, and that equipment.yaml populates every section
  (SVIDs/ECIDs/CEIDs/alarms/recipes/commands).  SECSGEM_DATA_DIR
  CMake define points at ${CMAKE_SOURCE_DIR}/data so tests don't
  depend on cwd.

CMakeLists.txt, Dockerfile
  find_package(yaml-cpp) and link.  libyaml-cpp-dev added to the
  Ubuntu base image (yaml-cpp 0.8 ships the modern target name).

File consolidation
------------------

Five small files removed; their content lives in fewer headers:

  - secs2/item.cpp        -> inline in secs2/item.hpp
  - secs2/message.cpp     -> inline in secs2/message.hpp
  - hsms/types.hpp        -> merged into hsms/header.hpp
  - hsms/frame.hpp        -> merged into hsms/header.hpp
  - hsms/frame.cpp        -> merged into hsms/header.cpp

hsms/header.hpp is now "the HSMS wire format" in one place: SType + status
enums + Timers + Header + Frame + constants.  All includers updated.

Net effect
----------

Before: equipment data dictionary lived in 50 lines of imperative
populate() inside secs_server.cpp; dispatch in a 20-branch if-ladder.

After: equipment data dictionary lives in 47 lines of YAML; dispatch
is a Router built once.  Adding a new capability is now a YAML edit
in the common case.

Test count up to 67 cases / 384 assertions (+4 cases / +106 assertions)
covering the loader and the new table-driven SM paths.

What's NOT changed
------------------

The per-SxFy reply construction still lives in C++ (each message has a
unique body shape).  Moving those into YAML/JSON message-shape
definitions is the next refactor step but requires a generic typed
encoder/decoder driven by shape descriptors; out of scope here.

Spooling, the S9 error stream, S1F19/F20, and the other gaps in
COMPLIANCE.md remain unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 08:57:38 +02:00
raphael 96b02f8b50 Initial commit: C++20 SECS-II / HSMS / GEM client + server
A fully containerised SECS/GEM toolchain. Single docker compose project,
no host build tools. 63 unit-test cases / 278 assertions, two demo
executables, end-to-end two-container demo exercising every implemented
capability.

Architecture (bottom-up):

  secs2/   E5 SECS-II codec
    Item        variant over L/A/B/BOOLEAN/I1-8/U1-8/F4/F8
    encode/decode  big-endian, 1/2/3-byte length encoding
    Message     SxFy + W-bit + optional root item
    to_sml      human-readable text rendering

  hsms/    E37 HSMS transport (TCP)
    Header      10-byte header + SType enum (Data/Select/Deselect/
                Linktest/Reject/Separate)
    Frame       4-byte length prefix + payload encode/decode
    Connection  async Asio TCP, NOT-SELECTED -> SELECTED state machine,
                T3/T5/T6/T7/T8 timers, system-bytes reply correlation,
                graceful close-after-flush separation

  endpoint  active Client (connect with T5 retry) and passive Server
            (accept loop) wrappers over Connection

  gem/     E30 GEM logic
    ControlStateMachine  5-state E30 control model with operator
                         actions, host requests, SEMI-mandated ack
                         codes (OnlineAck, OfflineAck, CommAck), and
                         a state-change handler
    EquipmentDataModel   in-memory dictionary: SVIDs, DVIDs, ECIDs
                         (with EAC), CEIDs, report defs, CEID->report
                         links, enabled-events set, alarm table
                         (ALCD, enabled, active), process programs,
                         host command registry, clock (16-char
                         YYYYMMDDhhmmsscc with offset)
    messages.hpp         builders + parsers for every SxFy below

GEM message coverage (full list):

  S1F1/F2    Are You There / On Line Data
  S1F3/F4    Selected Equipment Status Request / Data
  S1F11/F12  Status Variable Namelist Request / Data
  S1F13/F14  Establish Communications (+ CommAck)
  S1F15/F16  Request OFFLINE (+ OfflineAck)
  S1F17/F18  Request ONLINE (+ OnlineAck)
  S2F13/F14  Equipment Constant Request / Data
  S2F15/F16  EC Send + EquipmentAck (Accept/UnknownEcid/Busy/OutOfRange)
  S2F17/F18  Date and Time Request / Data
  S2F29/F30  Equipment Constant Namelist Request / Data
  S2F31/F32  Date and Time Set Request / TimeAck
  S2F33/F34  Define Report + DefineReportAck (5 enum values)
  S2F35/F36  Link Event Report + LinkEventAck
  S2F37/F38  Enable / Disable Event Report + EnableEventAck
  S2F41/F42  Host Command + HostCmdAck (7 values) + per-param CPACKs
  S5F1/F2    Alarm Report Send + AlarmAck (ALCD bit-7 set/cleared
             + lower-7 category)
  S5F3/F4    Enable/Disable Alarm Send + AlarmAck
  S5F5/F6    List Alarms Request / Data (active alarms tagged in ALCD)
  S6F11/F12  Event Report Send (equipment-initiated CEID emission
             with full report data) + EventReportAck
  S7F3/F4    Process Program Send + ProcessProgramAck (7 values)
  S7F5/F6    Process Program Request / Data
  S7F19/F20  Current EPPD List Request / Data
  S10F1/F2   Terminal Display Single (host->equipment) + TerminalAck
  S10F3/F4   Terminal Display Single (equipment->host)

Demo apps:

  apps/secs_server.cpp   passive equipment. Populates the data model
                         with 3 SVIDs (ControlState, Clock,
                         EventsEnabled), 2 ECIDs, 3 CEIDs
                         (ControlStateChanged, AlarmSetEvent,
                         ProcessStarted), 2 alarms (Chiller Temp High
                         cat 4, Door Open cat 1), 2 recipes
                         (RECIPE-A, RECIPE-B), and 4 host commands
                         (START, STOP, PAUSE, FAULT). Emits S6F11 on
                         every control state transition + on START;
                         emits S5F1 + the AlarmSetEvent CEID on FAULT.
                         Pushes an S10F3 welcome message when the host
                         comes online.

  apps/secs_client.cpp   active host. Walks 17 steps: Establish ->
                         Online -> S1F11 SVID namelist -> S1F3 read ->
                         S2F29 EC namelist -> S2F13 read ->
                         S2F17 clock -> S2F33/S2F35/S2F37 dynamic
                         event subscription -> S2F41 START
                         (-> receives S6F11) -> S5F5 alarm list ->
                         S5F3 enable alarm 1 -> S2F41 FAULT
                         (-> receives S5F1 + S6F11) -> S7F19/S7F5
                         recipe list + body -> S10F1 terminal ->
                         S1F15 Offline -> Separate. Handles inbound
                         S6F11, S5F1, S10F3 primaries.

Testing:

  tests/test_secs2.cpp         codec round-trip for every format,
                               byte-layout assertions for known values,
                               truncation/trailing-byte rejection,
                               nested list round-trip, SML rendering
  tests/test_hsms.cpp          header byte layout, data + control
                               header round-trip, full frame round-
                               trip with length prefix, short-payload
                               rejection
  tests/test_control_state.cpp every (state, event) pair in the E30
                               control state machine, including
                               AlreadyOnline / NotAccept rejections
                               and idempotent offline-while-offline
  tests/test_data_model.cpp    SVID/ECID/Alarm/Recipe CRUD, clock
                               format + parse, host command registry,
                               full event-report pipeline (define ->
                               link -> enable -> compose) with
                               every error path (InvalidVid,
                               UnknownCeid, UnknownRptid), alarm
                               set/clear with ALCD bit-7 semantics
  tests/test_messages.cpp      round-trip + byte-layout for every
                               builder/parser pair, including S6F11
                               event reports with mixed item types

Toolchain:

  Dockerfile          Ubuntu 24.04, g++-13, CMake, Ninja, libasio-dev
  docker-compose.yml  builder / tests / server / client services,
                      source bind-mounted, build artifacts in a
                      named volume so the host tree stays clean
  CMakeLists.txt      C++20, -Wall -Wextra -Wpedantic, standalone
                      Asio (ASIO_STANDALONE), doctest via FetchContent

Documentation:

  README.md           architecture, quick start, demo log
  COMPLIANCE.md       honest per-capability E5/E30/E37 audit with
                      spec section refs. Calls out what's implemented,
                      what's partial (Reject.req, Alarms missing F7/F8,
                      EC range validation, PP without verify, terminal
                      single-line only), and what's intentionally not
                      yet implemented (spooling, S9 error stream,
                      Documentation S1F19/F20+F21/F22, limits monitoring,
                      trace data collection, multi-block, material
                      movement). Does NOT claim "100% GEM-compliant" and
                      lists the work required to honestly make that claim.

This is Layer 0 + the start of Layer 1 from implementation_plan.md.
The transition-table-driven "spec-as-data" architecture (Layer 1
proper) is not yet implemented; the current code uses imperative
state machines that are structurally ready to be refactored onto
tables.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 00:21:10 +02:00