From e5419456f478d61e40620659ac4ed6db1aa25056 Mon Sep 17 00:00:00 2001 From: Martin Dobrev Date: Wed, 26 Aug 2026 16:51:31 +0100 Subject: [PATCH 1/2] AnnounceHandler: optional packet-carrying callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a second virtual received_announce(destination_hash, identity, app_data, packet) that Transport now calls; its default forwards to the classic three-argument callback, so existing handlers are unaffected. Handlers that override the new form get the announce packet: receiving interface, hop count, RSSI/SNR and transport id — enough to build neighbour lists without re-parsing and re-verifying the raw packet. --- README.md | 19 +++++++++++++++++++ src/microReticulum/Transport.cpp | 3 ++- src/microReticulum/Transport.h | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 96629e3c..d196c95f 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,25 @@ Also note the following preprocessor directives used to tune `microStore::FileSt `-DRNS_PATH_TABLE_SEGMENT_COUNT`: Maximum number of segment files to rotate (minimum of 3) Appropriate settings should be selected to match the storage and memory resources available on the target platform. +## Announce Handlers + +Applications receive announces by registering a subclass of `RNS::AnnounceHandler` with `RNS::Transport::register_announce_handler()`. Override either callback: + +```cpp +class MyHandler : public RNS::AnnounceHandler { +public: + // Basic form: destination hash, announced identity and app data. + void received_announce(const RNS::Bytes& destination_hash, const RNS::Identity& announced_identity, const RNS::Bytes& app_data) override; + + // Extended form: additionally receives the announce packet, giving access to + // packet.receiving_interface(), packet.hops(), packet.rssi() and packet.snr(). + // The default implementation forwards to the basic form. + void received_announce(const RNS::Bytes& destination_hash, const RNS::Identity& announced_identity, const RNS::Bytes& app_data, const RNS::Packet& packet) override; +}; +``` + +Pass an aspect filter such as `"lxmf.delivery"` to the `AnnounceHandler` constructor to receive only announces for that destination name, or `nullptr` for all announces. + ## Provisioning microReticulum ships an optional schema-driven configuration subsystem under `src/microReticulum/Provisioning/`. It maintains a registry of namespaces and fields, holds a "working" config in RAM plus a "draft" overlay for pending edits, and (when filesystem support is enabled) persists committed values atomically to flash. Apps frame and transport MsgPack request/response payloads using their preferred link (KISS, BLE GATT, Web Serial, etc.); the library handles everything from the engine inward. diff --git a/src/microReticulum/Transport.cpp b/src/microReticulum/Transport.cpp index 89b5238b..12212c23 100644 --- a/src/microReticulum/Transport.cpp +++ b/src/microReticulum/Transport.cpp @@ -2726,7 +2726,8 @@ TRACEF("path_announce_emitted=%lu", path_announce_emitted); handler->received_announce( packet.destination_hash(), announce_identity, - Identity::recall_app_data(packet.destination_hash()) + Identity::recall_app_data(packet.destination_hash()), + packet ); } } diff --git a/src/microReticulum/Transport.h b/src/microReticulum/Transport.h index 4ce06bd6..aaf6af1f 100644 --- a/src/microReticulum/Transport.h +++ b/src/microReticulum/Transport.h @@ -66,6 +66,15 @@ namespace RNS { // configured aspect filter. Filters must be specific, // and cannot use wildcards. virtual void received_announce(const Bytes& destination_hash, const Identity& announced_identity, const Bytes& app_data) = 0; + // Richer variant with the announce packet itself: receiving interface + // (packet.receiving_interface()), hop count (packet.hops()), signal + // stats (packet.rssi()/snr()), transport id. Defaults to the classic + // callback so existing handlers keep working; override this one to + // get the metadata. + virtual void received_announce(const Bytes& destination_hash, const Identity& announced_identity, const Bytes& app_data, const Packet& packet) { + (void)packet; + received_announce(destination_hash, announced_identity, app_data); + } std::string& aspect_filter() { return _aspect_filter; } private: std::string _aspect_filter; From 35ac3c9ba338d8ceaee57f58a9f705433276ebed Mon Sep 17 00:00:00 2001 From: Martin Dobrev Date: Tue, 10 Nov 2026 18:18:33 +0000 Subject: [PATCH 2/2] Test the packet-carrying announce callback and document its lifetime The loopback suite now replays its own announce with the destination deregistered, so it reaches announce handlers the way a remote one would. A handler overriding only the basic callback is still called; a handler overriding the packet-carrying one gets the packet (destination, type, receiving interface) and is not called twice. The header and README now say the Packet is only valid for the duration of the callback, and the README no longer suggests either callback may be overridden alone: the basic one is pure virtual. CHANGELOG entry added. --- CHANGELOG.md | 8 +++ README.md | 4 +- src/microReticulum/Transport.h | 4 +- test/test_rns_loopback/test_rns_loopback.cpp | 72 ++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4be8bb..4dc27c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Unreleased + +## Added + +### Transport + +* Added an optional packet-carrying `AnnounceHandler::received_announce(destination_hash, announced_identity, app_data, packet)` overload, giving handlers the receiving interface, hop count and signal quality of the announce. Its default forwards to the existing three-argument callback, so existing handlers are unchanged. + # microReticulum v0.5.1 ## Changed diff --git a/README.md b/README.md index 3a76ad40..6008206f 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Appropriate settings should be selected to match the storage and memory resource ## Announce Handlers -Applications receive announces by registering a subclass of `RNS::AnnounceHandler` with `RNS::Transport::register_announce_handler()`. Override either callback: +Applications receive announces by registering a subclass of `RNS::AnnounceHandler` with `RNS::Transport::register_announce_handler()`. The basic callback is pure virtual and must always be implemented; override the extended one as well when the handler needs the announce packet: ```cpp class MyHandler : public RNS::AnnounceHandler { @@ -91,6 +91,8 @@ public: }; ``` +The `Packet` passed to the extended form is only valid for the duration of the callback. Copy anything the handler needs to keep, such as `packet.hops()` or the receiving interface, rather than storing a reference to the packet. + Pass an aspect filter such as `"lxmf.delivery"` to the `AnnounceHandler` constructor to receive only announces for that destination name, or `nullptr` for all announces. ## Provisioning diff --git a/src/microReticulum/Transport.h b/src/microReticulum/Transport.h index 13975853..72625721 100644 --- a/src/microReticulum/Transport.h +++ b/src/microReticulum/Transport.h @@ -70,7 +70,9 @@ namespace RNS { // (packet.receiving_interface()), hop count (packet.hops()), signal // stats (packet.rssi()/snr()), transport id. Defaults to the classic // callback so existing handlers keep working; override this one to - // get the metadata. + // get the metadata. The packet is only valid for the duration of the + // call: copy anything you need to keep (e.g. packet.hops(), or the + // receiving interface) rather than holding the reference. virtual void received_announce(const Bytes& destination_hash, const Identity& announced_identity, const Bytes& app_data, const Packet& packet) { (void)packet; received_announce(destination_hash, announced_identity, app_data); diff --git a/test/test_rns_loopback/test_rns_loopback.cpp b/test/test_rns_loopback/test_rns_loopback.cpp index d6d37cff..fb54e528 100644 --- a/test/test_rns_loopback/test_rns_loopback.cpp +++ b/test/test_rns_loopback/test_rns_loopback.cpp @@ -32,6 +32,12 @@ class OutInterface : public RNS::InterfaceImpl { virtual bool send_outgoing(const RNS::Bytes &data) { HEADF(RNS::LOG_TRACE, "OutInterface.send_outgoing: data: %s", data.toHex().c_str()); + // Keep the last announce sent, so a test can replay it as if it had + // arrived from another node + if (data.size() > 0 && (data.data()[0] & 0x03) == RNS::Type::Packet::ANNOUNCE) { + last_announce = data; + } + // Loop data back to InInterface for testing // NOTE: This sends the incoming data directory to RNS::Interface (which relays to Transport) // and does not actually go through InInterface. @@ -42,9 +48,11 @@ class OutInterface : public RNS::InterfaceImpl { return true; } + static RNS::Bytes last_announce; private: RNS::Interface& _in_interface; }; +RNS::Bytes OutInterface::last_announce; // Test AnnounceHandler class ExampleAnnounceHandler : public RNS::AnnounceHandler { @@ -65,6 +73,44 @@ class ExampleAnnounceHandler : public RNS::AnnounceHandler { } }; +// Overrides only the basic callback: must still be called now that Transport +// dispatches through the packet-carrying overload +class BasicAnnounceHandler : public RNS::AnnounceHandler { +public: + BasicAnnounceHandler() : AnnounceHandler(nullptr) {} + virtual void received_announce(const RNS::Bytes& destination_hash, const RNS::Identity& announced_identity, const RNS::Bytes& app_data) { + ++calls; + last_destination_hash = destination_hash; + } + int calls = 0; + RNS::Bytes last_destination_hash; +}; + +// Overrides the packet-carrying callback as well, and records what it saw +class PacketAnnounceHandler : public RNS::AnnounceHandler { +public: + PacketAnnounceHandler() : AnnounceHandler(nullptr) {} + virtual void received_announce(const RNS::Bytes& destination_hash, const RNS::Identity& announced_identity, const RNS::Bytes& app_data) { + ++basic_calls; + } + virtual void received_announce(const RNS::Bytes& destination_hash, const RNS::Identity& announced_identity, const RNS::Bytes& app_data, const RNS::Packet& packet) { + ++packet_calls; + last_destination_hash = destination_hash; + last_packet_destination_hash = packet.destination_hash(); + last_packet_type = packet.packet_type(); + last_receiving_interface = packet.receiving_interface().get_hash(); + } + int basic_calls = 0; + int packet_calls = 0; + RNS::Bytes last_destination_hash; + RNS::Bytes last_packet_destination_hash; + uint8_t last_packet_type = 0xFF; + RNS::Bytes last_receiving_interface; +}; + +std::shared_ptr basic_announce_handler(new BasicAnnounceHandler()); +std::shared_ptr packet_announce_handler(new PacketAnnounceHandler()); + //ExampleAnnounceHandler announce_handler((const char*)"example_utilities.announcesample.fruits"); //RNS::HAnnounceHandler announce_handler(new ExampleAnnounceHandler("example_utilities.announcesample.fruits")); RNS::HAnnounceHandler announce_handler(new ExampleAnnounceHandler()); @@ -158,6 +204,8 @@ void testReticulum() { HEAD("Registering announce handler with Transport...", RNS::LOG_TRACE); RNS::Transport::register_announce_handler(announce_handler); + RNS::Transport::register_announce_handler(basic_announce_handler); + RNS::Transport::register_announce_handler(packet_announce_handler); HEAD("Announcing destination...", RNS::LOG_TRACE); // test path @@ -166,6 +214,30 @@ void testReticulum() { destination.announce(RNS::bytesFromString("test")); // 23.9% (+0.8%) + // The announce looped back above is for a local destination, so Transport + // does not hand it to announce handlers. Deregister the destination and + // replay the same announce, so it arrives as one from another node would. + HEAD("Replaying announce as a remote one...", RNS::LOG_TRACE); + TEST_ASSERT_TRUE(OutInterface::last_announce.size() > 0); + RNS::Transport::deregister_destination(destination); + in_interface.handle_incoming(OutInterface::last_announce); + + // A handler overriding only the basic callback is still called + TEST_ASSERT_EQUAL_INT(1, basic_announce_handler->calls); + TEST_ASSERT_TRUE(basic_announce_handler->last_destination_hash == destination.hash()); + + // A handler overriding the packet-carrying callback gets the packet, and + // is not called a second time through the basic one + TEST_ASSERT_EQUAL_INT(1, packet_announce_handler->packet_calls); + TEST_ASSERT_EQUAL_INT(0, packet_announce_handler->basic_calls); + TEST_ASSERT_TRUE(packet_announce_handler->last_destination_hash == destination.hash()); + TEST_ASSERT_TRUE(packet_announce_handler->last_packet_destination_hash == destination.hash()); + TEST_ASSERT_EQUAL_UINT8(RNS::Type::Packet::ANNOUNCE, packet_announce_handler->last_packet_type); + TEST_ASSERT_TRUE(packet_announce_handler->last_receiving_interface == in_interface.get_hash()); + + RNS::Transport::deregister_announce_handler(basic_announce_handler); + RNS::Transport::deregister_announce_handler(packet_announce_handler); + HEAD("Destroying Reticulum instance...", RNS::LOG_TRACE); reticulum = {RNS::Type::NONE};