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,132 @@
|
||||
#include "secsgem/secsi/tcp_transport.hpp"
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace secsgem::secsi {
|
||||
|
||||
TcpTransport::TcpTransport(asio::ip::tcp::socket socket, Role role,
|
||||
uint16_t device_id, Timers timers)
|
||||
: socket_(std::move(socket)),
|
||||
fsm_(role, timers),
|
||||
timers_(timers),
|
||||
device_id_(device_id),
|
||||
t1_(socket_.get_executor()),
|
||||
t2_(socket_.get_executor()) {}
|
||||
|
||||
void TcpTransport::start() {
|
||||
if (started_) return;
|
||||
started_ = true;
|
||||
read_loop();
|
||||
}
|
||||
|
||||
void TcpTransport::send(secs2::Message msg) {
|
||||
Header tmpl;
|
||||
tmpl.device_id = device_id_;
|
||||
tmpl.system_bytes = next_system_bytes_++;
|
||||
auto blocks = split_message(msg, tmpl);
|
||||
std::vector<Action> actions;
|
||||
for (auto& b : blocks) {
|
||||
fsm_.on_event(EventSend{std::move(b)}, actions);
|
||||
}
|
||||
run_actions(actions);
|
||||
}
|
||||
|
||||
void TcpTransport::close() {
|
||||
if (closed_) return;
|
||||
closed_ = true;
|
||||
asio::error_code ec;
|
||||
socket_.shutdown(asio::socket_base::shutdown_both, ec);
|
||||
socket_.close(ec);
|
||||
t1_.cancel();
|
||||
t2_.cancel();
|
||||
}
|
||||
|
||||
void TcpTransport::read_loop() {
|
||||
if (closed_) return;
|
||||
auto self = shared_from_this();
|
||||
socket_.async_read_some(
|
||||
asio::buffer(read_buf_),
|
||||
[self](std::error_code ec, std::size_t n) {
|
||||
if (ec) {
|
||||
if (self->on_error_ && ec != asio::error::operation_aborted)
|
||||
self->on_error_(std::string("read: ") + ec.message());
|
||||
self->close();
|
||||
return;
|
||||
}
|
||||
std::vector<Action> actions;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
self->fsm_.on_event(EventByte{self->read_buf_[i]}, actions);
|
||||
}
|
||||
self->run_actions(actions);
|
||||
self->read_loop();
|
||||
});
|
||||
}
|
||||
|
||||
void TcpTransport::run_actions(std::vector<Action>& actions) {
|
||||
for (auto& act : actions) {
|
||||
if (auto* t = std::get_if<ActionTransmit>(&act)) {
|
||||
auto buf = std::make_shared<std::vector<uint8_t>>(std::move(t->bytes));
|
||||
auto self = shared_from_this();
|
||||
asio::async_write(socket_, asio::buffer(*buf),
|
||||
[self, buf](std::error_code ec, std::size_t) {
|
||||
if (ec && self->on_error_ &&
|
||||
ec != asio::error::operation_aborted) {
|
||||
self->on_error_(std::string("write: ") + ec.message());
|
||||
self->close();
|
||||
}
|
||||
});
|
||||
} else if (auto* d = std::get_if<ActionDeliverBlock>(&act)) {
|
||||
deliver_block(std::move(d->block));
|
||||
} else if (auto* st = std::get_if<ActionStartTimer>(&act)) {
|
||||
start_timer(st->which);
|
||||
} else if (auto* ct = std::get_if<ActionCancelTimer>(&act)) {
|
||||
cancel_timer(ct->which);
|
||||
} else if (auto* e = std::get_if<ActionRaiseError>(&act)) {
|
||||
if (on_error_) on_error_(e->reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TcpTransport::deliver_block(Block block) {
|
||||
assembler_.push_back(std::move(block));
|
||||
if (!assembler_.back().header.end_block) return;
|
||||
auto msg = assemble_message(assembler_);
|
||||
assembler_.clear();
|
||||
if (!msg) {
|
||||
if (on_error_) on_error_("assemble_message: malformed block sequence");
|
||||
return;
|
||||
}
|
||||
if (on_message_) on_message_(std::move(*msg));
|
||||
}
|
||||
|
||||
void TcpTransport::start_timer(Timer which) {
|
||||
asio::steady_timer* timer = nullptr;
|
||||
std::chrono::milliseconds dur{0};
|
||||
switch (which) {
|
||||
case Timer::T1: timer = &t1_; dur = timers_.t1; break;
|
||||
case Timer::T2: timer = &t2_; dur = timers_.t2; break;
|
||||
case Timer::T3: case Timer::T4: return; // upper-layer concern
|
||||
}
|
||||
timer->expires_after(dur);
|
||||
auto self = shared_from_this();
|
||||
timer->async_wait([self, which](std::error_code ec) {
|
||||
if (ec) return; // cancelled or destroyed
|
||||
std::vector<Action> actions;
|
||||
self->fsm_.on_event(EventTimeout{which}, actions);
|
||||
self->run_actions(actions);
|
||||
});
|
||||
}
|
||||
|
||||
void TcpTransport::cancel_timer(Timer which) {
|
||||
switch (which) {
|
||||
case Timer::T1: t1_.cancel(); break;
|
||||
case Timer::T2: t2_.cancel(); break;
|
||||
case Timer::T3: case Timer::T4: break;
|
||||
}
|
||||
}
|
||||
|
||||
void TcpTransport::log(const std::string& msg) {
|
||||
if (on_log_) on_log_(msg);
|
||||
}
|
||||
|
||||
} // namespace secsgem::secsi
|
||||
Reference in New Issue
Block a user