# 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)