100%/B: S9F3/F5/F11 emission + Router fallback
tests / build-and-test (push) Failing after 31s

Closes the S9 stream.  Every documented protocol-error condition is now
auto-emitted by Connection (with the assist of one Router predicate),
without involving the application.

Router (include/secsgem/gem/router.hpp)
  Adds two predicates: has_handler(stream, function) and
  has_handler_for_stream(stream).  Lets the wrapping message handler
  decide whether an unhandled message is "unknown stream" (S9F3) or
  "unknown function in a known stream" (S9F5).

Connection (include/secsgem/hsms/{connection.hpp, connection.cpp})
  - emit_s9() goes public so the message_handler can call it.
  - New current_header() accessor returns the HSMS header of the
    primary currently being dispatched.  Non-null only inside the
    on_message_ call; cleared on the way out.
  - handle_data sets current_header_ before invoking on_message_.
  - on_length on oversized frame: synthesizes a 10-byte MHEAD whose
    first 4 bytes are the offending length prefix, emits S9F11, and
    sets close_after_flush so the S9F11 goes out before the socket
    closes.

Server (apps/secs_server.cpp)
  The conn->set_message_handler lambda now wraps router.dispatch.  For
  any inbound primary without a registered handler, it captures the
  MHEAD via current_header() and emits either S9F3 (stream unknown) or
  S9F5 (function unknown).  The wrapper still returns the Router's
  reply (SxF0 for primaries with W) so transactional semantics are
  preserved.

COMPLIANCE.md
  Error Messages row flips from 🟡 to .  S9F3/F5/F11 rows in the
  coverage matrix flip from 🟡 to .  Each row in the matrix now
  states its trigger condition explicitly.  Drops the
  "Finish S9 wiring" bullet from the "what would 100% take" list.

Verified
  - Tests: 78 cases / 454 assertions still pass (no behavioural change
    on the happy path; new emission paths fire only on protocol errors
    that the demo doesn't induce).
  - Build clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 01:57:35 +02:00
parent 88205037ec
commit 65db38d9f2
5 changed files with 68 additions and 17 deletions
+12
View File
@@ -45,6 +45,18 @@ class Router {
return std::nullopt;
}
// Introspection: lets the message_handler wrapper decide whether an
// unhandled message is "unknown stream" (S9F3) or "unknown function in
// a known stream" (S9F5).
bool has_handler(uint8_t stream, uint8_t function) const {
return handlers_.find({stream, function}) != handlers_.end();
}
bool has_handler_for_stream(uint8_t stream) const {
for (const auto& [key, _] : handlers_)
if (key.first == stream) return true;
return false;
}
std::size_t size() const { return handlers_.size(); }
private:
+14 -7
View File
@@ -60,6 +60,18 @@ class Connection : public std::enable_shared_from_this<Connection> {
State state() const { return state_; }
bool selected() const { return state_ == State::Selected; }
// The HSMS header of the primary currently being dispatched by the
// message_handler. Only non-null inside the handler. Used so the
// handler can capture MHEAD when it needs to emit S9F3 / S9F5 for an
// unrecognized stream/function.
const Header* current_header() const { return current_header_; }
// Equipment-initiated S9F<function> primary carrying a 10-byte MHEAD.
// Public so a wrapping message_handler can emit S9F3 / S9F5 when it
// detects an unhandled message; Connection itself uses this for S9F7
// (illegal data), S9F9 (T3 timeout), and S9F11 (data too long).
void emit_s9(uint8_t function, const std::array<uint8_t, 10>& mhead);
private:
// --- read path ---
void read_length();
@@ -83,13 +95,6 @@ class Connection : public std::enable_shared_from_this<Connection> {
const char* what);
void clear_control_transaction();
// --- S9 error stream emissions ---
// Build and send an unsolicited S9F<function> primary with a single
// <B 10> body carrying the offending message's 10-byte MHEAD. Used for
// F1/F3/F5/F7/F9/F11 (all of which share that body shape). No reply is
// tracked — these notifications are typically W=0.
void emit_s9(uint8_t function, const std::array<uint8_t, 10>& mhead);
uint32_t next_system_bytes();
void log(const std::string& msg);
@@ -133,6 +138,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
MessageHandler on_message_;
SelectedHandler on_selected_;
ClosedHandler on_closed_;