#pragma once #include #include #include #include #include #include #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 bytes; }; struct ActionStartTimer { Timer which; }; struct ActionCancelTimer { Timer which; }; struct ActionDeliverBlock { Block block; }; struct ActionRaiseError { std::string reason; }; using Action = std::variant; // --- 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; // --- 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& out); // Test-only: peek the queue of blocks waiting to send. std::size_t pending_send_size() const { return send_queue_.size(); } private: // Start of one send transaction (when send_queue_ non-empty and we're Idle). void begin_send(std::vector& out); // Re-attempt the current block after a NAK or timeout. void retry_send(std::vector& out); // Successful send: pop the block from the queue and return to Idle. void complete_send(std::vector& out); // Recv path: deliver the assembled block. void deliver_recv(std::vector& out); // Hard abort: give up, raise error, reset to Idle. void abort(std::string reason, std::vector& out); Role role_; Timers timers_; State state_ = State::Idle; // --- send-side state --- std::deque send_queue_; std::vector send_block_bytes_; // encoded bytes of the current block uint8_t rty_ = 0; // retries left for the current block // --- receive-side state --- std::vector recv_buf_; // bytes collected since EOT std::size_t recv_expected_ = 0; // length byte + payload + checksum }; const char* state_name(Protocol::State s); } // namespace secsgem::secsi