Files
secs-gem/README.md
T
raphael d470442a8c docs: drop implementation_plan.md, rewrite README for fab deployment
implementation_plan.md was a Layer-0..6 roadmap from the project's
spec-as-data exploration phase; every layer it described is now
shipped (Layer 0 foundations through Layer 4 message catalog +
state machines).  Removed.

README rewritten for the fab-deployment audience.  Sections added:

  1. Persistence directory layout (storage rules, disk budget, DR)
  2. Security (network isolation, TLS tunnels, audit logging,
     config signing)
  3. Monitoring + observability (signals → hooks table, Prometheus
     pattern)
  4. High availability (active/standby on shared persistence)
  5. Deployment patterns (Docker / systemd / k8s)
  6. Upgrade path (YAML reload, code rollout, schema versioning)
  7. Integration with the fab stack (MES / AMHS / OHT / recipe
     engine table)
  8. Compliance + certification (fork COMPLIANCE.md per tool, run
     RTS)
  9. Testing in production (canary, synthetic transactions, shadow
     traffic)
 10. Operational runbook (incident → first check → mitigation)

Stale stats refreshed: test count went 148/794 → 384/2390;
catalog grew to 164 messages; HSMS-GS, SECS-I T3/T4, per-port E84,
E42 formatted PPs all mentioned.

COMPLIANCE.md §9 lost its stale `implementation_plan.md` reference.

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

385 lines
17 KiB
Markdown

