Compare commits

..

18 Commits

Author SHA1 Message Date
pdxlocations
08f19e653a add note in draw_node_list 2025-02-09 20:46:55 -08:00
pdxlocations
6e05446d6a Merge branch 'main' into fix-startup-error 2025-02-09 06:45:53 -08:00
pdxlocations
a0cb4f9480 sir locks a lilttle less 2025-02-08 21:07:54 -08:00
pdxlocations
89d5890338 sir locks a lot 2025-02-08 21:04:51 -08:00
pdxlocations
a645e41146 lock it down 2025-02-08 18:07:12 -08:00
pdxlocations
aa736472cc fix conflict 2025-02-08 18:02:49 -08:00
pdxlocations
4dc1b4b791 try a threading lock 2025-02-08 18:02:25 -08:00
pdxlocations
41050577aa Merge branch 'main' into fix-startup-error 2025-02-08 15:57:46 -08:00
pdxlocations
d9c249af56 db snapshot 2025-02-08 14:21:44 -08:00
pdxlocations
ad64004e79 back up 2025-02-08 13:25:27 -08:00
pdxlocations
d9a84c4b29 more global 2025-02-08 13:19:48 -08:00
pdxlocations
6cf46a202a grasping at straws 2025-02-08 11:48:07 -08:00
pdxlocations
351f4d7f8f less is more 2025-02-08 08:38:35 -08:00
pdxlocations
5f0277c460 refactor 2025-02-08 08:29:57 -08:00
pdxlocations
f34db01a78 typo 2025-02-08 08:04:52 -08:00
pdxlocations
463b655684 more checks 2025-02-08 08:03:59 -08:00
pdxlocations
14aa5c00a3 none isn't better than nothing 2025-02-08 08:00:20 -08:00
pdxlocations
a201bcccb2 more excuses 2025-02-08 07:57:09 -08:00
12 changed files with 73 additions and 213 deletions

View File

@@ -1,5 +1,4 @@
interface = None
lock = None
display_log = False
all_messages = {}
channel_list = []
@@ -10,4 +9,5 @@ myNodeNum = 0
selected_channel = 0
selected_message = 0
selected_node = 0
current_window = 0
current_window = 0
lock = None

35
main.py
View File

