mirror of
https://github.com/pdxlocations/contact.git
synced 2026-05-14 13:15:37 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ecf441cd8 | |||
| 7e67c41c4c | |||
| d869e316b8 | |||
| 0142127564 | |||
| df8ceed3da | |||
| 20a9e11d24 | |||
| aa8a66ef22 | |||
| 498be2c859 | |||
| b086125962 |
@@ -9,9 +9,11 @@ V 1.2.1
|
||||
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
|
||||
@@ -19,6 +21,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 +40,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 +53,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 = None
|
||||
globals.interface = initialize_interface(args)
|
||||
|
||||
logging.info("Interface initialized")
|
||||
globals.myNodeNum = get_nodeNum()
|
||||
globals.channel_list = get_channels()
|
||||
@@ -74,8 +83,9 @@ def main(stdscr):
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
curses.wrapper(main)
|
||||
except Exception as e:
|
||||
logging.error("Fatal error in curses wrapper: %s", e)
|
||||
logging.error("Traceback: %s", traceback.format_exc())
|
||||
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())
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
@@ -8,8 +10,6 @@ import default_config as config
|
||||
import globals
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
def on_receive(packet, interface):
|
||||
|
||||
with globals.lock:
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -119,55 +120,64 @@ def on_response_traceroute(packet):
|
||||
|
||||
|
||||
def send_message(message, destination=BROADCAST_NUM, channel=0):
|
||||
myid = globals.myNodeNum
|
||||
send_on_channel = 0
|
||||
channel_id = globals.channel_list[channel]
|
||||
if isinstance(channel_id, int):
|
||||
# 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
|
||||
send_on_channel = 0
|
||||
destination = channel_id
|
||||
elif isinstance(channel_id, str):
|
||||
send_on_channel = channel
|
||||
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
|
||||
|
||||
sent_message_data = globals.interface.sendText(
|
||||
text=message,
|
||||
destinationId=destination,
|
||||
wantAck=True,
|
||||
wantResponse=False,
|
||||
onResponse=onAckNak,
|
||||
channelIndex=send_on_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,
|
||||
)
|
||||
|
||||
# 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()) # Get current timestamp
|
||||
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
|
||||
# Handle timestamp logic
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
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]
|
||||
if channel_messages:
|
||||
# Check the last entry for a timestamp
|
||||
channel_messages = globals.all_messages[channel_id]
|
||||
last_hour = None
|
||||
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
|
||||
|
||||
# Add a new timestamp if it's a new hour
|
||||
if last_hour != current_hour:
|
||||
globals.all_messages[channel_id].append((f"-- {current_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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -189,7 +189,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 == "Yes":
|
||||
if overwrite == "No":
|
||||
logging.info("Export cancelled: User chose not to overwrite.")
|
||||
continue # Return to menu
|
||||
os.makedirs(os.path.dirname(yaml_file_path), exist_ok=True)
|
||||
|
||||
+19
-56
@@ -1,62 +1,43 @@
|
||||
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
|
||||
|
||||
|
||||
FIELD_EXCLUSION_MAP = {
|
||||
"global": {"sessionkey"},
|
||||
"channel": {"channel_num", "id"},
|
||||
"radio": {"ignore_incoming"},
|
||||
"module": None
|
||||
}
|
||||
|
||||
def extract_fields(message_instance, current_config=None, parent_context=None, exclusion_map=None):
|
||||
def extract_fields(message_instance, current_config=None):
|
||||
if isinstance(current_config, dict): # Handle dictionaries
|
||||
return {key: (None, current_config.get(key, "Not Set")) for key in current_config}
|
||||
|
||||
if not hasattr(message_instance, "DESCRIPTOR"):
|
||||
return {}
|
||||
|
||||
if exclusion_map is None:
|
||||
exclusion_map = FIELD_EXCLUSION_MAP # Use default if not provided
|
||||
|
||||
# Combine global exclusions with context-specific exclusions
|
||||
global_exclusions = exclusion_map.get("global", set())
|
||||
context_exclusions = exclusion_map.get(parent_context, set())
|
||||
total_exclusions = global_exclusions.union(context_exclusions)
|
||||
|
||||
|
||||
menu = {}
|
||||
fields = message_instance.DESCRIPTOR.fields
|
||||
|
||||
for field in fields:
|
||||
if field.name in total_exclusions:
|
||||
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):
|
||||
continue
|
||||
|
||||
if field.message_type: # Nested message
|
||||
nested_instance = getattr(message_instance, field.name)
|
||||
nested_config = getattr(current_config, field.name, None) if current_config else None
|
||||
menu[field.name] = extract_fields(
|
||||
nested_instance,
|
||||
nested_config,
|
||||
parent_context=parent_context,
|
||||
exclusion_map=exclusion_map
|
||||
)
|
||||
menu[field.name] = extract_fields(nested_instance, nested_config)
|
||||
elif field.enum_type: # Handle enum fields
|
||||
current_value = getattr(current_config, field.name, "Not Set") if current_config else "Not Set"
|
||||
if isinstance(current_value, int): # Map enum number to name
|
||||
if isinstance(current_value, int): # If the value is a number, map it to its name
|
||||
enum_value = field.enum_type.values_by_number.get(current_value)
|
||||
current_value_name = f"{enum_value.name}" if enum_value else f"Unknown ({current_value})"
|
||||
if enum_value: # Check if the enum value exists
|
||||
current_value_name = f"{enum_value.name}"
|
||||
else:
|
||||
current_value_name = f"Unknown ({current_value})"
|
||||
menu[field.name] = (field, current_value_name)
|
||||
else:
|
||||
menu[field.name] = (field, current_value)
|
||||
else: # Other field types
|
||||
menu[field.name] = (field, current_value) # Non-integer values
|
||||
else: # Handle other field types
|
||||
current_value = getattr(current_config, field.name, "Not Set") if current_config else "Not Set"
|
||||
menu[field.name] = (field, current_value)
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def generate_menu_from_protobuf(interface):
|
||||
# Function to generate the menu structure from protobuf messages
|
||||
menu_structure = {"Main Menu": {}}
|
||||
@@ -88,18 +69,9 @@ def generate_menu_from_protobuf(interface):
|
||||
for i in range(8):
|
||||
current_channel = interface.localNode.getChannelByChannelIndex(i)
|
||||
if current_channel:
|
||||
# Apply 'channel' context here
|
||||
channel_config = extract_fields(
|
||||
channel,
|
||||
current_channel.settings,
|
||||
parent_context="channel", # Dynamic context
|
||||
exclusion_map=FIELD_EXCLUSION_MAP # Pass exclusion map
|
||||
)
|
||||
# Convert 'psk' to Base64
|
||||
channel_config["psk"] = (
|
||||
channel_config["psk"][0],
|
||||
base64.b64encode(channel_config["psk"][1]).decode('utf-8')
|
||||
)
|
||||
channel_config = extract_fields(channel, current_channel.settings)
|
||||
# Convert 'psk' field to Base64
|
||||
channel_config["psk"] = (channel_config["psk"][0], base64.b64encode(channel_config["psk"][1]).decode('utf-8'))
|
||||
menu_structure["Main Menu"]["Channels"][f"Channel {i + 1}"] = channel_config
|
||||
|
||||
# Add Radio Settings
|
||||
@@ -128,23 +100,14 @@ def generate_menu_from_protobuf(interface):
|
||||
ordered_position_menu[key] = value
|
||||
|
||||
# Update the menu with the new order
|
||||
menu_structure["Main Menu"]["Radio Settings"] = extract_fields(
|
||||
radio,
|
||||
current_radio_config,
|
||||
parent_context="radio",
|
||||
exclusion_map=FIELD_EXCLUSION_MAP
|
||||
)
|
||||
menu_structure["Main Menu"]["Radio Settings"]["position"] = ordered_position_menu
|
||||
|
||||
|
||||
# Add Module Settings
|
||||
module = module_config_pb2.ModuleConfig()
|
||||
current_module_config = interface.localNode.moduleConfig if interface else None
|
||||
menu_structure["Main Menu"]["Module Settings"] = extract_fields(module, current_module_config)
|
||||
|
||||
menu_structure["Main Menu"]["Module Settings"] = extract_fields(
|
||||
module,
|
||||
current_module_config,
|
||||
parent_context="module", # Apply 'module' context
|
||||
exclusion_map=FIELD_EXCLUSION_MAP
|
||||
)
|
||||
# Add App Settings
|
||||
menu_structure["Main Menu"]["App Settings"] = {"Open": "app_settings"}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+13
-5
@@ -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}")
|
||||
|
||||
logging.critical(f"Fatal error initializing interface: {ex}")
|
||||
@@ -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...")
|
||||
Reference in New Issue
Block a user