92afbd2a37209259c2efdb55108d47186fbf6329
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc898f8410 |
feat: EquipmentRuntime engine owner + secs_gemd gRPC daemon
Extract the SECS/GEM engine wiring out of the secs_server app into a reusable class, and stand up a language-agnostic gRPC daemon on top so a tool's software (any language) can drive the equipment without linking C++ or knowing SEMI. Foundation for replacing a vendor's SECS/GEM server. Engine reuse: - EquipmentRuntime (include/secsgem/gem/runtime.hpp, src/gem/runtime.cpp): owns io_context, passive Server, model, control-state machine, Router; thread-safe outbound API (set_variable/emit_event/set_alarm/clear_alarm), on_command hook, deliver_or_spool, run()/run_async()/poll()/stop(). - register_default_handlers (src/gem/default_handlers.cpp): the 56 GEM handlers + domain emitters, relocated from secs_server so the app and the daemon speak byte-identical GEM. secs_server.cpp reduced ~1270 -> 113 lines. - name_index.hpp: resolve_variable(name) -> VID (the name->id binding layer). Daemon (apps/secs_gemd.cpp, proto/secsgem/v1/equipment.proto): - runs the engine + HSMS link on a background thread; serves the gRPC Equipment service. Increment 1: SetVariables (name-resolved, plain value->Item) and GetControlState. proto carries the full v1 surface (universal + carrier/recipe/job tiers); remaining RPCs + the Subscribe command stream are next (docs/DAEMON_ROADMAP.md). - CMake: opt-in SECSGEM_DAEMON, protoc/grpc_cpp_plugin codegen, gracefully skipped where protobuf/grpc++ are absent. Dockerfile gains the grpc deps. Tests (proof): test_runtime, test_default_handlers (S1F1->S1F2, S2F41->hook), test_name_index. Full suite 458/458, 2795 assertions; live server<->client GEM300 demo still passes on the refactored server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
5baf3f4dc7 |
verify: tshark HSMS dissector validation (independent third codec)
Wireshark's built-in HSMS dissector — written by network-protocol authors who don't know us, didn't talk to us, and don't share implementation details with secsgem-py — is a third independent codec for our framing. If they parse our pcap without warnings, our HSMS framing is wire-correct independently of both our internal tests and the secsgem-py interop path. interop/tshark_validate.sh: - Boots secs_server on 127.0.0.1:5099 (away from the demo port) - Captures the loopback wire traffic with tcpdump - Runs secs_client through ~24 transactions plus Separate.req + TCP FIN - Parses the pcap with tshark -V using the HSMS dissector - Asserts: no "Malformed Packet", no "Dissector bug", at least one HSMS frame, expected tokens present (Select.req/rsp, Separate.req, Data message), reports histogram (count by control type + distinct S/F pairs) Result against the demo: 69 HSMS frames dissected, 49 distinct S/F pairs (S01F01..S16F28), all clean. Dockerfile gains tshark + tcpdump. .gitea/workflows/ci.yml gains a `tshark-dissector` job that runs this validator as part of every push to main. README proof table grows to 6 commands. VERIFICATION.md §1a documents a follow-up: round-trip the KAT fixtures through secsgem-py to corroborate that the format codes we used match an independent implementation. Strengthens the KAT proof from "internally consistent" to "confirmed by a second implementer who read the spec without talking to us." Plan: VERIFICATION.md §2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|