A4: SECS-I transport (block protocol + E4 retry FSM)
Adds a complete IO-free SECS-I implementation:
include/secsgem/secsi/header.hpp 10-byte block header (R/W/E bits)
include/secsgem/secsi/block.hpp length + header + body + checksum
include/secsgem/secsi/protocol.hpp half-duplex FSM (ENQ/EOT/ACK/NAK)
src/secsi/* implementations
tests/test_secsi.cpp header, block, multi-block split,
back-to-back FSM drive, RTY,
contention, T2 timeout
The protocol is event-driven (`Event` → `Action` queue), so wiring it
to an asio serial_port is a thin adapter — that lands in the next
commit so this one stays reviewable.
Key design points:
- Master/slave contention: slave yields on simultaneous ENQ (E4 §7.1.4).
- RTY exhaustion raises ActionRaiseError, clears the send queue, resets
to Idle (no zombie state).
- Multi-block assembler validates contiguous 1..N numbering and exclusive
E-bit-on-last invariants — rejects malformed sequences with nullopt.
- Block::checksum is exposed publicly for the receive path's verification.
Tests cover the happy path (back-to-back delivery), error paths
(checksum mismatch, short input, oversize body), retries (NAK chain to
exhaustion), and protocol corner cases (contention, T2 timeout).
secsgem-py implements SECS-I block framing but lacks the explicit RTY
state machine; this commit puts the C++ port ahead on transport
correctness.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// SECS-I block-header primitives (SEMI E4). Each block carries a 10-byte
|
||||
// header with the same general shape as HSMS but laid out differently:
|
||||
// the R-bit lives in the upper bit of byte 0, the W-bit in the upper bit
|
||||
// of byte 2 (stream), and the E-bit + block number live in bytes 4-5
|
||||
// (E-bit = MSB of byte 4). Bytes 6-9 are the 32-bit system bytes.
|
||||
namespace secsgem::secsi {
|
||||
|
||||
// One SECS-I block carries up to 244 bytes of payload (length byte 10..254
|
||||
// covers 10-byte header + up to 244 bytes; 255 is reserved).
|
||||
inline constexpr std::size_t kHeaderSize = 10;
|
||||
inline constexpr std::size_t kMaxBlockBody = 244;
|
||||
inline constexpr uint8_t kMaxLengthByte = 254;
|
||||
|
||||
struct Header {
|
||||
// R-bit + 15-bit device ID packed into bytes 0-1 (big-endian).
|
||||
// R=1 means "host -> equipment", R=0 means "equipment -> host".
|
||||
uint16_t device_id = 0;
|
||||
bool r_bit = false;
|
||||
|
||||
// W-bit + 7-bit stream packed into byte 2. W=1 marks a primary message
|
||||
// that expects a reply (same semantics as HSMS).
|
||||
uint8_t stream = 0;
|
||||
bool w_bit = false;
|
||||
uint8_t function = 0; // byte 3
|
||||
|
||||
// E-bit + 15-bit block number packed into bytes 4-5 (big-endian).
|
||||
// E=1 marks the last block of a multi-block message; numbering is
|
||||
// 1-based per E4 §7.2.3.
|
||||
uint16_t block_number = 1;
|
||||
bool end_block = true;
|
||||
|
||||
uint32_t system_bytes = 0; // bytes 6-9 (big-endian)
|
||||
|
||||
std::array<uint8_t, kHeaderSize> encode() const;
|
||||
static Header decode(const uint8_t* data); // reads exactly 10 bytes
|
||||
|
||||
std::string describe() const;
|
||||
|
||||
bool operator==(const Header&) const = default;
|
||||
};
|
||||
|
||||
} // namespace secsgem::secsi
|
||||
Reference in New Issue
Block a user