d4d1a411d7
The SECS-I Protocol FSM now enforces T3 (reply timeout) and T4
(inter-block timeout) directly, instead of leaving them as
upper-layer hooks.
T3: on complete_send, if the block we just acked had W=1, record its
system_bytes in awaiting_reply_sys_ and emit ActionStartTimer{T3}.
deliver_recv cancels T3 when a block arrives whose system_bytes
match the outstanding request. EventTimeout{T3} aborts the FSM with
"T3 reply timeout".
T4: deliver_recv emits ActionStartTimer{T4} whenever the delivered
block has end_block=false. The next block's deliver_recv cancels
the timer; EventTimeout{T4} aborts with "T4 inter-block timeout".
abort() now also cancels T3/T4 and clears the tracking state.
Test changes:
- Old "T3/T4 are FSM-level no-ops" test → REPLACED by four new
tests: T3 arm+expire, T3 arm+matching-reply cancels, T4
arm+expire, T4 arm+next-block cancels.
- Two new observer accessors on Protocol (awaiting_reply,
awaiting_next_block) so the tests can assert tracking state
without poking internals.
COMPLIANCE.md §1a: T3 + T4 rows go ⬜ → ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
147 lines
5.5 KiB
C++
147 lines
5.5 KiB
C++
#pragma once
|
|
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <optional>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
#include "secsgem/secsi/block.hpp"
|
|
|
|
// SECS-I half-duplex line-turnaround state machine (E4 §7).
|
|
//
|
|
// The protocol is event-driven and IO-free: it takes inputs (byte
|
|
// received, "I want to send this block", timer fired, peer offline) and
|
|
// produces actions (transmit these bytes, deliver this block, raise an
|
|
// error). Wrapping it in a serial-port driver is straightforward — see
|
|
// the tests for an in-memory back-to-back example.
|
|
//
|
|
// SECS-I control bytes (E4 §6.1):
|
|
// ENQ 0x05 "I want to send"
|
|
// EOT 0x04 "go ahead, send"
|
|
// ACK 0x06 "block received OK"
|
|
// NAK 0x15 "block bad / resend"
|
|
//
|
|
// Master/slave contention: per E4 §7.1.4, when both peers ENQ
|
|
// simultaneously the master (typically the host) holds, the slave
|
|
// (typically the equipment) yields. Our Role enum captures that.
|
|
namespace secsgem::secsi {
|
|
|
|
inline constexpr uint8_t kENQ = 0x05;
|
|
inline constexpr uint8_t kEOT = 0x04;
|
|
inline constexpr uint8_t kACK = 0x06;
|
|
inline constexpr uint8_t kNAK = 0x15;
|
|
|
|
enum class Role { Master, Slave };
|
|
|
|
enum class Timer : uint8_t {
|
|
T1, // inter-character (default 0.5s, E4 §10.1)
|
|
T2, // protocol (default 10s)
|
|
T3, // reply (default 45s) — driven at a higher layer
|
|
T4, // inter-block (default 45s)
|
|
};
|
|
|
|
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; // retries before giving up on a block
|
|
};
|
|
|
|
// --- Actions the FSM asks its host to perform ----------------------------
|
|
|
|
struct ActionTransmit { std::vector<uint8_t> bytes; };
|
|
struct ActionStartTimer { Timer which; };
|
|
struct ActionCancelTimer { Timer which; };
|
|
struct ActionDeliverBlock { Block block; };
|
|
struct ActionRaiseError { std::string reason; };
|
|
|
|
using Action = std::variant<ActionTransmit, ActionStartTimer,
|
|
ActionCancelTimer, ActionDeliverBlock,
|
|
ActionRaiseError>;
|
|
|
|
// --- Inputs the host feeds in --------------------------------------------
|
|
|
|
struct EventByte { uint8_t byte; }; // one byte received on the line
|
|
struct EventSend { Block block; }; // application asks to send a block
|
|
struct EventTimeout { Timer which; }; // a previously-armed timer fired
|
|
|
|
using Event = std::variant<EventByte, EventSend, EventTimeout>;
|
|
|
|
// --- State machine -------------------------------------------------------
|
|
|
|
class Protocol {
|
|
public:
|
|
enum class State {
|
|
Idle, // line free
|
|
SendEnqSent, // we sent ENQ, waiting for peer EOT
|
|
SendBlock, // peer cleared us; we're transmitting the block
|
|
SendAwaitAck, // block sent, waiting for ACK/NAK
|
|
RecvEnqSeen, // peer sent ENQ; we owe them an EOT
|
|
RecvEotSent, // we sent EOT; expecting block bytes
|
|
RecvBlock, // block bytes arriving
|
|
RecvAcking, // block fully received; we'll send ACK/NAK
|
|
};
|
|
|
|
explicit Protocol(Role role, Timers timers = {})
|
|
: role_(role), timers_(timers) {}
|
|
|
|
Role role() const { return role_; }
|
|
State state() const { return state_; }
|
|
uint8_t rty_remaining() const { return rty_; }
|
|
|
|
// Feed an event in; the FSM appends its desired actions to `out`.
|
|
void on_event(const Event& ev, std::vector<Action>& out);
|
|
|
|
// Test-only: peek the queue of blocks waiting to send.
|
|
std::size_t pending_send_size() const { return send_queue_.size(); }
|
|
|
|
// Test/observer: whether we're currently awaiting a reply (T3 arm)
|
|
// or the next block of a multi-block recv (T4 arm).
|
|
bool awaiting_reply() const { return awaiting_reply_sys_.has_value(); }
|
|
bool awaiting_next_block() const { return awaiting_next_block_; }
|
|
|
|
private:
|
|
// Start of one send transaction (when send_queue_ non-empty and we're Idle).
|
|
void begin_send(std::vector<Action>& out);
|
|
// Re-attempt the current block after a NAK or timeout.
|
|
void retry_send(std::vector<Action>& out);
|
|
// Successful send: pop the block from the queue and return to Idle.
|
|
void complete_send(std::vector<Action>& out);
|
|
// Recv path: deliver the assembled block.
|
|
void deliver_recv(std::vector<Action>& out);
|
|
// Hard abort: give up, raise error, reset to Idle.
|
|
void abort(std::string reason, std::vector<Action>& out);
|
|
|
|
Role role_;
|
|
Timers timers_;
|
|
State state_ = State::Idle;
|
|
|
|
// --- send-side state ---
|
|
std::deque<Block> send_queue_;
|
|
std::vector<uint8_t> send_block_bytes_; // encoded bytes of the current block
|
|
uint8_t rty_ = 0; // retries left for the current block
|
|
|
|
// --- receive-side state ---
|
|
std::vector<uint8_t> recv_buf_; // bytes collected since EOT
|
|
std::size_t recv_expected_ = 0; // length byte + payload + checksum
|
|
|
|
// --- transaction-level state (T3 / T4) ---------------------------------
|
|
// T3: when we send a block with W=1, we expect a reply block whose
|
|
// system_bytes match. We track the outstanding system_bytes and arm
|
|
// T3 in complete_send; deliver_recv cancels it when the matching reply
|
|
// arrives, or T3 expires and we raise an error.
|
|
std::optional<uint32_t> awaiting_reply_sys_;
|
|
// T4: when a recv block delivers with end_block=false we expect at
|
|
// least one more block in the same message. We arm T4 in
|
|
// deliver_recv and cancel when the next block arrives (or T4 expires
|
|
// and we abort).
|
|
bool awaiting_next_block_ = false;
|
|
};
|
|
|
|
const char* state_name(Protocol::State s);
|
|
|
|
} // namespace secsgem::secsi
|