mirror of
https://github.com/AkitaEngineering/Akita-Meshtastic-Meshcore-Bridge.git
synced 2026-03-28 17:42:42 +01:00
261 lines
12 KiB
Python
261 lines
12 KiB
Python
# ammb/meshcore_handler.py
|
|
"""
|
|
Handles interactions with an external device via a **Serial** port.
|
|
"""
|
|
|
|
import logging
|
|
import threading
|
|
import time
|
|
import json
|
|
from queue import Queue, Empty, Full
|
|
from typing import Optional, Dict, Any
|
|
import serial
|
|
|
|
from .config_handler import BridgeConfig
|
|
from .protocol import MeshcoreProtocolHandler, get_serial_protocol_handler
|
|
|
|
class MeshcoreHandler:
|
|
"""Manages Serial connection and communication with an external device."""
|
|
RECONNECT_DELAY_S = 10
|
|
|
|
def __init__(self, config: BridgeConfig, to_meshtastic_queue: Queue, from_meshtastic_queue: Queue, shutdown_event: threading.Event):
|
|
self.logger = logging.getLogger(__name__)
|
|
self.config = config
|
|
self.to_meshtastic_queue = to_meshtastic_queue
|
|
self.to_serial_queue = from_meshtastic_queue
|
|
self.shutdown_event = shutdown_event
|
|
|
|
self.serial_port: Optional[serial.Serial] = None
|
|
self.receiver_thread: Optional[threading.Thread] = None
|
|
self.sender_thread: Optional[threading.Thread] = None
|
|
self._lock = threading.Lock()
|
|
self._is_connected = threading.Event()
|
|
|
|
if not config.serial_port or not config.serial_baud or not config.serial_protocol:
|
|
raise ValueError("Serial transport selected, but required SERIAL configuration options are missing.")
|
|
|
|
try:
|
|
self.protocol_handler: MeshcoreProtocolHandler = get_serial_protocol_handler(config.serial_protocol)
|
|
except ValueError as e:
|
|
self.logger.critical(f"Failed to initialize serial protocol handler '{config.serial_protocol}': {e}.")
|
|
class DummyHandler(MeshcoreProtocolHandler):
|
|
def read(self, port): return None
|
|
def encode(self, data): return None
|
|
def decode(self, line): return None
|
|
self.protocol_handler = DummyHandler()
|
|
|
|
self.logger.info("Serial Handler (MeshcoreHandler) Initialized.")
|
|
|
|
|
|
def connect(self) -> bool:
|
|
with self._lock:
|
|
if self.serial_port and self.serial_port.is_open:
|
|
self.logger.info(f"Serial port {self.config.serial_port} already connected.")
|
|
self._is_connected.set()
|
|
return True
|
|
|
|
try:
|
|
self.logger.info(f"Attempting connection to Serial device on {self.config.serial_port} at {self.config.serial_baud} baud...")
|
|
self._is_connected.clear()
|
|
if self.serial_port:
|
|
try:
|
|
self.serial_port.close()
|
|
except Exception: pass
|
|
|
|
self.serial_port = serial.Serial(
|
|
port=self.config.serial_port,
|
|
baudrate=self.config.serial_baud,
|
|
timeout=1,
|
|
)
|
|
if self.serial_port.is_open:
|
|
self.logger.info(f"Connected to Serial device on {self.config.serial_port}")
|
|
self._is_connected.set()
|
|
return True
|
|
else:
|
|
self.logger.error(f"Failed to open serial port {self.config.serial_port}, but no exception was raised.")
|
|
self.serial_port = None
|
|
self._is_connected.clear()
|
|
return False
|
|
|
|
except serial.SerialException as e:
|
|
self.logger.error(f"Serial error connecting to device {self.config.serial_port}: {e}")
|
|
self.serial_port = None
|
|
self._is_connected.clear()
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error connecting to serial device: {e}", exc_info=True)
|
|
self.serial_port = None
|
|
self._is_connected.clear()
|
|
return False
|
|
|
|
def start_threads(self):
|
|
if self.receiver_thread and self.receiver_thread.is_alive():
|
|
self.logger.warning("Serial receiver thread already started.")
|
|
else:
|
|
self.logger.info("Starting Serial receiver thread...")
|
|
self.receiver_thread = threading.Thread(target=self._serial_receiver_loop, daemon=True, name="SerialReceiver")
|
|
self.receiver_thread.start()
|
|
|
|
if self.sender_thread and self.sender_thread.is_alive():
|
|
self.logger.warning("Serial sender thread already started.")
|
|
else:
|
|
self.logger.info("Starting Serial sender thread...")
|
|
self.sender_thread = threading.Thread(target=self._serial_sender_loop, daemon=True, name="SerialSender")
|
|
self.sender_thread.start()
|
|
|
|
def stop(self):
|
|
self.logger.info("Stopping Serial handler...")
|
|
if self.receiver_thread and self.receiver_thread.is_alive():
|
|
self.receiver_thread.join(timeout=2)
|
|
if self.sender_thread and self.sender_thread.is_alive():
|
|
self.sender_thread.join(timeout=5)
|
|
self._close_serial()
|
|
self.logger.info("Serial handler stopped.")
|
|
|
|
def _close_serial(self):
|
|
with self._lock:
|
|
if self.serial_port and self.serial_port.is_open:
|
|
port_name = self.config.serial_port
|
|
try:
|
|
self.serial_port.close()
|
|
self.logger.info(f"Serial port {port_name} closed.")
|
|
except Exception as e:
|
|
self.logger.error(f"Error closing serial port {port_name}: {e}", exc_info=True)
|
|
finally:
|
|
self.serial_port = None
|
|
self._is_connected.clear()
|
|
|
|
def _serial_receiver_loop(self):
|
|
"""Continuously reads from serial using Protocol Handler, translates, and queues."""
|
|
self.logger.info("Serial receiver loop started.")
|
|
while not self.shutdown_event.is_set():
|
|
# --- Connection Check ---
|
|
if not self._is_connected.is_set():
|
|
self.logger.warning(f"Serial port {self.config.serial_port} not connected. Attempting reconnect...")
|
|
if self.connect():
|
|
self.logger.info(f"Serial device reconnected successfully on {self.config.serial_port}.")
|
|
else:
|
|
self.shutdown_event.wait(self.RECONNECT_DELAY_S)
|
|
continue
|
|
|
|
# --- Read and Process Data ---
|
|
try:
|
|
raw_data: Optional[bytes] = None
|
|
with self._lock:
|
|
if self.serial_port and self.serial_port.is_open:
|
|
# Delegate reading to protocol handler
|
|
raw_data = self.protocol_handler.read(self.serial_port)
|
|
else:
|
|
self._is_connected.clear()
|
|
continue
|
|
|
|
if raw_data:
|
|
self.logger.debug(f"Serial RAW RX: {raw_data!r}")
|
|
|
|
# Decode using the selected protocol handler
|
|
decoded_msg: Optional[Dict[str, Any]] = self.protocol_handler.decode(raw_data)
|
|
|
|
if decoded_msg:
|
|
# Basic Translation Logic (Serial -> Meshtastic)
|
|
dest_meshtastic_id = decoded_msg.get("destination_meshtastic_id")
|
|
payload = decoded_msg.get("payload")
|
|
payload_json = decoded_msg.get("payload_json")
|
|
channel_index = decoded_msg.get("channel_index", 0)
|
|
want_ack = decoded_msg.get("want_ack", False)
|
|
|
|
text_payload_str: Optional[str] = None
|
|
if isinstance(payload, str):
|
|
text_payload_str = payload
|
|
elif payload_json is not None:
|
|
try:
|
|
text_payload_str = json.dumps(payload_json)
|
|
except (TypeError, ValueError) as e:
|
|
self.logger.error(f"Failed to serialize payload_json: {e}")
|
|
elif payload is not None:
|
|
text_payload_str = str(payload)
|
|
|
|
if dest_meshtastic_id and text_payload_str is not None:
|
|
meshtastic_msg = {
|
|
"destination": dest_meshtastic_id,
|
|
"text": text_payload_str,
|
|
"channel_index": channel_index,
|
|
"want_ack": want_ack,
|
|
}
|
|
|
|
try:
|
|
self.to_meshtastic_queue.put_nowait(meshtastic_msg)
|
|
self.logger.info(f"Queued message from Serial for Meshtastic node {dest_meshtastic_id}")
|
|
except Full:
|
|
self.logger.warning("Meshtastic send queue is full. Dropping incoming message from Serial.")
|
|
else:
|
|
self.logger.warning(f"Serial RX: Decoded message lacks required fields: {decoded_msg}")
|
|
else:
|
|
# No data available from serial, sleep briefly to prevent CPU spin
|
|
time.sleep(0.1)
|
|
|
|
except serial.SerialException as e:
|
|
self.logger.error(f"Serial error in receiver loop ({self.config.serial_port}): {e}. Attempting to reconnect...")
|
|
self._close_serial()
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error in serial_receiver_loop: {e}", exc_info=True)
|
|
self._close_serial()
|
|
time.sleep(self.RECONNECT_DELAY_S / 2)
|
|
|
|
self.logger.info("Serial receiver loop stopped.")
|
|
|
|
|
|
def _serial_sender_loop(self):
|
|
"""Continuously reads from the queue, encodes, and sends messages via Serial."""
|
|
self.logger.info("Serial sender loop started.")
|
|
while not self.shutdown_event.is_set():
|
|
if not self._is_connected.is_set():
|
|
time.sleep(self.RECONNECT_DELAY_S / 2)
|
|
continue
|
|
|
|
try:
|
|
item: Optional[Dict[str, Any]] = self.to_serial_queue.get(timeout=1)
|
|
if not item:
|
|
continue
|
|
|
|
encoded_message: Optional[bytes] = self.protocol_handler.encode(item)
|
|
|
|
if encoded_message:
|
|
# Truncate log for binary safety
|
|
log_preview = repr(encoded_message[:50])
|
|
self.logger.info(f"Serial TX -> Port: {self.config.serial_port}, Payload: {log_preview}")
|
|
|
|
send_success = False
|
|
with self._lock:
|
|
if self.serial_port and self.serial_port.is_open:
|
|
try:
|
|
self.serial_port.write(encoded_message)
|
|
self.serial_port.flush()
|
|
send_success = True
|
|
except serial.SerialException as e:
|
|
self.logger.error(f"Serial error during send ({self.config.serial_port}): {e}.")
|
|
self._close_serial()
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error sending Serial message: {e}", exc_info=True)
|
|
else:
|
|
self.logger.warning(f"Serial port disconnected just before send attempt.")
|
|
self._is_connected.clear()
|
|
|
|
if send_success:
|
|
self.to_serial_queue.task_done()
|
|
else:
|
|
self.logger.error("Failed to send Serial message. Discarding.")
|
|
self.to_serial_queue.task_done()
|
|
else:
|
|
self.logger.error(f"Failed to encode message for Serial: {item}")
|
|
self.to_serial_queue.task_done()
|
|
|
|
except Empty:
|
|
continue
|
|
except Exception as e:
|
|
self.logger.error(f"Critical error in serial_sender_loop: {e}", exc_info=True)
|
|
self._is_connected.clear()
|
|
time.sleep(5)
|
|
|
|
self.logger.info("Serial sender loop stopped.")
|