Files
Akita-Meshtastic-Meshcore-B…/ammb/meshtastic_handler.py
2025-11-18 19:32:37 -05:00

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.")