From 858ca22975e841957a01eccbae0042e5c5ad401a Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Tue, 9 Jun 2026 20:07:31 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20chapters=2011=E2=80=9313=20=E2=80=94=20?= =?UTF-8?q?HSMS,=20SECS-I,=20GEM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more chapters of Part 2: 11 — E37 HSMS. 4-byte length prefix + 10-byte header (R-bit + session id + W-bit + stream + function + PType + SType + system_bytes), the 9 SType control messages, the NOT-SELECTED → SELECTED state machine, T3/T5/T6/T7/T8 with what each one bounds, the auto-S9 paths (S9F1/F3/F5/F7/F9/F11), HSMS-SS vs HSMS-GS, the asio single-threaded contract. 12 — E4 SECS-I. Half-duplex line turnaround (ENQ/EOT/ACK/NAK), the 10-byte block header bit-packing (R-bit / W-bit / E-bit / system bytes), the 244-byte block cap and multi-block split/assemble, the event-driven IO-free FSM with its Action / Event variants, T1/T2/T3/T4 with semantics + defaults, master/slave contention. Notes the deferred asio serial_port adapter; explains why this chapter matters even for HSMS-only readers. 13 — E30 GEM. Disambiguates the three state machines (HSMS transport vs GEM communication vs GEM control), walks the comm-state FSM (DISABLED → WAIT-CRA → COMMUNICATING with T_CRA / T_DELAY) and the control-state FSM (5 states + the YAML transition table). Lists every Fundamental and Additional capability with its messages, code locations, and store assignments. One worked Event-Notification scenario tracing seven on-wire steps to their EquipmentDataModel internals. Co-Authored-By: Claude Opus 4.7 --- docs/11_e37_hsms.md | 444 +++++++++++++++++++++++++++++++++++++++++++ docs/12_e4_secs_i.md | 259 +++++++++++++++++++++++++ docs/13_e30_gem.md | 259 +++++++++++++++++++++++++ 3 files changed, 962 insertions(+) create mode 100644 docs/11_e37_hsms.md create mode 100644 docs/12_e4_secs_i.md create mode 100644 docs/13_e30_gem.md diff --git a/docs/11_e37_hsms.md b/docs/11_e37_hsms.md new file mode 100644 index 0000000..b673d3a --- /dev/null +++ b/docs/11_e37_hsms.md @@ -0,0 +1,444 @@ +# 11 — E37: HSMS — TCP transport for SECS-II + +← [10 E5 — SECS-II data items](10_e5_secs_ii_data_items.md) | [Back to index](00_index.md) | Next: [12 E4 — SECS-I](12_e4_secs_i.md) → + +You have an Item. You have its byte encoding from chapter 10. Now +those bytes need to travel from equipment to host (or vice versa). + +**SEMI E37 — HSMS, High-Speed SECS Message Services** — published +1995, defines how SECS-II bytes travel over TCP/IP. It's not a +codec (chapter 10 was the codec); it's a framing + connection-state ++ timer protocol that sits between SECS-II and TCP. + +This chapter covers: + +- The 14-byte HSMS framing. +- The connection state machine (NOT-CONNECTED → NOT-SELECTED → + SELECTED). +- The five T-timers (T3, T5, T6, T7, T8) and what each one bounds. +- The control-message handshakes: Select.req, Linktest.req, + Separate.req, Reject.req. +- HSMS-SS (single-session) vs HSMS-GS (multi-session). +- The auto-S9 protocol-error replies the implementation emits. +- Where every part of this is in + [`include/secsgem/hsms/`](../include/secsgem/hsms/) and + [`src/hsms/`](../src/hsms/). + +--- + +## The frame + +Every HSMS message on the wire looks the same: + +``` +┌─────────────────┬──────────────────────────┬──────────────────────────┐ +│ 4-byte length │ 10-byte header │ body (0+ bytes) │ +│ big-endian │ session_id / byte2 / … │ SECS-II Item bytes │ +└─────────────────┴──────────────────────────┴──────────────────────────┘ +``` + +The length prefix counts the **header + body** bytes (it does *not* +include itself). So a frame on the wire is at least 14 bytes long +(4-byte length + 10-byte header, with an empty body — typical for +control messages). + +The length is big-endian: the high byte first, then the next, etc. +Defined in [`include/secsgem/hsms/header.hpp:113`](../include/secsgem/hsms/header.hpp): + +```cpp +inline constexpr std::size_t kLengthPrefixSize = 4; +inline constexpr std::size_t kHeaderSize = 10; +``` + +The body is a fully-encoded SECS-II Item (chapter 10). For control +messages (Select, Linktest, etc.) the body is empty. For data +messages it carries whatever Item the higher layer wants to send. + +--- + +## The 10-byte header + +The header is the only HSMS-specific structure besides the length +prefix. Six fields, packed in a fixed order: + +``` + byte 0 1 2 3 4 5 6 7 8 9 + ┌──────┬──────────┬──────────┬─────┬─────┬───────────┐ + │ ses │ byte2 │ byte3 │ Ptype│ Stype│ system_bytes │ + │ _id │ (W+strm) │ function │ (=0) │ │ (correlation)│ + └──────┴──────────┴──────────┴─────┴─────┴───────────┘ + ↑ ↑ ↑ ↑ ↑ ↑ + u16 bit7 W func id 0 SType u32 + bits6-0 + stream +``` + +Per field: + +- **`session_id`** (u16, big-endian). For HSMS-SS this is the + sentinel `0xFFFF` (`kControlSessionId` in + [`header.hpp:48`](../include/secsgem/hsms/header.hpp)). For + HSMS-GS, it carries the device ID identifying which session this + frame belongs to. +- **`byte2`** (u8). Bit 7 is the **W-bit** (1 = reply expected, + 0 = no reply). Bits 0–6 are the **stream** number (S1–S127). +- **`byte3`** (u8). The **function** number (F0–F255). +- **`PType`** (u8). Presentation type. Always 0 for SECS-II + bodies. Any other value triggers `S9F1` "Unrecognized Device ID" + (or `RejectReq` PtypeNotSupported, depending on state). +- **`SType`** (u8). Session type. 0 for data messages; 1–9 for the + various control messages (see next section). +- **`system_bytes`** (u32, big-endian). An opaque correlation + token. Picked by the sender; the receiver MUST echo the same + value in its reply. This is how we match a `S1F2` to the + `S1F1` that asked for it. + +The header is encoded by [`Header::encode()`](../include/secsgem/hsms/header.hpp) +and decoded by [`Header::decode()`](../include/secsgem/hsms/header.hpp); +both implementations in +[`src/hsms/header.cpp`](../src/hsms/header.cpp) are ~30 lines each. + +### Helpful constructors + +The header has two named constructors that wire the right fields +together: + +```cpp +// Data message: W-bit + stream + function + system_bytes. +Header::data_message(session_id, stream, function, w_bit, system_bytes); + +// Control message: SType + system_bytes (other fields default). +Header::control(stype, system_bytes); +``` + +Both in [`header.hpp:74-97`](../include/secsgem/hsms/header.hpp). + +### A worked frame + +Encode the data message `S1F1 W` (Are You There, reply expected), in +HSMS-SS, with `system_bytes = 1`: + +``` +length prefix: 00 00 00 0A (header only = 10 bytes; body is empty) +session_id: FF FF (0xFFFF — kControlSessionId) +byte2: 81 (bit 7 W=1 | stream 1 = 0x80 | 0x01) +byte3: 01 (function 1) +PType: 00 (SECS-II) +SType: 00 (data) +system_bytes: 00 00 00 01 (correlation = 1) +``` + +That's a complete frame. Send those 14 bytes over TCP and you've +just asked an equipment "are you there?" + +The reply, `S1F2`, would echo `system_bytes = 1` and carry a body +(the MDLN + SOFTREV ``). + +--- + +## The session types (`SType`) + +Defined as +[`enum class SType`](../include/secsgem/hsms/header.hpp): + +| SType | Name | Purpose | +|-------|----------------|---------------------------------------------------------------| +| 0 | `Data` | Carries a SECS-II message body. | +| 1 | `SelectReq` | Request to enter SELECTED state. | +| 2 | `SelectRsp` | Reply to Select.req. Body carries a 1-byte `SelectStatus`. | +| 3 | `DeselectReq` | Request to leave SELECTED state (rarely used). | +| 4 | `DeselectRsp` | Reply to Deselect.req. | +| 5 | `LinktestReq` | "Are you still there?" probe. | +| 6 | `LinktestRsp` | Reply to Linktest.req. | +| 7 | `RejectReq` | "I don't understand that." Sent in response to malformed control. | +| 9 | `SeparateReq` | "I'm closing the connection." No reply expected. | + +SType 8 is reserved. + +For every primary control message (Req types 1, 3, 5), the matching +Rsp type comes back — except for `RejectReq` and `SeparateReq` +which are fire-and-forget. All control messages use the +`kControlSessionId = 0xFFFF` in HSMS-SS. + +--- + +## The connection state machine + +A new TCP connection is `NOT-SELECTED`. Until both sides +SELECT, no data messages can be exchanged — even if data frames +arrive, they're rejected with `RejectReq(EntityNotSelected)`. + +``` + (TCP connected; T7 armed) + ┌────────────────┐ Select.req → ┌────────────────┐ + │ NOT-SELECTED │ ────────────────────────► │ SELECTED │ + │ │ ← Select.rsp(Ok) │ │ + └────────────────┘ └────────────────┘ + │ │ + │ T7 expires │ Separate.req → close + │ ─────────────────► close │ or TCP FIN + │ │ + │ data frame received while NOT-SELECTED │ + │ ─────────────────► RejectReq(EntityNotSelected) + │ │ + └─────── close (any reason) ───────────────────┘ +``` + +Per side: + +- **Active** initiates the TCP connect, then sends `Select.req`. +- **Passive** binds and listens; T7 arms when the TCP connection + comes in; it waits for the peer's `Select.req`. + +The state transitions live in +[`src/hsms/connection.cpp`](../src/hsms/connection.cpp) — `start()` +either calls `connect()` (active) or `accept()` (passive); reaching +SELECTED fires `on_selected_` and arms the linktest timer. + +The convention for SECS/GEM is that **the equipment is passive** +(binds the port) and **the host is active** (initiates connect + +Select). This is configurable — `Connection::Mode` is `Active` or +`Passive` — but every example in this codebase follows the GEM +default. + +--- + +## The five T-timers + +The HSMS spec defines T1, T2, T3, T4, T5, T6, T7, T8 — but **HSMS +only uses T3, T5, T6, T7, T8**. T1, T2, T4 are SECS-I (chapter 12). + +Defaults in +[`Timers` struct](../include/secsgem/hsms/header.hpp), line 51: + +```cpp +struct Timers { + std::chrono::milliseconds t3{45000}; // reply + std::chrono::milliseconds t5{10000}; // connect separation + std::chrono::milliseconds t6{5000}; // control transaction + std::chrono::milliseconds t7{10000}; // not-selected + std::chrono::milliseconds t8{5000}; // intercharacter + std::chrono::milliseconds linktest{0}; // 0 disables +}; +``` + +### T3 — reply timeout (45 s default) + +When a side sends a W=1 data message, it arms T3 for the matching +reply. T3 cancels when the reply arrives (same system_bytes). If +it expires, the implementation **auto-emits `S9F9` Transaction Timer +Timeout** carrying the original 10-byte MHEAD so the peer knows +which transaction timed out. + +Tested in [`tests/test_hsms_timers.cpp:164`](../tests/test_hsms_timers.cpp). + +### T5 — connect separation timeout (10 s default) + +After a TCP connection fails or drops, the active side waits T5 +before retrying. Stops a misbehaving peer from getting hammered +with reconnects. + +### T6 — control transaction timeout (5 s default) + +For Select.req → Select.rsp, Linktest.req → Linktest.rsp, and +Deselect.req → Deselect.rsp. If the response doesn't come back +within T6, the connection closes. + +Tested in [`tests/test_hsms_timers.cpp:206`](../tests/test_hsms_timers.cpp). + +### T7 — not-selected timeout (10 s default) + +On a passive side: when TCP comes up but no Select.req has arrived +yet, T7 is armed. If it expires before Select.req, the passive +side closes the connection. This prevents an unauthenticated +connection from camping forever. + +Tested in [`tests/test_hsms_timers.cpp:230`](../tests/test_hsms_timers.cpp). + +### T8 — inter-character timeout (5 s default) + +After reading the 4-byte length prefix, the implementation waits up +to T8 for the next byte. If the peer goes silent mid-payload, the +connection closes. Protects against half-open connections that +think they're alive but never finish sending. + +Tested in [`tests/test_hsms_timers.cpp:248`](../tests/test_hsms_timers.cpp). + +### Linktest cadence + +`Timers::linktest` (not a timeout — a cadence). If non-zero, the +side periodically emits `Linktest.req` to verify the peer is still +responsive. Default is 0 (disabled); turn it on when you suspect +silent drops. Linktest is the only HSMS health-check between +data messages. + +--- + +## The auto-S9 paths + +When the connection sees a *protocol* error — malformed bytes, +unknown SType, unhandled stream/function — it doesn't silently +drop. It emits an `S9F` message back to the peer so the peer +knows what went wrong: + +| Trigger | Reply | Where | +|------------------------------------------------------|-----------------|-----------------------------------------------| +| Unknown PType | `S9F1` | (auto) | +| Unrecognized device ID (SS mode with non-control sid) | `S9F1` | `Connection::handle_data` | +| Frame body fails SECS-II decode | `S9F7` | `Connection::handle_data` | +| W=1 message arrived for unknown stream | `S9F3` | Router::dispatch_with_s9 → `Connection::emit_s9` | +| W=1 message arrived for unknown function in known stream | `S9F5` | Router::dispatch_with_s9 → `Connection::emit_s9` | +| W=1 reply timed out (T3 expired) | `S9F9` | `Connection::on_t3_expire` | +| Body larger than configured cap | `S9F11` | `Connection::on_payload` (16 MiB-ish cap) | +| Frame valid but received in NOT-SELECTED | `RejectReq(EntityNotSelected)` | `handle_data` | + +`emit_s9(function, mhead)` is exposed publicly +([`connection.hpp:97`](../include/secsgem/hsms/connection.hpp)) so a +higher-level dispatcher (like `gem::Router`) can call it after its +own unknown-stream / unknown-function check. + +Tested across +[`tests/test_hsms_s9.cpp`](../tests/test_hsms_s9.cpp) (3 cases for +the malformed-body paths) and +[`tests/test_s9_fallback.cpp`](../tests/test_s9_fallback.cpp) (2 +cases for the Router dispatch path). + +--- + +## HSMS-SS vs HSMS-GS + +### Single-Session (HSMS-SS) + +The default. One TCP socket carries one SECS conversation between +one equipment and one host. The `session_id` field in every frame +header carries the sentinel `0xFFFF` (control session ID), even for +data messages — it's not used for routing. + +In code: pass any value to `Connection`'s constructor as +`device_id` — it's stored, but for SS-mode handshakes the header's +session_id is forced to `0xFFFF`. + +### General-Session (HSMS-GS) + +E37 §11. One TCP socket multiplexes *several* sessions, each with +its own device_id. The Select.req frame's `session_id` field +carries the device_id of the session being selected; data frames +likewise carry the session's device_id in the header. + +Use case: one equipment with one TCP socket to a host that wants +several logical conversations — say, production traffic on one +session, maintenance traffic on another. Real-world example: a +fab where the production MES and the maintenance MES connect to +the same equipment over one link via separate gateways. + +In code: + +```cpp +auto conn = std::make_shared(std::move(sock), Mode::Passive, + /*primary device_id=*/100, timers); +conn->add_session(/*device_id=*/200); // second session +conn->add_session(/*device_id=*/300); // third session + +conn->set_session_message_handler(100, ...); +conn->set_session_message_handler(200, ...); +conn->set_session_message_handler(300, ...); +conn->start(); +``` + +Each session has its own SELECTED state, independent message +handler, independent send queue. When a Select.req arrives, the +implementation routes it by `session_id` field to find the right +registered session. + +Tested in +[`tests/test_hsms_gs.cpp`](../tests/test_hsms_gs.cpp) (5 wire-level +cases) and +[`tests/test_hsms_gs_integration.cpp`](../tests/test_hsms_gs_integration.cpp) +(one end-to-end three-session scenario). + +Implementation walk-through with code snippets: +[`docs/INTEGRATION.md`](INTEGRATION.md) §7. + +--- + +## The single-threaded contract + +`Connection` is **single-threaded**. All state mutations — the +read loop, the send queue, the timers, the SELECTED transition — +run on the socket's `asio::executor`. Callers that want to send +from another thread must marshal onto the executor via +`asio::post()`. + +The contract is documented in +[`docs/INTEGRATION.md`](INTEGRATION.md) §3 and exercised under +ThreadSanitizer by +[`tests/test_thread_safety.cpp`](../tests/test_thread_safety.cpp). +N producer threads `asio::post` updates; TSan reports zero races. + +This avoids every mutex you'd otherwise need for the queue, the +session map, the in-flight request table, the timer state machine. +The cost is that the caller has to know about it. See chapter +[33](33_transport.md) for a deeper look at the asio strand model. + +--- + +## How send/receive feels from the caller + +Send a request (W=1): + +```cpp +conn->send_request(secs2::Message{stream, function, /*w=*/true, body}, + [](std::error_code ec, const secs2::Message& reply) { + if (ec) { + // T3 expired, or connection closed before reply. + } else { + // reply.body() is the decoded Item. + } + }); +``` + +Send a one-way data message (W=0): + +```cpp +conn->send_data(secs2::Message{6, 11, /*w=*/false, event_report_body}); +``` + +Register a primary-message handler (called for inbound W=1 messages +in SELECTED state): + +```cpp +conn->set_message_handler([](const secs2::Message& m) -> std::optional { + // Build a reply Message and return it; system_bytes are auto-filled. + return secs2::Message{m.stream(), m.function() + 1, /*w=*/false, reply_body}; +}); +``` + +Higher layers (`gem::Router`) wrap this with stream/function dispatch. + +--- + +## What's still ahead at this layer + +The connection is **just transport**. It doesn't know about GEM +states, doesn't know about CEIDs or alarms, doesn't validate the +body's shape (just decodes it as an Item). When `gem::Router` +hands a request to the alarm handler and the alarm handler returns +a reply, all the Router does is bundle that reply into a +`secs2::Message` and tell the connection to send it. + +Three things to revisit in later chapters: + +- **The behavioural layer above HSMS** is E30 (chapter + [13](13_e30_gem.md)). GEM has its own communication state machine + on top of HSMS's transport state machine — they look similar but + are *different* state machines. +- **The Router** that dispatches stream/function to handlers lives + in `secsgem::gem::Router` and is covered in chapter + [35](35_state_machines_and_dispatch.md). +- **The asio strand model** that makes the single-threaded contract + work is in chapter [33](33_transport.md). + +But first, the *other* transport — the one this one replaced, but +that hasn't gone away. **SECS-I over serial**. + +Next: [→ 12 E4 — SECS-I: the serial origin](12_e4_secs_i.md) diff --git a/docs/12_e4_secs_i.md b/docs/12_e4_secs_i.md new file mode 100644 index 0000000..1438c93 --- /dev/null +++ b/docs/12_e4_secs_i.md @@ -0,0 +1,259 @@ +# 12 — E4: SECS-I — the serial origin + +← [11 E37 — HSMS transport](11_e37_hsms.md) | [Back to index](00_index.md) | Next: [13 E30 — GEM](13_e30_gem.md) → + +HSMS (chapter 11) is what every modern 300 mm tool runs. But SECS-I +— published 1980, the *original* SECS transport — is still on the +wire. Older 200 mm fabs, smaller specialty tools (e.g. inspection +microscopes, simple metrology), some legacy lithography steppers, +even some new equipment shipping into mixed-fleet fabs all speak +SECS-I over RS-232 or RS-422. + +This chapter is short, because the protocol is small. By the end: + +- You'll understand half-duplex line turnaround. +- You'll know the ENQ / EOT / ACK / NAK handshake. +- You'll be able to read the 10-byte block header. +- You'll know exactly what's implemented here, what isn't, and + why. + +--- + +## What SECS-I actually is + +A **half-duplex block protocol** for RS-232 / RS-422 serial links. +Half-duplex means: at any moment, exactly one side is allowed to be +transmitting. Switching direction requires an explicit handshake. + +The handshake uses four single-byte control codes: + +| Byte | Mnemonic | Meaning | +|------|----------|----------------------| +| 0x05 | `ENQ` | I want to send | +| 0x04 | `EOT` | Go ahead, send | +| 0x06 | `ACK` | Block received OK | +| 0x15 | `NAK` | Block bad, retry | + +A successful transmission looks like: + +``` +sender receiver +────── ──────── + ENQ ──────────────► "I want to send" + ◄────────── EOT "go ahead" + ────► "here you go" + ◄────────── ACK "got it" +``` + +Compare to HSMS: HSMS gets all this for free from TCP. TCP is +full-duplex, segments are framed by the operating system, and the +ACK semantics are at the byte level not the message level. SECS-I +predates that affordance — it was designed for a UART straight on +the equipment's serial port. + +--- + +## The 10-byte block header + +A block carries the same logical information as an HSMS data +message — session, stream, function, W-bit, system bytes — packed +slightly differently: + +``` + byte 0 1 2 3 4 5 6 7 8 9 + ┌──────┬──────────┬──────────┬────────┬───────────┐ + │R+sid │ W+stream │ function │E+block#│ sys bytes │ + └──────┴──────────┴──────────┴────────┴───────────┘ + u16 bit7 W func id bit15 E u32 + bit15 R bits6-0 (byte 3) bits14-0 (BE) + bits14-0 stream block # + device id +``` + +Three bit-packings, all in [`Header::encode/decode`](../include/secsgem/secsi/header.hpp): + +- **Byte 0–1**: R-bit (bit 15) + 15-bit device ID. `R=1` means + "host → equipment", `R=0` means "equipment → host". +- **Byte 2**: W-bit (bit 7) + 7-bit stream. Same W-bit semantics as + HSMS. +- **Byte 4–5**: E-bit (bit 15) + 15-bit block number. `E=1` marks + the *last* block of a multi-block message. Block numbers are + 1-based. +- **Byte 6–9**: 32-bit `system_bytes` correlation token. + +Defined in [`include/secsgem/secsi/header.hpp`](../include/secsgem/secsi/header.hpp). + +--- + +## Multi-block messages + +A single SECS-I block carries at most **244 bytes** of body: + +```cpp +// include/secsgem/secsi/block.hpp +inline constexpr std::size_t kMaxBlockBody = 244; +``` + +Why 244? The framing uses a one-byte length field that counts +10 (header) + body, with byte values 10–254 valid (255 reserved). +254 − 10 = 244. + +A SECS-II body larger than 244 bytes is **split into multiple +blocks**, each with the same header except for the incrementing +block number and the E-bit (set only on the last block). +[`secsi::split_message`](../include/secsgem/secsi/block.hpp) does +the split; [`secsi::assemble_message`](../include/secsgem/secsi/block.hpp) +recombines them. + +Each block has a 2-byte checksum after the body — sum of every +byte in the header + body modulo `2^16`, big-endian. + +Multi-block round-trip is verified by [`tests/test_secsi.cpp`](../tests/test_secsi.cpp) +(15 cases) and [`tests/test_secsi_tcp.cpp`](../tests/test_secsi_tcp.cpp) +(3 cases — an end-to-end split / send / reassemble over the test +TCP transport). + +> **Why HSMS doesn't need this.** HSMS frames have a 4-byte length +> prefix, so a single frame can carry up to 4 GiB. Multi-block is +> a SECS-I concept that simply doesn't apply on TCP. + +--- + +## The line-turnaround FSM + +The interesting part of SECS-I — and the part that bites every +implementer — is the **half-duplex** state machine. Both sides +might want to send at the same time. Both might `ENQ` +simultaneously. Both must agree on who yields. + +E4 §7.1.4: **the master holds, the slave yields**. By convention +the host is master and the equipment is slave, but this is +configurable. In code: + +```cpp +// include/secsgem/secsi/protocol.hpp +enum class Role { Master, Slave }; +``` + +The FSM is event-driven and IO-free. It takes: + +- **`EventByte`** — one received byte. +- **`EventSend`** — the application wants to send a block. +- **`EventTimeout`** — a previously-armed timer fired. + +And produces a sequence of actions: + +- **`ActionTransmit`** — push these bytes onto the wire. +- **`ActionStartTimer`** / **`ActionCancelTimer`** — arm or + cancel one of T1/T2/T3/T4. +- **`ActionDeliverBlock`** — pass this received block up to the + application. +- **`ActionRaiseError`** — fatal: retries exhausted, line + protocol violated, etc. + +State names from [`Protocol::State`](../include/secsgem/secsi/protocol.hpp): + +``` +Idle ──ENQ─► SendEnqSent ──EOT─► SendBlock ──bytes─► WaitAck ──ACK─► Idle + └─NAK─► retry (RTY budget) + +Idle ──ENQ(rx)─► RecvEnq ──EOT(tx)─► RecvBlock ──bytes(rx)─► RecvAck ──ACK(tx)─► Idle + └─bad checksum─► NAK(tx) → RecvBlock (retry) +``` + +Tests in [`tests/test_secsi.cpp`](../tests/test_secsi.cpp) and +[`tests/test_secsi_timers.cpp`](../tests/test_secsi_timers.cpp) +walk every transition. + +--- + +## The four SECS-I T-timers + +Distinct from HSMS T-timers despite the name overlap: + +| Name | Default | Bounds | +|------|---------|-------------------------------------------------| +| T1 | 500 ms | Inter-character — gap between bytes in one block | +| T2 | 10 s | Protocol — waiting for EOT after our ENQ, or vice versa | +| T3 | 45 s | Reply — primary (W=1) waiting for the reply block | +| T4 | 45 s | Inter-block — gap between blocks of a multi-block message | + +Defaults in +[`secsi::Timers`](../include/secsgem/secsi/protocol.hpp): + +```cpp +struct Timers { + std::chrono::milliseconds t1{500}; + std::chrono::milliseconds t2{10000}; + std::chrono::milliseconds t3{45000}; + std::chrono::milliseconds t4{45000}; + uint8_t rty = 3; +}; +``` + +Each timer is armed by the FSM via `ActionStartTimer`, cancelled +by `ActionCancelTimer`, and fired by the wrapping host's wall +clock. The FSM itself has no wall clock — it only sees +`EventTimeout` when the host tells it the timer fired. + +Tested independently in +[`tests/test_secsi_timers.cpp`](../tests/test_secsi_timers.cpp) +(9 cases — every armed-and-cancelled scenario, every expiry). + +--- + +## What's implemented here vs. what isn't + +Per [docs/COMPLIANCE.md](COMPLIANCE.md) §1a: + +| Item | Status | +|--------------------------------------------|--------| +| 10-byte block header bit-packing/unpacking | ✅ | +| Length-prefixed block + 2-byte checksum | ✅ | +| Multi-block split / assemble (E-bit, block#) | ✅ | +| ENQ/EOT/ACK/NAK half-duplex handshake | ✅ | +| RTY retry budget | ✅ | +| T1/T2/T3/T4 timer hooks (event-driven) | ✅ | +| Master/slave contention resolution | ✅ | +| TCP tunnel for testing | ✅ | +| **Serial-port driver (asio `serial_port`)** | **⬜ deferred** | + +The FSM is complete and tested end-to-end **over a TCP transport**: +[`secsi::TcpTransport`](../include/secsgem/secsi/tcp_transport.hpp) +wraps the FSM behind an asio TCP socket. This is enough for +testing and for the docker-compose interop flows, but it's not a +real serial port. + +The remaining piece — a serial driver that pumps bytes between +the FSM and an `asio::serial_port` — has not been written. Most +modern GEM equipment runs HSMS; the deferral is documented in +the README "Deferred follow-ups" section. Mirror `TcpTransport` +to add it. + +--- + +## Why this matters even if you only run HSMS + +Two reasons to read this chapter even if you'll never touch serial: + +1. **The line-turnaround FSM informs the GEM communication state + machine.** E30 §6.5 reuses the establish-comms pattern that + originated in SECS-I — T_CRA / T_DELAY echo T3 / T2. See + chapter [13](13_e30_gem.md). +2. **Block-level error recovery is a useful mental model.** Even + on HSMS, the per-message correlation by `system_bytes` and the + T3 reply timer are direct descendants of SECS-I's block-level + tracking. Understanding one helps you read the other. + +--- + +## Where to go next + +Now you know both transports. Chapter [13](13_e30_gem.md) lifts up +one level: **E30 — GEM behaviour**. This is where the protocol +stops being plumbing and starts encoding *what equipment is +supposed to do*: communication state, control state, the GEM +Fundamental + Additional capabilities, scenarios for every +typical interaction. + +Next: [→ 13 E30 — GEM behaviour](13_e30_gem.md) diff --git a/docs/13_e30_gem.md b/docs/13_e30_gem.md new file mode 100644 index 0000000..dedb497 --- /dev/null +++ b/docs/13_e30_gem.md @@ -0,0 +1,259 @@ +# 13 — E30: GEM — the behavioural model + +← [12 E4 — SECS-I](12_e4_secs_i.md) | [Back to index](00_index.md) | Next: [14 E40 + E94 — Process and control jobs](14_e40_e94_jobs.md) → + +E5 (chapter 10) is the data encoding. E37 / E4 (chapters 11–12) +move the encoded bytes. This chapter is the first one where +*behaviour* enters the picture. + +**SEMI E30 — Generic Equipment Model (GEM)**, published 1992, +specifies what an equipment must *do* when its host sends specific +messages. E5 says how to encode S1F13; E30 says what state must +change when an S1F13 arrives, what reply must come back, and under +what conditions either side may refuse. + +E30 has two top-level concepts: + +1. **Two state machines** — communication state (above HSMS) and + control state (governs who's allowed to issue commands). +2. **GEM Capabilities** — Fundamentals (mandatory) and Additionals + (optional but de-facto required). Each capability defines its + own scenarios + messages. + +By the end of this chapter you'll know both state machines, the +14 Fundamentals + Additionals, and where each one lives in code. + +--- + +## The two GEM state machines + +GEM has **two** state machines that live on top of HSMS's transport +state machine. Read carefully — beginners conflate these all the +time: + +| State machine | Lives where | Concerns | +|----------------------|---------------------------------------------------|-----------------------------------------------| +| **HSMS transport** | `secsgem::hsms::Connection` | NOT-CONNECTED → NOT-SELECTED → SELECTED | +| **GEM communication**| `secsgem::gem::CommunicationStateMachine` | DISABLED / WAIT-CRA / WAIT-DELAY / COMMUNICATING | +| **GEM control** | `secsgem::gem::ControlStateMachine` | EquipmentOffline / OnlineLocal / OnlineRemote / … | + +All three can be in independent states. `SELECTED` (HSMS) doesn't +imply `COMMUNICATING` (GEM-comm); `COMMUNICATING` doesn't imply +`OnlineRemote` (control). + +### Communication state (E30 §6.5) + +What it answers: **have host and equipment agreed they can talk to +each other at the GEM level?** This is *above* HSMS — even after +HSMS is SELECTED, GEM-comm starts at WAIT-CRA and only reaches +COMMUNICATING after a successful `S1F13 / S1F14 (COMMACK=Accept)` +exchange. + +``` + wire: S1F13 → +DISABLED ─enable──► WAIT-CRA ─────────────► COMMUNICATING + │ ◄─ S1F14(Accept) + │ + │ S1F14(Deny) or T_CRA expires + ▼ + WAIT-DELAY + │ T_DELAY expires + ▼ + WAIT-CRA (retry) +``` + +Two timers, both in `gem::CommunicationStateMachine`: + +- **T_CRA** (default 45 s): how long to wait for the S1F14 reply + after sending S1F13. +- **T_DELAY** (default 10 s): how long to back off after a + rejected S1F14 before retrying. + +Code: +[`include/secsgem/gem/communication_state.hpp`](../include/secsgem/gem/communication_state.hpp); +tests in +[`tests/test_communication_state.cpp`](../tests/test_communication_state.cpp) +(12 cases — every transition, every timer expiry). + +The state machine is **IO-free** — it raises actions (send S1F13, +arm T_CRA, …) that the caller translates into asio work. This +makes it unit-testable without spinning up a TCP socket. Same +design pattern as `secsi::Protocol` from chapter 12. + +### Control state (E30 §6.2) + +What it answers: **who's allowed to issue commands right now?** + +Five states: + +| State | Meaning | +|------------------|------------------------------------------------------------| +| `EquipmentOffline` | Off-network. Both panel and host commands disabled. | +| `AttemptOnline` | Transient: equipment is dialing host. Rare. | +| `HostOffline` | Host disconnected (or never connected). Operator can act, host cannot. | +| `OnlineLocal` | Operator at the local panel has control. Host can read, not act. | +| `OnlineRemote` | Host has full control. | + +Defined in +[`include/secsgem/gem/control_state.hpp`](../include/secsgem/gem/control_state.hpp). + +Transitions are driven by **events** (operator pressed Online, +host sent S1F17, AttemptOnline succeeded or failed, …) and +encoded as a transition table loaded from +[`data/control_state.yaml`](../data/control_state.yaml): + +```yaml +# data/control_state.yaml +transitions: + - {from: EquipmentOffline, on: operator_switch_online, to: AttemptOnline, then: OnlineRemote} + - {from: OnlineRemote, on: host_request_offline, to: HostOffline, ack: Accept} + - {from: OnlineLocal, on: host_request_remote, ack: NotAccept} + ... +``` + +The table is **pure data**. `ControlTransitionTable` looks up +rows; `ControlStateMachine` applies them. No `if/else` ladders +embedded in C++. + +```cpp +// include/secsgem/gem/control_state.hpp:53 +struct ControlTransition { + ControlState from; + ControlEvent on; + std::optional to; + std::optional then; // chain through AttemptOnline + std::optional ack_code; +}; +``` + +This is **spec-as-data** in its purest form: the SEMI standard +section 6.2 is one YAML file. Add a state, add a transition, edit +the YAML — no recompile, no C++ change. See chapter +[31](31_spec_as_data_and_codegen.md) for the wider story. + +Tests: [`tests/test_control_state.cpp`](../tests/test_control_state.cpp) +(15 cases — every YAML-defined transition, both ACK codes). + +--- + +## GEM Fundamentals (E30 §5.2) + +The **mandatory** capabilities. An equipment that doesn't ship +these isn't GEM-compliant, end of story. + +| Fundamental | Messages | Code | +|---------------------------------------------|---------------------------------------------------------|---------------------------------------------------------------| +| State models | — | `ControlStateMachine`, `CommunicationStateMachine` | +| Equipment Processing States | — | `ControlTransitionTable` (vendor supplies concrete states) | +| Host-Initiated S1F13/F14 | S1F13 / S1F14 | `gem::CommunicationStateMachine` | +| Event Notification | S6F11 / S6F12 | `EventStore` + `EquipmentDataModel::compose_reports_for` | +| On-Line Identification | S1F1 / S1F2 | Router handler in `apps/secs_server.cpp` | +| Error Messages | S9F1/F3/F5/F7/F9/F11 | `Connection::emit_s9` + `Router::dispatch_with_s9` | +| Documentation | S1F19/F20, S1F21/F22, S1F23/F24 | `gem::compliance` / namelist handlers | +| Control (Operator-Initiated) | — | `ControlStateMachine::operator_online/offline/local/remote` | + +Full per-capability accounting with status + spec section + code +ref: [docs/COMPLIANCE.md](COMPLIANCE.md) §3. + +--- + +## GEM Additionals (E30 §5.3) + +The **optional** capabilities — but every commercial MES will +require all of them. In practice "Additional" means "optional per +the SEMI spec, but mandatory for procurement." + +| Additional | Messages | Code | +|---------------------------------------|---------------------------------------------------------------------------|-----------------------------------------------| +| Establish Communications | S1F13/F14 | `CommunicationStateMachine` (also in Fundamentals) | +| Dynamic Event Report Configuration | S2F33/F34, S2F35/F36, S2F37/F38 | `ReportStore`, `EventStore` | +| Variable Data Collection | S1F21/F22 + DVID values via `vid_value` | `DataVariableStore` | +| Trace Data Collection | S2F23/F24, S6F1/F2 | `TraceStore` | +| Status Data Collection | S1F3/F4, S1F11/F12 | `SvidStore` | +| Alarm Management | S5F1/F2, S5F3/F4, S5F5/F6, S5F7/F8 | `AlarmStore`, `AlarmDispatcher` | +| Remote Control | S2F41/F42, S2F49/F50, S2F21/F22 | `HostCommandRegistry` | +| Equipment Constants | S2F13/F14, S2F15/F16, S2F29/F30 | `EquipmentConstantStore` | +| Process Program Management | S7F1–F6, S7F17–F20, S7F23–F26 | `RecipeStore` | +| Material Movement | (handled by E40 + E94 + E87 + E90 + E157) | see chapters 14–16 | +| Equipment Terminal Services | S10F1/F2, S10F3/F4, S10F5/F6 | `TerminalServiceStore` | +| Clock | S2F17/F18, S2F31/F32 | `ClockStore` (+ E148 in chapter 19) | +| Limits Monitoring | S2F45/F46, S2F47/F48 | `LimitMonitorStore` | +| Spooling | S2F43/F44, S6F23/F24, S6F25/F26 | `SpoolStore` (persistent file-backed journal) | + +Every capability has its **own store** (a namespace bundle of +state + behaviour) and its **own Router handlers** for the messages +that drive it. Stores compose into `EquipmentDataModel`. Chapter +[32](32_stores_and_the_data_model.md) is the deep dive. + +--- + +## How a typical scenario lands in code + +Pick **Event Notification** — the canonical GEM scenario: + +``` +1. Host sends S2F33 (DefineReport): "RPTID 100 = [SVID 1, SVID 5]" +2. Equipment stores the definition in ReportStore; replies S2F34(DRACK=0). +3. Host sends S2F35 (LinkEvent): "CEID 300 → RPTID 100" +4. Equipment stores the link in EventStore; replies S2F36(LRACK=0). +5. Host sends S2F37 (EnableEvent CEED=true, CEID=[300]) +6. Equipment marks CEID 300 enabled in EventStore; replies S2F38(ERACK=0). +7. Later: some FSM transition decides to fire CEID 300. + compose_reports_for(300) walks EventStore → ReportStore → SvidStore + and assembles {RPTID=100, V=[svid1_val, svid5_val]}. +8. Equipment emits S6F11 with the assembled body. +9. Host replies S6F12(ACKC6=0). +``` + +Steps 1, 3, 5 are inbound — `gem::Router` dispatches by +`(stream, function)` to a registered handler. Steps 2, 4, 6, 8 +are outbound — the handler or the FSM hands a built `secs2::Message` +to the Connection. Step 7 is *internal* — the EquipmentDataModel +walks its own stores; nothing on the wire happens until step 8. + +Router and dispatch is in chapter +[35](35_state_machines_and_dispatch.md); store internals in +chapter [32](32_stores_and_the_data_model.md). + +--- + +## The host-side analogue + +Everything above describes the equipment side. The host side has +its own E30 state — every Additional capability has a host-side +view too (the host can disable an alarm, change a host command, +etc.). This codebase implements the host-side as a thin module: + +```cpp +// include/secsgem/gem/host_handler.hpp +class HostHandler { + // Decode equipment-initiated S5F1 / S6F11 / S9Fx. + // Maintain the host's view of CEID enables, alarm enables, … +}; +``` + +`apps/secs_client.cpp` is the canonical host binary. In the +two-container demo it walks ~24 transactions against +`apps/secs_server.cpp` — the host side mostly *reads* what the +equipment reports and acknowledges. Driving an MES is a much +bigger story (see chapter [41](41_integration_hardware_mes_production.md)). + +--- + +## Where to go next + +You now have: + +- E5 codec. +- E37/E4 transport. +- E30 state machines and capabilities. + +That's the complete **base GEM stack**. Modern fab automation +needs more — process job lifecycles, carrier management, substrate +tracking — and that's what **GEM 300** adds. + +The next six chapters tackle the GEM 300 standards one family at a +time. Each one fits on top of E30 in the same way: a state +machine + a store + Router handlers + per-CEID emissions. + +Next: [→ 14 E40 + E94 — Process and control jobs](14_e40_e94_jobs.md)