hsms: HSMS-GS multi-session support (E37 §11)
Connection now supports both HSMS-SS (single session — the
constructor's behaviour, unchanged) and HSMS-GS (multi-session).
add_session(device_id) registers additional sessions; each one has
its own NotSelected/Selected state and its own message/selected
handlers. In GS mode the Select.req carries session_id=device_id;
in SS mode it stays at 0xFFFF (legacy). Linktest/Separate remain
connection-scope per spec.
Public API additions:
add_session(device_id)
set_session_message_handler(device_id, h)
set_session_selected_handler(device_id, h)
session_state(device_id) -> State
is_session_selected(device_id) -> bool
send_request(device_id, msg, cb)
send_data(device_id, msg)
Internal refactor: state_/on_message_/on_selected_ folded into a
SessionSlot map keyed by device_id; SS-style getters/setters route
through the primary session. T7 + linktest are connection-scope —
T7 fires only when no session is selected; linktest runs while at
least one is.
Five wire-level tests:
- passive: two sessions selected independently via Select.req
with their own session_id
- GS Select.req for an unregistered session id is Rejected
(EntityNotSelected)
- data routed by session_id; data on a not-selected session is
Rejected
- active: two registered sessions both end up selected via
serialized Select.req per session
- SS legacy: existing single-session API still works (session_id
0xFFFF in Select.req)
COMPLIANCE.md §1 updated: HSMS-GS row goes ⬜ → ✅.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,7 @@ add_executable(secsgem_tests
|
|||||||
tests/test_hsms_connection.cpp
|
tests/test_hsms_connection.cpp
|
||||||
tests/test_hsms_timers.cpp
|
tests/test_hsms_timers.cpp
|
||||||
tests/test_hsms_s9.cpp
|
tests/test_hsms_s9.cpp
|
||||||
|
tests/test_hsms_gs.cpp
|
||||||
tests/test_secsi.cpp
|
tests/test_secsi.cpp
|
||||||
tests/test_secsi_timers.cpp
|
tests/test_secsi_timers.cpp
|
||||||
tests/test_secsi_tcp.cpp
|
tests/test_secsi_tcp.cpp
|
||||||
|
|||||||
+2
-2
@@ -41,8 +41,8 @@ Legend:
|
|||||||
| T6 control transaction timeout | ✅ | E37 §10 | One concurrent control transaction. |
|
| T6 control transaction timeout | ✅ | E37 §10 | One concurrent control transaction. |
|
||||||
| T7 not-selected timeout (passive) | ✅ | E37 §10 | Armed on connect / on Deselect.req. |
|
| T7 not-selected timeout (passive) | ✅ | E37 §10 | Armed on connect / on Deselect.req. |
|
||||||
| T8 intercharacter timeout | ✅ | E37 §10 | Bounds the payload read after length prefix. |
|
| T8 intercharacter timeout | ✅ | E37 §10 | Bounds the payload read after length prefix. |
|
||||||
| HSMS-SS (single-session) | ✅ | E37 §11 | The codebase is HSMS-SS only by design. |
|
| HSMS-SS (single-session) | ✅ | E37 §11 | Default mode: the constructor registers a single session. |
|
||||||
| HSMS-GS (general-session) | ⬜ | E37 §11 | Multi-session; out of scope for this revision. |
|
| HSMS-GS (general-session) | ✅ | E37 §11 | `Connection::add_session(device_id)` registers extra sessions; per-session SELECTED state + message handlers; Select.req carries session_id=device_id in GS mode. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,17 @@
|
|||||||
|
|
||||||
namespace secsgem::hsms {
|
namespace secsgem::hsms {
|
||||||
|
|
||||||
// One HSMS session over a connected TCP socket. Drives the connection state
|
// One HSMS session-set over a connected TCP socket. Drives the connection
|
||||||
// machine (NOT_SELECTED -> SELECTED), runs the SELECT/LINKTEST/SEPARATE control
|
// state machine (NOT_SELECTED -> SELECTED, per session), runs the
|
||||||
// handshakes and the T3/T6/T7/T8 timers, and correlates data replies by system
|
// SELECT/LINKTEST/SEPARATE control handshakes and the T3/T6/T7/T8 timers,
|
||||||
// bytes. Single-threaded: all work runs on the socket's executor.
|
// and correlates data replies by system bytes. Single-threaded: all work
|
||||||
|
// runs on the socket's executor.
|
||||||
|
//
|
||||||
|
// Supports HSMS-SS (single-session, default — created by the constructor)
|
||||||
|
// and HSMS-GS (multi-session, E37 §11) via add_session(). In GS mode the
|
||||||
|
// Select.req carries the session's device_id in the header session_id
|
||||||
|
// field; in SS mode it carries the kControlSessionId = 0xFFFF sentinel
|
||||||
|
// per the legacy convention.
|
||||||
class Connection : public std::enable_shared_from_this<Connection> {
|
class Connection : public std::enable_shared_from_this<Connection> {
|
||||||
public:
|
public:
|
||||||
enum class Mode { Active, Passive };
|
enum class Mode { Active, Passive };
|
||||||
@@ -35,11 +42,27 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
|||||||
|
|
||||||
Connection(asio::ip::tcp::socket socket, Mode mode, uint16_t device_id, Timers timers);
|
Connection(asio::ip::tcp::socket socket, Mode mode, uint16_t device_id, Timers timers);
|
||||||
|
|
||||||
void set_message_handler(MessageHandler h) { on_message_ = std::move(h); }
|
// SS-style setters: operate on the primary (constructor-supplied) session.
|
||||||
void set_selected_handler(SelectedHandler h) { on_selected_ = std::move(h); }
|
void set_message_handler(MessageHandler h);
|
||||||
|
void set_selected_handler(SelectedHandler h);
|
||||||
void set_closed_handler(ClosedHandler h) { on_closed_ = std::move(h); }
|
void set_closed_handler(ClosedHandler h) { on_closed_ = std::move(h); }
|
||||||
void set_log_handler(LogHandler h) { on_log_ = std::move(h); }
|
void set_log_handler(LogHandler h) { on_log_ = std::move(h); }
|
||||||
|
|
||||||
|
// ---- HSMS-GS (multi-session) ------------------------------------------
|
||||||
|
// Register an additional session. In Active mode each registered
|
||||||
|
// session triggers a Select.req on its device_id once the prior
|
||||||
|
// session reaches SELECTED. In Passive mode the session can be
|
||||||
|
// selected by a peer's Select.req(session_id=device_id).
|
||||||
|
void add_session(uint16_t device_id);
|
||||||
|
void set_session_message_handler(uint16_t device_id, MessageHandler);
|
||||||
|
void set_session_selected_handler(uint16_t device_id, SelectedHandler);
|
||||||
|
State session_state(uint16_t device_id) const;
|
||||||
|
bool is_session_selected(uint16_t device_id) const;
|
||||||
|
|
||||||
|
// Multi-session send overloads — frames carry session_id = device_id.
|
||||||
|
void send_request(uint16_t device_id, secs2::Message msg, ReplyHandler cb);
|
||||||
|
void send_data(uint16_t device_id, secs2::Message msg);
|
||||||
|
|
||||||
// Begin the read loop. Active mode also initiates the SELECT handshake;
|
// Begin the read loop. Active mode also initiates the SELECT handshake;
|
||||||
// Passive mode arms the T7 not-selected timer and waits for Select.req.
|
// Passive mode arms the T7 not-selected timer and waits for Select.req.
|
||||||
void start();
|
void start();
|
||||||
@@ -57,8 +80,9 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
|||||||
// Hard close.
|
// Hard close.
|
||||||
void close(const std::string& reason);
|
void close(const std::string& reason);
|
||||||
|
|
||||||
State state() const { return state_; }
|
// SS-style state getters: report the primary session's state.
|
||||||
bool selected() const { return state_ == State::Selected; }
|
State state() const;
|
||||||
|
bool selected() const;
|
||||||
|
|
||||||
// The HSMS header of the primary currently being dispatched by the
|
// The HSMS header of the primary currently being dispatched by the
|
||||||
// message_handler. Only non-null inside the handler. Used so the
|
// message_handler. Only non-null inside the handler. Used so the
|
||||||
@@ -86,9 +110,11 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
|||||||
void write_next();
|
void write_next();
|
||||||
|
|
||||||
// --- handshakes & timers ---
|
// --- handshakes & timers ---
|
||||||
void send_select_req();
|
void send_select_req(); // primary session
|
||||||
|
void send_select_req(uint16_t device_id); // explicit session (GS)
|
||||||
void send_linktest_req();
|
void send_linktest_req();
|
||||||
void enter_selected();
|
void enter_selected(); // primary session
|
||||||
|
void enter_selected(uint16_t device_id); // explicit session (GS)
|
||||||
void arm_t7();
|
void arm_t7();
|
||||||
void arm_linktest();
|
void arm_linktest();
|
||||||
void start_control_transaction(SType expected_response, uint32_t system_bytes,
|
void start_control_transaction(SType expected_response, uint32_t system_bytes,
|
||||||
@@ -105,11 +131,41 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
|||||||
asio::steady_timer linktest_timer_; // periodic linktest interval
|
asio::steady_timer linktest_timer_; // periodic linktest interval
|
||||||
|
|
||||||
Mode mode_;
|
Mode mode_;
|
||||||
uint16_t device_id_;
|
uint16_t device_id_; // primary session's device_id
|
||||||
Timers timers_;
|
Timers timers_;
|
||||||
State state_ = State::NotSelected;
|
|
||||||
bool closed_ = false;
|
bool closed_ = false;
|
||||||
|
|
||||||
|
// Per-session state. In SS mode this map has a single entry keyed
|
||||||
|
// by device_id_. In GS mode add_session() inserts more entries.
|
||||||
|
struct SessionSlot {
|
||||||
|
uint16_t device_id;
|
||||||
|
State state = State::NotSelected;
|
||||||
|
MessageHandler on_message;
|
||||||
|
SelectedHandler on_selected;
|
||||||
|
};
|
||||||
|
std::map<uint16_t, SessionSlot> sessions_;
|
||||||
|
|
||||||
|
// Active mode walk-list: device_ids still awaiting their initial
|
||||||
|
// Select.req when running multi-session. Empty for SS.
|
||||||
|
std::deque<uint16_t> pending_active_selects_;
|
||||||
|
|
||||||
|
bool gs_mode() const { return sessions_.size() > 1; }
|
||||||
|
SessionSlot* find_session(uint16_t device_id) {
|
||||||
|
auto it = sessions_.find(device_id);
|
||||||
|
return it == sessions_.end() ? nullptr : &it->second;
|
||||||
|
}
|
||||||
|
const SessionSlot* find_session(uint16_t device_id) const {
|
||||||
|
auto it = sessions_.find(device_id);
|
||||||
|
return it == sessions_.end() ? nullptr : &it->second;
|
||||||
|
}
|
||||||
|
// Pick which session an inbound Select.req with session_id=`sid` is
|
||||||
|
// targeting. SS legacy: any session_id (often 0xFFFF) maps to the
|
||||||
|
// single primary session. GS: must match a registered device_id.
|
||||||
|
SessionSlot* resolve_select_target(uint16_t sid);
|
||||||
|
// Pick which session an inbound data frame with session_id=`sid`
|
||||||
|
// is for. SS: primary. GS: must match a registered device_id.
|
||||||
|
SessionSlot* resolve_data_target(uint16_t sid);
|
||||||
|
|
||||||
std::array<uint8_t, kLengthPrefixSize> len_buf_{};
|
std::array<uint8_t, kLengthPrefixSize> len_buf_{};
|
||||||
std::vector<uint8_t> payload_;
|
std::vector<uint8_t> payload_;
|
||||||
|
|
||||||
@@ -138,10 +194,8 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
|||||||
};
|
};
|
||||||
std::optional<PendingControl> pending_control_;
|
std::optional<PendingControl> pending_control_;
|
||||||
|
|
||||||
const Header* current_header_ = nullptr; // set only inside on_message_ dispatch
|
const Header* current_header_ = nullptr; // set only inside dispatch
|
||||||
|
|
||||||
MessageHandler on_message_;
|
|
||||||
SelectedHandler on_selected_;
|
|
||||||
ClosedHandler on_closed_;
|
ClosedHandler on_closed_;
|
||||||
LogHandler on_log_;
|
LogHandler on_log_;
|
||||||
};
|
};
|
||||||
|
|||||||
+147
-37
@@ -39,12 +39,66 @@ Connection::Connection(asio::ip::tcp::socket socket, Mode mode, uint16_t device_
|
|||||||
linktest_timer_(socket_.get_executor()),
|
linktest_timer_(socket_.get_executor()),
|
||||||
mode_(mode),
|
mode_(mode),
|
||||||
device_id_(device_id),
|
device_id_(device_id),
|
||||||
timers_(timers) {}
|
timers_(timers) {
|
||||||
|
SessionSlot primary;
|
||||||
|
primary.device_id = device_id;
|
||||||
|
sessions_.emplace(device_id, std::move(primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connection::set_message_handler(MessageHandler h) {
|
||||||
|
if (auto* s = find_session(device_id_)) s->on_message = std::move(h);
|
||||||
|
}
|
||||||
|
void Connection::set_selected_handler(SelectedHandler h) {
|
||||||
|
if (auto* s = find_session(device_id_)) s->on_selected = std::move(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection::State Connection::state() const {
|
||||||
|
auto* s = find_session(device_id_);
|
||||||
|
return s ? s->state : State::NotSelected;
|
||||||
|
}
|
||||||
|
bool Connection::selected() const { return state() == State::Selected; }
|
||||||
|
|
||||||
|
void Connection::add_session(uint16_t device_id) {
|
||||||
|
if (sessions_.count(device_id)) return;
|
||||||
|
SessionSlot s;
|
||||||
|
s.device_id = device_id;
|
||||||
|
sessions_.emplace(device_id, std::move(s));
|
||||||
|
if (mode_ == Mode::Active) {
|
||||||
|
pending_active_selects_.push_back(device_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connection::set_session_message_handler(uint16_t device_id, MessageHandler h) {
|
||||||
|
if (auto* s = find_session(device_id)) s->on_message = std::move(h);
|
||||||
|
}
|
||||||
|
void Connection::set_session_selected_handler(uint16_t device_id, SelectedHandler h) {
|
||||||
|
if (auto* s = find_session(device_id)) s->on_selected = std::move(h);
|
||||||
|
}
|
||||||
|
Connection::State Connection::session_state(uint16_t device_id) const {
|
||||||
|
auto* s = find_session(device_id);
|
||||||
|
return s ? s->state : State::NotSelected;
|
||||||
|
}
|
||||||
|
bool Connection::is_session_selected(uint16_t device_id) const {
|
||||||
|
return session_state(device_id) == State::Selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection::SessionSlot* Connection::resolve_select_target(uint16_t sid) {
|
||||||
|
if (gs_mode()) return find_session(sid);
|
||||||
|
// SS legacy: session_id is typically 0xFFFF in Select.req but some
|
||||||
|
// peers also send the device_id. Either way, route to the primary.
|
||||||
|
return find_session(device_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection::SessionSlot* Connection::resolve_data_target(uint16_t sid) {
|
||||||
|
if (gs_mode()) return find_session(sid);
|
||||||
|
return find_session(device_id_);
|
||||||
|
}
|
||||||
|
|
||||||
void Connection::start() {
|
void Connection::start() {
|
||||||
read_length();
|
read_length();
|
||||||
if (mode_ == Mode::Active) {
|
if (mode_ == Mode::Active) {
|
||||||
send_select_req();
|
if (gs_mode()) send_select_req(device_id_);
|
||||||
|
else send_select_req();
|
||||||
} else {
|
} else {
|
||||||
arm_t7();
|
arm_t7();
|
||||||
}
|
}
|
||||||
@@ -167,9 +221,8 @@ void Connection::handle_data(const Frame& frame) {
|
|||||||
const Header& h = frame.header;
|
const Header& h = frame.header;
|
||||||
|
|
||||||
// Reply correlation: match on (system_bytes, stream, function) exactly.
|
// Reply correlation: match on (system_bytes, stream, function) exactly.
|
||||||
// SECS-II replies use the same system_bytes as their request and have
|
// System bytes are connection-scope (E37 §8.3); replies route by system
|
||||||
// stream==request.stream, function==request.function+1; we recorded both
|
// bytes regardless of which session they came in on.
|
||||||
// when send_request stored the pending transaction.
|
|
||||||
auto it = pending_requests_.find(h.system_bytes);
|
auto it = pending_requests_.find(h.system_bytes);
|
||||||
if (it != pending_requests_.end() &&
|
if (it != pending_requests_.end() &&
|
||||||
it->second.expected_stream == h.stream() &&
|
it->second.expected_stream == h.stream() &&
|
||||||
@@ -208,8 +261,10 @@ void Connection::handle_data(const Frame& frame) {
|
|||||||
// handler can deal with it; the pending request stays open until T3.
|
// handler can deal with it; the pending request stays open until T3.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Primary message; only valid while SELECTED.
|
// Primary message; route by header.session_id (GS) or to primary (SS).
|
||||||
if (state_ != State::Selected) {
|
// Reject if no matching session OR if that session isn't SELECTED.
|
||||||
|
SessionSlot* target = resolve_data_target(h.session_id);
|
||||||
|
if (!target || target->state != State::Selected) {
|
||||||
send_frame(Frame(Header::control(SType::RejectReq, h.system_bytes, kControlSessionId,
|
send_frame(Frame(Header::control(SType::RejectReq, h.system_bytes, kControlSessionId,
|
||||||
static_cast<uint8_t>(SType::Data),
|
static_cast<uint8_t>(SType::Data),
|
||||||
static_cast<uint8_t>(RejectReason::EntityNotSelected))));
|
static_cast<uint8_t>(RejectReason::EntityNotSelected))));
|
||||||
@@ -228,16 +283,16 @@ void Connection::handle_data(const Frame& frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("<- " + h.describe());
|
log("<- " + h.describe());
|
||||||
if (!on_message_) return;
|
if (!target->on_message) return;
|
||||||
|
|
||||||
// Expose the originating header to the handler in case it needs to emit
|
// Expose the originating header to the handler in case it needs to emit
|
||||||
// an S9F3 / S9F5 in response. Cleared on the way out.
|
// an S9F3 / S9F5 in response. Cleared on the way out.
|
||||||
current_header_ = &h;
|
current_header_ = &h;
|
||||||
auto reply = on_message_(msg);
|
auto reply = target->on_message(msg);
|
||||||
current_header_ = nullptr;
|
current_header_ = nullptr;
|
||||||
|
|
||||||
if (reply) {
|
if (reply) {
|
||||||
Frame out(Header::data_message(device_id_, reply->stream, reply->function,
|
Frame out(Header::data_message(target->device_id, reply->stream, reply->function,
|
||||||
reply->reply_expected, h.system_bytes),
|
reply->reply_expected, h.system_bytes),
|
||||||
reply->encode_body());
|
reply->encode_body());
|
||||||
log("-> " + out.header.describe());
|
log("-> " + out.header.describe());
|
||||||
@@ -249,10 +304,18 @@ void Connection::handle_control(const Frame& frame) {
|
|||||||
const Header& h = frame.header;
|
const Header& h = frame.header;
|
||||||
switch (h.stype) {
|
switch (h.stype) {
|
||||||
case SType::SelectReq: {
|
case SType::SelectReq: {
|
||||||
// E37 §7.2: a Select.req while already SELECTED is answered with
|
// Route by header.session_id (GS) or to primary (SS legacy).
|
||||||
// SelectStatus::AlreadyActive; we do NOT transition (we're already
|
SessionSlot* tgt = resolve_select_target(h.session_id);
|
||||||
// there) and we do NOT call the selected handler twice.
|
if (!tgt) {
|
||||||
const auto status = (state_ == State::Selected)
|
// GS: no such session on this connection.
|
||||||
|
log("<- Select.req for unknown session " + std::to_string(h.session_id));
|
||||||
|
send_frame(Frame(Header::control(
|
||||||
|
SType::RejectReq, h.system_bytes, h.session_id,
|
||||||
|
static_cast<uint8_t>(SType::SelectReq),
|
||||||
|
static_cast<uint8_t>(RejectReason::EntityNotSelected))));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto status = (tgt->state == State::Selected)
|
||||||
? SelectStatus::AlreadyActive
|
? SelectStatus::AlreadyActive
|
||||||
: SelectStatus::Ok;
|
: SelectStatus::Ok;
|
||||||
log(std::string("<- Select.req") +
|
log(std::string("<- Select.req") +
|
||||||
@@ -261,7 +324,7 @@ void Connection::handle_control(const Frame& frame) {
|
|||||||
static_cast<uint8_t>(status))));
|
static_cast<uint8_t>(status))));
|
||||||
log(std::string("-> Select.rsp (") +
|
log(std::string("-> Select.rsp (") +
|
||||||
(status == SelectStatus::Ok ? "Ok" : "AlreadyActive") + ")");
|
(status == SelectStatus::Ok ? "Ok" : "AlreadyActive") + ")");
|
||||||
if (status == SelectStatus::Ok) enter_selected();
|
if (status == SelectStatus::Ok) enter_selected(tgt->device_id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SType::SelectRsp: {
|
case SType::SelectRsp: {
|
||||||
@@ -270,7 +333,16 @@ void Connection::handle_control(const Frame& frame) {
|
|||||||
clear_control_transaction();
|
clear_control_transaction();
|
||||||
if (h.byte3 == static_cast<uint8_t>(SelectStatus::Ok)) {
|
if (h.byte3 == static_cast<uint8_t>(SelectStatus::Ok)) {
|
||||||
log("<- Select.rsp (Ok)");
|
log("<- Select.rsp (Ok)");
|
||||||
enter_selected();
|
// Identify which session this rsp completes by session_id; SS
|
||||||
|
// legacy mode uses 0xFFFF on the wire, default to primary.
|
||||||
|
uint16_t selected = gs_mode() ? h.session_id : device_id_;
|
||||||
|
enter_selected(selected);
|
||||||
|
// Active mode walk-list: kick off next pending select.
|
||||||
|
if (mode_ == Mode::Active && !pending_active_selects_.empty()) {
|
||||||
|
uint16_t next = pending_active_selects_.front();
|
||||||
|
pending_active_selects_.pop_front();
|
||||||
|
send_select_req(next);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
close("Select rejected, status=" + std::to_string(h.byte3));
|
close("Select rejected, status=" + std::to_string(h.byte3));
|
||||||
}
|
}
|
||||||
@@ -278,18 +350,20 @@ void Connection::handle_control(const Frame& frame) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SType::DeselectReq: {
|
case SType::DeselectReq: {
|
||||||
// E37 §7.4: Deselect.req while NOT_SELECTED returns NotEstablished
|
SessionSlot* tgt = resolve_select_target(h.session_id);
|
||||||
// and leaves state untouched.
|
const auto status = (tgt && tgt->state == State::Selected)
|
||||||
const auto status = (state_ == State::Selected)
|
|
||||||
? DeselectStatus::Ok
|
? DeselectStatus::Ok
|
||||||
: DeselectStatus::NotEstablished;
|
: DeselectStatus::NotEstablished;
|
||||||
log(std::string("<- Deselect.req") +
|
log(std::string("<- Deselect.req") +
|
||||||
(status == DeselectStatus::NotEstablished ? " (not SELECTED)" : ""));
|
(status == DeselectStatus::NotEstablished ? " (not SELECTED)" : ""));
|
||||||
send_frame(Frame(Header::control(SType::DeselectRsp, h.system_bytes, h.session_id, 0,
|
send_frame(Frame(Header::control(SType::DeselectRsp, h.system_bytes, h.session_id, 0,
|
||||||
static_cast<uint8_t>(status))));
|
static_cast<uint8_t>(status))));
|
||||||
if (status == DeselectStatus::Ok) {
|
if (status == DeselectStatus::Ok && tgt) {
|
||||||
state_ = State::NotSelected;
|
tgt->state = State::NotSelected;
|
||||||
arm_t7();
|
// Re-arm T7 only if NO sessions are selected.
|
||||||
|
bool any_selected = false;
|
||||||
|
for (auto& kv : sessions_) if (kv.second.state == State::Selected) { any_selected = true; break; }
|
||||||
|
if (!any_selected) arm_t7();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -297,7 +371,8 @@ void Connection::handle_control(const Frame& frame) {
|
|||||||
if (pending_control_ && pending_control_->expected_response == SType::DeselectRsp &&
|
if (pending_control_ && pending_control_->expected_response == SType::DeselectRsp &&
|
||||||
pending_control_->system_bytes == h.system_bytes) {
|
pending_control_->system_bytes == h.system_bytes) {
|
||||||
clear_control_transaction();
|
clear_control_transaction();
|
||||||
state_ = State::NotSelected;
|
uint16_t target = gs_mode() ? h.session_id : device_id_;
|
||||||
|
if (auto* s = find_session(target)) s->state = State::NotSelected;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -357,32 +432,33 @@ void Connection::write_next() {
|
|||||||
// --- public send API ------------------------------------------------------
|
// --- public send API ------------------------------------------------------
|
||||||
|
|
||||||
void Connection::send_request(secs2::Message msg, ReplyHandler cb) {
|
void Connection::send_request(secs2::Message msg, ReplyHandler cb) {
|
||||||
|
send_request(device_id_, std::move(msg), std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connection::send_request(uint16_t device_id, secs2::Message msg, ReplyHandler cb) {
|
||||||
if (closed_) {
|
if (closed_) {
|
||||||
cb(make_error(Error::Closed), {});
|
cb(make_error(Error::Closed), {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const uint32_t sys = next_system_bytes();
|
const uint32_t sys = next_system_bytes();
|
||||||
Frame f(Header::data_message(device_id_, msg.stream, msg.function, true, sys),
|
Frame f(Header::data_message(device_id, msg.stream, msg.function, true, sys),
|
||||||
msg.encode_body());
|
msg.encode_body());
|
||||||
|
|
||||||
auto t3 = std::make_shared<asio::steady_timer>(socket_.get_executor());
|
auto t3 = std::make_shared<asio::steady_timer>(socket_.get_executor());
|
||||||
t3->expires_after(timers_.t3);
|
t3->expires_after(timers_.t3);
|
||||||
// SECS-II reply convention: same stream, function+1.
|
|
||||||
pending_requests_.emplace(sys, PendingRequest{
|
pending_requests_.emplace(sys, PendingRequest{
|
||||||
/*expected_stream=*/msg.stream,
|
/*expected_stream=*/msg.stream,
|
||||||
/*expected_function=*/static_cast<uint8_t>(msg.function + 1),
|
/*expected_function=*/static_cast<uint8_t>(msg.function + 1),
|
||||||
std::move(cb), t3});
|
std::move(cb), t3});
|
||||||
|
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
t3->async_wait([this, self, sys](std::error_code ec) {
|
t3->async_wait([this, self, sys, device_id](std::error_code ec) {
|
||||||
if (ec) return;
|
if (ec) return;
|
||||||
auto it = pending_requests_.find(sys);
|
auto it = pending_requests_.find(sys);
|
||||||
if (it == pending_requests_.end()) return;
|
if (it == pending_requests_.end()) return;
|
||||||
// Reconstruct the MHEAD of the request that timed out and notify the
|
|
||||||
// peer with S9F9 before surfacing Timeout to the caller.
|
|
||||||
const uint8_t req_stream = it->second.expected_stream;
|
const uint8_t req_stream = it->second.expected_stream;
|
||||||
const uint8_t req_function = static_cast<uint8_t>(it->second.expected_function - 1);
|
const uint8_t req_function = static_cast<uint8_t>(it->second.expected_function - 1);
|
||||||
Header h = Header::data_message(device_id_, req_stream, req_function, true, sys);
|
Header h = Header::data_message(device_id, req_stream, req_function, true, sys);
|
||||||
emit_s9(9, h.encode());
|
emit_s9(9, h.encode());
|
||||||
auto cb2 = std::move(it->second.cb);
|
auto cb2 = std::move(it->second.cb);
|
||||||
pending_requests_.erase(it);
|
pending_requests_.erase(it);
|
||||||
@@ -394,9 +470,13 @@ void Connection::send_request(secs2::Message msg, ReplyHandler cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Connection::send_data(secs2::Message msg) {
|
void Connection::send_data(secs2::Message msg) {
|
||||||
|
send_data(device_id_, std::move(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connection::send_data(uint16_t device_id, secs2::Message msg) {
|
||||||
if (closed_) return;
|
if (closed_) return;
|
||||||
const uint32_t sys = next_system_bytes();
|
const uint32_t sys = next_system_bytes();
|
||||||
Frame f(Header::data_message(device_id_, msg.stream, msg.function, msg.reply_expected, sys),
|
Frame f(Header::data_message(device_id, msg.stream, msg.function, msg.reply_expected, sys),
|
||||||
msg.encode_body());
|
msg.encode_body());
|
||||||
log("-> " + f.header.describe());
|
log("-> " + f.header.describe());
|
||||||
send_frame(std::move(f));
|
send_frame(std::move(f));
|
||||||
@@ -436,12 +516,23 @@ void Connection::close(const std::string& reason) {
|
|||||||
// --- handshakes & timers --------------------------------------------------
|
// --- handshakes & timers --------------------------------------------------
|
||||||
|
|
||||||
void Connection::send_select_req() {
|
void Connection::send_select_req() {
|
||||||
|
// SS legacy: Select.req uses 0xFFFF session id. Used unchanged when
|
||||||
|
// running with a single registered session.
|
||||||
const uint32_t sys = next_system_bytes();
|
const uint32_t sys = next_system_bytes();
|
||||||
log("-> Select.req");
|
log("-> Select.req");
|
||||||
send_frame(Frame(Header::control(SType::SelectReq, sys)));
|
send_frame(Frame(Header::control(SType::SelectReq, sys)));
|
||||||
start_control_transaction(SType::SelectRsp, sys, "Select");
|
start_control_transaction(SType::SelectRsp, sys, "Select");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Connection::send_select_req(uint16_t device_id) {
|
||||||
|
// GS: Select.req carries the session's device_id in the session_id
|
||||||
|
// field (E37 §11.4.2).
|
||||||
|
const uint32_t sys = next_system_bytes();
|
||||||
|
log("-> Select.req session=" + std::to_string(device_id));
|
||||||
|
send_frame(Frame(Header::control(SType::SelectReq, sys, device_id)));
|
||||||
|
start_control_transaction(SType::SelectRsp, sys, "Select");
|
||||||
|
}
|
||||||
|
|
||||||
void Connection::send_linktest_req() {
|
void Connection::send_linktest_req() {
|
||||||
const uint32_t sys = next_system_bytes();
|
const uint32_t sys = next_system_bytes();
|
||||||
send_frame(Frame(Header::control(SType::LinktestReq, sys)));
|
send_frame(Frame(Header::control(SType::LinktestReq, sys)));
|
||||||
@@ -449,12 +540,18 @@ void Connection::send_linktest_req() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Connection::enter_selected() {
|
void Connection::enter_selected() {
|
||||||
if (state_ == State::Selected) return;
|
enter_selected(device_id_);
|
||||||
state_ = State::Selected;
|
}
|
||||||
|
|
||||||
|
void Connection::enter_selected(uint16_t device_id) {
|
||||||
|
auto* s = find_session(device_id);
|
||||||
|
if (!s) return;
|
||||||
|
if (s->state == State::Selected) return;
|
||||||
|
s->state = State::Selected;
|
||||||
t7_timer_.cancel();
|
t7_timer_.cancel();
|
||||||
log("== SELECTED ==");
|
log("== SELECTED session=" + std::to_string(device_id) + " ==");
|
||||||
arm_linktest();
|
arm_linktest();
|
||||||
if (on_selected_) on_selected_();
|
if (s->on_selected) s->on_selected();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Connection::arm_t7() {
|
void Connection::arm_t7() {
|
||||||
@@ -463,17 +560,30 @@ void Connection::arm_t7() {
|
|||||||
t7_timer_.expires_after(timers_.t7);
|
t7_timer_.expires_after(timers_.t7);
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
t7_timer_.async_wait([this, self](std::error_code ec) {
|
t7_timer_.async_wait([this, self](std::error_code ec) {
|
||||||
if (!ec && !closed_ && state_ == State::NotSelected) close("T7 not-selected timeout");
|
if (!ec && !closed_) {
|
||||||
|
// Fire only if NO session has been selected — T7 is a single
|
||||||
|
// connection-scope timer per E37 §10, not per-session.
|
||||||
|
bool any = false;
|
||||||
|
for (auto& kv : sessions_) if (kv.second.state == State::Selected) { any = true; break; }
|
||||||
|
if (!any) close("T7 not-selected timeout");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Connection::arm_linktest() {
|
void Connection::arm_linktest() {
|
||||||
if (timers_.linktest.count() <= 0) return;
|
if (timers_.linktest.count() <= 0) return;
|
||||||
if (state_ != State::Selected) return;
|
// Run linktest while any session is selected — that proves the
|
||||||
|
// underlying TCP path is alive regardless of which session uses it.
|
||||||
|
bool any = false;
|
||||||
|
for (auto& kv : sessions_) if (kv.second.state == State::Selected) { any = true; break; }
|
||||||
|
if (!any) return;
|
||||||
linktest_timer_.expires_after(timers_.linktest);
|
linktest_timer_.expires_after(timers_.linktest);
|
||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
linktest_timer_.async_wait([this, self](std::error_code ec) {
|
linktest_timer_.async_wait([this, self](std::error_code ec) {
|
||||||
if (!ec && !closed_ && state_ == State::Selected) send_linktest_req();
|
if (ec || closed_) return;
|
||||||
|
bool any2 = false;
|
||||||
|
for (auto& kv : sessions_) if (kv.second.state == State::Selected) { any2 = true; break; }
|
||||||
|
if (any2) send_linktest_req();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
// HSMS-GS (multi-session, E37 §11) tests.
|
||||||
|
//
|
||||||
|
// The same hsms::Connection class supports HSMS-SS by default (one
|
||||||
|
// session registered by the constructor) and HSMS-GS once
|
||||||
|
// `add_session(device_id)` registers more. These tests cover the
|
||||||
|
// distinct behaviours: per-session selected state, session-routed
|
||||||
|
// data dispatch, rejection of data on the wrong session, and
|
||||||
|
// Reject(EntityNotSelected) for a Select.req targeting an unknown
|
||||||
|
// session id.
|
||||||
|
|
||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include <asio.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "secsgem/hsms/connection.hpp"
|
||||||
|
#include "secsgem/hsms/header.hpp"
|
||||||
|
#include "secsgem/secs2/message.hpp"
|
||||||
|
|
||||||
|
using namespace secsgem;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct SocketPair {
|
||||||
|
asio::io_context io;
|
||||||
|
asio::ip::tcp::socket a{io}, b{io};
|
||||||
|
SocketPair() {
|
||||||
|
asio::ip::tcp::acceptor acc(io, asio::ip::tcp::endpoint(
|
||||||
|
asio::ip::address_v4::loopback(), 0));
|
||||||
|
const auto port = acc.local_endpoint().port();
|
||||||
|
bool da = false, db = false;
|
||||||
|
std::error_code ea, eb;
|
||||||
|
acc.async_accept(a, [&](std::error_code ec) { ea = ec; da = true; });
|
||||||
|
b.async_connect({asio::ip::address_v4::loopback(), port},
|
||||||
|
[&](std::error_code ec) { eb = ec; db = true; });
|
||||||
|
while (!(da && db)) {
|
||||||
|
if (io.stopped()) io.restart();
|
||||||
|
if (io.poll() == 0) std::this_thread::sleep_for(1ms);
|
||||||
|
}
|
||||||
|
REQUIRE_FALSE(ea);
|
||||||
|
REQUIRE_FALSE(eb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Pred>
|
||||||
|
void pump_until(asio::io_context& io, Pred pred,
|
||||||
|
std::chrono::milliseconds budget = 3s) {
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + budget;
|
||||||
|
while (!pred()) {
|
||||||
|
if (std::chrono::steady_clock::now() > deadline) FAIL("pump_until budget exceeded");
|
||||||
|
if (io.stopped()) io.restart();
|
||||||
|
if (io.poll() == 0) std::this_thread::sleep_for(1ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_bytes(SocketPair& sp, std::vector<uint8_t> bytes) {
|
||||||
|
auto buf = std::make_shared<std::vector<uint8_t>>(std::move(bytes));
|
||||||
|
bool done = false;
|
||||||
|
asio::async_write(sp.b, asio::buffer(*buf),
|
||||||
|
[buf, &done](std::error_code ec, std::size_t) {
|
||||||
|
REQUIRE_FALSE(ec);
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
pump_until(sp.io, [&] { return done; });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<hsms::Frame> try_recv_frame(SocketPair& sp,
|
||||||
|
std::chrono::milliseconds budget = 2s) {
|
||||||
|
auto lenbuf = std::make_shared<std::array<uint8_t, 4>>();
|
||||||
|
bool len_done = false;
|
||||||
|
std::error_code rec_ec;
|
||||||
|
asio::async_read(sp.b, asio::buffer(*lenbuf),
|
||||||
|
[lenbuf, &len_done, &rec_ec](std::error_code ec, std::size_t) {
|
||||||
|
rec_ec = ec;
|
||||||
|
len_done = true;
|
||||||
|
});
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + budget;
|
||||||
|
while (!len_done) {
|
||||||
|
if (std::chrono::steady_clock::now() > deadline) return std::nullopt;
|
||||||
|
if (sp.io.stopped()) sp.io.restart();
|
||||||
|
if (sp.io.poll() == 0) std::this_thread::sleep_for(1ms);
|
||||||
|
}
|
||||||
|
if (rec_ec) return std::nullopt;
|
||||||
|
const uint32_t len = (uint32_t((*lenbuf)[0]) << 24) | (uint32_t((*lenbuf)[1]) << 16) |
|
||||||
|
(uint32_t((*lenbuf)[2]) << 8) | uint32_t((*lenbuf)[3]);
|
||||||
|
auto payload = std::make_shared<std::vector<uint8_t>>(len);
|
||||||
|
bool payload_done = false;
|
||||||
|
asio::async_read(sp.b, asio::buffer(*payload),
|
||||||
|
[payload, &payload_done](std::error_code, std::size_t) {
|
||||||
|
payload_done = true;
|
||||||
|
});
|
||||||
|
pump_until(sp.io, [&] { return payload_done; });
|
||||||
|
return hsms::Frame::decode(payload->data(), payload->size());
|
||||||
|
}
|
||||||
|
|
||||||
|
hsms::Timers permissive_timers() {
|
||||||
|
hsms::Timers t;
|
||||||
|
t.t3 = 5s; t.t6 = 5s; t.t7 = 5s; t.t8 = 5s; t.linktest = 0ms;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("HSMS-GS passive: two sessions selected independently") {
|
||||||
|
SocketPair sp;
|
||||||
|
auto conn = std::make_shared<hsms::Connection>(
|
||||||
|
std::move(sp.a), hsms::Connection::Mode::Passive,
|
||||||
|
/*primary_device_id=*/1, permissive_timers());
|
||||||
|
conn->add_session(2);
|
||||||
|
|
||||||
|
std::atomic<int> sel_count_1{0};
|
||||||
|
std::atomic<int> sel_count_2{0};
|
||||||
|
conn->set_session_selected_handler(1, [&] { ++sel_count_1; });
|
||||||
|
conn->set_session_selected_handler(2, [&] { ++sel_count_2; });
|
||||||
|
conn->start();
|
||||||
|
|
||||||
|
// Select session 1 via Select.req(session_id=1).
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectReq, 100, /*session_id=*/1))
|
||||||
|
.encode());
|
||||||
|
auto rsp1 = try_recv_frame(sp);
|
||||||
|
REQUIRE(rsp1.has_value());
|
||||||
|
CHECK(rsp1->header.stype == hsms::SType::SelectRsp);
|
||||||
|
CHECK(rsp1->header.byte3 == static_cast<uint8_t>(hsms::SelectStatus::Ok));
|
||||||
|
pump_until(sp.io, [&] { return sel_count_1.load() == 1; });
|
||||||
|
CHECK(conn->is_session_selected(1));
|
||||||
|
CHECK_FALSE(conn->is_session_selected(2));
|
||||||
|
|
||||||
|
// Select session 2.
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectReq, 101, /*session_id=*/2))
|
||||||
|
.encode());
|
||||||
|
auto rsp2 = try_recv_frame(sp);
|
||||||
|
REQUIRE(rsp2.has_value());
|
||||||
|
CHECK(rsp2->header.byte3 == static_cast<uint8_t>(hsms::SelectStatus::Ok));
|
||||||
|
pump_until(sp.io, [&] { return sel_count_2.load() == 1; });
|
||||||
|
CHECK(conn->is_session_selected(2));
|
||||||
|
|
||||||
|
conn->close("test done");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("HSMS-GS: Select.req for unknown session is Rejected") {
|
||||||
|
SocketPair sp;
|
||||||
|
auto conn = std::make_shared<hsms::Connection>(
|
||||||
|
std::move(sp.a), hsms::Connection::Mode::Passive,
|
||||||
|
/*primary_device_id=*/1, permissive_timers());
|
||||||
|
conn->add_session(2);
|
||||||
|
conn->start();
|
||||||
|
|
||||||
|
// Request select for session 7 — never registered.
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectReq, 5, /*session_id=*/7))
|
||||||
|
.encode());
|
||||||
|
auto rej = try_recv_frame(sp);
|
||||||
|
REQUIRE(rej.has_value());
|
||||||
|
CHECK(rej->header.stype == hsms::SType::RejectReq);
|
||||||
|
CHECK(rej->header.byte3 ==
|
||||||
|
static_cast<uint8_t>(hsms::RejectReason::EntityNotSelected));
|
||||||
|
|
||||||
|
conn->close("test done");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("HSMS-GS: data routed by session_id; wrong session is rejected") {
|
||||||
|
SocketPair sp;
|
||||||
|
auto conn = std::make_shared<hsms::Connection>(
|
||||||
|
std::move(sp.a), hsms::Connection::Mode::Passive,
|
||||||
|
/*primary_device_id=*/1, permissive_timers());
|
||||||
|
conn->add_session(2);
|
||||||
|
|
||||||
|
std::vector<std::pair<uint16_t, std::pair<uint8_t, uint8_t>>> seen;
|
||||||
|
conn->set_session_message_handler(1, [&](const secs2::Message& m)
|
||||||
|
-> std::optional<secs2::Message> {
|
||||||
|
seen.emplace_back(1, std::make_pair(m.stream, m.function));
|
||||||
|
return std::nullopt;
|
||||||
|
});
|
||||||
|
conn->set_session_message_handler(2, [&](const secs2::Message& m)
|
||||||
|
-> std::optional<secs2::Message> {
|
||||||
|
seen.emplace_back(2, std::make_pair(m.stream, m.function));
|
||||||
|
return std::nullopt;
|
||||||
|
});
|
||||||
|
conn->start();
|
||||||
|
|
||||||
|
// Select session 1 only.
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectReq, 10, /*session_id=*/1))
|
||||||
|
.encode());
|
||||||
|
auto rsp = try_recv_frame(sp);
|
||||||
|
REQUIRE(rsp.has_value());
|
||||||
|
pump_until(sp.io, [&] { return conn->is_session_selected(1); });
|
||||||
|
|
||||||
|
// Data for session 1 — should route to session 1's handler.
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::data_message(
|
||||||
|
/*session_id=*/1, /*stream=*/1, /*function=*/3,
|
||||||
|
/*reply_expected=*/false, /*sys=*/200))
|
||||||
|
.encode());
|
||||||
|
pump_until(sp.io, [&] {
|
||||||
|
for (auto& s : seen) if (s.first == 1) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data for session 2 (not selected) — should be Reject(EntityNotSelected).
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::data_message(
|
||||||
|
/*session_id=*/2, /*stream=*/1, /*function=*/3,
|
||||||
|
/*reply_expected=*/false, /*sys=*/201))
|
||||||
|
.encode());
|
||||||
|
auto rej = try_recv_frame(sp);
|
||||||
|
REQUIRE(rej.has_value());
|
||||||
|
CHECK(rej->header.stype == hsms::SType::RejectReq);
|
||||||
|
CHECK(rej->header.byte3 ==
|
||||||
|
static_cast<uint8_t>(hsms::RejectReason::EntityNotSelected));
|
||||||
|
|
||||||
|
// Session 2's handler must NOT have fired.
|
||||||
|
for (auto& s : seen) CHECK(s.first != 2);
|
||||||
|
|
||||||
|
conn->close("test done");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("HSMS-GS active: two registered sessions both end up selected") {
|
||||||
|
// Active mode: the connection sends Select.req for each session
|
||||||
|
// serially, waiting for each Ok before moving to the next.
|
||||||
|
SocketPair sp;
|
||||||
|
// Build active "host" with two sessions.
|
||||||
|
auto host = std::make_shared<hsms::Connection>(
|
||||||
|
std::move(sp.a), hsms::Connection::Mode::Active,
|
||||||
|
/*primary_device_id=*/1, permissive_timers());
|
||||||
|
host->add_session(2);
|
||||||
|
std::atomic<int> host_sel_1{0};
|
||||||
|
std::atomic<int> host_sel_2{0};
|
||||||
|
host->set_session_selected_handler(1, [&] { ++host_sel_1; });
|
||||||
|
host->set_session_selected_handler(2, [&] { ++host_sel_2; });
|
||||||
|
|
||||||
|
// Use the *other* socket as a raw peer. The host will issue
|
||||||
|
// Select.req in GS mode (session_id=device_id of each session);
|
||||||
|
// we ack each one Ok.
|
||||||
|
host->start();
|
||||||
|
|
||||||
|
// First Select.req should arrive for session 1.
|
||||||
|
auto sel1 = try_recv_frame(sp);
|
||||||
|
REQUIRE(sel1.has_value());
|
||||||
|
CHECK(sel1->header.stype == hsms::SType::SelectReq);
|
||||||
|
CHECK(sel1->header.session_id == 1);
|
||||||
|
// Ack Ok.
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectRsp, sel1->header.system_bytes,
|
||||||
|
/*session_id=*/1, /*byte2=*/0,
|
||||||
|
/*byte3=*/static_cast<uint8_t>(hsms::SelectStatus::Ok)))
|
||||||
|
.encode());
|
||||||
|
pump_until(sp.io, [&] { return host_sel_1.load() == 1; });
|
||||||
|
|
||||||
|
// Second Select.req should arrive for session 2.
|
||||||
|
auto sel2 = try_recv_frame(sp);
|
||||||
|
REQUIRE(sel2.has_value());
|
||||||
|
CHECK(sel2->header.stype == hsms::SType::SelectReq);
|
||||||
|
CHECK(sel2->header.session_id == 2);
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectRsp, sel2->header.system_bytes,
|
||||||
|
/*session_id=*/2, /*byte2=*/0,
|
||||||
|
/*byte3=*/static_cast<uint8_t>(hsms::SelectStatus::Ok)))
|
||||||
|
.encode());
|
||||||
|
pump_until(sp.io, [&] { return host_sel_2.load() == 1; });
|
||||||
|
|
||||||
|
CHECK(host->is_session_selected(1));
|
||||||
|
CHECK(host->is_session_selected(2));
|
||||||
|
|
||||||
|
host->close("test done");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("HSMS-SS: existing single-session API still works (backwards compat)") {
|
||||||
|
// Sanity that the SS-only path hasn't regressed.
|
||||||
|
SocketPair sp;
|
||||||
|
auto conn = std::make_shared<hsms::Connection>(
|
||||||
|
std::move(sp.a), hsms::Connection::Mode::Passive, 0, permissive_timers());
|
||||||
|
bool selected = false;
|
||||||
|
conn->set_selected_handler([&] { selected = true; });
|
||||||
|
conn->start();
|
||||||
|
|
||||||
|
// SS-style Select.req (session_id=0xFFFF).
|
||||||
|
send_bytes(sp, hsms::Frame(hsms::Header::control(
|
||||||
|
hsms::SType::SelectReq, /*sys=*/1))
|
||||||
|
.encode());
|
||||||
|
auto rsp = try_recv_frame(sp);
|
||||||
|
REQUIRE(rsp.has_value());
|
||||||
|
CHECK(rsp->header.stype == hsms::SType::SelectRsp);
|
||||||
|
CHECK(rsp->header.byte3 == static_cast<uint8_t>(hsms::SelectStatus::Ok));
|
||||||
|
pump_until(sp.io, [&] { return selected; });
|
||||||
|
CHECK(conn->selected());
|
||||||
|
|
||||||
|
conn->close("test done");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user