@@ -3,26 +3,21 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.2.1
V 1.2.0
'''
import curses
from pubsub import pub
import os
import contextlib
import logging
import traceback
import threading
import asyncio
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
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
import globals
@@ -40,7 +35,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.WARNING, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
format="%(asctime)s - %(levelname)s - %(message)s"
)
@@ -53,20 +48,8 @@ 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"])
if confirmation == "Yes":
set_region()
globals.interface = None
globals.interface = initialize_interface(args)
logging.info("Interface initialized")
globals.myNodeNum = get_nodeNum()
globals.channel_list = get_channels()
@@ -75,7 +58,6 @@ def main(stdscr):
init_nodedb()
load_messages_from_db()
logging.info("Starting main UI")
main_ui(stdscr)
except Exception as e:
logging.error("An error occurred: %s", e)
@@ -83,9 +65,8 @@ def main(stdscr):
raise
if __name__ == "__main__":
with open(os.devnull, 'w') as fnull, contextlib.redirect_stderr(fnull), contextlib.redirect_stdout(fnull):
try:
curses.wrapper(main)
except Exception as e:
logging.error("Fatal error in curses wrapper: %s", e)
logging.error("Traceback: %s", traceback.format_exc())
try:
curses.wrapper(main)
except Exception as e:
logging.error("Fatal error in curses wrapper: %s", e)
logging.error("Traceback: %s", traceback.format_exc())

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
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
def save_changes(menu_path, modified_settings):
def save_changes(interface, menu_path, modified_settings):
"""
Save changes to the device based on modified settings.
:param interface: Meshtastic interface instance
@@ -17,7 +17,7 @@ def save_changes(menu_path, modified_settings):
logging.info("No changes to save. modified_settings is empty.")
return
node = globals.interface.getNode('^local')
node = interface.getNode('^local')
if menu_path[1] == "Radio Settings" or menu_path[1] == "Module Settings":
config_category = menu_path[2].lower() # for radio and module configs
@@ -27,7 +27,7 @@ def save_changes(menu_path, modified_settings):
lon = float(modified_settings.get('longitude', 0.0))
alt = int(modified_settings.get('altitude', 0))
globals.interface.localNode.setFixedPosition(lat, lon, alt)
interface.localNode.setFixedPosition(lat, lon, alt)
logging.info(f"Updated {config_category} with Latitude: {lat} and Longitude {lon} and Altitude {alt}")
return
@@ -122,5 +122,10 @@ def save_changes(menu_path, modified_settings):
except Exception as e:
logging.error(f"Failed to write configuration for category '{config_category}': {e}")
node.writeConfig(config_category)
logging.info(f"Changes written to config category: {config_category}")
except Exception as e:
logging.error(f"Error saving changes: {e}")
logging.error(f"Error saving changes: {e}")

View File

@@ -9,7 +9,6 @@ from ui.menus import generate_menu_from_protobuf
from ui.colors import setup_colors, get_color
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
from ui.dialog import dialog
from user_config import json_editor
import globals
@@ -152,7 +151,7 @@ def settings_menu(stdscr, interface):
menu_win.erase()
menu_win.refresh()
if show_save_option and selected_index == len(options):
save_changes(menu_path, modified_settings)
save_changes(interface, menu_path, modified_settings)
modified_settings.clear()
logging.info("Changes Saved")
@@ -189,7 +188,7 @@ def settings_menu(stdscr, interface):
if os.path.exists(yaml_file_path):
overwrite = get_list_input(f"{filename} already exists. Overwrite?", None, ["Yes", "No"])
if overwrite == "No":
if overwrite == "Yes":
logging.info("Export cancelled: User chose not to overwrite.")
continue # Return to menu
os.makedirs(os.path.dirname(yaml_file_path), exist_ok=True)
@@ -349,25 +348,6 @@ def settings_menu(stdscr, interface):
menu_win.refresh()
break
def set_region():
node = globals.interface.getNode('^local')
device_config = node.localConfig
lora_descriptor = device_config.lora.DESCRIPTOR
# Get the enum mapping of region names to their numerical values
region_enum = lora_descriptor.fields_by_name["region"].enum_type
region_name_to_number = {v.name: v.number for v in region_enum.values}
regions = list(region_name_to_number.keys())
new_region_name = get_list_input('Select your region:', 'UNSET', regions)
# Convert region name to corresponding enum number
new_region_number = region_name_to_number.get(new_region_name, 0) # Default to 0 if not found
node.localConfig.lora.region = new_region_number
node.writeConfig("lora")
def main(stdscr):
logging.basicConfig( # Run `tail -f client.log` in another terminal to view live

View File

@@ -74,6 +74,7 @@ def handle_resize(stdscr, firstrun):
win.box()
win.refresh()
entry_win.keypad(True)
curses.curs_set(1)
@@ -89,12 +90,13 @@ def handle_resize(stdscr, firstrun):
def main_ui(stdscr):
global input_text
input_text = ""
stdscr.keypad(True)
get_channels()
handle_resize(stdscr, True)
input_text = ""
while True:
draw_text_field(entry_win, f"Input: {input_text[-(stdscr.getmaxyx()[1] - 10):]}", get_color("input"))
@@ -420,7 +422,7 @@ def draw_node_list():
# nodes_pad = curses.newpad(1, 1)
nodes_pad = curses.newpad(1, 1)
try:
nodes_pad.erase()
box_width = nodes_win.getmaxyx()[1]
@@ -442,11 +444,6 @@ def draw_node_list():
refresh_pad(2)
# Restore cursor to input field
entry_win.move(1, len("Input: ") + len(input_text)+1)
entry_win.refresh()
curses.curs_set(1)
def select_channel(idx):
old_selected_channel = globals.selected_channel
globals.selected_channel = max(0, min(idx, len(globals.channel_list) - 1))
@@ -543,11 +540,6 @@ def draw_packetlog_win():
packetlog_win.box()
packetlog_win.refresh()
# Restore cursor to input field
entry_win.move(1, len("Input: ") + len(input_text)+1)
entry_win.refresh()
curses.curs_set(1)
def search(win):
start_idx = globals.selected_node
select_func = select_node

View File

@@ -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):
@@ -14,8 +14,7 @@ def extract_fields(message_instance, current_config=None):
menu = {}
fields = message_instance.DESCRIPTOR.fields
for field in fields:
skip_fields = {"sessionkey", "ChannelSettings.channel_num", "ChannelSettings.id", "LoRaConfig.ignore_incoming"}
if any(skip_field in field.full_name for skip_field in skip_fields):
if field.name in {"sessionkey", "channel_num", "id", "ignore_incoming"}: # Skip certain fields
continue
if field.message_type: # Nested message

View File

View File

@@ -1,8 +1,8 @@
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

View File

@@ -1,10 +1,7 @@
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:
@@ -13,19 +10,14 @@ def initialize_interface(args):
return meshtastic.tcp_interface.TCPInterface(args.host)
else:
try:
# 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)
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:
# 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
logging.error(f"Unexpected error initializing interface: {ex}")
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}")
logging.critical(f"Fatal error initializing interface: {ex}")

View File

@@ -1,79 +0,0 @@
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...")