mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
Compare commits
48 Commits
rm-node-fr
...
fix-startu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08f19e653a | ||
|
|
6e05446d6a | ||
|
|
a0cb4f9480 | ||
|
|
89d5890338 | ||
|
|
a645e41146 | ||
|
|
aa736472cc | ||
|
|
4dc1b4b791 | ||
|
|
e69c51f9c3 | ||
|
|
41050577aa | ||
|
|
d9c249af56 | ||
|
|
ad64004e79 | ||
|
|
3c3bf0ad37 | ||
|
|
d9a84c4b29 | ||
|
|
6cf46a202a | ||
|
|
804f82cbe6 | ||
|
|
57042d2050 | ||
|
|
351f4d7f8f | ||
|
|
5f0277c460 | ||
|
|
f34db01a78 | ||
|
|
463b655684 | ||
|
|
14aa5c00a3 | ||
|
|
a201bcccb2 | ||
|
|
8342753c51 | ||
|
|
5690329b06 | ||
|
|
a080af3e84 | ||
|
|
dd11932a53 | ||
|
|
dae71984bc | ||
|
|
3668d47119 | ||
|
|
fe3980bc5a | ||
|
|
9c380c18fd | ||
|
|
30d14a6a9e | ||
|
|
bbfe361173 | ||
|
|
0d6f234191 | ||
|
|
16c8e3032a | ||
|
|
611d59fefe | ||
|
|
651d381c78 | ||
|
|
e7850b9204 | ||
|
|
4306971871 | ||
|
|
ba86108316 | ||
|
|
83393e2a25 | ||
|
|
9073da802d | ||
|
|
5907807b71 | ||
|
|
cc7124b6f5 | ||
|
|
353412be11 | ||
|
|
8382da07a3 | ||
|
|
01cfe4c681 | ||
|
|
1675b0a116 | ||
|
|
b717d46441 |
37
README.md
37
README.md
@@ -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 12 PM" 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
|
||||
```
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -9,4 +9,5 @@ myNodeNum = 0
|
||||
selected_channel = 0
|
||||
selected_message = 0
|
||||
selected_node = 0
|
||||
current_window = 0
|
||||
current_window = 0
|
||||
lock = None
|
||||
@@ -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
22
main.py
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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]:
|
||||
|
||||
937
ui/curses_ui.py
937
ui/curses_ui.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user