diff --git a/main.py b/main.py index c39c3af..75615a9 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ import os import logging import traceback import threading +import asyncio from utilities.arg_parser import setup_parser from utilities.interfaces import initialize_interface @@ -19,6 +20,7 @@ from message_handlers.rx_handler import on_receive from ui.curses_ui import main_ui, draw_splash from input_handlers import get_list_input from utilities.utils import get_channels, get_node_list, get_nodeNum +from utilities.watchdog import watchdog from settings import set_region from db_handler import init_nodedb, load_messages_from_db import default_config as config @@ -37,7 +39,7 @@ if os.environ.get("COLORTERM") == "gnome-terminal": # Run `tail -f client.log` in another terminal to view live logging.basicConfig( filename=config.log_file_path, - level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL) + level=logging.WARNING, # DEBUG, INFO, WARNING, ERROR, CRITICAL) format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -50,14 +52,20 @@ def main(stdscr): args = parser.parse_args() logging.info("Initializing interface %s", args) - with globals.lock: + with globals.lock: globals.interface = initialize_interface(args) + + # Run watchdog in a separate thread + threading.Thread(target=lambda: asyncio.run(watchdog(args)), daemon=True).start() + + # Continue with the rest of the initialization if globals.interface.localNode.localConfig.lora.region == 0: - confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"]) + confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"]) if confirmation == "Yes": set_region() globals.interface.close() globals.interface = initialize_interface(args) + logging.info("Interface initialized") globals.myNodeNum = get_nodeNum() globals.channel_list = get_channels() diff --git a/message_handlers/tx_handler.py b/message_handlers/tx_handler.py index c9b545d..4bc0552 100644 --- a/message_handlers/tx_handler.py +++ b/message_handlers/tx_handler.py @@ -1,6 +1,7 @@ from datetime import datetime import google.protobuf.json_format from meshtastic import BROADCAST_NUM + from meshtastic.protobuf import mesh_pb2, portnums_pb2 from db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db diff --git a/save_to_radio.py b/save_to_radio.py index 3fc7464..6718a94 100644 --- a/save_to_radio.py +++ b/save_to_radio.py @@ -1,7 +1,7 @@ -from meshtastic.protobuf import channel_pb2 -from google.protobuf.message import Message import logging import base64 +from google.protobuf.message import Message +from meshtastic.protobuf import channel_pb2 from db_handler import update_node_info_in_db import globals diff --git a/ui/menus.py b/ui/menus.py index 6596a4e..99b7f19 100644 --- a/ui/menus.py +++ b/ui/menus.py @@ -1,7 +1,7 @@ from collections import OrderedDict -from meshtastic.protobuf import config_pb2, module_config_pb2, channel_pb2 import logging import base64 +from meshtastic.protobuf import config_pb2, module_config_pb2, channel_pb2 def extract_fields(message_instance, current_config=None): diff --git a/utilities/__init__.py b/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utilities/config_io.py b/utilities/config_io.py index 3f54ed4..3fd52a5 100644 --- a/utilities/config_io.py +++ b/utilities/config_io.py @@ -3,6 +3,7 @@ import yaml import logging from typing import List from google.protobuf.json_format import MessageToDict + from meshtastic import BROADCAST_ADDR, mt_config from meshtastic.util import camel_to_snake, snake_to_camel, fromStr diff --git a/utilities/interfaces.py b/utilities/interfaces.py index 66c6e7b..d88b5bf 100644 --- a/utilities/interfaces.py +++ b/utilities/interfaces.py @@ -1,7 +1,10 @@ import logging +import contextlib +import io import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface import globals + def initialize_interface(args): try: if args.ble: @@ -10,14 +13,19 @@ def initialize_interface(args): return meshtastic.tcp_interface.TCPInterface(args.host) else: try: - return meshtastic.serial_interface.SerialInterface(args.port) + # Suppress stdout and stderr during SerialInterface initialization + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + return meshtastic.serial_interface.SerialInterface(args.port) except PermissionError as ex: logging.error(f"You probably need to add yourself to the `dialout` group to use a serial connection. {ex}") except Exception as ex: - logging.error(f"Unexpected error initializing interface: {ex}") + # Suppress specific message but log unexpected errors + if "No Serial Meshtastic device detected" not in str(ex): + logging.error(f"Unexpected error initializing interface: {ex}") + + # Attempt TCP connection if Serial fails if globals.interface.devPath is None: return meshtastic.tcp_interface.TCPInterface("meshtastic.local") - + except Exception as ex: - logging.critical(f"Fatal error initializing interface: {ex}") - \ No newline at end of file + logging.critical(f"Fatal error initializing interface: {ex}") \ No newline at end of file diff --git a/utilities/watchdog.py b/utilities/watchdog.py new file mode 100644 index 0000000..9018fc1 --- /dev/null +++ b/utilities/watchdog.py @@ -0,0 +1,75 @@ +import asyncio +import io +import contextlib +import socket +import logging +import logging + +from .interfaces import initialize_interface +import globals + + +test_connection_seconds = 10 +retry_connection_seconds = 3 + +# Function to get firmware version +def getNodeFirmware(interface): + try: + output_capture = io.StringIO() + with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture): + interface.localNode.getMetadata() + + console_output = output_capture.getvalue() + + if "firmware_version" in console_output: + return console_output.split("firmware_version: ")[1].split("\n")[0] + + return -1 + except (socket.error, BrokenPipeError, ConnectionResetError, Exception) as e: + logging.info(f"Error retrieving firmware: {e}") + raise e # Propagate the error to handle reconnection + +# Async function to retry connection +async def retry_interface(args): + logging.info("Retrying connection to the interface...") + await asyncio.sleep(retry_connection_seconds) # Wait before retrying + + try: + globals.interface = initialize_interface(args) + + logging.info("Interface reinitialized successfully.") + return globals.interface + except (ConnectionRefusedError, socket.error, Exception) as e: + logging.error(f"Failed to reinitialize interface: {e}") + return None + +# Function to check connection and reconnect if needed +async def check_and_reconnect(args): + if globals.interface is None: + logging.error("No valid interface. Attempting to reconnect...") + interface = await retry_interface(args) + return interface + + try: + # logging.info("Checking interface connection...") + fw_ver = getNodeFirmware(globals.interface) + if fw_ver != -1: + return globals.interface + else: + raise Exception("Failed to retrieve firmware version.") + + except (socket.error, BrokenPipeError, ConnectionResetError, Exception) as e: + logging.error(f"Error with the interface, setting to None and attempting reconnect: {e}") + return await retry_interface(args) + +# Main watchdog loop +async def watchdog(args): + while True: # Infinite loop for continuous monitoring + await asyncio.sleep(test_connection_seconds) + globals.interface = await check_and_reconnect(args) + if globals.interface: + pass # Interface is connected + else: + logging.error("Interface connection failed. Retrying...") + +