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>
132 lines
5.0 KiB
Python
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()
|