Compare commits

...

6 Commits

Author SHA1 Message Date
pdxlocations
1ecf441cd8 don't close interfact after region 2025-02-25 21:29:23 -08:00
pdxlocations
7e67c41c4c longer test delay 2025-02-25 10:16:29 -08:00
pdxlocations
d869e316b8 suppress printing errors to the terminal 2025-02-24 22:23:47 -08:00
pdxlocations
0142127564 catch sending message with no connection 2025-02-24 22:11:32 -08:00
pdxlocations
df8ceed3da fixes and cleanup 2025-02-24 22:06:05 -08:00
pdxlocations
20a9e11d24 initial commit 2025-02-24 21:49:55 -08:00
9 changed files with 162 additions and 55 deletions

28
main.py
View File

@@ -9,9 +9,11 @@ V 1.2.1
import curses import curses
from pubsub import pub from pubsub import pub
import os import os
import contextlib
import logging import logging
import traceback import traceback
import threading import threading
import asyncio
from utilities.arg_parser import setup_parser from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface from utilities.interfaces import initialize_interface
@@ -19,6 +21,7 @@ from message_handlers.rx_handler import on_receive
from ui.curses_ui import main_ui, draw_splash from ui.curses_ui import main_ui, draw_splash
from input_handlers import get_list_input from input_handlers import get_list_input
from utilities.utils import get_channels, get_node_list, get_nodeNum from utilities.utils import get_channels, get_node_list, get_nodeNum
from utilities.watchdog import watchdog
from settings import set_region from settings import set_region
from db_handler import init_nodedb, load_messages_from_db from db_handler import init_nodedb, load_messages_from_db
import default_config as config import default_config as config
@@ -37,7 +40,7 @@ if os.environ.get("COLORTERM") == "gnome-terminal":
# Run `tail -f client.log` in another terminal to view live # Run `tail -f client.log` in another terminal to view live
logging.basicConfig( logging.basicConfig(
filename=config.log_file_path, 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" format="%(asctime)s - %(levelname)s - %(message)s"
) )
@@ -50,14 +53,20 @@ def main(stdscr):
args = parser.parse_args() args = parser.parse_args()
logging.info("Initializing interface %s", args) logging.info("Initializing interface %s", args)
with globals.lock: with globals.lock:
globals.interface = initialize_interface(args) 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: 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": if confirmation == "Yes":
set_region() set_region()
globals.interface.close() globals.interface = None
globals.interface = initialize_interface(args) globals.interface = initialize_interface(args)
logging.info("Interface initialized") logging.info("Interface initialized")
globals.myNodeNum = get_nodeNum() globals.myNodeNum = get_nodeNum()
globals.channel_list = get_channels() globals.channel_list = get_channels()
@@ -74,8 +83,9 @@ def main(stdscr):
raise raise
if __name__ == "__main__": if __name__ == "__main__":
try: with open(os.devnull, 'w') as fnull, contextlib.redirect_stderr(fnull), contextlib.redirect_stdout(fnull):
curses.wrapper(main) try:
except Exception as e: curses.wrapper(main)
logging.error("Fatal error in curses wrapper: %s", e) except Exception as e:
logging.error("Traceback: %s", traceback.format_exc()) logging.error("Fatal error in curses wrapper: %s", e)
logging.error("Traceback: %s", traceback.format_exc())

View File

@@ -1,5 +1,7 @@
import logging import logging
import time import time
from datetime import datetime
from utilities.utils import refresh_node_list from utilities.utils import refresh_node_list
from datetime import datetime from datetime import datetime
from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification
@@ -8,8 +10,6 @@ import default_config as config
import globals import globals
from datetime import datetime
def on_receive(packet, interface): def on_receive(packet, interface):
with globals.lock: with globals.lock:

View File

