Compare commits

..

50 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
pdxlocations
aa8a66ef22 fix config overwrite option 2025-02-22 18:40:20 -08:00
pdxlocations
498be2c859 Merge pull request #133 from pdxlocations:don't-skip-lora-channel-num-2
Restore missing frequency slot to settings
2025-02-21 18:25:06 -08:00
pdxlocations
b086125962 new skip fields check 2025-02-21 18:24:19 -08:00
pdxlocations
f644b92356 use global interface in save 2025-02-20 22:32:17 -08:00
pdxlocations
3db44f4ae3 remove double writeconfig 2025-02-20 21:57:09 -08:00
pdxlocations
8c837e68a0 keep cursor in the input window (#130) 2025-02-18 22:30:42 -08:00
pdxlocations
5dd06624e3 do close interface after region set 2025-02-12 16:08:18 -08:00
pdxlocations
c83ccea4ef use protubuf number when setting region 2025-02-12 15:27:33 -08:00
pdxlocations
d7f0bee54c try setting region earlier 2025-02-12 15:22:02 -08:00
pdxlocations
fb60773ae6 don't close interface after region set 2025-02-12 15:18:58 -08:00
pdxlocations
47ab0a5b9a bump version 2025-02-09 22:09:59 -08:00
pdxlocations
989c3cf44e add region check at startup (#127) 2025-02-09 22:09:15 -08:00
pdxlocations
71aeae4f92 add note in draw_node_list 2025-02-09 20:49:49 -08:00
pdxlocations
34cd21b323 Fix Startup Error with Thread Lock (#126)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more

* grasping at straws

* more global

* back up

* db snapshot

* try a threading lock

* fix conflict

* lock it down

* sir locks a lot

* sir locks a lilttle less
2025-02-09 06:46:42 -08:00
pdxlocations
e69c51f9c3 Another attempt to fix startup errors (#125)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more

* grasping at straws

* more global

* back up

* db snapshot
2025-02-08 15:57:54 -08:00
pdxlocations
3c3bf0ad37 rename enum to list 2025-02-08 13:23:30 -08:00
pdxlocations
804f82cbe6 where have all the nodes_pad gone? 2025-02-08 10:03:51 -08:00
pdxlocations
57042d2050 Maybe fix startup error (again) (#124)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more
2025-02-08 09:14:25 -08:00
pdxlocations
8342753c51 Merge pull request #123 from pdxlocations/fix-startup-error
Maybe Fix Startup Error
2025-02-07 22:18:58 -08:00
pdxlocations
5690329b06 notes 2025-02-07 22:11:53 -08:00
pdxlocations
a080af3e84 add logging 2025-02-07 21:50:14 -08:00
pdxlocations
dd11932a53 make sure nodes_pad exists legitely 2025-02-07 21:44:17 -08:00
pdxlocations
dae71984bc make sure nodes pad is created 2025-02-07 21:01:06 -08:00
pdxlocations
3668d47119 define windows in resize 2025-02-07 20:49:38 -08:00
pdxlocations
fe3980bc5a ok I'll stop 2025-02-07 20:23:47 -08:00
pdxlocations
9c380c18fd maybe this 2025-02-07 20:19:26 -08:00
pdxlocations
30d14a6a9e big test 2025-02-07 18:08:03 -08:00
pdxlocations
bbfe361173 morer testing 2025-02-07 18:03:10 -08:00
pdxlocations
0d6f234191 more testing 2025-02-06 23:10:01 -08:00
pdxlocations
16c8e3032a fix startup error test 2025-02-06 23:06:19 -08:00
pdxlocations
611d59fefe Merge pull request #120 from rfschmid/fix-receiving-traceroute-from-archived-node 2025-02-05 17:31:01 -08:00
Russell Schmidt
651d381c78 Fix receiving traceroute from archived chat
They were invisible
2025-02-05 19:15:18 -06:00
pdxlocations
e7850b9204 add role to node details 2025-02-05 16:31:12 -08:00
pdxlocations
4306971871 switch locked emoji 2025-02-05 16:05:39 -08:00
pdxlocations
ba86108316 Merge pull request #119 from rfschmid/update-main-ui-screenshot
Update README.md main UI screenshot
2025-02-05 10:41:53 -08:00
Russell Schmidt
83393e2a25 Update README.md main UI screenshot 2025-02-05 12:25:20 -06:00
pdxlocations
9073da802d Update Settings Image 2025-02-05 08:16:15 -08:00
pdxlocations
5907807b71 Merge pull request #118 from pdxlocations/add-commands-to-readme
Update ReadMe with Commands
2025-02-05 08:11:22 -08:00
pdxlocations
cc7124b6f5 add commands 2025-02-05 08:10:43 -08:00
pdxlocations
353412be11 Merge pull request #117 from rfschmid:add-node-search-feature
Add channel and node search feature
2025-02-04 17:21:20 -08:00
Russell Schmidt
8382da07a3 Fix indexing if list changes while searching 2025-02-04 19:16:00 -06:00
pdxlocations
01cfe4c681 Merge pull request #116 from rfschmid/add-lock-icon-for-PSK 2025-02-04 16:02:31 -08:00
Russell Schmidt
1675b0a116 Add channel and node search feature
Press Ctrl + / while the nodes or channels window is highlighted to
start search

Type text to search as you type, first matching item will be selected,
starting at current selected index

Press Tab to find next match starting from the current index - search
wraps around if necessary

Press Esc or Enter to exit search mode
2025-02-04 17:34:41 -06:00
Russell Schmidt
b717d46441 Add lock/unlock icon for nodes with/without PSK 2025-02-04 17:23:44 -06:00
15 changed files with 818 additions and 615 deletions

View File

@@ -5,13 +5,37 @@
This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores.
<img width="846" alt="Screenshot_2024-03-29_at_4 00 29_PM" src="https://github.com/pdxlocations/meshtastic-curses-client/assets/117498748/e99533b7-5c0c-463d-8d5f-6e3cccaeced7">
<img width="846" alt="Contact - Main UI Screenshot" src="https://github.com/user-attachments/assets/d2996bfb-2c6d-46a8-b820-92a9143375f4">
<br><br>
Settings can be accessed within the client or can be run standalone
The settings dialogue can be accessed within the client or may be run standalone to configure your node by launching `settings.py`
<img width="509" alt="Screenshot 2024-04-15 at 3 39 12PM" src="https://github.com/pdxlocations/meshtastic-curses-client/assets/117498748/37bc57db-fe2d-4ba4-adc8-679b4cb642f9">
<img width="441" alt="Contact - Settings Dialogue" src="https://github.com/user-attachments/assets/dd47f52a-d4d8-4e40-8001-9ea53d87f816" />
## Message Persistence
All messages will saved in a SQLite DB and restored upon relaunch of the app. You may delete `client.db` if you wish to erase all stored messages and node data. If multiple nodes are used, each will independently store data in the database, but the data will not be shared or viewable between nodes.
## Client Configuration
By navigating to Settings -> App Settings, you may customize your UI's icons, colors, and more!
## Commands
- `↑→↓←` = Navigate around the UI.
- `ENTER` = Send a message typed in the Input Window, or with the Node List highlighted, select a node to DM
- `` ` `` = Open the Settings dialogue
- `CTRL` + `p` = Hide/show a log of raw received packets.
- `CTRL` + `t` = With the Node List highlighted, send a traceroute to the selected node
- `CTRL` + `d` = With the Channel List hightlighted, archive a chat to reduce UI clutter. Messages will be saved in the db and repopulate if you send or receive a DM from this user.
- `ESC` = Exit out of the Settings Dialogue, or Quit the application if settings are not displayed.
### Search
- Press `CTRL` + `/` while the nodes or channels window is highlighted to start search
- Type text to search as you type, first matching item will be selected, starting at current selected index
- Press Tab to find next match starting from the current index - search wraps around if necessary
- Press Esc or Enter to exit search mode
## Arguments
@@ -22,7 +46,7 @@ You can pass the following arguments to the client:
Optional arguments to specify a device to connect to and how.
- `--port`, `--serial`, `-s`: The port to connect to via serial, e.g. `/dev/ttyUSB0`.
- `--host`, `--tcp`, `-t`: The hostname or IP address to connect to using TCP.
- `--host`, `--tcp`, `-t`: The hostname or IP address to connect to using TCP, will default to localhost if no host is passed.
- `--ble`, `-b`: The BLE device MAC address or name to connect to.
If no connection arguments are specified, the client will attempt a serial connection and then a TCP connection to localhost.
@@ -33,3 +57,8 @@ If no connection arguments are specified, the client will attempt a serial conne
python main.py --port /dev/ttyUSB0
python main.py --host 192.168.1.1
python main.py --ble BlAddressOfDevice
```
To quickly connect to localhost, use:
```sh
python main.py -t
```

View File

@@ -144,14 +144,16 @@ def load_messages_from_db():
def init_nodedb():
"""Initialize the node database and update it with nodes from the interface."""
try:
if not globals.interface.nodes:
return # No nodes to initialize
ensure_node_table_exists() # Ensure the table exists before insertion
nodes_snapshot = list(globals.interface.nodes.values())
# Insert or update all nodes
for node in globals.interface.nodes.values():
for node in nodes_snapshot:
update_node_info_in_db(
user_id=node['num'],
long_name=node['user'].get('longName', ''),

View File

@@ -1,4 +1,5 @@
interface = None
lock = None
display_log = False
all_messages = {}
channel_list = []

View File

@@ -207,19 +207,19 @@ def get_list_input(prompt, current_option, list_options):
return current_option
def move_highlight(old_idx, new_idx, options, enum_win, enum_pad):
def move_highlight(old_idx, new_idx, options, list_win, list_pad):
if old_idx == new_idx:
return # no-op
enum_pad.chgat(old_idx, 0, enum_pad.getmaxyx()[1], get_color("settings_default"))
enum_pad.chgat(new_idx, 0, enum_pad.getmaxyx()[1], get_color("settings_default", reverse = True))
list_pad.chgat(old_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default"))
list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse = True))
enum_win.refresh()
list_win.refresh()
start_index = max(0, new_idx - (enum_win.getmaxyx()[0] - 4))
start_index = max(0, new_idx - (list_win.getmaxyx()[0] - 4))
enum_win.refresh()
enum_pad.refresh(start_index, 0,
enum_win.getbegyx()[0] + 3, enum_win.getbegyx()[1] + 4,
enum_win.getbegyx()[0] + enum_win.getmaxyx()[0] - 2, enum_win.getbegyx()[1] + 4 + enum_win.getmaxyx()[1] - 4)
list_win.refresh()
list_pad.refresh(start_index, 0,
list_win.getbegyx()[0] + 3, list_win.getbegyx()[1] + 4,
list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2, list_win.getbegyx()[1] + 4 + list_win.getmaxyx()[1] - 4)

55
main.py
View File

@@ -3,20 +3,26 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.2.0
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
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
@@ -34,10 +40,12 @@ 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"
)
globals.lock = threading.Lock()
def main(stdscr):
try:
draw_splash(stdscr)
@@ -45,15 +53,29 @@ def main(stdscr):
args = parser.parse_args()
logging.info("Initializing interface %s", args)
globals.interface = initialize_interface(args)
logging.info("Interface initialized")
globals.myNodeNum = get_nodeNum()
globals.channel_list = get_channels()
globals.node_list = get_node_list()
pub.subscribe(on_receive, 'meshtastic.receive')
init_nodedb()
load_messages_from_db()
logging.info("Starting main UI")
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()
globals.node_list = get_node_list()
pub.subscribe(on_receive, 'meshtastic.receive')
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)
@@ -61,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())

View File

@@ -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,97 +10,96 @@ import default_config as config
import globals
from datetime import datetime
def on_receive(packet, interface):
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
draw_packetlog_win()
try:
if 'decoded' not in packet:
return
with globals.lock:
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
draw_packetlog_win()
try:
if 'decoded' not in packet:
return
# Assume any incoming packet could update the last seen time for a node
changed = refresh_node_list()
if(changed):
draw_node_list()
# Assume any incoming packet could update the last seen time for a node
changed = refresh_node_list()
if(changed):
draw_node_list()
if packet['decoded']['portnum'] == 'NODEINFO_APP':
if "user" in packet['decoded'] and "longName" in packet['decoded']["user"]:
maybe_store_nodeinfo_in_db(packet)
if packet['decoded']['portnum'] == 'NODEINFO_APP':
if "user" in packet['decoded'] and "longName" in packet['decoded']["user"]:
maybe_store_nodeinfo_in_db(packet)
elif packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
elif packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
refresh_channels = False
refresh_messages = False
refresh_channels = False
refresh_messages = False
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
if packet.get('channel'):
channel_number = packet['channel']
else:
globals.channel_list.append(packet['from'])
if(packet['from'] not in globals.all_messages):
globals.all_messages[packet['from']] = []
update_node_info_in_db(packet['from'], chat_archived=False)
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
else:
globals.channel_list.append(packet['from'])
if(packet['from'] not in globals.all_messages):
globals.all_messages[packet['from']] = []
update_node_info_in_db(packet['from'], chat_archived=False)
refresh_channels = True
channel_number = globals.channel_list.index(packet['from'])
if globals.channel_list[channel_number] != globals.channel_list[globals.selected_channel]:
add_notification(channel_number)
refresh_channels = True
else:
refresh_messages = True
channel_number = globals.channel_list.index(packet['from'])
# Add received message to the messages list
message_from_id = packet['from']
message_from_string = get_name_from_database(message_from_id, type='short') + ":"
if globals.channel_list[channel_number] != globals.channel_list[globals.selected_channel]:
add_notification(channel_number)
refresh_channels = True
else:
refresh_messages = True
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
# Add received message to the messages list
message_from_id = packet['from']
message_from_string = get_name_from_database(message_from_id, type='short') + ":"
# Timestamp handling
current_timestamp = time.time()
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
# Timestamp handling
current_timestamp = time.time()
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
# Retrieve the last timestamp if available
channel_messages = globals.all_messages[globals.channel_list[channel_number]]
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
# Retrieve the last timestamp if available
channel_messages = globals.all_messages[globals.channel_list[channel_number]]
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
else:
last_hour = None
# Add a new timestamp if it's a new hour
if last_hour != current_hour:
globals.all_messages[globals.channel_list[channel_number]].append((f"-- {current_hour} --", ""))
# Add a new timestamp if it's a new hour
if last_hour != current_hour:
globals.all_messages[globals.channel_list[channel_number]].append((f"-- {current_hour} --", ""))
globals.all_messages[globals.channel_list[channel_number]].append((f"{config.message_prefix} {message_from_string} ", message_string))
globals.all_messages[globals.channel_list[channel_number]].append((f"{config.message_prefix} {message_from_string} ", message_string))
if refresh_channels:
draw_channel_list()
if refresh_messages:
draw_messages_window(True)
if refresh_channels:
draw_channel_list()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
except KeyError as e:
logging.error(f"Error processing packet: {e}")
except KeyError as e:
logging.error(f"Error processing packet: {e}")

View File

@@ -2,8 +2,9 @@ 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
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 globals
@@ -94,6 +95,9 @@ def on_response_traceroute(packet):
globals.channel_list.append(packet['from'])
refresh_channels = True
if(is_chat_archived(packet['from'])):
update_node_info_in_db(packet['from'], chat_archived=False)
channel_number = globals.channel_list.index(packet['from'])
if globals.channel_list[channel_number] == globals.channel_list[globals.selected_channel]:
@@ -116,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(

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(interface, menu_path, modified_settings):
def save_changes(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(interface, menu_path, modified_settings):
logging.info("No changes to save. modified_settings is empty.")
return
node = interface.getNode('^local')
node = globals.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(interface, menu_path, modified_settings):
lon = float(modified_settings.get('longitude', 0.0))
alt = int(modified_settings.get('altitude', 0))
interface.localNode.setFixedPosition(lat, lon, alt)
globals.interface.localNode.setFixedPosition(lat, lon, alt)
logging.info(f"Updated {config_category} with Latitude: {lat} and Longitude {lon} and Altitude {alt}")
return
@@ -122,10 +122,5 @@ def save_changes(interface, 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,6 +9,7 @@ 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
@@ -151,7 +152,7 @@ def settings_menu(stdscr, interface):
menu_win.erase()
menu_win.refresh()
if show_save_option and selected_index == len(options):
save_changes(interface, menu_path, modified_settings)
save_changes(menu_path, modified_settings)
modified_settings.clear()
logging.info("Changes Saved")
@@ -188,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)
@@ -348,6 +349,25 @@ 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

File diff suppressed because it is too large Load Diff

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

0
utilities/__init__.py Normal file
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,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}")

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...")