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()