a400ef3160
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>
61 lines
2.2 KiB
C++
61 lines
2.2 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "secsgem/secs2/message.hpp"
|
|
#include "secsgem/secsi/header.hpp"
|
|
|
|
// SECS-I block framing. Each block on the wire is:
|
|
//
|
|
// <length> 1 byte, 10..254 (header size + body size)
|
|
// <header> 10 bytes (see secsi/header.hpp)
|
|
// <body> 0..244 bytes
|
|
// <checksum> 2 bytes, big-endian sum of <header + body> mod 65536
|
|
//
|
|
// Long SECS-II messages are split into multiple blocks; only the final
|
|
// block has the E-bit set in its header (E4 §7.2.3). The block number
|
|
// is 1-based and increments monotonically across the message.
|
|
namespace secsgem::secsi {
|
|
|
|
class BlockError : public std::runtime_error {
|
|
public:
|
|
using std::runtime_error::runtime_error;
|
|
};
|
|
|
|
struct Block {
|
|
Header header;
|
|
std::vector<uint8_t> body;
|
|
|
|
// Full wire encoding: length byte + 10-byte header + body + 2-byte
|
|
// checksum.
|
|
std::vector<uint8_t> encode() const;
|
|
|
|
// Parse a block whose first byte is the length. Returns the consumed
|
|
// byte count via `bytes_consumed`; throws BlockError on a short read,
|
|
// a bad length, or a checksum mismatch.
|
|
static Block decode(const uint8_t* data, std::size_t len,
|
|
std::size_t& bytes_consumed);
|
|
|
|
// Compute the 16-bit checksum of the header + body bytes (everything
|
|
// between the length byte and the checksum on the wire).
|
|
static uint16_t checksum(const uint8_t* data, std::size_t len);
|
|
};
|
|
|
|
// Split a SECS-II message into one or more SECS-I blocks. Each block
|
|
// carries up to kMaxBlockBody payload bytes; only the final block has
|
|
// the E-bit set. The header template is duplicated across all blocks
|
|
// (block_number / end_block are filled in by the splitter).
|
|
std::vector<Block> split_message(const secs2::Message& msg,
|
|
const Header& header_template);
|
|
|
|
// Reassemble a SECS-II message from a sequence of blocks. Blocks must
|
|
// be contiguous (block_number 1..N) and the last must have end_block=true;
|
|
// the function returns nullopt if either invariant is violated.
|
|
std::optional<secs2::Message> assemble_message(const std::vector<Block>& blocks);
|
|
|
|
} // namespace secsgem::secsi
|