A5: SECS-I-over-TCP convenience layer
Wires the SECS-I Protocol FSM behind an asio TCP socket so the block protocol can run over loopback without serial hardware. Mirrors secsgem-py's `secsitcp/` adapter — useful for back-to-back simulators and CI without a serial device. Adds: include/secsgem/secsi/tcp_transport.hpp src/secsi/tcp_transport.cpp tests/test_secsi_tcp.cpp The transport: - Splits outgoing SECS-II messages into blocks (transparent multi-block). - Accumulates incoming blocks until end_block=true, then assembles and delivers as a single SECS-II message — same surface as the HSMS Connection's MessageHandler. - Drives T1 / T2 timers from asio steady_timer; T3/T4 stay upper-layer per the FSM contract. - Auto-allocates monotonic system bytes per send. Tests cover single-block delivery, multi-block reassembly (700-byte ASCII body spanning multiple SECS-I blocks), and bidirectional exchange. This closes Tranche A (catch-up to secsgem-py wire/transport surface). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "secsgem/secs2/message.hpp"
|
||||
#include "secsgem/secsi/block.hpp"
|
||||
#include "secsgem/secsi/header.hpp"
|
||||
#include "secsgem/secsi/protocol.hpp"
|
||||
|
||||
// SECS-I block framing tunneled over a TCP socket. Mirrors the
|
||||
// secsgem-py `secsitcp/` convenience layer — useful for back-to-back
|
||||
// testing and simulators where you want SECS-I semantics (ENQ/EOT/ACK,
|
||||
// retries) but no actual serial hardware. The on-wire bytes are
|
||||
// identical to what would travel over RS-232; only the carrier differs.
|
||||
namespace secsgem::secsi {
|
||||
|
||||
class TcpTransport : public std::enable_shared_from_this<TcpTransport> {
|
||||
public:
|
||||
using MessageHandler = std::function<void(secs2::Message)>;
|
||||
using ErrorHandler = std::function<void(const std::string&)>;
|
||||
using LogHandler = std::function<void(const std::string&)>;
|
||||
|
||||
TcpTransport(asio::ip::tcp::socket socket, Role role, uint16_t device_id,
|
||||
Timers timers = {});
|
||||
|
||||
void set_message_handler(MessageHandler h) { on_message_ = std::move(h); }
|
||||
void set_error_handler(ErrorHandler h) { on_error_ = std::move(h); }
|
||||
void set_log_handler(LogHandler h) { on_log_ = std::move(h); }
|
||||
|
||||
// Begin the read loop. Idempotent.
|
||||
void start();
|
||||
|
||||
// Queue a SECS-II message for transmission. Internally split into
|
||||
// SECS-I blocks if larger than kMaxBlockBody; system bytes are
|
||||
// auto-assigned monotonically.
|
||||
void send(secs2::Message msg);
|
||||
|
||||
// Hard-close the underlying socket. No graceful teardown — SECS-I has
|
||||
// no equivalent of HSMS Separate.req at the protocol level.
|
||||
void close();
|
||||
|
||||
Protocol::State state() const { return fsm_.state(); }
|
||||
|
||||
private:
|
||||
void read_loop();
|
||||
void run_actions(std::vector<Action>& actions);
|
||||
void deliver_block(Block block);
|
||||
void start_timer(Timer which);
|
||||
void cancel_timer(Timer which);
|
||||
void log(const std::string& msg);
|
||||
|
||||
asio::ip::tcp::socket socket_;
|
||||
Protocol fsm_;
|
||||
Timers timers_;
|
||||
uint16_t device_id_;
|
||||
bool started_ = false;
|
||||
bool closed_ = false;
|
||||
|
||||
std::array<uint8_t, 256> read_buf_{};
|
||||
std::vector<Block> assembler_; // partial multi-block message
|
||||
uint32_t next_system_bytes_ = 1;
|
||||
|
||||
asio::steady_timer t1_;
|
||||
asio::steady_timer t2_;
|
||||
|
||||
MessageHandler on_message_;
|
||||
ErrorHandler on_error_;
|
||||
LogHandler on_log_;
|
||||
};
|
||||
|
||||
} // namespace secsgem::secsi
|
||||
Reference in New Issue
Block a user