raphael 547fd2116b
tests / build-and-test (push) Failing after 43s
Close COMPLIANCE.md gap: S9 error stream
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

secs-gem

A C++20 SECS-II / HSMS / GEM client + server toolchain, fully containerized.

The long-term vision is the "spec-as-data" stack described in implementation_plan.md: the SEMI E30 behavioral spec encoded as transition tables, from which a runtime, passive analyzer, simulator, and conformance test generator are derived. The current code is the Layer 0 + Layer 1 base — a working HSMS client/server, the SECS-II codec, the E30 control state machine, and the GEM core capabilities (status variables, equipment constants, dynamic event reporting, alarms, remote control, clock, process programs, terminal services).

For a per-capability E5/E30/E37 audit see COMPLIANCE.md.

Quick start

Everything runs in Docker — no compiler or build tools on the host.

docker compose run --rm builder     # configure + compile
docker compose run --rm tests       # run unit tests (63 cases / 278 assertions)
docker compose up --no-deps server client   # live two-container demo

Architecture

The "spec-as-data" first step: equipment capabilities and the E30 control state machine are loaded from YAML at startup; the SECS dispatch is a (stream, function) -> handler Router rather than an if-ladder.

   ┌──────────────────────────────────────────────────────────────┐
   │ data/equipment.yaml   data/control_state.yaml                │
   │   SVIDs, ECIDs, CEIDs, alarms, recipes, host commands;       │
   │   E30 control state transition table                         │
   └──────────────────────┬───────────────────────────────────────┘
                          │  (loaded at startup)
                          ▼
   ┌──────────────────────────────────────────────────────────────┐
   │                        app / demo                            │
   │   apps/secs_server.cpp        apps/secs_client.cpp           │
   │   uses gem::Router for SECS dispatch                         │
   └────────────┬───────────────────────────┬─────────────────────┘
                │                           │
                ▼                           ▼
   ┌──────────────────────────────────────────────────────────────┐
   │ secsgem::config   loader.hpp  YAML -> tables + data model    │
   │ secsgem::gem      control_state (table-driven), data_model,  │
   │                   messages (SxFy builders), router           │
   │ secsgem::hsms     Connection (Asio), Header, Frame, Timers   │
   │ secsgem::secs2    Item, codec (encode/decode), Message       │
   └──────────────────────────────────────────────────────────────┘

Tree

