"""Reticulum adapter — wraps RNS + LXMF (the standard host-side Reticulum messaging stack; see docs/ARCHITECTURE.md for why archy-messh talks to a real RNode-flashed radio via RNS rather than reimplementing Reticulum's wire format from scratch). Reticulum has no built-in broadcast-channel concept the way Meshtastic ("channel" PSK) or MeshCore ("public channel index") do — LXMF messaging is addressed point-to-point between Destinations. To bridge broadcast-style text, this adapter tracks a roster of peer LXMF delivery destinations learned from their announces (any Reticulum/Sideband/NomadNet/MeshChat user who has announced is added), and fans outbound bridge messages out to that roster individually. This is the simplest correct v0; a dedicated shared GROUP destination (symmetric-key, closer to a real "channel") is a known follow-up once Phase 0 proves the roster approach works at all. """ import threading from collections.abc import Callable class ReticulumAdapter: NETWORK = "reticulum" ASPECT = "lxmf.delivery" def __init__( self, on_message: Callable[[str, str, str], None], storage_path: str, display_name: str = "archy-messh-bridge", ): self._on_message = on_message self._storage_path = storage_path self._display_name = display_name self._router = None self._identity = None self._destination = None self._peers: set[bytes] = set() self._lock = threading.Lock() # RNS.Transport.register_announce_handler() requires an object with # an `aspect_filter` attribute — set here so `self` can be registered # directly as the announce handler. self.aspect_filter = self.ASPECT def connect(self) -> None: import RNS import LXMF RNS.Reticulum() self._router = LXMF.LXMRouter(storagepath=self._storage_path) self._identity = RNS.Identity() self._destination = self._router.register_delivery_identity( self._identity, display_name=self._display_name ) self._router.register_delivery_callback(self._handle_delivery) RNS.Transport.register_announce_handler(self) self._router.announce(self._destination.hash) def received_announce(self, destination_hash, announced_identity, app_data) -> None: # noqa: ANN001 if self._destination is not None and destination_hash == self._destination.hash: return # ignore our own announce with self._lock: self._peers.add(destination_hash) def _handle_delivery(self, message) -> None: # noqa: ANN001 import RNS text = message.content_as_string() sender = RNS.prettyhexrep(message.source_hash) if text: self._on_message(self.NETWORK, sender, text) def send(self, text: str) -> None: import RNS import LXMF if self._router is None or self._destination is None: raise RuntimeError("ReticulumAdapter.send() called before connect()") with self._lock: peer_hashes = list(self._peers) for peer_hash in peer_hashes: identity = RNS.Identity.recall(peer_hash) if identity is None: continue # no path/identity known yet for this peer dest = RNS.Destination( identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery" ) lxm = LXMF.LXMessage( dest, self._destination, text, desired_method=LXMF.LXMessage.OPPORTUNISTIC ) self._router.handle_outbound(lxm) def close(self) -> None: pass # RNS/LXMF manage their own threads; no explicit teardown needed for a v0 prototype