secs-gem-host
v0.2.0
Published
SECS/GEM host test tool: E30 startup conformance, JSON scenarios, and an MCP server for AI agents.
Maintainers
Readme
secs-gem-host
The open-source SECS/GEM host you point at equipment-side software to see whether it actually works. One command runs the SEMI E30 host startup sequence and prints a PASS/FAIL conformance table; the same engine is exposed as an MCP server so an AI agent can drive it.
npx secs-gem-host connect 127.0.0.1:5501 --reportNode ≥ 20. MIT. No runtime dependency except the MCP SDK.
Commands
secs-gem-host connect <host:port> [startup|state] [options]
secs-gem-host mcp| Option | Meaning |
|---|---|
| --device-id <n> | HSMS session / device id (default 0) |
| --passive | listen for the equipment instead of connecting to it; the <host:port> positional is the bind address, so use 127.0.0.1:<port> unless you need other machines to reach it |
| --report | run a conformance report, print the table, exit 1 on any FAIL. The kind is the positional after <host:port>: startup (default) is the 10-step E30 startup; state is the 6 E30 state rules — S1F1 aborted with S1F0 before S1F13, S2F41 refused with HCACK 2 until S1F17 — and must run on a fresh connection |
| --scenario <file> | run a JSON scenario after startup |
| --model <file> | equipment model used by startup steps 6–8 (default: bundled models/default.json) |
| --quiet | suppress the decoded message log |
| --linktest <sec> | linktest interval, 0 disables (default 30) |
| --t3 --t5 --t6 --t7 --t8 <sec> | HSMS timers in seconds (E37 defaults 45 10 5 10 5) |
Plain connect stays up, prints every message in and out in SML with a timestamp and a
direction, and re-runs the whole startup after a reconnect — like a real host.
The E30 startup
| Step | Send | Expect | PASS when | |---|---|---|---| | HSMS | Select.req | Select.rsp | status 0 within T6 | | 1 | S1F13 W | S1F14 | COMMACK 0, MDLN/SOFTREV present | | 2 | S1F17 W | S1F18 | ONLACK 0 or 2 | | 3 | S1F3 W (empty) | S1F4 | list, within T3 | | 4 | S1F11 W (empty) | S1F12 | non-empty SVID list | | 5 | S2F29 W (empty) | S2F30 | list | | 6 | S2F33 W (delete all, then define) | S2F34 | DRACK 0 | | 7 | S2F35 W | S2F36 | LRACK 0 | | 8 | S2F37 W (CEED true, all) | S2F38 | ERACK 0 | | 9 | S5F3 W (ALED enable, zero-length ALID = all alarms) | S5F4 | ACKC5 0 | | 10 | S2F31 W (now) | S2F32 | TIACK 0 | | idle | Linktest.req every 30 s | Linktest.rsp | within T6 |
The host answers S1F13 with S1F14, S1F1 with S1F2, S5F1 with S5F2, S6F11 with S6F12 and S6F1 with S6F2, and sends S9F3/S9F5/S9F9 on the standard conditions.
Beyond the per-step result, --report flags: S9Fx received, unsolicited <SxFy> before
S1F14, reply outside T3 on <SxFy>, wrong system bytes on reply <SxFy>, W-bit set on
reply <SxFy>, and primary with even function <SxFy>.
Scenarios
A scenario is a JSON array of steps run after startup. Bodies use the same item-tree shape as the SECS-II codec API.
[
{ "send": "S2F41", "body": { "type": "L", "children": [
{ "type": "A", "value": "START" },
{ "type": "L", "children": [] }
] } },
{ "expect": "S6F11", "ceid": 10001, "within": 5000 },
{ "wait": 1000 },
{ "drop-ack": "S6F11" },
{ "sleep-t3": true }
]| Step | Effect |
|---|---|
| {"send": "SxFy", "body": ...} | send a message; an odd function sets the W-bit and waits for the reply |
| {"expect": "SxFy", "ceid": n, "within": ms} | wait for a received message, optionally matching a CEID |
| {"wait": ms} | pause |
| {"drop-ack": "SxFy"} | do not ack the next SxFy primary, to exercise the equipment's retry/spool path |
| {"sleep-t3": true} | answer the next primary 46 s late, past T3, on purpose |
Exit code is 1 if any scenario step or any startup step failed.
Item-tree body syntax
{ "type": "L", "children": [
{ "type": "A", "value": "TEXT" },
{ "type": "U4", "value": [1, 2, 3] },
{ "type": "BOOLEAN", "value": [true] },
{ "type": "B", "value": [0, 255] }
] }Types: L B BOOLEAN A J I1 I2 I4 I8 U1 U2 U4 U8 F4 F8. Non-list scalar values are arrays;
A/J take a string.
MCP server
Register it with Claude Code:
claude mcp add secs-gem-host -- npx -y secs-gem-host mcpor in .mcp.json:
{
"mcpServers": {
"secs-gem-host": { "command": "npx", "args": ["-y", "secs-gem-host", "mcp"] }
}
}Tools: connect, disconnect, send, startup, events, status. startup returns
the report object (rows, passed, total, flags) plus the rendered table; its optional
kind is "startup" (default) or "state" for the six E30 state rules, which needs a
fresh connection.
scadathings_feedback is the one tool that does not talk to equipment: it POSTs to
scadathings.com to say whether one of its articles helped you solve a SECS/GEM problem, or
that the article is wrong or outdated. It takes slug (e.g.
secs-gem-s1f13-establish-communications), helped, and optionally a note (≤ 500 chars)
and an agent name. Only what you pass is sent.
Equipment model
--model takes a JSON file describing what steps 6–8 should define:
{
"mdln": "GENERIC-1",
"softrev": "1.0.0",
"svs": [{ "id": 101, "name": "SV_101", "type": "U2", "units": "", "value": 0 }],
"reports": [{ "rptid": 10001, "vids": [10001, 10002] }],
"events": [{ "ceid": 10001, "name": "EVENT_10001", "rptids": [10001] }],
"alarms": [{ "alid": 1001, "text": "ALARM_1001" }]
}The bundled default follows a common industrial numbering scheme: SVIDs 1–3 for the fixed
GEM variables, 101+ for process values, 10000+ for named SVs and DVs, RPTID and CEID
in parallel from 10001, ALIDs from 1001.
Testing your own equipment: write a model file
The bundled model is a guess. Real equipment has its own ids, and steps 6-8 define reports and link events by number — so with the default model the equipment answers DRACK 4 / LRACK 4 / ERACK 1 ("that id does not exist here"), which is a mismatch, not a defect.
- Copy
models/default.jsonnext to your run (keep it out of any public repo if the ids are customer data). - Replace
svs,reports,eventsandalarmswith the equipment's own SVIDs, VIDs, RPTIDs, CEIDs and ALIDs. Step 4'sS1F12namelist prints the SVIDs the equipment actually has; the run also flagsmodel VIDs not in equipment SVID list: <ids>before it sends S2F33. That flag is advisory — a VID that is a DV or an EC is legitimately absent from the S1F12 namelist. - Run with
--model <file>.
node dist/cli.js connect 127.0.0.1:5000 --report --t3 5 --device-id 1 --model site-model.json--device-id must match the equipment's configured session id. If it does not, the
equipment answers every primary with S9F1 (unrecognised device id) and every step fails.
Known findings
Results of running this tool against real equipment implementations.
secs-gem-simulator EQ1 (2026-09-05)
node dist/cli.js connect 127.0.0.1:5501 --report --quiet
Step Sent Expect Got Ack Time(ms) Result Note
------------------------------------------------------------------------
1 S1F13 S1F14 - - 45009/45000 FAIL T3 (S1F13) timeout
2 S1F17 S1F18 - - 45018/45000 FAIL T3 (S1F17) timeout
3 S1F3 S1F4 - - 45005/45000 FAIL T3 (S1F3) timeout
4 S1F11 S1F12 - - 45026/45000 FAIL T3 (S1F11) timeout
5 S2F29 S2F30 - - 45005/45000 FAIL T3 (S2F29) timeout
6 S2F33 S2F34 - - 45022/45000 FAIL T3 (S2F33) timeout
7 S2F35 S2F36 - - 45010/45000 FAIL T3 (S2F35) timeout
8 S2F37 S2F38 - - 45020/45000 FAIL T3 (S2F37) timeout
9 S5F3 S5F4 - - 45007/45000 FAIL T3 (S5F3) timeout
10 S2F31 S2F32 - - 45021/45000 FAIL T3 (S2F31) timeout
------------------------------------------------------------------------
E30 startup: 0/10 passed
flag: reply outside T3 on S1F13
flag: reply outside T3 on S1F17
flag: reply outside T3 on S1F3
flag: reply outside T3 on S1F11
flag: reply outside T3 on S2F29
flag: reply outside T3 on S2F33
flag: reply outside T3 on S2F35
flag: reply outside T3 on S2F37
flag: reply outside T3 on S5F3
flag: reply outside T3 on S2F31
exit=1Every row is the same failure, so it is one finding, not ten.
- Step 1 — the simulator did not send S1F14; the host waited the full T3 and gave up.
- Steps 2–10 — S1F18, S1F4, S1F12, S2F30, S2F34, S2F36, S2F38, S5F4 and S2F32 never arrived either, for the same reason. The host sent S9F9 (transaction timer timeout) after each one and carried on to the next step, as E30 requires.
As of 2026-09-05 the simulator completes HSMS Select and answers Linktest, but sends no
reply to any SECS-II data message. Every step therefore timed out at T3 (45 s), so the
run takes about 7.5 minutes; pass --t3 5 to shorten it.
Changelog
0.2.0
--report state: six steps that score the E30 state rules instead of the startup sequence. Run on a fresh connection: S1F1 must come back as S1F0 before S1F13, and S2F41 must be refused (HCACK ≠ 0) until S1F17 has put the tool ON-LINE REMOTE.secs-gem-host connect 127.0.0.1:5501 state --report.- The MCP
startuptool takeskind: "startup" | "state".
0.1.1
- S5F3 "enable all alarms" is now E5-correct. The body was
L[B 0x80, U4 [0]]; "all alarms" is a zero-length ALID item,L[B 0x80, U4 []]. Equipment that rejects an unknown ALID answered ACKC5 1 to the old body. - DRACK 4 / LRACK 4 / ERACK 1 now say what to do: the note names the model mismatch and
points at
--model, and the SVID namelist from step 4 is compared against the model's report VIDs, so the run flagsmodel VIDs not in equipment SVID list: <ids>before S2F33 goes out.
Development
npm install
npm test # tsc && chmod +x dist/cli.js && node --test "dist/test/**/*.js"Licence
MIT © Yoseok Jung
