mirror of
https://github.com/AkitaEngineering/Akita-Meshtastic-Meshcore-Bridge.git
synced 2026-03-28 17:42:42 +01:00
226 lines
9.5 KiB
Python
226 lines
9.5 KiB
Python
# ammb/meshtastic_handler.py
|
|
"""
|
|
Handles all interactions with the Meshtastic device and network.
|
|
"""
|
|
|
|
import logging
|
|
import threading
|
|
import time
|
|
from queue import Queue, Empty, Full
|
|
from typing import Optional, Dict, Any
|
|
import meshtastic
|
|
import meshtastic.serial_interface
|
|
from pubsub import pub
|
|
import serial
|
|
|
|
from .config_handler import BridgeConfig
|
|
|
|
class MeshtasticHandler:
|
|
"""Manages connection and communication with the Meshtastic network."""
|
|
RECONNECT_DELAY_S = 10
|
|
|
|
def __init__(self, config: BridgeConfig, to_external_queue: Queue, from_external_queue: Queue, shutdown_event: threading.Event):
|
|
self.logger = logging.getLogger(__name__)
|
|
self.config = config
|
|
self.to_external_queue = to_external_queue
|
|
self.to_meshtastic_queue = from_external_queue
|
|
self.shutdown_event = shutdown_event
|
|
|
|
self.interface: Optional[meshtastic.serial_interface.SerialInterface] = None
|
|
self.my_node_id: Optional[str] = None
|
|
self.sender_thread: Optional[threading.Thread] = None
|
|
self._lock = threading.Lock()
|
|
self._is_connected = threading.Event()
|
|
|
|
def connect(self) -> bool:
|
|
with self._lock:
|
|
if self.interface and self._is_connected.is_set():
|
|
return True
|
|
|
|
try:
|
|
self.logger.info(f"Attempting connection to Meshtastic on {self.config.meshtastic_port}...")
|
|
self._is_connected.clear()
|
|
self.my_node_id = None
|
|
if self.interface:
|
|
try: self.interface.close()
|
|
except Exception: pass
|
|
|
|
self.interface = meshtastic.serial_interface.SerialInterface(
|
|
self.config.meshtastic_port
|
|
)
|
|
|
|
my_info = self.interface.getMyNodeInfo()
|
|
retry_count = 0
|
|
while (not my_info or 'num' not in my_info) and retry_count < 3:
|
|
time.sleep(2)
|
|
my_info = self.interface.getMyNodeInfo()
|
|
retry_count += 1
|
|
|
|
if my_info and 'num' in my_info:
|
|
self.my_node_id = f"!{my_info['num']:x}"
|
|
user_id = my_info.get('user', {}).get('id', 'N/A')
|
|
self.logger.info(f"Connected to Meshtastic device. Node ID: {self.my_node_id} ('{user_id}')")
|
|
self._is_connected.set()
|
|
else:
|
|
self.logger.warning("Connected to Meshtastic, but failed to retrieve node info. Loopback detection unreliable.")
|
|
self._is_connected.set()
|
|
|
|
pub.subscribe(self._on_meshtastic_receive, "meshtastic.receive", weak=False)
|
|
self.logger.info("Meshtastic receive callback registered.")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error connecting to Meshtastic device {self.config.meshtastic_port}: {e}", exc_info=False)
|
|
if self.interface:
|
|
try: self.interface.close()
|
|
except Exception: pass
|
|
self.interface = None
|
|
self.my_node_id = None
|
|
self._is_connected.clear()
|
|
return False
|
|
|
|
def start_sender(self):
|
|
if self.sender_thread and self.sender_thread.is_alive():
|
|
return
|
|
self.logger.info("Starting Meshtastic sender thread...")
|
|
self.sender_thread = threading.Thread(target=self._meshtastic_sender_loop, daemon=True, name="MeshtasticSender")
|
|
self.sender_thread.start()
|
|
|
|
def stop(self):
|
|
self.logger.info("Stopping Meshtastic handler...")
|
|
try:
|
|
pub.unsubscribe(self._on_meshtastic_receive, "meshtastic.receive")
|
|
except Exception: pass
|
|
|
|
with self._lock:
|
|
if self.interface:
|
|
try:
|
|
self.interface.close()
|
|
except Exception as e:
|
|
self.logger.error(f"Error closing Meshtastic interface: {e}")
|
|
finally:
|
|
self.interface = None
|
|
self.my_node_id = None
|
|
self._is_connected.clear()
|
|
|
|
if self.sender_thread and self.sender_thread.is_alive():
|
|
self.sender_thread.join(timeout=5)
|
|
|
|
self.logger.info("Meshtastic handler stopped.")
|
|
|
|
def _on_meshtastic_receive(self, packet: Dict[str, Any], interface: Any):
|
|
try:
|
|
if not packet or 'from' not in packet:
|
|
return
|
|
|
|
sender_id_num = packet.get('from')
|
|
sender_id_hex = f"!{sender_id_num:x}" if isinstance(sender_id_num, int) else "UNKNOWN"
|
|
portnum = packet.get('decoded', {}).get('portnum', 'UNKNOWN')
|
|
payload_bytes = packet.get('decoded', {}).get('payload')
|
|
|
|
# Loopback Prevention
|
|
bridge_id_lower = self.config.bridge_node_id.lower() if self.config.bridge_node_id else None
|
|
my_node_id_lower = self.my_node_id.lower() if self.my_node_id else None
|
|
sender_id_lower = sender_id_hex.lower()
|
|
|
|
if (bridge_id_lower and sender_id_lower == bridge_id_lower) or \
|
|
(my_node_id_lower and sender_id_lower == my_node_id_lower):
|
|
return
|
|
|
|
portnum_str = str(portnum) if portnum else 'UNKNOWN'
|
|
translated_payload = None
|
|
message_type = "meshtastic_message"
|
|
|
|
if portnum_str == 'TEXT_MESSAGE_APP' and payload_bytes:
|
|
try:
|
|
text_payload = payload_bytes.decode('utf-8', errors='replace')
|
|
self.logger.info(f"Meshtastic RX <{portnum_str}> From {sender_id_hex}: '{text_payload}'")
|
|
translated_payload = text_payload
|
|
except UnicodeDecodeError:
|
|
translated_payload = repr(payload_bytes)
|
|
|
|
elif portnum_str == 'POSITION_APP':
|
|
pos_data = packet.get('decoded', {}).get('position', {})
|
|
translated_payload = {
|
|
"latitude": pos_data.get('latitude'),
|
|
"longitude": pos_data.get('longitude'),
|
|
"altitude": pos_data.get('altitude'),
|
|
"timestamp_gps": pos_data.get('time')
|
|
}
|
|
message_type = "meshtastic_position"
|
|
|
|
else:
|
|
return
|
|
|
|
if translated_payload is not None:
|
|
external_message = {
|
|
"type": message_type,
|
|
"sender_meshtastic_id": sender_id_hex,
|
|
"portnum": portnum_str,
|
|
"payload": translated_payload,
|
|
"timestamp_rx": time.time(),
|
|
"rx_rssi": packet.get('rxRssi'),
|
|
"rx_snr": packet.get('rxSnr'),
|
|
}
|
|
|
|
try:
|
|
self.to_external_queue.put_nowait(external_message)
|
|
self.logger.debug(f"Queued message from {sender_id_hex} for external handler.")
|
|
except Full:
|
|
self.logger.warning("External handler send queue is full.")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in _on_meshtastic_receive callback: {e}", exc_info=True)
|
|
|
|
def _meshtastic_sender_loop(self):
|
|
self.logger.info("Meshtastic sender loop started.")
|
|
while not self.shutdown_event.is_set():
|
|
try:
|
|
item: Optional[Dict[str, Any]] = self.to_meshtastic_queue.get(timeout=1)
|
|
if not item: continue
|
|
|
|
if not self._is_connected.is_set():
|
|
self.to_meshtastic_queue.task_done()
|
|
time.sleep(self.RECONNECT_DELAY_S / 2)
|
|
continue
|
|
|
|
destination = item.get('destination')
|
|
text_to_send = item.get('text')
|
|
channel_index = item.get('channel_index', 0)
|
|
want_ack = item.get('want_ack', False)
|
|
|
|
if destination and isinstance(text_to_send, str):
|
|
log_payload = (text_to_send[:100] + '...') if len(text_to_send) > 100 else text_to_send
|
|
self.logger.info(f"Meshtastic TX -> Dest: {destination}, Payload: '{log_payload}'")
|
|
|
|
send_success = False
|
|
with self._lock:
|
|
if self.interface and self._is_connected.is_set():
|
|
try:
|
|
self.interface.sendText(
|
|
text=text_to_send,
|
|
destinationId=destination,
|
|
channelIndex=channel_index,
|
|
wantAck=want_ack
|
|
)
|
|
send_success = True
|
|
except Exception as e:
|
|
self.logger.error(f"Error sending Meshtastic message: {e}")
|
|
if "Not connected" in str(e): self._is_connected.clear()
|
|
else:
|
|
self._is_connected.clear()
|
|
|
|
self.to_meshtastic_queue.task_done()
|
|
else:
|
|
self.to_meshtastic_queue.task_done()
|
|
|
|
except Empty:
|
|
if not self._is_connected.is_set(): time.sleep(self.RECONNECT_DELAY_S)
|
|
continue
|
|
except Exception as e:
|
|
self.logger.error(f"Critical error in meshtastic_sender_loop: {e}", exc_info=True)
|
|
self._is_connected.clear()
|
|
time.sleep(5)
|
|
|
|
self.logger.info("Meshtastic sender loop stopped.")
|