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,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 `<L[2] A "MDLN" A "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<n>` 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<Connection>(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<secs2::Message> {
|
||||||
|
// 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)
|
||||||
@@ -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)
|
||||||
@@ -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<ControlState> to;
|
||||||
|
std::optional<ControlState> then; // chain through AttemptOnline
|
||||||
|
std::optional<uint8_t> 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)
|
||||||
Reference in New Issue
Block a user