Compare commits

...

48 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
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
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
3c3bf0ad37 rename enum to list 2025-02-08 13:23:30 -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
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
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
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
8 changed files with 619 additions and 540 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

@@ -9,4 +9,5 @@ myNodeNum = 0
selected_channel = 0
selected_message = 0
selected_node = 0
current_window = 0
current_window = 0
lock = None

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)

22
main.py
View File

@@ -11,6 +11,7 @@ from pubsub import pub
import os
import logging
import traceback
import threading
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
@@ -38,6 +39,8 @@ logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(message)s"
)
globals.lock = threading.Lock()
def main(stdscr):
try:
draw_splash(stdscr)
@@ -45,15 +48,16 @@ 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)
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)

View File

@@ -12,93 +12,94 @@ 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

@@ -3,7 +3,7 @@ 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
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 +94,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]:

File diff suppressed because it is too large Load Diff