@@ -2,6 +2,7 @@ from datetime import datetime
import google.protobuf.json_format import google.protobuf.json_format
from meshtastic import BROADCAST_NUM from meshtastic import BROADCAST_NUM
from meshtastic.protobuf import mesh_pb2, portnums_pb2 from meshtastic.protobuf import mesh_pb2, portnums_pb2
import logging
from db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db from db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db
import default_config as config import default_config as config
@@ -119,55 +120,64 @@ def on_response_traceroute(packet):
def send_message(message, destination=BROADCAST_NUM, channel=0): def send_message(message, destination=BROADCAST_NUM, channel=0):
myid = globals.myNodeNum # Check if the interface is initialized and connected
send_on_channel = 0 if not globals.interface or not getattr(globals.interface, 'isConnected', False):
channel_id = globals.channel_list[channel] logging.error("Cannot send message: No active connection to Meshtastic device.")
if isinstance(channel_id, int): return # Or raise an exception if you prefer
try:
myid = globals.myNodeNum
send_on_channel = 0 send_on_channel = 0
destination = channel_id channel_id = globals.channel_list[channel]
elif isinstance(channel_id, str): if isinstance(channel_id, int):
send_on_channel = channel send_on_channel = 0
destination = channel_id
elif isinstance(channel_id, str):
send_on_channel = channel
sent_message_data = globals.interface.sendText( # Attempt to send the message
text=message, sent_message_data = globals.interface.sendText(
destinationId=destination, text=message,
wantAck=True, destinationId=destination,
wantResponse=False, wantAck=True,
onResponse=onAckNak, wantResponse=False,
channelIndex=send_on_channel, onResponse=onAckNak,
) channelIndex=send_on_channel,
)
# Add sent message to the messages dictionary # Add sent message to the messages dictionary
if channel_id not in globals.all_messages: if channel_id not in globals.all_messages:
globals.all_messages[channel_id] = [] globals.all_messages[channel_id] = []
# Handle timestamp logic # Handle timestamp logic
current_timestamp = int(datetime.now().timestamp()) # Get current timestamp current_timestamp = int(datetime.now().timestamp())
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00') current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
# Retrieve the last timestamp if available channel_messages = globals.all_messages[channel_id]
channel_messages = globals.all_messages[channel_id] last_hour = None
if channel_messages:
# Check the last entry for a timestamp
for entry in reversed(channel_messages): for entry in reversed(channel_messages):
if entry[0].startswith("--"): if entry[0].startswith("--"):
last_hour = entry[0].strip("- ").strip() last_hour = entry[0].strip("- ").strip()
break break
else:
last_hour = None
else:
last_hour = None
# Add a new timestamp if it's a new hour if last_hour != current_hour:
if last_hour != current_hour: globals.all_messages[channel_id].append((f"-- {current_hour} --", ""))
globals.all_messages[channel_id].append((f"-- {current_hour} --", ""))
globals.all_messages[channel_id].append((config.sent_message_prefix + config.ack_unknown_str + ": ", message)) globals.all_messages[channel_id].append((config.sent_message_prefix + config.ack_unknown_str + ": ", message))
timestamp = save_message_to_db(channel_id, myid, message) timestamp = save_message_to_db(channel_id, myid, message)
ack_naks[sent_message_data.id] = {'channel': channel_id, 'messageIndex': len(globals.all_messages[channel_id]) - 1, 'timestamp': timestamp} ack_naks[sent_message_data.id] = {
'channel': channel_id,
'messageIndex': len(globals.all_messages[channel_id]) - 1,
'timestamp': timestamp
}
except Exception as e:
# Catch any error and log it
logging.error(f"Failed to send message due to unexpected error: {e}", exc_info=True)
def send_traceroute(): def send_traceroute():
r = mesh_pb2.RouteDiscovery() r = mesh_pb2.RouteDiscovery()
globals.interface.sendData( globals.interface.sendData(

View File

@@ -1,7 +1,7 @@
from meshtastic.protobuf import channel_pb2
from google.protobuf.message import Message
import logging import logging
import base64 import base64
from google.protobuf.message import Message
from meshtastic.protobuf import channel_pb2
from db_handler import update_node_info_in_db from db_handler import update_node_info_in_db
import globals import globals

View File

@@ -1,7 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from meshtastic.protobuf import config_pb2, module_config_pb2, channel_pb2
import logging import logging
import base64 import base64
from meshtastic.protobuf import config_pb2, module_config_pb2, channel_pb2
def extract_fields(message_instance, current_config=None): def extract_fields(message_instance, current_config=None):

0
utilities/__init__.py Normal file
View File

View File

@@ -1,8 +1,8 @@
import yaml import yaml
import logging import logging
from typing import List from typing import List
from google.protobuf.json_format import MessageToDict from google.protobuf.json_format import MessageToDict
from meshtastic import BROADCAST_ADDR, mt_config from meshtastic import BROADCAST_ADDR, mt_config
from meshtastic.util import camel_to_snake, snake_to_camel, fromStr from meshtastic.util import camel_to_snake, snake_to_camel, fromStr

View File

@@ -1,7 +1,10 @@
import logging import logging
import contextlib
import io
import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface
import globals import globals
def initialize_interface(args): def initialize_interface(args):
try: try:
if args.ble: if args.ble:
@@ -10,14 +13,19 @@ def initialize_interface(args):
return meshtastic.tcp_interface.TCPInterface(args.host) return meshtastic.tcp_interface.TCPInterface(args.host)
else: else:
try: 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: except PermissionError as ex:
logging.error(f"You probably need to add yourself to the `dialout` group to use a serial connection. {ex}") logging.error(f"You probably need to add yourself to the `dialout` group to use a serial connection. {ex}")
except Exception as 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: if globals.interface.devPath is None:
return meshtastic.tcp_interface.TCPInterface("meshtastic.local") return meshtastic.tcp_interface.TCPInterface("meshtastic.local")
except Exception as ex: except Exception as ex:
logging.critical(f"Fatal error initializing interface: {ex}") logging.critical(f"Fatal error initializing interface: {ex}")

79
utilities/watchdog.py Normal file
View File

@@ -0,0 +1,79 @@
import asyncio
import io
import contextlib
import socket
import logging
from .interfaces import initialize_interface
import globals
test_connection_seconds = 20
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.warning(f"Error retrieving firmware: {e}")
raise e # Propagate the error to handle reconnection
# Async function to retry connection
async def retry_interface(args):
logging.warning("Retrying connection to the interface...")
await asyncio.sleep(retry_connection_seconds) # Wait before retrying
try:
globals.interface = initialize_interface(args)
if globals.interface and hasattr(globals.interface, 'localNode'):
logging.warning("Interface reinitialized successfully.")
return globals.interface
else:
logging.error("Failed to reinitialize interface: Missing localNode or invalid interface.")
globals.interface = None # Clear invalid interface
return None
except (ConnectionRefusedError, socket.error, Exception) as e:
logging.error(f"Failed to reinitialize interface: {e}")
globals.interface = None
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}")
globals.interface = None
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...")