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>
9.7 KiB
12 — E4: SECS-I — the serial origin
← 11 E37 — HSMS transport | Back to index | Next: 13 E30 — GEM →
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:
- Byte 0–1: R-bit (bit 15) + 15-bit device ID.
R=1means "host → equipment",R=0means "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=1marks the last block of a multi-block message. Block numbers are 1-based. - Byte 6–9: 32-bit
system_bytescorrelation token.
Defined in include/secsgem/secsi/header.hpp.
Multi-block messages
A single SECS-I block carries at most 244 bytes of body:
// 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 does
the split; secsi::assemble_message
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
(15 cases) and 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:
// 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:
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 and
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:
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
(9 cases — every armed-and-cancelled scenario, every expiry).
What's implemented here vs. what isn't
Per docs/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
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:
- 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.
- Block-level error recovery is a useful mental model. Even
on HSMS, the per-message correlation by
system_bytesand 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 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