# secs-gem A C++20 SECS-II / HSMS / GEM client and server, fully containerised, with every behavioural rule encoded as YAML data (control state, equipment data dictionary, SECS-II message shapes). See [COMPLIANCE.md](COMPLIANCE.md) for the per-capability E5/E30/E37 audit. Every GEM Fundamental and every GEM Additional capability that E30 binds to a concrete SECS-II message set is implemented and round-trip-tested. ## 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 # 84+ test cases / 480+ 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 (44 SxFy) │ │ control_state.yaml E30 §6.2 transition table │ │ equipment.yaml SVIDs / DVIDs / ECIDs / CEIDs / │ │ alarms / recipes / commands / │ │ capabilities / spool / DVID list │ └──────────────────────┬───────────────────────────────────────┘ │ (loaded at startup, codegened 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 ControlTransitionTable + ControlStateMachine,│ │ EquipmentDataModel composing nine stores: │ │ SVID, DVID, ECID, Event Subscriptions, │ │ Alarms, Recipes, Clock, Commands, Spool, │ │ Limits, Traces │ │ Router (stream, function) -> handler │ │ generated messages.hpp (all 44 SxFy) │ │ secsgem::hsms Connection (Asio), Header, Frame, Timers │ │ Auto-emits S9F3/F5/F7/F9/F11 on protocol │ │ error conditions. │ │ secsgem::secs2 Item (variant), encode/decode, Message │ └──────────────────────────────────────────────────────────────┘ ``` ### Tree ``` secs-gem/ ├── Dockerfile, docker-compose.yml # toolchain + demo ├── CMakeLists.txt ├── implementation_plan.md # 7-layer spec-as-data roadmap ├── COMPLIANCE.md # per-capability E5/E30/E37 audit ├── data/ │ ├── messages.yaml # SECS-II message catalog │ ├── control_state.yaml # E30 control state transitions │ └── equipment.yaml # equipment data dictionary ├── tools/ │ └── gen_messages.py # codegen (messages.yaml -> .hpp) ├── include/secsgem/ │ ├── secs2/{item,codec,message}.hpp │ ├── hsms/{header,connection}.hpp │ ├── gem/{control_state,data_model,messages_helpers,router}.hpp │ ├── gem/store/ # one file per focused store: │ │ ├── status_variables.hpp # SVIDs + DVIDs │ │ ├── equipment_constants.hpp # ECIDs + EAC range validation │ │ ├── event_reports.hpp # CEIDs + Reports + Links │ │ ├── alarms.hpp # alarm registry │ │ ├── recipes.hpp # process program store │ │ ├── clock.hpp # 16-char TIME with offset │ │ ├── host_commands.hpp # RCMD registry │ │ ├── spool.hpp # spool queue + state │ │ ├── limits.hpp # variable limit definitions │ │ └── trace.hpp # active trace configs │ ├── 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 The point of "spec-as-data" is that adding behaviour almost never requires a C++ change. ### Add a new SVID ```yaml # data/equipment.yaml svids: - {id: 4, name: ChamberTemp, units: "C", type: U4, value: 25} ``` Restart the server. The host sees the new SVID via S1F11/F3 immediately. ### Add a new host command with side effects ```yaml # data/equipment.yaml host_commands: - {name: VENT, ack: Accept, emit_ceid: 400, set_alarm: 2} ``` ### Add a new state transition ```yaml # data/control_state.yaml transitions: - {from: OnlineRemote, on: host_request_offline, to: EquipmentOffline, ack: Accept} ``` ### Add a 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. ## Demo The two-container demo walks ~20 SECS transactions: ``` [host] -> Select.req [equip] <- Select.req [host] == SELECTED == [equip] == SELECTED == [host] -> S1F13 W [equip] -> S1F14 (COMMACK=0) [host] -> S1F17 W [equip] HostOffline -> AttemptOnline -> OnlineRemote [host] -> S1F19 W [equip] -> S1F20 (12 capabilities) [host] CCODE 1 Establish Communications [host] CCODE 2 Dynamic Event Report Configuration [host] ... [host] CCODE 14 Spooling [host] -> S1F21 W [equip] -> S1F22 (2 DVIDs) [host] -> S1F11 W [equip] -> S1F12 (3 SVIDs) [host] -> S1F3 W [equip] -> S1F4 [host] -> S2F29 W [equip] -> S2F30 (2 EC entries) [host] -> S2F33/F35/F37 W [equip] subscribes CEIDs 200, 300 [host] -> S2F41 W START [equip] emit S6F11 CEID=300 [host] EVENT CEID=300 (1 reports) [host] -> S5F5 W [equip] -> S5F6 (2 alarms) [host] -> S5F3 W [equip] enables alarm 1 [host] -> S2F41 W FAULT [equip] emit S5F1 + S6F11 CEID=200 [host] ALARM SET ALID=1 cat=4 "Chiller Temp High" [host] -> S2F41 W SPOOL_ON [equip] force_spool=true [host] -> S2F41 W START spool: S6F11 CEID=300 queued [host] -> S2F41 W SPOOL_OFF [equip] force_spool=false (depth=1) [host] -> S6F23 W Transmit [equip] drains 1 spooled message [host] EVENT CEID=300 (from spool, post-fact) [host] -> S7F19 W [equip] -> S7F20 (2 PPIDs) [host] -> S7F5 W RECIPE-A [equip] -> S7F6 [host] -> S10F1 W [equip] TERMINAL[0] Hello equipment! [host] -> S1F15 W [equip] OnlineRemote -> HostOffline [host] -> Separate.req [equip] <- Separate.req ``` ## 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.