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:
@@ -16,10 +16,17 @@
|
||||
|
||||
namespace secsgem::hsms {
|
||||
|
||||
// One HSMS session over a connected TCP socket. Drives the connection state
|
||||
// machine (NOT_SELECTED -> SELECTED), runs the SELECT/LINKTEST/SEPARATE control
|
||||
// handshakes and the T3/T6/T7/T8 timers, and correlates data replies by system
|
||||
// bytes. Single-threaded: all work runs on the socket's executor.
|
||||
// One HSMS session-set over a connected TCP socket. Drives the connection
|
||||
// state machine (NOT_SELECTED -> SELECTED, per session), runs the
|
||||
// SELECT/LINKTEST/SEPARATE control handshakes and the T3/T6/T7/T8 timers,
|
||||
// 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> {
|
||||
public:
|
||||
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);
|
||||
|
||||
void set_message_handler(MessageHandler h) { on_message_ = std::move(h); }
|
||||
void set_selected_handler(SelectedHandler h) { on_selected_ = std::move(h); }
|
||||
// SS-style setters: operate on the primary (constructor-supplied) session.
|
||||
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_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;
|
||||
// Passive mode arms the T7 not-selected timer and waits for Select.req.
|
||||
void start();
|
||||
@@ -57,8 +80,9 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
||||
// Hard close.
|
||||
void close(const std::string& reason);
|
||||
|
||||
State state() const { return state_; }
|
||||
bool selected() const { return state_ == State::Selected; }
|
||||
// SS-style state getters: report the primary session's state.
|
||||
State state() const;
|
||||
bool selected() const;
|
||||
|
||||
// The HSMS header of the primary currently being dispatched by 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();
|
||||
|
||||
// --- 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 enter_selected();
|
||||
void enter_selected(); // primary session
|
||||
void enter_selected(uint16_t device_id); // explicit session (GS)
|
||||
void arm_t7();
|
||||
void arm_linktest();
|
||||
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
|
||||
|
||||
Mode mode_;
|
||||
uint16_t device_id_;
|
||||
uint16_t device_id_; // primary session's device_id
|
||||
Timers timers_;
|
||||
State state_ = State::NotSelected;
|
||||
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::vector<uint8_t> payload_;
|
||||
|
||||
@@ -138,10 +194,8 @@ class Connection : public std::enable_shared_from_this<Connection> {
|
||||
};
|
||||
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_;
|
||||
LogHandler on_log_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user