Files
lora-xmpp-bridge/bridge.py
google-labs-jules[bot] 9c79475477 feat: Implement magic prefix to prevent bridging loops
This commit introduces a `MAGIC_PREFIX` (`🌐 `) that is prepended to
every message forwarded from the internet (XMPP) onto the local
Meshtastic mesh.

The `on_meshtastic_receive` handler has been updated to ignore any
incoming messages on the mesh that start with this prefix. This
prevents multiple bridges running in the same physical mesh network
from continually forwarding the same message back and forth, creating
an infinite communication loop.

Co-authored-by: jamessucla <2191476+jamessucla@users.noreply.github.com>
2026-03-06 09:22:26 +00:00

132 lines
5.0 KiB
Python

import sys
import time
import logging
import asyncio
from pubsub import pub
import meshtastic
import meshtastic.serial_interface
import slixmpp
logging.basicConfig(level=logging.INFO, format='%(levelname)-8s %(message)s')
class SovereignBridge(slixmpp.ClientXMPP):
# Magic prefix used to identify messages that have already been bridged from the internet.
# This prevents multiple bridges on the same mesh from creating an infinite forwarding loop.
MAGIC_PREFIX = "🌐 "
def __init__(self, jid, password, room, nick):
super().__init__(jid, password)
self.room = room
self.nick = nick
self.meshtastic_interface = None
# Plugins for MUC
self.register_plugin('xep_0030') # Service Discovery
self.register_plugin('xep_0045') # Multi-User Chat
self.register_plugin('xep_0199') # XMPP Ping
self.add_event_handler("session_start", self.start)
self.add_event_handler("groupchat_message", self.muc_message)
# Setup Meshtastic pubsub listeners
pub.subscribe(self.on_meshtastic_receive, "meshtastic.receive.text")
async def start(self, event):
self.send_presence()
await self.get_roster()
try:
await self.plugin['xep_0045'].join_muc_wait(self.room, self.nick)
logging.info(f"Joined XMPP MUC: {self.room} as {self.nick}")
except asyncio.TimeoutError:
logging.error(f"Timed out trying to join XMPP MUC: {self.room}")
except Exception as e:
logging.error(f"Error joining XMPP MUC: {e}")
def muc_message(self, msg):
# Ignore our own messages to avoid loops
if msg['mucnick'] == self.nick:
return
body = msg['body']
sender = msg['mucnick']
# Prepend the magic prefix before sending it to the mesh
out_msg = f"{self.MAGIC_PREFIX}[{sender}] {body}"
logging.info(f"[XMPP -> Meshtastic] {out_msg}")
if self.meshtastic_interface:
# Run the synchronous meshtastic call in a thread pool to avoid blocking asyncio
asyncio.ensure_future(
asyncio.to_thread(self._send_to_meshtastic, out_msg)
)
def _send_to_meshtastic(self, msg_text):
try:
self.meshtastic_interface.sendText(msg_text)
except Exception as e:
logging.error(f"Error sending to Meshtastic: {e}")
def on_meshtastic_receive(self, packet, interface):
try:
if 'decoded' in packet and 'text' in packet['decoded']:
text = packet['decoded']['text']
sender_id = packet.get('fromId', 'Unknown')
# If the message starts with our magic prefix, it means another bridge
# (or ourselves) just sent this from the internet. Ignore it to prevent loops!
if text.startswith(self.MAGIC_PREFIX):
logging.debug(f"Ignoring bridged message from {sender_id} to prevent loop.")
return
logging.info(f"[Meshtastic -> XMPP] {sender_id}: {text}")
# Forward to XMPP MUC safely from the meshtastic pubsub thread
self.loop.call_soon_threadsafe(self._send_xmpp_message, sender_id, text)
except Exception as e:
logging.error(f"Error processing Meshtastic packet: {e}")
def _send_xmpp_message(self, sender_id, text):
try:
msg = self.make_message(mto=self.room, mbody=f"[{sender_id}] {text}", mtype='groupchat')
msg.send()
except Exception as e:
logging.error(f"Error sending XMPP message: {e}")
def connect_meshtastic(self):
logging.info("Connecting to Meshtastic node...")
try:
# Connect to the first available serial port
self.meshtastic_interface = meshtastic.serial_interface.SerialInterface()
logging.info("Connected to Meshtastic node.")
except Exception as e:
logging.error(f"Could not connect to Meshtastic node: {e}")
sys.exit(1)
def main():
import argparse
parser = argparse.ArgumentParser(description="Meshtastic <-> XMPP Bridge")
parser.add_argument("-j", "--jid", required=True, help="JID to use")
parser.add_argument("-P", "--password-file", required=False, help="File containing password to use")
parser.add_argument("-p", "--password", required=False, help="Password to use")
parser.add_argument("-r", "--room", required=True, help="MUC room to join")
parser.add_argument("-n", "--nick", required=True, help="MUC nickname")
args = parser.parse_args()
password = args.password
if args.password_file:
with open(args.password_file, 'r') as f:
password = f.read().strip()
if not password:
parser.error("Either --password or --password-file is required")
xmpp = SovereignBridge(args.jid, password, args.room, args.nick)
xmpp.connect_meshtastic()
logging.info("Connecting to XMPP server...")
xmpp.connect()
xmpp.loop.run_forever()
if __name__ == '__main__':
main()