The dispatcher deduped on inbound text but relayed a prefixed copy
("[net/sender] text"), so a re-heard relayed message hashed differently
and was re-relayed, stacking another prefix each hop — an unbounded loop,
exactly what the dedup was meant to prevent.
Add MessageBus.mark_seen() and record the on-air relayed string before
sending, so the bridge recognises and drops its own relayed copies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.1 KiB
Python
109 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""archy-messh Phase 0 host-bridge.
|
|
|
|
Connects to one radio per network (Meshtastic, MeshCore, Reticulum/RNode),
|
|
each over its official client protocol, and relays text messages between
|
|
all three. See docs/ARCHITECTURE.md for why this is Phase 0 (prove the
|
|
bridging logic on 3 separate boards) rather than Phase 1 (single
|
|
time-multiplexed radio on one Heltec V3).
|
|
|
|
NOT YET TESTED END-TO-END — written without physical hardware attached to
|
|
the dev machine. Needs Meshtastic + MeshCore + RNode-flashed boards to
|
|
validate; each adapter's wire-level behavior is only as good as its
|
|
upstream library's docs/examples (cited in each adapter's docstring/header).
|
|
|
|
Usage (see README.md for full option list):
|
|
python3 main.py \\
|
|
--meshtastic-port /dev/ttyUSB0 \\
|
|
--meshcore-port /dev/ttyUSB1 \\
|
|
--reticulum-storage ./rns-storage
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
from adapters.meshcore_adapter import MeshCoreAdapter
|
|
from adapters.meshtastic_adapter import MeshtasticAdapter
|
|
from adapters.reticulum_adapter import ReticulumAdapter
|
|
from bridge_core import BridgeMessage, MessageBus
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(description="archy-messh Phase 0 tri-protocol bridge")
|
|
p.add_argument("--meshtastic-port", help="Meshtastic serial device (e.g. /dev/ttyUSB0)")
|
|
p.add_argument("--meshtastic-host", help="Meshtastic TCP host, instead of serial")
|
|
p.add_argument("--meshcore-port", help="MeshCore serial device (e.g. /dev/ttyUSB1)")
|
|
p.add_argument("--meshcore-host", help="MeshCore TCP host, instead of serial")
|
|
p.add_argument("--meshcore-channel", type=int, default=0, help="MeshCore public channel index")
|
|
p.add_argument(
|
|
"--reticulum-storage", default="./rns-storage", help="LXMF/RNS storage directory"
|
|
)
|
|
p.add_argument(
|
|
"--dedup-ttl", type=float, default=300.0, help="Seconds to suppress repeat-content loops"
|
|
)
|
|
return p
|
|
|
|
|
|
def main() -> int:
|
|
args = build_arg_parser().parse_args()
|
|
|
|
bus = MessageBus(dedup_ttl_seconds=args.dedup_ttl)
|
|
|
|
def on_message(network: str, sender: str, text: str) -> None:
|
|
bus.publish(BridgeMessage(network=network, sender=sender, text=text, received_at=time.time()))
|
|
|
|
meshtastic = MeshtasticAdapter(on_message, port=args.meshtastic_port, host=args.meshtastic_host)
|
|
meshcore = MeshCoreAdapter(
|
|
on_message,
|
|
port=args.meshcore_port,
|
|
host=args.meshcore_host,
|
|
channel_idx=args.meshcore_channel,
|
|
)
|
|
reticulum = ReticulumAdapter(on_message, storage_path=args.reticulum_storage)
|
|
|
|
adapters = {
|
|
MeshtasticAdapter.NETWORK: meshtastic,
|
|
MeshCoreAdapter.NETWORK: meshcore,
|
|
ReticulumAdapter.NETWORK: reticulum,
|
|
}
|
|
|
|
print("Connecting to Meshtastic...")
|
|
meshtastic.connect()
|
|
print("Connecting to MeshCore...")
|
|
meshcore.connect()
|
|
print("Connecting to Reticulum...")
|
|
reticulum.connect()
|
|
print("All three adapters connected. Bridging...")
|
|
|
|
try:
|
|
while True:
|
|
message = bus.get()
|
|
if bus.is_duplicate(message.text):
|
|
continue
|
|
print(f"[{message.network}] {message.sender}: {message.text}")
|
|
relayed = f"[{message.network}/{message.sender}] {message.text}"
|
|
# Record the exact on-air string so that if this relayed copy is
|
|
# re-heard from another network it's recognised as a duplicate and
|
|
# not re-relayed (which would stack another prefix each hop).
|
|
bus.mark_seen(relayed)
|
|
for network, adapter in adapters.items():
|
|
if network == message.network:
|
|
continue # don't echo back onto the network it came from
|
|
try:
|
|
adapter.send(relayed)
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f" ! failed to relay onto {network}: {exc}", file=sys.stderr)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
meshtastic.close()
|
|
meshcore.close()
|
|
reticulum.close()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|