Adds S9F1, F3, F5, F7, F9, F11, F13 to the message catalog and wires
the two emission paths that the Connection layer can drive without help
from the Router or the application: S9F7 on a body-decode failure and
S9F9 on a T3 transaction-timer timeout.
Catalog (data/messages.yaml -> generated messages.hpp)
All six MHEAD-carrying messages (F1/F3/F5/F7/F9/F11) use the same
shape — a single <B 10> body with the offending 10-byte HSMS header.
S9F13 (conversation timeout) carries <L,2 <A MEXP> <A EDID>>.
Connection-side emissions (src/hsms/connection.cpp)
emit_s9(function, mhead) New private helper. Builds a 9/function/W=0
data message whose body is <B 10> with the
MHEAD bytes, allocates a fresh sys_bytes,
and queues it onto the write path. No
reply is tracked.
S9F7 on body decode handle_data wraps Message::from_body in a
try/catch. Previously any decode error
closed the connection; now it emits S9F7
with the offending header and continues
reading. Reply-side decode failure also
emits S9F7 and surfaces the new
Error::IllegalData to the waiting
ReplyHandler (rather than making the
caller wait out T3).
S9F9 on T3 timeout The send_request T3 callback rebuilds the
original outgoing MHEAD from
(device_id, expected_stream,
expected_function-1, sys, W=1) and emits
S9F9 before invoking the callback with
Error::Timeout (unchanged).
What's intentionally not yet wired (logged in COMPLIANCE.md)
- S9F3 / S9F5 — "unknown stream / function". These need to live in
the Router's fallback path, which would require either the Router
knowing about a Connection-shaped sender or the Connection's
message wrapper learning which streams the Router has handlers
for. Deferred — today the fallback returns SxF0 only.
- S9F11 — "Data Too Long". Currently we close on oversized frames;
we'd need to also build a synthetic 10-byte MHEAD substitute (the
real header isn't yet available at the point of detection) and
flush it through close_after_flush.
Tests + docs
tests/test_messages.cpp Round-trip every S9F* using a representative
10-byte MHEAD literal; check S9F13 carries
MEXP + EDID. +2 cases / +37 assertions.
COMPLIANCE.md Error Messages row moved from "no S9 stream"
to a detailed status describing what's
emitted vs catalog-only. Coverage matrix
expanded per-message (F1/F7/F9/F13 ✅;
F3/F5/F11 🟡 catalog-only).
Build/demo unaffected: 75 cases / 420 assertions pass; the happy-path
demo never trips a decode error or T3, so the S9 path isn't exercised
end-to-end (but unit tests prove the wire shape).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+36
-4
@@ -1,5 +1,7 @@
|
||||
#include "secsgem/hsms/connection.hpp"
|
||||
|
||||
#include "secsgem/secs2/codec.hpp"
|
||||
|
||||
#include <exception>
|
||||
|
||||
namespace secsgem::hsms {
|
||||
@@ -10,8 +12,9 @@ class HsmsCategory : public std::error_category {
|
||||
const char* name() const noexcept override { return "hsms"; }
|
||||
std::string message(int ev) const override {
|
||||
switch (static_cast<Error>(ev)) {
|
||||
case Error::Timeout: return "HSMS transaction timeout";
|
||||
case Error::Closed: return "HSMS connection closed";
|
||||
case Error::Timeout: return "HSMS transaction timeout";
|
||||
case Error::Closed: return "HSMS connection closed";
|
||||
case Error::IllegalData: return "peer sent a body that failed to decode";
|
||||
}
|
||||
return "unknown HSMS error";
|
||||
}
|
||||
@@ -125,7 +128,14 @@ void Connection::handle_data(const Frame& frame) {
|
||||
try {
|
||||
reply = secs2::Message::from_body(h.stream(), h.function(), h.w_bit(), frame.body);
|
||||
} catch (const std::exception& e) {
|
||||
close(std::string("reply body decode: ") + e.what());
|
||||
// Malformed reply: notify the peer with S9F7 and surface IllegalData
|
||||
// to the caller right now (rather than making them wait out T3).
|
||||
log(std::string("reply body decode: ") + e.what() + "; emitting S9F7");
|
||||
emit_s9(7, h.encode());
|
||||
auto cb = std::move(it->second.cb);
|
||||
it->second.t3->cancel();
|
||||
pending_requests_.erase(it);
|
||||
cb(make_error(Error::IllegalData), {});
|
||||
return;
|
||||
}
|
||||
auto cb = std::move(it->second.cb);
|
||||
@@ -160,7 +170,10 @@ void Connection::handle_data(const Frame& frame) {
|
||||
try {
|
||||
msg = secs2::Message::from_body(h.stream(), h.function(), h.w_bit(), frame.body);
|
||||
} catch (const std::exception& e) {
|
||||
close(std::string("message body decode: ") + e.what());
|
||||
// E5: a malformed body in a primary is reported via S9F7 (Illegal Data).
|
||||
// The connection stays up — the peer may send corrected data next.
|
||||
log(std::string("message body decode: ") + e.what() + "; emitting S9F7");
|
||||
emit_s9(7, h.encode());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -294,6 +307,12 @@ void Connection::send_request(secs2::Message msg, ReplyHandler cb) {
|
||||
if (ec) return;
|
||||
auto it = pending_requests_.find(sys);
|
||||
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_function = static_cast<uint8_t>(it->second.expected_function - 1);
|
||||
Header h = Header::data_message(device_id_, req_stream, req_function, true, sys);
|
||||
emit_s9(9, h.encode());
|
||||
auto cb2 = std::move(it->second.cb);
|
||||
pending_requests_.erase(it);
|
||||
cb2(make_error(Error::Timeout), {});
|
||||
@@ -403,6 +422,19 @@ void Connection::clear_control_transaction() {
|
||||
t6_timer_.cancel();
|
||||
}
|
||||
|
||||
void Connection::emit_s9(uint8_t function, const std::array<uint8_t, 10>& mhead) {
|
||||
if (closed_) return;
|
||||
const uint32_t sys = next_system_bytes();
|
||||
// S9F1/F3/F5/F7/F9/F11 all carry the offending MHEAD as <B 10> in the body.
|
||||
secs2::Item body_item =
|
||||
secs2::Item::binary(std::vector<uint8_t>(mhead.begin(), mhead.end()));
|
||||
Frame f(Header::data_message(device_id_, /*stream=*/9, function,
|
||||
/*w=*/false, sys),
|
||||
secs2::encode(body_item));
|
||||
log("-> S9F" + std::to_string(function) + " (sys=" + std::to_string(sys) + ")");
|
||||
send_frame(std::move(f));
|
||||
}
|
||||
|
||||
uint32_t Connection::next_system_bytes() {
|
||||
uint32_t s = next_system_bytes_++;
|
||||
if (next_system_bytes_ == 0) next_system_bytes_ = 1;
|
||||
|
||||
Reference in New Issue
Block a user