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:
@@ -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 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)
|
||||
Reference in New Issue
Block a user