# secs-gem
A C++20 SECS-II / HSMS / SECS-I / GEM / GEM 300 runtime, fully containerized,
with every behavioural rule encoded as YAML data (control state, equipment
data dictionary, E40 process-job state machine, E94 control-job state
machine, SECS-II message shapes).
Implements **all of E5, E30, E37 (SS + GS), E4 SECS-I, E40, E42, E84, E87,
E90, E94, E116, E120, E148, E157, E39**. Per-store persistence on every
mutable in-memory entity (spool, carriers, load-ports, substrates,
process-jobs, control-jobs, exceptions). See **[COMPLIANCE.md](COMPLIANCE.md)**
for the per-capability audit and **[INTEGRATION.md](INTEGRATION.md)** for
the vendor-side tutorial.
## Quick start
Everything runs in Docker — no compiler or build tools on the host.
```bash
docker compose run --rm builder # configure + compile
docker compose run --rm tests # 384 cases / 2390 assertions
docker compose up --no-deps server client # live two-container demo
```
## Architecture
The project is "spec-as-data": the SEMI behavioural rules live in YAML;
the C++ is the engine that reads them.
```
┌──────────────────────────────────────────────────────────────┐
│ data/ │
│ messages.yaml SECS-II message catalog │
│ control_state.yaml E30 §6.2 control transition table │
│ process_job_state.yaml E40 §6 PJ transition table │
│ control_job_state.yaml E94 §6 CJ transition table │
│ equipment.yaml SVIDs / DVIDs / ECIDs / CEIDs / │
│ alarms / recipes / commands │
└──────────────────────┬───────────────────────────────────────┘
│ (loaded at startup, codegen at build)
┌──────────────────────────────────────────────────────────────┐
│ tools/gen_messages.py │
│ reads messages.yaml -> emits generated/secsgem/gem/messages.hpp
└──────────────────────┬───────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ apps/ │
│ secs_server.cpp passive equipment │
│ secs_client.cpp active host │
│ (both use gem::Router for dispatch) │
└──────────────────────────────────────────────────────────────┘
secsgem::config loader.hpp: YAML -> tables + data model
secsgem::gem every per-standard FSM (E30, E40, E84, E87,
E90, E94, E116, E120, E148, E157, E39, E5
exceptions), each per-store-persistable.
EquipmentDataModel composes all stores.
Router (stream, function) -> handler.
Generated messages.hpp covers 164 SxFy.
secsgem::hsms Connection (Asio): HSMS-SS + HSMS-GS, all
T-timers enforced, auto S9F3/F5/F7/F9/F11.
secsgem::secsi SECS-I Protocol FSM (E4): T1/T2/T3/T4 enforced
in-FSM, TCP transport for tunnel testing.
secsgem::secs2 Item (variant), encode/decode, Message,
SML parser/printer.
```
### Tree
```
secs-gem/
├── Dockerfile, docker-compose.yml # toolchain + demo
├── CMakeLists.txt
├── README.md
├── COMPLIANCE.md # per-capability audit
├── INTEGRATION.md # vendor integration tutorial
├── data/
│ ├── messages.yaml # SECS-II message catalog (164 msgs)
│ ├── control_state.yaml # E30 control state transitions
│ ├── process_job_state.yaml # E40 PJ transitions
│ ├── control_job_state.yaml # E94 CJ transitions
│ └── equipment.yaml # equipment data dictionary
├── tools/
│ └── gen_messages.py # codegen (messages.yaml -> .hpp)
├── include/secsgem/
│ ├── secs2/{item,codec,sml,message}.hpp
│ ├── hsms/{header,connection}.hpp
│ ├── secsi/{header,block,protocol,tcp_transport}.hpp
│ ├── gem/ # FSMs per SEMI standard
│ ├── gem/store/ # one file per focused store
│ ├── config/loader.hpp
│ └── endpoint.hpp
├── src/{secs2,hsms,secsi,gem,config}/*.cpp
├── apps/
│ ├── secs_server.cpp # passive equipment demo
│ ├── secs_client.cpp # active host demo
│ └── secs_interop_probe.cpp # cross-test against secsgem-py
├── interop/ # secsgem-py 0.3.0 cross-validation
└── tests/test_*.cpp # 384 cases / 2390 assertions
```
## Adding a capability
The point of "spec-as-data" is that adding behaviour almost never
requires a C++ change.
### New SVID
```yaml
# data/equipment.yaml
svids:
- {id: 4, name: ChamberTemp, units: "C", type: U4, value: 25}
```
### New host command with side effects
```yaml
host_commands:
- {name: VENT, ack: Accept, emit_ceid: 400, set_alarm: 2}
```
### New state transition
```yaml
# data/control_state.yaml
transitions:
- {from: OnlineRemote, on: host_request_offline, to: EquipmentOffline, ack: Accept}
```
### New SECS-II message
```yaml
# data/messages.yaml
- id: S6F30
stream: 6
function: 30
w: true
builder: s6f30_something
parser: parse_s6f30
body:
kind: list
struct_name: Something
fields:
- {name: field_a, shape: {kind: scalar, item_type: U4}}
- {name: field_b, shape: {kind: scalar, item_type: ASCII}}
```
`docker compose run --rm builder` regenerates `messages.hpp`. The typed
builder, parser, and struct definition appear automatically.
---
# Production / fab deployment
The library is a runtime stack. Shipping it on a real tool involves
more than building the binary. This section enumerates the work
that sits between "tests pass" and "this is running on the fab floor."
## 1. Persistence directory layout
Enable persistence per store at startup, before the connection comes up.
Pattern (the call sites are equivalent on every store):
```cpp
auto base = std::filesystem::path("/var/lib/acme-secsgem");
model->spool.enable_persistence(base / "spool");
model->carriers.enable_persistence(base / "carriers");
model->load_ports.enable_persistence(base / "loadports");
model->substrates.enable_persistence(base / "substrates");
model->process_jobs.enable_persistence(base / "pjobs");
model->control_jobs.enable_persistence(base / "cjobs");
model->exceptions.enable_persistence(base / "exceptions");
```
Storage rules:
- **Mount this volume on the same physical disk as the binary** —
network filesystems (NFS) can introduce latency that interferes
with the rename-based atomic write pattern.
- **Back this volume up daily**. Journal files are small (a few
hundred bytes each) and rsync-friendly.
- **Set sane retention**. Cleared exceptions and dequeued PJs are
removed automatically; complete carriers / substrates / CJs are
the application's responsibility to sweep. Cap by file count
(a million files in one directory is fine on ext4 / xfs; less
on others).
- **Disk space**: budget 100 MB for a busy fab tool over a year
(~500 K transitions, ~200 bytes each). In practice it's far
less because terminal-state records are removed.
After a crash, the next process start replays every store and is
back to the prior in-memory state before the HSMS port opens.
## 2. Security
HSMS over plain TCP is the spec's wire protocol. The library
ships unencrypted by design — that's what equipment manufacturers
expect. In a real fab:
- **Network isolation**: HSMS must run on a control LAN, never
exposed to engineering / corporate networks. Default the
`--port` to 5000 / 5005 on a dedicated VLAN behind firewall ACLs
that whitelist your MES host's IP.
- **TLS tunnel**: for cross-site HSMS (rare but real for multi-fab
shared hosts), tunnel the TCP through stunnel or a sidecar
proxy. Don't modify the HSMS wire protocol; wrap the socket.
- **Authentication**: HSMS doesn't include peer auth. Rely on
network-layer mTLS (sidecar proxy) and per-tool firewall rules.
- **Audit logging**: enable `Connection::set_log_handler` and
ship to a SIEM. Every SECS-II message in/out should be
retrievable for a configurable retention window — many fabs
require 90 days.
- **YAML config integrity**: sign your config bundles
(`equipment.yaml`, `control_state.yaml`, etc.) and verify the
signature on load. Misconfiguration is one of the top
root-causes of GEM-related fab incidents.
## 3. Monitoring and observability
The library exposes hooks at every layer. Wire them to whatever
your fab already runs.
| Signal | Hook | Why it matters |
| ---------------------------- | ------------------------------------------ | -------------------------------------------------- |
| HSMS connection lifecycle | `Connection::set_log_handler`, `set_selected_handler`, `set_closed_handler` | reconnect storms, unexpected separates |
| T3 / T6 / T7 / T8 timer fires | `set_closed_handler` reason starts with "T*" | host MES unreachable, fab network event |
| Auto S9F* emission | `set_log_handler` line containing "-> S9F" | malformed peer traffic, schema drift |
| Spool depth | `model->spool.size()` | host MES backpressure / outage |
| FSM transitions (every store) | `set_*_change_handler` | tool state, throughput, anomaly detection |
| Persistence directory size | `du -s var/lib/acme-secsgem` | journal growth, untracked terminal-state records |
Recommended metrics export pattern: aggregate into Prometheus
via a sidecar that polls the data model. Per-CEID emission rates,
alarm set/clear rates, T-timer expiry counts, and spool depth
form a reasonable starter dashboard.
## 4. High availability
The library is single-threaded per HSMS connection — that's how
HSMS works. For HA:
- **Run two equipment processes** in active/standby on the same
tool, sharing the persistence volume. Only the active accepts
the HSMS port; the standby tails the journal. Failover is
filesystem-locked.
- **Reconnect on the host side**: an MES-side disconnect should
trigger T5-based reconnect. Configure `Timers::t5` to your
MES's policy (default 10s).
- **Graceful shutdown**: SIGTERM should flush the write queue,
call `conn->separate()`, and exit cleanly so the journal is
point-consistent. The provided `apps/secs_server.cpp` shows
the pattern.
## 5. Deployment patterns
Three common shapes:
### Docker / podman on a tool PC
```dockerfile
FROM ubuntu:24.04
COPY build/secs_server /usr/local/bin/
COPY etc/ /etc/acme-secsgem/
VOLUME /var/lib/acme-secsgem
EXPOSE 5000
ENTRYPOINT ["/usr/local/bin/secs_server", \
"--port", "5000", \
"--config", "/etc/acme-secsgem/equipment.yaml", \
"--state-table", "/etc/acme-secsgem/control_state.yaml", \
"--spool-dir", "/var/lib/acme-secsgem/spool"]
```
### systemd unit
```ini
[Unit]
Description=ACME SECS/GEM equipment
After=network.target
[Service]
Type=simple
User=secsgem
Group=secsgem
ExecStart=/usr/local/bin/secs_server --port 5000 \
--config /etc/acme-secsgem/equipment.yaml \
--state-table /etc/acme-secsgem/control_state.yaml \
--spool-dir /var/lib/acme-secsgem/spool
Restart=always
RestartSec=5
LimitNOFILE=8192
[Install]
WantedBy=multi-user.target
```
### Kubernetes (multi-tool cell controller)
Run one Pod per tool with the persistence volume mounted from
local-storage (not NFS). The Service exposes the HSMS port on the
control LAN. Use a PodDisruptionBudget to ensure the standby is
available during rolling updates.
## 6. Upgrade path
YAML edits don't require a rebuild — restart the process and the
new dictionary loads. Code changes do require rebuild + restart.
- **Zero-downtime for YAML**: if you're using the active/standby
HA pattern, edit YAML on the standby, restart the standby,
promote.
- **Code upgrades**: deploy to a canary tool first; bake-test for
at least a full wafer batch before fleet-wide rollout.
- **Schema migrations**: persistence records are versioned (v1, v2)
and forward-compatible. Older versions still load; newer
versions ignore unknown trailers. Always test the upgrade with
a real on-disk journal before fleet rollout.
## 7. Integration with the fab stack
| Other system | How this library talks to it |
| ------------------- | --------------------------------------------------------------------- |
| MES (Camstar, Mozaic, Camstar) | HSMS-SS over TCP (`secs_server` listens on a port the MES is configured to connect to) |
| Multi-MES (HSMS-GS) | `Connection::add_session(device_id)` registers extra sessions on one TCP socket |
| AMHS / OHT | E84 per-port FSMs (`E84PortStore::on_signal_change(port, signal, value)`); wire to your I/O bridge |
| Recipe engine | RecipeStore.add (opaque) + RecipeStore.add_formatted (E42 structured) |
| Alarm sources | `AlarmRegistry::set(alid, active)` from your sensor poll |
| Carrier scanner | `CarrierStore::create / fire_id_event / set_slot_state` |
| Wafer tracker | `SubstrateStore::create / fire_*_event` |
| EPT / shift report | `EptStateMachine::accumulated(state)` reads the time-bucket counters |
## 8. Compliance and certification
- Fork `COMPLIANCE.md` and prune it to *your* tool's claimed
coverage; ship that copy with the tool.
- Run an independent validator (GEM RTS or equivalent) against
your specific tool — a passing library is necessary but not
sufficient.
- Capture wire traces from the validator run; archive for
audit.
## 9. Testing in production
- **Canary**: deploy to one or two tools per fab before fleet
rollout.
- **Synthetic transactions**: a heartbeat that issues S1F1 every
60s and alerts on T3 timeout. Catches MES-side outages before
a real recipe does.
- **Shadow traffic**: for upgrades, run the new version listening
on a second port; have MES dual-connect; diff the responses.
## 10. Operational runbook (starting point)
Common production incidents and the right response:
| Incident | First check | Mitigation |
| ----------------------------------- | ------------------------------------ | ----------------------------------------- |
| HSMS connection flapping | T7 / T6 timer fires in logs | check MES reachability, network MTU |
| Spool depth growing | host MES connectivity / ACK rate | force-drain via S6F23, escalate to MES |
| State machine "stuck" | last state-change handler log line | host-issued offline + re-establish |
| Alarm storm | AlarmRegistry `all()` snapshot | check upstream sensor; quench via S5F3 |
| Persistence dir growing unbounded | `du -s` + file count | sweep terminal-state records |
| Cross-tool inconsistency | `secsgem_tests` on canary tool | compare wire trace vs validator |
---
## Demo
The two-container demo walks ~24 SECS transactions end-to-end
through the data model. Run `docker compose up --no-deps server client`
and watch the logs interleave.
## Build details
The toolchain image (`Dockerfile`) is Ubuntu 24.04 with `g++-13`, CMake,
Ninja, `libasio-dev`, `libyaml-cpp-dev`, and Python 3 for the codegen.
doctest is fetched via CMake FetchContent. Build artifacts live in a
named Docker volume so the host filesystem stays clean.
Standalone Asio is used in header-only mode (`ASIO_STANDALONE`). No
Boost dependency.
## Interop
`interop/` contains the secsgem-py 0.3.0 cross-validation harness —
secsgem-py active host driving our C++ passive server, our C++ active
host probing secsgem-py's passive equipment, and a raw GEM-300 harness
that round-trips S3 (E87), S14 (E94), S16 (E40), S12 (wafer maps)
through hand-crafted `SecsStreamFunction` subclasses. See
`interop/README.md`.