secs-gem/
├── Dockerfile, docker-compose.yml      # toolchain + demo
├── CMakeLists.txt
├── implementation_plan.md              # the 7-layer spec-as-data roadmap
├── COMPLIANCE.md                       # per-capability E5/E30/E37 audit
├── data/
│   ├── equipment.yaml                  # SVIDs/ECIDs/CEIDs/alarms/recipes/commands
│   └── control_state.yaml              # E30 transition table
├── include/secsgem/
│   ├── secs2/{item,codec,message}.hpp
│   ├── hsms/{header,connection}.hpp    # header.hpp also holds Frame + Timers
│   ├── gem/{control_state,data_model,messages,router}.hpp
│   ├── config/loader.hpp
│   └── endpoint.hpp
├── src/{secs2,hsms,gem,config}/*.cpp + endpoint.cpp
├── apps/{secs_server,secs_client}.cpp
└── tests/test_*.cpp

Adding a capability without recompiling the server

# data/equipment.yaml — append a new SVID
svids:
  - {id: 4, name: ChamberTemp, units: "C", type: U4, value: 25}
# data/equipment.yaml — append a new host command + side effect
host_commands:
  - {name: VENT, ack: Accept, emit_ceid: 400, set_alarm: 2}
# data/control_state.yaml — append a new transition
transitions:
  - {from: OnlineRemote, on: host_request_offline, to: EquipmentOffline, ack: Accept}

Restart the server; the new behaviour is live. The C++ code unchanged.

What's implemented

HSMS (E37)

SELECT/DESELECT/LINKTEST/SEPARATE/REJECT control messages, the 4-byte length-prefixed frame, the 10-byte header, the NOT-CONNECTED → SELECTED state machine, and the T3/T5/T6/T7/T8 timers. Active (client) and passive (server) modes. System-bytes based reply correlation.

SECS-II (E5)

A variant-based Item covering every standard format (List, ASCII, Binary, Boolean, I1I8, U1U8, F4, F8), big-endian numerics, 1/2/3-byte length encoding, and an to_sml() rendering for readable logs.

GEM (E30)

  • Control state machine — 5 states (EquipmentOffline, AttemptOnline, HostOffline, OnlineLocal, OnlineRemote), operator actions, host requests with SEMI-mandated ACK codes, change-handler callback.
  • Status variables — S1F3/F4, S1F11/F12.
  • Equipment constants — S2F13/F14, S2F15/F16, S2F29/F30 + EAC enum.
  • Clock — S2F17/F18, S2F31/F32 (16-char YYYYMMDDhhmmsscc).
  • Establish Comms — S1F13/F14 + CommAck.
  • Online/Offline — S1F15/F16 + OfflineAck, S1F17/F18 + OnlineAck.
  • Remote control — S2F41/F42 + 7-value HostCmdAck + per-param CPACKs.
  • Dynamic event reporting — S2F33 Define Report (DRACK), S2F35 Link Event (LRACK), S2F37 Enable (ERACK), S6F11 emission, S6F12 ack.
  • Alarms — S5F1/F2 emit + ACKC5, S5F3/F4 enable/disable, S5F5/F6 list, ALCD bit-7 set/cleared, lower-7 category.
  • Process programs — S7F3/F4 send, S7F5/F6 request, S7F19/F20 list.
  • Terminal services — S10F1/F2 host→equipment, S10F3/F4 equipment→host.

Not (yet) implemented

Spooling, S9 error stream, S1F19/F20 + S1F21/F22 documentation, multi-block, trace data collection (S2F23/F24, S6F1/F2), limits monitoring, material movement (E40), EC range validation, S5F7/F8 list-enabled-alarms, S6F15/F16 event-report request. See COMPLIANCE.md for the honest accounting.

Demo session

The demo client walks 17 distinct steps end-to-end. From the live log (condensed):

[host]  -> Select.req          [equip] <- Select.req
[host]  <- Select.rsp (Ok)     [equip] -> Select.rsp (Ok)
[host]  == SELECTED ==         [equip] == SELECTED ==
[host]  -> S1F13 W             [equip] S1F13; replying S1F14 (COMMACK=0)
[host]  -> S1F17 W             [equip] control: HostOffline -> AttemptOnline
                               [equip] control: AttemptOnline -> OnlineRemote
[host]  S1F18 ONLACK=0         [equip] -> S10F3 W   (welcome)
[host]  TERMINAL[0] Welcome from SECSGEM-SIM
[host]  -> S1F11 W             [equip] S1F11; replying S1F12 (status namelist)
[host]    SVID 1 ControlState
[host]    SVID 2 Clock
[host]    SVID 3 EventsEnabled
[host]  -> S1F3 W              [equip] S1F3; replying S1F4 (3 values)
[host]  -> S2F29 W             [equip] -> S2F30 (2 EC entries)
[host]  -> S2F33 W             [equip] -> S2F34 (DRACK=0)
[host]  -> S2F35 W             [equip] -> S2F36 (LRACK=0)
[host]  -> S2F37 W             [equip] -> S2F38 (ERACK=0)
[host]  -> S2F41 W START       [equip] RCMD START; emit S6F11 CEID=300
[host]  EVENT CEID=300 (1 reports)
[host]    RPTID 1000: <A "OnlineRemote"> <A "2026060122162336"> <BOOLEAN T>
[host]  -> S5F5 W              [equip] -> S5F6 (2 alarms)
[host]  -> S5F3 W              [equip] -> S5F4 (ACKC5=0)
[host]  -> S2F41 W FAULT       [equip] RCMD FAULT triggers alarm 1
                               [equip] emit S5F1 alarm set ALID=1
                               [equip] emit S6F11 CEID=200
[host]  ALARM SET ALID=1 cat=4 "Chiller Temp High"
[host]  EVENT CEID=200 (1 reports)
[host]  -> S7F19 W             [equip] -> S7F20 (2 PPIDs)
[host]  -> S7F5 W              [equip] -> S7F6 RECIPE-A
[host]  -> S10F1 W             [equip] TERMINAL[0] Hello equipment!
[host]  -> S1F15 W             [equip] control: OnlineRemote -> HostOffline
[host]  -> Separate.req        [equip] <- Separate.req
[host]  exiting

Build details

The toolchain image (Dockerfile) is Ubuntu 24.04 with g++-13, CMake, Ninja, and libasio-dev. doctest is fetched via CMake FetchContent. Build artifacts live in a named Docker volume (build:) so the host filesystem stays clean.

Standalone Asio is used in header-only mode (ASIO_STANDALONE). No Boost dependency.

S
Description
No description provided
Readme 2.4 MiB
Languages
C++ 84.8%
Python 10.8%
Java 1.9%
Shell 1.4%
CMake 0.9%
Other 0.2%