docs: chapters 11–13 — HSMS, SECS-I, GEM

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:07:31 +02:00
parent 338d0b974d
commit 858ca22975
3 changed files with 962 additions and 0 deletions
+259
View File
@@ -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"
<block bytes> ────► "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 01**: 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 45**: 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 69**: 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 10254 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)