1
0
forked from iarv/contact

Compare commits

...

84 Commits

Author SHA1 Message Date
pdxlocations
f0554ec1f6 bump version 2025-01-25 20:42:16 -08:00
Russell Schmidt
bb623d149c Fix crash when cancelling shutdown/reboot/etc (#73) 2025-01-25 20:39:04 -08:00
pdxlocations
4a92ad49ce missed some coloring 2025-01-25 18:57:04 -08:00
pdxlocations
9d0470d55b just call it green 2025-01-25 18:27:48 -08:00
pdxlocations
e96ea7ffef Green Theme for Josh (#72)
* add themes

* change tty defaults
2025-01-25 18:23:30 -08:00
pdxlocations
44b1a3071b fix packet log border color 2025-01-25 18:02:37 -08:00
pdxlocations
702d20a011 fix sesitive settings highlight 2025-01-25 16:25:47 -08:00
pdxlocations
fba4642ff8 fix settings crash 2025-01-25 16:02:40 -08:00
pdxlocations
916b0cfe53 fix splash border 2025-01-25 15:36:24 -08:00
pdxlocations
e086814b83 fix color fixes 2025-01-25 15:30:06 -08:00
pdxlocations
92f08d020e cleanup comments 2025-01-25 12:29:05 -08:00
pdxlocations
86463f6f84 maybe fix setting enum (#69) 2025-01-25 11:30:53 -08:00
pdxlocations
e58340fa65 extra bool check 2025-01-25 09:10:35 -08:00
pdxlocations
c243daf253 Fix channels saving to wrong index 2025-01-25 08:43:17 -08:00
pdxlocations
70f1f5d4bf late version bump 2025-01-25 07:48:41 -08:00
pdxlocations
67f1bde217 rm incorrect comment 2025-01-24 22:34:50 -08:00
pdxlocations
ca17bbee31 color fixes 2025-01-24 22:12:25 -08:00
pdxlocations
659dad493c check for new default colors 2025-01-24 21:38:51 -08:00
pdxlocations
4359d37979 add settings box color 2025-01-24 21:25:22 -08:00
pdxlocations
96612b3e1b rm print statements 2025-01-24 21:14:17 -08:00
pdxlocations
84246aefd9 Move user config into JSON file (#68)
* working changes

* Reduce redraws in settings (#66)

* Reduce redraws in settings

* Fix drawing sensitive settings in red

* Update other settings to use new color API

* Update reduce redraw changes to use new color API

* Fix highlight/unhighlight red settings

* add settings colors

* format json file

---------

Co-authored-by: Russell Schmidt <russ@sumonkey.com>
2025-01-24 21:00:00 -08:00
Russell Schmidt
7cd98a39f8 Reduce redraws in settings (#66)
* Reduce redraws in settings

* Fix drawing sensitive settings in red

* Update other settings to use new color API

* Update reduce redraw changes to use new color API

* Fix highlight/unhighlight red settings
2025-01-24 18:12:57 -08:00
pdxlocations
978e2942cb fix setting is_licensed bool 2025-01-24 09:55:42 -08:00
pdxlocations
92790ddca6 use local time for timestamp 2025-01-24 08:35:39 -08:00
pdxlocations
d5a6a0462f Color Customization (#64)
* init

* customize colors

* fix splash frame

* cleanup
2025-01-23 23:08:01 -08:00
pdxlocations
3816a3f166 Merge pull request #62 from rfschmid/stop-sending-blank
Don't allow sending empty string
2025-01-23 18:21:09 -08:00
Russell Schmidt
c61dc19319 Don't allow sending empty string 2025-01-23 19:15:35 -06:00
pdxlocations
91d331af4f Merge pull request #60 from rfschmid/add-home-end-pgup-pgdn-to-channels-nodes 2025-01-23 16:15:43 -08:00
pdxlocations
be92ac5de3 Merge pull request #61 from rfschmid/box-active-window-in-green 2025-01-23 16:12:31 -08:00
Russell Schmidt
10179c4179 Add home/end/pgup/pgdn to channels and nodes 2025-01-23 17:38:43 -06:00
pdxlocations
6e45caaac2 clear menu_win on escape 2025-01-23 15:34:08 -08:00
Russell Schmidt
152555156e Outline active window in green 2025-01-23 17:28:12 -06:00
pdxlocations
056b8b5f5f Merge pull request #59 from pdxlocations/timestamps 2025-01-23 15:20:27 -08:00
pdxlocations
07f0721fd5 add hourly timestamp to rx and tx messages 2025-01-23 15:19:45 -08:00
pdxlocations
cf9276d399 add argparse to settings.py 2025-01-23 12:44:16 -08:00
pdxlocations
482d158b15 sort globals 2025-01-23 12:29:14 -08:00
pdxlocations
11bd9c75ed Add Hourly Timestamps (#58)
* init

* formatting

* white stamps and no seconds
2025-01-23 12:21:38 -08:00
pdxlocations
1b2701e1a1 white stamps and no seconds 2025-01-23 12:20:57 -08:00
pdxlocations
7fcc9abd76 formatting 2025-01-23 12:16:59 -08:00
pdxlocations
9bfddb954d init 2025-01-23 12:08:14 -08:00
pdxlocations
deb231fc63 Merge pull request #56 from rfschmid/stop-refreshing-when-changing-windows 2025-01-23 11:12:50 -08:00
pdxlocations
bbe9e66fa5 Merge pull request #54 from rfschmid:convert-channels-window-to-pad
Convert channels window to pad
2025-01-23 11:06:00 -08:00
Russell Schmidt
aa7d98b1b0 Stop refreshing when changing windows 2025-01-23 12:53:55 -06:00
Russell Schmidt
8895784503 Fix not clearing notifications 2025-01-23 12:29:32 -06:00
Russell Schmidt
695b4949c0 Fix drawing messages when switching channels 2025-01-23 12:09:21 -06:00
Russell Schmidt
404bac9133 Convert channels window to pad 2025-01-23 08:09:38 -06:00
pdxlocations
4698b81a3f Fix broken settings and simplify bool handlers (#53)
* underp modified settings and simplify bool  handling

* easier to read modified settings

* rm spaces
2025-01-22 22:21:52 -08:00
pdxlocations
e7e9f24fe2 Merge pull request #52 from pdxlocations/get-name-from-number-error
Don't iterate over the db while writing to it.
2025-01-22 20:56:12 -08:00
pdxlocations
01f67ea8b5 db snapshot 2025-01-22 20:54:51 -08:00
pdxlocations
c19684c119 Merge pull request #51 from rfschmid/convert-nodes-window-to-pad
Convert nodes window to pad
2025-01-22 20:43:44 -08:00
pdxlocations
0b0d8c482b Merge pull request #50 from rfschmid/add-home-end-page-support 2025-01-22 20:40:47 -08:00
Russell Schmidt
f9774b2248 Convert nodes window to pad 2025-01-22 21:39:19 -06:00
Russell Schmidt
d6db1e1832 Add home/end/pageup/pagedown message support 2025-01-22 20:48:14 -06:00
pdxlocations
4c85aaecdf sent messages cyan 2025-01-22 18:21:51 -08:00
pdxlocations
d8fc02b28a Merge pull request #36 from rfschmid/convert-messages-window-to-pad
Convert messages window to pad
2025-01-22 17:47:04 -08:00
Russell Schmidt
039673bb18 Disable message window looping 2025-01-22 19:41:30 -06:00
pdxlocations
d81e694ee6 Merge pull request #49 from pdxlocations/settings-cleanup
Add is_licensed to user settings
2025-01-22 17:32:38 -08:00
pdxlocations
dfea291d21 typoe 2025-01-22 17:31:33 -08:00
pdxlocations
41c60a49e9 Merge pull request #48 from rfschmid/fix-crash-on-long-input 2025-01-22 17:00:58 -08:00
pdxlocations
fb3138883f Merge pull request #47 from rfschmid/make-selected-channel-green 2025-01-22 16:54:56 -08:00
pdxlocations
767f0e2288 add is_licensed 2025-01-22 16:47:49 -08:00
Russell Schmidt
43680f8afb Fix crash on long input 2025-01-22 17:25:12 -06:00
Russell Schmidt
e15f625716 Fix default channel messages not showing on start 2025-01-22 17:14:49 -06:00
Russell Schmidt
c090b3dd58 Make messages pad actually work 2025-01-22 17:11:32 -06:00
pdxlocations
f472a3040c remove some unused code 2025-01-22 12:42:35 -08:00
pdxlocations
4a0c49b7d6 typo and cleanup 2025-01-22 12:18:58 -08:00
pdxlocations
3034a1464a ok to show duplicate messages from db 2025-01-22 11:59:06 -08:00
Russell Schmidt
80fe10c050 Make selected channel green
So you can tell which channel you're looking at when the channel list
isn't highlighted
2025-01-22 12:52:03 -06:00
pdxlocations
0962c5b284 break out of settings 2025-01-22 10:43:42 -08:00
pdxlocations
1b3abdebf2 some settings cleanup 2025-01-22 08:41:22 -08:00
pdxlocations
a710374fe9 refactor notification 2025-01-22 08:20:51 -08:00
Russell Schmidt
cb088c51d4 Count total lines of messages 2025-01-21 19:10:43 -06:00
Russell Schmidt
ccb46b8553 Account for packet log window 2025-01-21 19:10:33 -06:00
Russell Schmidt
35748d071e Various tweaks, added back messages border 2025-01-21 19:09:46 -06:00
Russell Schmidt
7e85085b98 WIP - Convert messages window to pad
Change to use a curses "pad" so we can scroll better
2025-01-21 19:09:44 -06:00
pdxlocations
7493d21c1a Merge pull request #45 from rfschmid/make-packet-log-ctrl-p 2025-01-21 15:54:50 -08:00
Russell Schmidt
d0af0e6af1 Make forward slash possible to type
Packet log is now Ctrl + P
2025-01-21 17:39:49 -06:00
pdxlocations
796c40b560 Merge pull request #43 from rfschmid/fix-wide-character-input
Fix wide character input
2025-01-21 15:14:59 -08:00
Russell Schmidt
1c0704b940 Handle backspace character
Co-authored-by: pdxlocations <117498748+pdxlocations@users.noreply.github.com>
2025-01-21 17:13:46 -06:00
Russell Schmidt
6384777bb6 Delete leftover comment 2025-01-21 14:58:34 -06:00
Russell Schmidt
2fbaee5fc5 Fix input of wide characters 2025-01-21 12:44:41 -06:00
pdxlocations
06e71331b6 select last message when changing channels 2025-01-21 09:54:33 -08:00
pdxlocations
fea705a09f Merge pull request #41 from pdxlocations/fix-repeated-input
ignore-incoming work - still not saving
2025-01-21 09:27:07 -08:00
pdxlocations
a47a4a9b32 ignore-incoming work - still not saving 2025-01-21 08:28:41 -08:00
15 changed files with 808 additions and 354 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ client.db
.DS_Store
client.log
settings.log
config.json
default_config.log

View File

@@ -1,6 +1,10 @@
import sqlite3
import globals
import time
from datetime import datetime
import logging
import globals
import default_config as config
from utilities.utils import get_name_from_number
def get_table_name(channel):
@@ -12,7 +16,7 @@ def get_table_name(channel):
def save_message_to_db(channel, user_id, message_text):
"""Save messages to the database, ensuring the table exists."""
try:
with sqlite3.connect(globals.db_file_path) as db_connection:
with sqlite3.connect(config.db_file_path) as db_connection:
db_cursor = db_connection.cursor()
quoted_table_name = get_table_name(channel)
@@ -42,14 +46,14 @@ def save_message_to_db(channel, user_id, message_text):
return timestamp
except sqlite3.Error as e:
print(f"SQLite error in save_message_to_db: {e}")
logging.error(f"SQLite error in save_message_to_db: {e}")
except Exception as e:
print(f"Unexpected error in save_message_to_db: {e}")
logging.error(f"Unexpected error in save_message_to_db: {e}")
def update_ack_nak(channel, timestamp, message, ack):
try:
with sqlite3.connect(globals.db_file_path) as db_connection:
with sqlite3.connect(config.db_file_path) as db_connection:
db_cursor = db_connection.cursor()
update_query = f"""
UPDATE {get_table_name(channel)}
@@ -63,16 +67,18 @@ def update_ack_nak(channel, timestamp, message, ack):
db_connection.commit()
except sqlite3.Error as e:
print(f"SQLite error in update_ack_nak: {e}")
logging.error(f"SQLite error in update_ack_nak: {e}")
except Exception as e:
print(f"Unexpected error in update_ack_nak: {e}")
logging.error(f"Unexpected error in update_ack_nak: {e}")
from datetime import datetime
def load_messages_from_db():
"""Load messages from the database for all channels and update globals.all_messages and globals.channel_list."""
try:
with sqlite3.connect(globals.db_file_path) as db_connection:
with sqlite3.connect(config.db_file_path) as db_connection:
db_cursor = db_connection.cursor()
# Retrieve all table names that match the pattern
@@ -82,18 +88,18 @@ def load_messages_from_db():
# Iterate through each table and fetch its messages
for table_name in tables:
quoted_table_name = f'"{table_name}"' # Quote the table name becuase we begin with numerics and contain spaces
quoted_table_name = f'"{table_name}"' # Quote the table name because we begin with numerics and contain spaces
table_columns = [i[1] for i in db_cursor.execute(f'PRAGMA table_info({quoted_table_name})')]
if("ack_type" not in table_columns):
if "ack_type" not in table_columns:
update_table_query = f"ALTER TABLE {quoted_table_name} ADD COLUMN ack_type TEXT"
db_cursor.execute(update_table_query)
query = f'SELECT user_id, message_text, ack_type FROM {quoted_table_name}'
query = f'SELECT user_id, message_text, timestamp, ack_type FROM {quoted_table_name}'
try:
# Fetch all messages from the table
db_cursor.execute(query)
db_messages = [(row[0], row[1], row[2]) for row in db_cursor.fetchall()] # Save as tuples
db_messages = [(row[0], row[1], row[2], row[3]) for row in db_cursor.fetchall()] # Save as tuples
# Extract the channel name from the table name
channel = table_name.split("_")[1]
@@ -109,35 +115,44 @@ def load_messages_from_db():
if channel not in globals.all_messages:
globals.all_messages[channel] = []
# Add messages to globals.all_messages in tuple format
for user_id, message, ack_type in db_messages:
if user_id == str(globals.myNodeNum):
ack_str = globals.ack_unknown_str
if(ack_type == "Implicit"):
ack_str = globals.ack_implicit_str
elif(ack_type == "Ack"):
ack_str = globals.ack_str
elif(ack_type == "Nak"):
ack_str = globals.nak_str
# Add messages to globals.all_messages grouped by hourly timestamp
hourly_messages = {}
for user_id, message, timestamp, ack_type in db_messages:
hour = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:00')
if hour not in hourly_messages:
hourly_messages[hour] = []
ack_str = config.ack_unknown_str
if ack_type == "Implicit":
ack_str = config.ack_implicit_str
elif ack_type == "Ack":
ack_str = config.ack_str
elif ack_type == "Nak":
ack_str = config.nak_str
formatted_message = (f"{globals.sent_message_prefix}{ack_str}: ", message)
else:
formatted_message = (f"{globals.message_prefix} {get_name_from_number(int(user_id), 'short')}: ", message)
if formatted_message not in globals.all_messages[channel]:
globals.all_messages[channel].append(formatted_message)
if user_id == str(globals.myNodeNum):
formatted_message = (f"{config.sent_message_prefix}{ack_str}: ", message)
else:
formatted_message = (f"{config.message_prefix} {get_name_from_number(int(user_id), 'short')}: ", message)
hourly_messages[hour].append(formatted_message)
# Flatten the hourly messages into globals.all_messages[channel]
for hour, messages in sorted(hourly_messages.items()):
globals.all_messages[channel].append((f"-- {hour} --", ""))
globals.all_messages[channel].extend(messages)
except sqlite3.Error as e:
print(f"SQLite error while loading messages from table '{table_name}': {e}")
logging.error(f"SQLite error while loading messages from table '{table_name}': {e}")
except sqlite3.Error as e:
print(f"SQLite error in load_messages_from_db: {e}")
logging.error(f"SQLite error in load_messages_from_db: {e}")
def init_nodedb():
"""Initialize the node database and update it with nodes from the interface."""
try:
with sqlite3.connect(globals.db_file_path) as db_connection:
with sqlite3.connect(config.db_file_path) as db_connection:
db_cursor = db_connection.cursor()
# Table name construction
@@ -183,14 +198,14 @@ def init_nodedb():
db_connection.commit()
except sqlite3.Error as e:
print(f"SQLite error in init_nodedb: {e}")
logging.error(f"SQLite error in init_nodedb: {e}")
except Exception as e:
print(f"Unexpected error in init_nodedb: {e}")
logging.error(f"Unexpected error in init_nodedb: {e}")
def maybe_store_nodeinfo_in_db(packet):
"""Save nodeinfo unless that record is already there."""
try:
with sqlite3.connect(globals.db_file_path) as db_connection:
with sqlite3.connect(config.db_file_path) as db_connection:
table_name = f"{str(globals.myNodeNum)}_nodedb"
nodeinfo_table = f'"{table_name}"' # Quote the table name becuase we might begin with numerics
@@ -267,6 +282,6 @@ def maybe_store_nodeinfo_in_db(packet):
except sqlite3.Error as e:
print(f"SQLite error in maybe_store_nodeinfo_in_db: {e}")
logging.error(f"SQLite error in maybe_store_nodeinfo_in_db: {e}")
finally:
db_connection.close()

183
default_config.py Normal file
View File

@@ -0,0 +1,183 @@
import os
import json
import logging
def format_json_single_line_arrays(data, indent=4):
"""
Formats JSON with arrays on a single line while keeping other elements properly indented.
"""
def format_value(value, current_indent):
if isinstance(value, dict):
items = []
for key, val in value.items():
items.append(
f'{" " * current_indent}"{key}": {format_value(val, current_indent + indent)}'
)
return "{\n" + ",\n".join(items) + f"\n{' ' * (current_indent - indent)}}}"
elif isinstance(value, list):
return f"[{', '.join(json.dumps(el, ensure_ascii=False) for el in value)}]"
else:
return json.dumps(value, ensure_ascii=False)
return format_value(data, indent)
# Recursive function to check and update nested dictionaries
def update_dict(default, actual):
updated = False
for key, value in default.items():
if key not in actual:
actual[key] = value
updated = True
elif isinstance(value, dict):
# Recursively check nested dictionaries
updated = update_dict(value, actual[key]) or updated
return updated
def initialize_config():
app_directory = os.path.dirname(os.path.abspath(__file__))
json_file_path = os.path.join(app_directory, "config.json")
COLOR_CONFIG_DARK = {
"default": ["white", "black"],
"background": [" ", "black"],
"splash_logo": ["green", "black"],
"splash_text": ["white", "black"],
"input": ["white", "black"],
"node_list": ["white", "black"],
"channel_list": ["white", "black"],
"channel_selected": ["green", "black"],
"rx_messages": ["cyan", "black"],
"tx_messages": ["green", "black"],
"timestamps": ["white", "black"],
"commands": ["white", "black"],
"window_frame": ["white", "black"],
"window_frame_selected": ["green", "black"],
"log_header": ["blue", "black"],
"log": ["green", "black"],
"settings_default": ["white", "black"],
"settings_sensitive": ["red", "black"],
"settings_save": ["green", "black"],
"settings_breadcrumbs": ["white", "black"]
}
COLOR_CONFIG_LIGHT = {
"default": ["black", "white"],
"background": [" ", "white"],
"splash_logo": ["green", "white"],
"splash_text": ["black", "white"],
"input": ["black", "white"],
"node_list": ["black", "white"],
"channel_list": ["black", "white"],
"channel_selected": ["green", "white"],
"rx_messages": ["cyan", "white"],
"tx_messages": ["green", "white"],
"timestamps": ["black", "white"],
"commands": ["black", "white"],
"window_frame": ["black", "white"],
"window_frame_selected": ["green", "white"],
"log_header": ["black", "white"],
"log": ["blue", "white"],
"settings_default": ["black", "white"],
"settings_sensitive": ["red", "white"],
"settings_save": ["green", "white"],
"settings_breadcrumbs": ["black", "white"]
}
COLOR_CONFIG_GREEN = {
"default": ["green", "black"],
"background": [" ", "black"],
"splash_logo": ["green", "black"],
"splash_text": ["green", "black"],
"input": ["green", "black"],
"node_list": ["green", "black"],
"channel_list": ["green", "black"],
"channel_selected": ["cyan", "black"],
"rx_messages": ["green", "black"],
"tx_messages": ["green", "black"],
"timestamps": ["green", "black"],
"commands": ["green", "black"],
"window_frame": ["green", "black"],
"window_frame_selected": ["cyan", "black"],
"log_header": ["green", "black"],
"log": ["green", "black"],
"settings_default": ["green", "black"],
"settings_sensitive": ["green", "black"],
"settings_save": ["green", "black"],
"settings_breadcrumbs": ["green", "black"]
}
default_config_variables = {
"db_file_path": os.path.join(app_directory, "client.db"),
"log_file_path": os.path.join(app_directory, "client.log"),
"message_prefix": ">>",
"sent_message_prefix": ">> Sent",
"notification_symbol": "*",
"ack_implicit_str": "[◌]",
"ack_str": "[✓]",
"nak_str": "[x]",
"ack_unknown_str": "[…]",
"theme": "dark",
"COLOR_CONFIG_DARK": COLOR_CONFIG_DARK,
"COLOR_CONFIG_LIGHT": COLOR_CONFIG_LIGHT,
"COLOR_CONFIG_GREEN": COLOR_CONFIG_GREEN
}
if not os.path.exists(json_file_path):
with open(json_file_path, "w", encoding="utf-8") as json_file:
formatted_json = format_json_single_line_arrays(default_config_variables)
json_file.write(formatted_json)
# Ensure all default variables exist in the JSON file
with open(json_file_path, "r", encoding="utf-8") as json_file:
loaded_config = json.load(json_file)
# Check and add missing variables
updated = update_dict(default_config_variables, loaded_config)
# Update the JSON file if any variables were missing
if updated:
formatted_json = format_json_single_line_arrays(loaded_config)
with open(json_file_path, "w", encoding="utf-8") as json_file:
json_file.write(formatted_json)
logging.info(f"JSON file updated with missing default variables and COLOR_CONFIG items.")
return loaded_config
# Call the function when the script is imported
loaded_config = initialize_config()
# Assign values to local variables
db_file_path = loaded_config["db_file_path"]
log_file_path = loaded_config["log_file_path"]
message_prefix = loaded_config["message_prefix"]
sent_message_prefix = loaded_config["sent_message_prefix"]
notification_symbol = loaded_config["notification_symbol"]
ack_implicit_str = loaded_config["ack_implicit_str"]
ack_str = loaded_config["ack_str"]
nak_str = loaded_config["nak_str"]
ack_unknown_str = loaded_config["ack_unknown_str"]
theme = loaded_config["theme"]
if theme == "dark":
COLOR_CONFIG = loaded_config["COLOR_CONFIG_DARK"]
elif theme == "light":
COLOR_CONFIG = loaded_config["COLOR_CONFIG_LIGHT"]
elif theme == "green":
COLOR_CONFIG = loaded_config["COLOR_CONFIG_GREEN"]
if __name__ == "__main__":
logging.basicConfig(
filename="default_config.log",
level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
format="%(asctime)s - %(levelname)s - %(message)s"
)
print("\nLoaded Configuration:")
print(f"Database File Path: {db_file_path}")
print(f"Log File Path: {log_file_path}")
print(f"Message Prefix: {message_prefix}")
print(f"Sent Message Prefix: {sent_message_prefix}")
print(f"Notification Symbol: {notification_symbol}")
print(f"ACK Implicit String: {ack_implicit_str}")
print(f"ACK String: {ack_str}")
print(f"NAK String: {nak_str}")
print(f"ACK Unknown String: {ack_unknown_str}")
print(f"Color Config: {COLOR_CONFIG}")

View File

@@ -1,25 +1,12 @@
import os
app_directory = os.path.dirname(os.path.abspath(__file__))
db_file_path = os.path.join(app_directory, "client.db")
log_file_path = os.path.join(app_directory, "client.log")
interface = None
display_log = False
all_messages = {}
channel_list = []
notifications = set()
packet_buffer = []
node_list = []
myNodeNum = 0
selected_channel = 0
selected_message = 0
selected_node = 0
current_window = 0
interface = None
display_log = False
message_prefix = ">>"
sent_message_prefix = message_prefix + " Sent"
ack_implicit_str = "[◌]"
ack_str = "[✓]"
nak_str = "[x]"
ack_unknown_str = "[…]"
notification_symbol = "*"
node_list = []
current_window = 0

View File

@@ -1,5 +1,6 @@
import curses
import ipaddress
from ui.colors import get_color
def get_user_input(prompt):
# Calculate the dynamic height and width for the input window
@@ -10,37 +11,34 @@ def get_user_input(prompt):
# Create a new window for user input
input_win = curses.newwin(height, width, start_y, start_x)
input_win.clear()
input_win.bkgd(get_color("background"))
input_win.attrset(get_color("window_frame"))
input_win.border()
# Display the prompt
input_win.addstr(1, 2, prompt, curses.A_BOLD)
input_win.addstr(3, 2, "Enter value: ")
input_win.addstr(1, 2, prompt, get_color("settings_default", bold=True))
input_win.addstr(3, 2, "Enter value: ", get_color("settings_default"))
input_win.refresh()
# Enable user input
curses.echo()
curses.curs_set(1)
user_input = ""
while True:
key = input_win.getch(3, 15 + len(user_input)) # Adjust cursor position dynamically
if key == 27 or key == curses.KEY_LEFT: # ESC or Left Arrow
curses.noecho()
curses.curs_set(0)
return None # Exit without returning a value
elif key == ord('\n'): # Enter key
break
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace
user_input = user_input[:-1]
input_win.addstr(3, 15, " " * (len(user_input) + 1)) # Clear the line
input_win.addstr(3, 15, user_input)
input_win.addstr(3, 15, " " * (len(user_input) + 1), get_color("settings_default")) # Clear the line
input_win.addstr(3, 15, user_input, get_color("settings_default"))
else:
user_input += chr(key)
input_win.addstr(3, 15, user_input)
input_win.addstr(3, 15, user_input, get_color("settings_default"))
curses.curs_set(0)
curses.noecho()
# Clear the input window
input_win.clear()
@@ -59,18 +57,20 @@ def get_bool_selection(message, current_value):
start_x = (curses.COLS - width) // 2
bool_win = curses.newwin(height, width, start_y, start_x)
bool_win.bkgd(get_color("background"))
bool_win.attrset(get_color("window_frame"))
bool_win.keypad(True)
while True:
bool_win.clear()
bool_win.border()
bool_win.addstr(1, 2, message, curses.A_BOLD)
bool_win.addstr(1, 2, message, get_color("settings_default", bold=True))
for idx, option in enumerate(options):
if idx == selected_index:
bool_win.addstr(idx + 3, 4, option, curses.A_REVERSE)
bool_win.addstr(idx + 3, 4, option, get_color("settings_default", reverse=True))
else:
bool_win.addstr(idx + 3, 4, option)
bool_win.addstr(idx + 3, 4, option, get_color("settings_default"))
bool_win.refresh()
key = bool_win.getch()
@@ -92,6 +92,8 @@ def get_repeated_input(current_value):
start_x = (curses.COLS - width) // 2
repeated_win = curses.newwin(height, width, start_y, start_x)
repeated_win.bkgd(get_color("background"))
repeated_win.attrset(get_color("window_frame"))
repeated_win.keypad(True) # Enable keypad for special keys
curses.echo()
@@ -101,9 +103,9 @@ def get_repeated_input(current_value):
while True:
repeated_win.clear()
repeated_win.border()
repeated_win.addstr(1, 2, "Enter comma-separated values:", curses.A_BOLD)
repeated_win.addstr(3, 2, f"Current: {', '.join(current_value)}")
repeated_win.addstr(5, 2, f"New value: {user_input}")
repeated_win.addstr(1, 2, "Enter comma-separated values:", get_color("settings_default", bold=True))
repeated_win.addstr(3, 2, f"Current: {', '.join(map(str, current_value))}", get_color("settings_default"))
repeated_win.addstr(5, 2, f"New value: {user_input}", get_color("settings_default"))
repeated_win.refresh()
key = repeated_win.getch()
@@ -133,18 +135,20 @@ def get_enum_input(options, current_value):
start_x = (curses.COLS - width) // 2
enum_win = curses.newwin(height, width, start_y, start_x)
enum_win.bkgd(get_color("background"))
enum_win.attrset(get_color("window_frame"))
enum_win.keypad(True)
while True:
enum_win.clear()
enum_win.border()
enum_win.addstr(1, 2, "Select an option:", curses.A_BOLD)
enum_win.addstr(1, 2, "Select an option:", get_color("settings_default", bold=True))
for idx, option in enumerate(options):
if idx == selected_index:
enum_win.addstr(idx + 2, 4, option, curses.A_REVERSE)
enum_win.addstr(idx + 2, 4, option, get_color("settings_default", reverse=True))
else:
enum_win.addstr(idx + 2, 4, option)
enum_win.addstr(idx + 2, 4, option, get_color("settings_default"))
enum_win.refresh()
key = enum_win.getch()
@@ -168,6 +172,8 @@ def get_fixed32_input(current_value):
start_x = (curses.COLS - width) // 2
fixed32_win = curses.newwin(height, width, start_y, start_x)
fixed32_win.bkgd(get_color("background"))
fixed32_win.attrset(get_color("window_frame"))
fixed32_win.keypad(True)
curses.echo()

View File

@@ -3,7 +3,7 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.0.1
V 1.1.2
'''
import curses
@@ -19,6 +19,7 @@ from ui.curses_ui import main_ui, draw_splash
from utilities.utils import get_channels, get_node_list, get_nodeNum
from db_handler import init_nodedb, load_messages_from_db
import globals
import default_config as config
# Set environment variables for ncurses compatibility
os.environ["NCURSES_NO_UTF8_ACS"] = "1"
@@ -28,7 +29,7 @@ os.environ["LANG"] = "C.UTF-8"
# Configure logging
# Run `tail -f client.log` in another terminal to view live
logging.basicConfig(
filename=globals.log_file_path,
filename=config.log_file_path,
level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
format="%(asctime)s - %(levelname)s - %(message)s"
)

View File

@@ -1,17 +1,22 @@
import logging
import time
from meshtastic import BROADCAST_NUM
from utilities.utils import get_node_list, decimal_to_hex, get_name_from_number
import globals
from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification
from db_handler import save_message_to_db, maybe_store_nodeinfo_in_db
import default_config as config
from datetime import datetime
def on_receive(packet, interface):
global nodes_win
# update packet log
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# trim buffer to 20 packets
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
@@ -20,10 +25,9 @@ def on_receive(packet, interface):
if 'decoded' not in packet:
return
# Assume any incoming packet could update the last seen time for a node, so we
# may need to reorder the list. This could probably be limited to specific packets.
# Assume any incoming packet could update the last seen time for a node
new_node_list = get_node_list()
if(new_node_list != globals.node_list):
if new_node_list != globals.node_list:
globals.node_list = new_node_list
draw_node_list()
@@ -42,6 +46,7 @@ def on_receive(packet, interface):
channel_number = packet['channel']
else:
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
@@ -65,15 +70,35 @@ def on_receive(packet, interface):
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
globals.all_messages[globals.channel_list[channel_number]].append((f"{globals.message_prefix} {message_from_string} ", message_string))
# Timestamp handling
current_timestamp = time.time()
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
if(refresh_channels):
# 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
# 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))
if refresh_channels:
draw_channel_list()
if(refresh_messages):
draw_messages_window()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
except KeyError as e:
print(f"Error processing packet: {e}")
logging.error(f"Error processing packet: {e}")

View File

@@ -1,9 +1,11 @@
from datetime import datetime
from meshtastic import BROADCAST_NUM
from db_handler import save_message_to_db, update_ack_nak
from meshtastic.protobuf import mesh_pb2, portnums_pb2
from utilities.utils import get_name_from_number
import globals
import google.protobuf.json_format
import default_config as config
ack_naks = {}
@@ -22,16 +24,16 @@ def onAckNak(packet):
ack_type = None
if(packet['decoded']['routing']['errorReason'] == "NONE"):
if(packet['from'] == globals.myNodeNum): # Ack "from" ourself means implicit ACK
confirm_string = globals.ack_implicit_str
confirm_string = config.ack_implicit_str
ack_type = "Implicit"
else:
confirm_string = globals.ack_str
confirm_string = config.ack_str
ack_type = "Ack"
else:
confirm_string = globals.nak_str
confirm_string = config.nak_str
ack_type = "Nak"
globals.all_messages[acknak['channel']][acknak['messageIndex']] = (globals.sent_message_prefix + confirm_string + ": ", message)
globals.all_messages[acknak['channel']][acknak['messageIndex']] = (config.sent_message_prefix + confirm_string + ": ", message)
update_ack_nak(acknak['channel'], acknak['timestamp'], message, ack_type)
@@ -104,16 +106,16 @@ def on_response_traceroute(packet):
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
globals.all_messages[globals.channel_list[channel_number]].append((f"{globals.message_prefix} {message_from_string}", msg_str))
globals.all_messages[globals.channel_list[channel_number]].append((f"{config.message_prefix} {message_from_string}", msg_str))
if(refresh_channels):
if refresh_channels:
draw_channel_list()
if(refresh_messages):
draw_messages_window()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], packet['from'], msg_str)
def send_message(message, destination=BROADCAST_NUM, channel=0):
def send_message(message, destination=BROADCAST_NUM, channel=0):
myid = globals.myNodeNum
send_on_channel = 0
channel_id = globals.channel_list[channel]
@@ -136,11 +138,32 @@ def send_message(message, destination=BROADCAST_NUM, channel=0):
if channel_id not in globals.all_messages:
globals.all_messages[channel_id] = []
globals.all_messages[channel_id].append((globals.sent_message_prefix + globals.ack_unknown_str + ": ", message))
# Handle timestamp logic
current_timestamp = int(datetime.now().timestamp()) # Get current 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
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} --", ""))
globals.all_messages[channel_id].append((config.sent_message_prefix + config.ack_unknown_str + ": ", 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}
def send_traceroute():
r = mesh_pb2.RouteDiscovery()

View File

@@ -40,20 +40,20 @@ def save_changes(interface, menu_path, modified_settings):
elif menu_path[1] == "User Settings": # for user configs
config_category = "User Settings"
long_name = modified_settings.get("longName", None)
short_name = modified_settings.get("shortName", None)
#TODO add is_licensed
node.setOwner(long_name, short_name, is_licensed=False)
logging.info(f"Updated {config_category} with Long Name: {long_name} and Short Name {short_name}")
long_name = modified_settings.get("longName")
short_name = modified_settings.get("shortName")
is_licensed = modified_settings.get("isLicensed")
is_licensed = is_licensed == "True" or is_licensed is True
node.setOwner(long_name, short_name, is_licensed)
logging.info(f"Updated {config_category} with Long Name: {long_name} and Short Name {short_name} and Licensed Mode {is_licensed}")
return
elif menu_path[1] == "Channels": # for channel configs
config_category = "Channels"
try:
channel = menu_path[-1]
channel_num = int(channel.split()[-1])
channel_num = int(channel.split()[-1]) - 1
except (IndexError, ValueError) as e:
channel_num = None
@@ -80,8 +80,6 @@ def save_changes(interface, menu_path, modified_settings):
else:
config_category = None
for config_item, new_value in modified_settings.items():
# Check if the category exists in localConfig
if hasattr(node.localConfig, config_category):

View File

@@ -1,14 +1,17 @@
import curses
import meshtastic.serial_interface
from save_to_radio import settings_factory_reset, settings_reboot, settings_reset_nodedb, settings_shutdown
from ui.menus import generate_menu_from_protobuf
from input_handlers import get_bool_selection, get_repeated_input, get_user_input, get_enum_input, get_fixed32_input
from save_to_radio import save_changes
from ui.colors import setup_colors
import logging
from save_to_radio import settings_factory_reset, settings_reboot, settings_reset_nodedb, settings_shutdown, save_changes
from ui.menus import generate_menu_from_protobuf
from input_handlers import get_bool_selection, get_repeated_input, get_user_input, get_enum_input, get_fixed32_input
from ui.colors import setup_colors, get_color
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
import globals
width = 60
save_option = "Save Changes"
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
def display_menu(current_menu, menu_path, selected_index, show_save_option):
global menu_win
@@ -16,13 +19,14 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option):
# Calculate the dynamic height based on the number of menu items
num_items = len(current_menu) + (1 if show_save_option else 0) # Add 1 for the "Save Changes" option if applicable
height = min(curses.LINES - 2, num_items + 5) # Ensure the menu fits within the terminal height
width = 60
start_y = (curses.LINES - height) // 2
start_x = (curses.COLS - width) // 2
# Create a new curses window with dynamic dimensions
menu_win = curses.newwin(height, width, start_y, start_x)
menu_win.clear()
menu_win.bkgd(get_color("background"))
menu_win.attrset(get_color("window_frame"))
menu_win.border()
menu_win.keypad(True)
@@ -30,7 +34,7 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option):
header = " > ".join(word.title() for word in menu_path)
if len(header) > width - 4:
header = header[:width - 7] + "..."
menu_win.addstr(1, 2, header, curses.A_BOLD)
menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True))
# Display the menu options
for idx, option in enumerate(current_menu):
@@ -41,27 +45,38 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option):
try:
# Use red color for "Reboot" or "Shutdown"
color = curses.color_pair(5) if option in ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"] else curses.color_pair(1)
if idx == selected_index:
menu_win.addstr(idx + 3, 4, f"{display_option:<{width // 2 - 2}} {display_value}", curses.A_REVERSE | color)
else:
menu_win.addstr(idx + 3, 4, f"{display_option:<{width // 2 - 2}} {display_value}", color)
color = get_color("settings_sensitive" if option in sensitive_settings else "settings_default", reverse = (idx == selected_index))
menu_win.addstr(idx + 3, 4, f"{display_option:<{width // 2 - 2}} {display_value}".ljust(width - 8), color)
except curses.error:
pass
# Show save option if applicable
if show_save_option:
save_option = "Save Changes"
save_position = height - 2
if selected_index == len(current_menu):
menu_win.addstr(save_position, (width - len(save_option)) // 2, save_option, curses.color_pair(2) | curses.A_REVERSE)
else:
menu_win.addstr(save_position, (width - len(save_option)) // 2, save_option, curses.color_pair(2))
menu_win.addstr(save_position, (width - len(save_option)) // 2, save_option, get_color("settings_save", reverse = (selected_index == len(current_menu))))
menu_win.refresh()
def settings_menu(sdscr, interface):
def move_highlight(old_idx, new_idx, options, show_save_option, menu_win):
if(old_idx == new_idx): # no-op
return
max_index = len(options) + (1 if show_save_option else 0) - 1
if show_save_option and old_idx == max_index: # special case un-highlight "Save" option
menu_win.chgat(max_index + 4, (width - len(save_option)) // 2, len(save_option), get_color("settings_save"))
else:
menu_win.chgat(old_idx + 3, 4, width - 8, get_color("settings_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
if show_save_option and new_idx == max_index: # special case highlight "Save" option
menu_win.chgat(max_index + 4, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse = True))
else:
menu_win.chgat(new_idx + 3, 4, width - 8, get_color("settings_sensitive", reverse=True) if options[new_idx] in sensitive_settings else get_color("settings_default", reverse = True))
menu_win.refresh()
def settings_menu(stdscr, interface):
menu = generate_menu_from_protobuf(interface)
current_menu = menu["Main Menu"]
@@ -69,31 +84,42 @@ def settings_menu(sdscr, interface):
selected_index = 0
modified_settings = {}
need_redraw = True
show_save_option = False
while True:
options = list(current_menu.keys())
if(need_redraw):
options = list(current_menu.keys())
show_save_option = (
len(menu_path) > 2 and ("Radio Settings" in menu_path or "Module Settings" in menu_path)
) or (
len(menu_path) == 2 and "User Settings" in menu_path
) or (
len(menu_path) == 3 and "Channels" in menu_path
)
show_save_option = (
len(menu_path) > 2 and ("Radio Settings" in menu_path or "Module Settings" in menu_path)
) or (
len(menu_path) == 2 and "User Settings" in menu_path
) or (
len(menu_path) == 3 and "Channels" in menu_path
)
# Display the menu
display_menu(current_menu, menu_path, selected_index, show_save_option)
# Display the menu
display_menu(current_menu, menu_path, selected_index, show_save_option)
need_redraw = False
# Capture user input
key = menu_win.getch()
if key == curses.KEY_UP:
old_selected_index = selected_index
selected_index = max(0, selected_index - 1)
move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win)
elif key == curses.KEY_DOWN:
old_selected_index = selected_index
max_index = len(options) + (1 if show_save_option else 0) - 1
selected_index = min(max_index, selected_index + 1)
move_highlight(old_selected_index, selected_index, options, show_save_option, menu_win)
elif key == curses.KEY_RIGHT or key == ord('\n'):
need_redraw = True
menu_win.clear()
menu_win.refresh()
if show_save_option and selected_index == len(options):
@@ -119,64 +145,59 @@ def settings_menu(sdscr, interface):
if confirmation == "True":
settings_reboot(interface)
logging.info(f"Node Reboot Requested by menu")
break
continue
elif selected_option == "Reset Node DB":
confirmation = get_bool_selection("Are you sure you want to Reset Node DB?", 0)
if confirmation == "True":
settings_reset_nodedb(interface)
logging.info(f"Node DB Reset Requested by menu")
break
continue
elif selected_option == "Shutdown":
confirmation = get_bool_selection("Are you sure you want to Shutdown?", 0)
if confirmation == "True":
settings_shutdown(interface)
logging.info(f"Node Shutdown Requested by menu")
break
continue
elif selected_option == "Factory Reset":
confirmation = get_bool_selection("Are you sure you want to Factory Reset?", 0)
if confirmation == "True":
settings_factory_reset(interface)
logging.info(f"Factory Reset Requested by menu")
break
continue
field_info = current_menu.get(selected_option)
field_info = current_menu.get(selected_option)
if isinstance(field_info, tuple):
field, current_value = field_info
if selected_option == 'longName' or selected_option == 'shortName':
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
modified_settings[selected_option] = (new_value)
current_menu[selected_option] = (field, new_value)
if selected_option in ['longName', 'shortName', 'isLicensed']:
if selected_option in ['longName', 'shortName']:
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
current_menu[selected_option] = (field, new_value)
elif selected_option == 'isLicensed':
new_value = get_bool_selection(f"Current value for {selected_option}: {current_value}", str(current_value))
new_value = new_value == "True"
current_menu[selected_option] = (field, new_value)
for option, (field, value) in current_menu.items():
modified_settings[option] = value
elif field.type == 8: # Handle boolean type
new_value = get_bool_selection(selected_option, str(current_value))
try:
# Validate and convert input to a valid boolean
if isinstance(new_value, str):
# Handle string representations of booleans
new_value_lower = new_value.lower()
if new_value_lower in ("true", "yes", "1", "on"):
new_value = True
elif new_value_lower in ("false", "no", "0", "off"):
new_value = False
else:
raise ValueError("Invalid string for boolean")
else:
# Convert other types directly to bool
new_value = bool(new_value)
except ValueError as e:
logging.info(f"Invalid input for boolean: {e}")
new_value = new_value == "True" or new_value is True
elif field.label == field.LABEL_REPEATED: # Handle repeated field
new_value = get_repeated_input(current_value)
new_value = current_value if new_value is None else [int(item) for item in new_value]
elif field.enum_type: # Enum field
enum_options = [v.name for v in field.enum_type.values]
new_value = get_enum_input(enum_options, current_value)
enum_options = {v.name: v.number for v in field.enum_type.values}
new_value_name = get_enum_input(list(enum_options.keys()), current_value)
new_value = enum_options.get(new_value_name, current_value)
elif field.type == 7: # Field type 7 corresponds to FIXED32
new_value = get_fixed32_input(current_value)
@@ -189,20 +210,21 @@ def settings_menu(sdscr, interface):
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
new_value = current_value if new_value is None else float(new_value)
else: # Handle other field types
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
new_value = current_value if new_value is None else new_value
# Navigate to the correct nested dictionary based on the menu_path
current_nested = modified_settings
for key in menu_path[3:]: # Skip "Main Menu"
current_nested = current_nested.setdefault(key, {})
modified_settings = modified_settings.setdefault(key, {})
# Add the new value to the appropriate level
current_nested[selected_option] = new_value
modified_settings[selected_option] = new_value
# Convert enum string to int
if field.enum_type:
enum_value_descriptor = field.enum_type.values_by_number.get(new_value)
new_value = enum_value_descriptor.name if enum_value_descriptor else new_value
# modified_settings[selected_option] = (new_value)
current_menu[selected_option] = (field, new_value)
else:
current_menu = current_menu[selected_option]
@@ -210,6 +232,7 @@ def settings_menu(sdscr, interface):
selected_index = 0
elif key == curses.KEY_LEFT:
need_redraw = True
menu_win.clear()
menu_win.refresh()
@@ -225,8 +248,11 @@ def settings_menu(sdscr, interface):
selected_index = 0
elif key == 27: # Escape key
menu_win.clear()
menu_win.refresh()
break
def main(stdscr):
logging.basicConfig( # Run `tail -f client.log` in another terminal to view live
filename="settings.log",
@@ -237,11 +263,11 @@ def main(stdscr):
curses.curs_set(0)
stdscr.keypad(True)
interface = meshtastic.serial_interface.SerialInterface()
parser = setup_parser()
args = parser.parse_args()
globals.interface = initialize_interface(args)
stdscr.clear()
stdscr.refresh()
settings_menu(stdscr, interface)
settings_menu(stdscr, globals.interface)
if __name__ == "__main__":
curses.wrapper(main)
curses.wrapper(main)

View File

@@ -1,9 +1,38 @@
import curses
import default_config as config
COLOR_MAP = {
"black": curses.COLOR_BLACK,
"red": curses.COLOR_RED,
"green": curses.COLOR_GREEN,
"yellow": curses.COLOR_YELLOW,
"blue": curses.COLOR_BLUE,
"magenta": curses.COLOR_MAGENTA,
"cyan": curses.COLOR_CYAN,
"white": curses.COLOR_WHITE
}
def setup_colors():
"""
Initialize curses color pairs based on the COLOR_CONFIG.
"""
curses.start_color()
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK)
curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK)
for idx, (category, (fg_name, bg_name)) in enumerate(config.COLOR_CONFIG.items(), start=1):
fg = COLOR_MAP.get(fg_name.lower(), curses.COLOR_WHITE)
bg = COLOR_MAP.get(bg_name.lower(), curses.COLOR_BLACK)
curses.init_pair(idx, fg, bg)
config.COLOR_CONFIG[category] = idx
def get_color(category, bold=False, reverse=False, underline=False):
"""
Retrieve a curses color pair with optional attributes.
"""
color = curses.color_pair(config.COLOR_CONFIG[category])
if bold:
color |= curses.A_BOLD
if reverse:
color |= curses.A_REVERSE
if underline:
color |= curses.A_UNDERLINE
return color

View File

@@ -5,30 +5,81 @@ from utilities.utils import get_name_from_number, get_channels
from settings import settings_menu
from message_handlers.tx_handler import send_message, send_traceroute
import ui.dialog
from ui.colors import setup_colors
from ui.colors import setup_colors, get_color
import default_config as config
def get_msg_window_lines():
packetlog_height = packetlog_win.getmaxyx()[0] if globals.display_log else 0
return messages_box.getmaxyx()[0] - 2 - packetlog_height
def refresh_pad(window):
win_height = channel_box.getmaxyx()[0]
selected_item = globals.selected_channel
pad = channel_pad
box = channel_box
lines = box.getmaxyx()[0] - 2
start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders
if(window == 1):
pad = messages_pad
box = messages_box
lines = get_msg_window_lines()
selected_item = globals.selected_message
start_index = globals.selected_message
if(window == 2):
pad = nodes_pad
box = nodes_box
lines = box.getmaxyx()[0] - 2
selected_item = globals.selected_node
start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders
pad.refresh(start_index, 0,
box.getbegyx()[0] + 1, box.getbegyx()[1] + 1,
box.getbegyx()[0] + lines, box.getbegyx()[1] + box.getmaxyx()[1] - 2)
def highlight_line(highlight, window, line):
pad = channel_pad
select_len = 0
ch_color = get_color("channel_list")
nd_color = get_color("node_list")
if(window == 2):
pad = nodes_pad
select_len = len(get_name_from_number(globals.node_list[line], "long"))
pad.chgat(line, 1, select_len, nd_color | curses.A_REVERSE if highlight else nd_color)
if(window == 0):
channel = list(globals.all_messages.keys())[line]
win_width = channel_box.getmaxyx()[1]
if(isinstance(channel, int)):
channel = get_name_from_number(channel, type="long")
select_len = min(len(channel), win_width - 4)
if line == globals.selected_channel and highlight == False:
ch_color = get_color("channel_selected")
pad.chgat(line, 1, select_len, ch_color | curses.A_REVERSE if highlight else ch_color)
def add_notification(channel_number):
handle_notification(channel_number, add=True)
globals.notifications.add(channel_number)
def remove_notification(channel_number):
handle_notification(channel_number, add=False)
channel_win.box()
globals.notifications.discard(channel_number)
def handle_notification(channel_number, add=True):
if add:
globals.notifications.add(channel_number) # Add the channel to the notification tracker
else:
globals.notifications.discard(channel_number) # Remove the channel from the notification tracker
def draw_text_field(win, text):
def draw_text_field(win, text, color):
win.border()
win.addstr(1, 1, text)
win.addstr(1, 1, text, color)
def draw_centered_text_field(win, text, y_offset = 0):
def draw_centered_text_field(win, text, y_offset, color):
height, width = win.getmaxyx()
x = (width - len(text)) // 2
y = (height // 2) + y_offset
win.addstr(y, x, text)
win.addstr(y, x, text, color)
win.refresh()
def draw_debug(value):
@@ -40,6 +91,8 @@ def draw_splash(stdscr):
curses.curs_set(0)
stdscr.clear()
stdscr.bkgd(get_color("background"))
height, width = stdscr.getmaxyx()
message_1 = "/ Λ"
message_2 = "/ / \\"
@@ -49,152 +102,162 @@ def draw_splash(stdscr):
start_x = width // 2 - len(message_1) // 2
start_x2 = width // 2 - len(message_4) // 2
start_y = height // 2 - 1
stdscr.addstr(start_y, start_x, message_1, curses.color_pair(2) | curses.A_BOLD)
stdscr.addstr(start_y+1, start_x-1, message_2, curses.color_pair(2) | curses.A_BOLD)
stdscr.addstr(start_y+2, start_x-2, message_3, curses.color_pair(2) | curses.A_BOLD)
stdscr.addstr(start_y+4, start_x2, message_4)
stdscr.addstr(start_y, start_x, message_1, get_color("splash_logo", bold=True))
stdscr.addstr(start_y+1, start_x-1, message_2, get_color("splash_logo", bold=True))
stdscr.addstr(start_y+2, start_x-2, message_3, get_color("splash_logo", bold=True))
stdscr.addstr(start_y+4, start_x2, message_4, get_color("splash_text"))
stdscr.attrset(get_color("window_frame"))
stdscr.box()
stdscr.refresh()
curses.napms(500)
def draw_channel_list():
channel_win.clear()
win_height, win_width = channel_win.getmaxyx()
channel_pad.clear()
win_height, win_width = channel_box.getmaxyx()
start_index = max(0, globals.selected_channel - (win_height - 3)) # Leave room for borders
for i, channel in enumerate(list(globals.all_messages.keys())[start_index:], start=0):
channel_pad.resize(len(globals.all_messages), channel_box.getmaxyx()[1])
for i, channel in enumerate(list(globals.all_messages.keys())):
# Convert node number to long name if it's an integer
if isinstance(channel, int):
channel = get_name_from_number(channel, type='long')
# Determine whether to add the notification
notification = " " + globals.notification_symbol if start_index + i in globals.notifications else ""
notification = " " + config.notification_symbol if i in globals.notifications else ""
# Truncate the channel name if it's too long to fit in the window
truncated_channel = channel[:win_width - 5] + '-' if len(channel) > win_width - 5 else channel
if i < win_height - 2 : # Check if there is enough space in the window
if start_index + i == globals.selected_channel and globals.current_window == 0:
channel_win.addstr(i + 1, 1, truncated_channel + notification, curses.color_pair(1) | curses.A_REVERSE)
if i == globals.selected_channel:
if globals.current_window == 0:
channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_list", reverse=True))
remove_notification(globals.selected_channel)
else:
channel_win.addstr(i + 1, 1, truncated_channel + notification, curses.color_pair(1))
channel_win.box()
channel_win.refresh()
channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_selected"))
else:
channel_pad.addstr(i, 1, truncated_channel + notification, get_color("channel_list"))
channel_box.attrset(get_color("window_frame_selected") if globals.current_window == 0 else get_color("window_frame"))
channel_box.box()
channel_box.attrset((get_color("window_frame")))
channel_box.refresh()
def draw_messages_window():
refresh_pad(0)
def draw_messages_window(scroll_to_bottom = False):
"""Update the messages window based on the selected channel and scroll position."""
messages_win.clear()
messages_pad.clear()
channel = globals.channel_list[globals.selected_channel]
if channel in globals.all_messages:
messages = globals.all_messages[channel]
num_messages = len(messages)
max_messages = messages_win.getmaxyx()[0] - 2 # Max messages that fit in the window
# Adjust for packetlog height if log is visible
if globals.display_log:
packetlog_height = packetlog_win.getmaxyx()[0]
max_messages -= packetlog_height - 1
if max_messages < 1:
max_messages = 1
msg_line_count = 0
# Calculate the scroll position based on the current selection
max_scroll_position = max(0, num_messages - max_messages)
start_index = max(0, min(globals.selected_message, max_scroll_position))
# Dynamically calculate max_messages based on visible messages and wraps
row = 1
visible_message_count = 0
for index, (prefix, message) in enumerate(messages[start_index:], start=start_index):
row = 0
for (prefix, message) in messages:
full_message = f"{prefix}{message}"
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
if row + len(wrapped_lines) - 1 > messages_win.getmaxyx()[0] - 2: # Check if it fits in the window
break
visible_message_count += 1
row += len(wrapped_lines)
# Adjust max_messages to match visible messages
max_messages = visible_message_count
# Re-render the visible messages
row = 1
for index, (prefix, message) in enumerate(messages[start_index:start_index + max_messages], start=start_index):
full_message = f"{prefix}{message}"
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
wrapped_lines = textwrap.wrap(full_message, messages_box.getmaxyx()[1] - 2)
msg_line_count += len(wrapped_lines)
messages_pad.resize(msg_line_count, messages_box.getmaxyx()[1])
for line in wrapped_lines:
# Highlight the row if it's the selected message
if index == globals.selected_message and globals.current_window == 1:
color = curses.A_REVERSE # Highlighted row color
if prefix.startswith("--"):
color = get_color("timestamps")
elif prefix.startswith(config.sent_message_prefix):
color = get_color("tx_messages")
else:
color = curses.color_pair(4) if prefix.startswith(globals.sent_message_prefix) else curses.color_pair(3)
messages_win.addstr(row, 1, line, color)
color = get_color("rx_messages")
messages_pad.addstr(row, 1, line, color)
row += 1
messages_win.box()
messages_win.refresh()
messages_box.attrset(get_color("window_frame_selected") if globals.current_window == 1 else get_color("window_frame"))
messages_box.box()
messages_box.attrset(get_color("window_frame"))
messages_box.refresh()
if(scroll_to_bottom):
globals.selected_message = max(msg_line_count - get_msg_window_lines(), 0)
else:
globals.selected_message = max(min(globals.selected_message, msg_line_count - get_msg_window_lines()), 0)
refresh_pad(1)
draw_packetlog_win()
def draw_node_list():
nodes_win.clear()
win_height = nodes_win.getmaxyx()[0]
nodes_pad.clear()
win_height = nodes_box.getmaxyx()[0]
start_index = max(0, globals.selected_node - (win_height - 3)) # Calculate starting index based on selected node and window height
for i, node in enumerate(globals.node_list[start_index:], start=1):
if i < win_height - 1 : # Check if there is enough space in the window
if globals.selected_node + 1 == start_index + i and globals.current_window == 2:
nodes_win.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1) | curses.A_REVERSE)
else:
nodes_win.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1))
nodes_pad.resize(len(globals.node_list), nodes_box.getmaxyx()[1])
nodes_win.box()
nodes_win.refresh()
for i, node in enumerate(globals.node_list):
if globals.selected_node == i and globals.current_window == 2:
nodes_pad.addstr(i, 1, get_name_from_number(node, "long"), get_color("node_list", reverse=True))
else:
nodes_pad.addstr(i, 1, get_name_from_number(node, "long"), get_color("node_list"))
nodes_box.attrset(get_color("window_frame_selected") if globals.current_window == 2 else get_color("window_frame"))
nodes_box.box()
nodes_box.attrset(get_color("window_frame"))
nodes_box.refresh()
def select_channels(direction):
channel_list_length = len(globals.channel_list)
globals.selected_channel += direction
refresh_pad(2)
if globals.selected_channel < 0:
globals.selected_channel = channel_list_length - 1
elif globals.selected_channel >= channel_list_length:
globals.selected_channel = 0
def select_channel(idx):
old_selected_channel = globals.selected_channel
globals.selected_channel = max(0, min(idx, len(globals.channel_list) - 1))
draw_messages_window(True)
draw_channel_list()
draw_messages_window()
# For now just re-draw channel list when clearing notifications, we can probably make this more efficient
if globals.selected_channel in globals.notifications:
remove_notification(globals.selected_channel)
draw_channel_list()
return
highlight_line(False, 0, old_selected_channel)
highlight_line(True, 0, globals.selected_channel)
refresh_pad(0)
def select_messages(direction):
messages_length = len(globals.all_messages[globals.channel_list[globals.selected_channel]])
def scroll_channels(direction):
new_selected_channel = globals.selected_channel + direction
if new_selected_channel < 0:
new_selected_channel = len(globals.channel_list) - 1
elif new_selected_channel >= len(globals.channel_list):
new_selected_channel = 0
select_channel(new_selected_channel)
def scroll_messages(direction):
globals.selected_message += direction
if globals.selected_message < 0:
globals.selected_message = messages_length - 1
elif globals.selected_message >= messages_length:
globals.selected_message = 0
msg_line_count = messages_pad.getmaxyx()[0]
globals.selected_message = max(0, min(globals.selected_message, msg_line_count - get_msg_window_lines()))
draw_messages_window()
refresh_pad(1)
def select_nodes(direction):
node_list_length = len(globals.node_list)
globals.selected_node += direction
def select_node(idx):
old_selected_node = globals.selected_node
globals.selected_node = max(0, min(idx, len(globals.node_list) - 1))
if globals.selected_node < 0:
globals.selected_node = node_list_length - 1
elif globals.selected_node >= node_list_length:
globals.selected_node = 0
highlight_line(False, 2, old_selected_node)
highlight_line(True, 2, globals.selected_node)
refresh_pad(2)
draw_node_list()
def scroll_nodes(direction):
new_selected_node = globals.selected_node + direction
if new_selected_node < 0:
new_selected_node = len(globals.node_list) - 1
elif new_selected_node >= len(globals.node_list):
new_selected_node = 0
select_node(new_selected_node)
def draw_packetlog_win():
@@ -210,7 +273,7 @@ def draw_packetlog_win():
# Add headers
headers = f"{'From':<{columns[0]}} {'To':<{columns[1]}} {'Port':<{columns[2]}} {'Payload':<{width-span}}"
packetlog_win.addstr(1, 1, headers[:width - 2],curses.A_UNDERLINE) # Truncate headers if they exceed window width
packetlog_win.addstr(1, 1, headers[:width - 2],get_color("log_header", underline=True)) # Truncate headers if they exceed window width
for i, packet in enumerate(reversed(globals.packet_buffer)):
if i >= height - 3: # Skip if exceeds the window height
@@ -234,14 +297,15 @@ def draw_packetlog_win():
logString = logString[:width - 3]
# Add to the window
packetlog_win.addstr(i + 2, 1, logString)
packetlog_win.addstr(i + 2, 1, logString, get_color("log"))
packetlog_win.attrset(get_color("window_frame"))
packetlog_win.box()
packetlog_win.refresh()
def main_ui(stdscr):
global messages_win, nodes_win, channel_win, function_win, packetlog_win
global messages_pad, messages_box, nodes_pad, nodes_box, channel_pad, channel_box, function_win, packetlog_win
stdscr.keypad(True)
get_channels()
@@ -254,68 +318,128 @@ def main_ui(stdscr):
nodes_width = 5 * (width // 16)
messages_width = width - channel_width - nodes_width
channel_win = curses.newwin(height - 6, channel_width, 3, 0)
messages_win = curses.newwin(height - 6, messages_width, 3, channel_width)
packetlog_win = curses.newwin(int(height / 3), messages_width, height - int(height / 3) - 3, channel_width)
nodes_win = curses.newwin(height - 6, nodes_width, 3, channel_width + messages_width)
channel_box = curses.newwin(height - 6, channel_width, 3, 0)
messages_box = curses.newwin(height - 6, messages_width, 3, channel_width)
nodes_box = curses.newwin(height - 6, nodes_width, 3, channel_width + messages_width)
entry_win.bkgd(get_color("background"))
channel_box.bkgd(get_color("background"))
messages_box.bkgd(get_color("background"))
nodes_box.bkgd(get_color("background"))
# Will be resized to what we need when drawn
messages_pad = curses.newpad(1, 1)
nodes_pad = curses.newpad(1,1)
channel_pad = curses.newpad(1,1)
messages_pad.bkgd(get_color("background"))
nodes_pad.bkgd(get_color("background"))
channel_pad.bkgd(get_color("background"))
function_win = curses.newwin(3, width, height - 3, 0)
packetlog_win = curses.newwin(int(height / 3), messages_width, height - int(height / 3) - 3, channel_width)
draw_centered_text_field(function_win, f"↑→↓← = Select ENTER = Send ` = Settings / = Toggle Log ESC = Quit")
function_win.bkgd(get_color("background"))
packetlog_win.bkgd(get_color("background"))
# Enable scrolling for messages and nodes windows
messages_win.scrollok(True)
nodes_win.scrollok(True)
channel_win.scrollok(True)
draw_channel_list()
draw_node_list()
draw_messages_window()
draw_centered_text_field(function_win, f"↑→↓← = Select ENTER = Send ` = Settings ^P = Packet Log ESC = Quit",0 ,get_color("commands"))
# Draw boxes around windows
channel_win.box()
# Set the normal frame color for the channel box
channel_box.attrset(get_color("window_frame"))
channel_box.box()
# Draw boxes for other windows
entry_win.attrset(get_color("window_frame"))
entry_win.box()
messages_win.box()
nodes_win.box()
function_win.box()
nodes_box.attrset(get_color("window_frame"))
nodes_box.box()
messages_box.attrset(get_color("window_frame"))
messages_box.box()
function_win.attrset(get_color("window_frame"))
function_win.box()
# Refresh all windows
entry_win.refresh()
messages_win.refresh()
nodes_win.refresh()
channel_win.refresh()
function_win.refresh()
channel_box.refresh()
function_win.refresh()
nodes_box.refresh()
messages_box.refresh()
input_text = ""
entry_win.keypad(True)
curses.curs_set(1)
draw_channel_list()
draw_node_list()
draw_messages_window(True)
while True:
draw_text_field(entry_win, f"Input: {input_text}")
draw_text_field(entry_win, f"Input: {input_text[-(width - 10):]}", get_color("input"))
# Get user input from entry window
entry_win.move(1, len(input_text) + 8)
char = entry_win.getch()
char = entry_win.get_wch()
# draw_debug(f"Keypress: {char}")
if char == curses.KEY_UP:
if globals.current_window == 0:
select_channels(-1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
scroll_channels(-1)
elif globals.current_window == 1:
select_messages(-1)
scroll_messages(-1)
elif globals.current_window == 2:
select_nodes(-1)
scroll_nodes(-1)
elif char == curses.KEY_DOWN:
if globals.current_window == 0:
select_channels(1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
scroll_channels(1)
elif globals.current_window == 1:
select_messages(1)
scroll_messages(1)
elif globals.current_window == 2:
select_nodes(1)
scroll_nodes(1)
elif char == curses.KEY_HOME:
if globals.current_window == 0:
select_channel(0)
elif globals.current_window == 1:
globals.selected_message = 0
refresh_pad(1)
elif globals.current_window == 2:
select_node(0)
elif char == curses.KEY_END:
if globals.current_window == 0:
select_channel(len(globals.channel_list) - 1)
elif globals.current_window == 1:
msg_line_count = messages_pad.getmaxyx()[0]
globals.selected_message = max(msg_line_count - get_msg_window_lines(), 0)
refresh_pad(1)
elif globals.current_window == 2:
select_node(len(globals.node_list) - 1)
elif char == curses.KEY_PPAGE:
if globals.current_window == 0:
select_channel(globals.selected_channel - (channel_box.getmaxyx()[0] - 2)) # select_channel will bounds check for us
elif globals.current_window == 1:
globals.selected_message = max(globals.selected_message - get_msg_window_lines(), 0)
refresh_pad(1)
elif globals.current_window == 2:
select_node(globals.selected_node - (nodes_box.getmaxyx()[0] - 2)) # select_node will bounds check for us
elif char == curses.KEY_NPAGE:
if globals.current_window == 0:
select_channel(globals.selected_channel + (channel_box.getmaxyx()[0] - 2)) # select_channel will bounds check for us
elif globals.current_window == 1:
msg_line_count = messages_pad.getmaxyx()[0]
globals.selected_message = min(globals.selected_message + get_msg_window_lines(), msg_line_count - get_msg_window_lines())
refresh_pad(1)
elif globals.current_window == 2:
select_node(globals.selected_node + (nodes_box.getmaxyx()[0] - 2)) # select_node will bounds check for us
elif char == curses.KEY_LEFT or char == curses.KEY_RIGHT:
delta = -1 if char == curses.KEY_LEFT else 1
@@ -323,25 +447,57 @@ def main_ui(stdscr):
old_window = globals.current_window
globals.current_window = (globals.current_window + delta) % 3
if old_window == 0 or globals.current_window == 0:
draw_channel_list()
if old_window == 1 or globals.current_window == 1:
draw_messages_window()
if old_window == 2 or globals.current_window == 2:
draw_node_list()
if old_window == 0:
channel_box.attrset(get_color("window_frame"))
channel_box.box()
channel_box.refresh()
highlight_line(False, 0, globals.selected_channel)
refresh_pad(0)
if old_window == 1:
messages_box.attrset(get_color("window_frame"))
messages_box.box()
messages_box.refresh()
refresh_pad(1)
elif old_window == 2:
nodes_box.attrset(get_color("window_frame"))
nodes_box.box()
nodes_box.refresh()
highlight_line(False, 2, globals.selected_node)
refresh_pad(2)
if globals.current_window == 0:
channel_box.attrset(get_color("window_frame_selected"))
channel_box.box()
channel_box.attrset(get_color("window_frame"))
channel_box.refresh()
highlight_line(True, 0, globals.selected_channel)
refresh_pad(0)
elif globals.current_window == 1:
messages_box.attrset(get_color("window_frame_selected"))
messages_box.box()
messages_box.attrset(get_color("window_frame"))
messages_box.refresh()
refresh_pad(1)
elif globals.current_window == 2:
nodes_box.attrset(get_color("window_frame_selected"))
nodes_box.box()
nodes_box.attrset(get_color("window_frame"))
nodes_box.refresh()
highlight_line(True, 2, globals.selected_node)
refresh_pad(2)
# Check for Esc
elif char == 27:
elif char == chr(27):
break
# Check for Ctrl + t
elif char == 20:
elif char == chr(20):
send_traceroute()
curses.curs_set(0) # Hide cursor
ui.dialog.dialog(stdscr, "Traceroute Sent", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.")
curses.curs_set(1) # Show cursor again
elif char == curses.KEY_ENTER or char == 10 or char == 13:
elif char in (chr(curses.KEY_ENTER), chr(10), chr(13)):
if globals.current_window == 2:
node_list = globals.node_list
if node_list[globals.selected_node] not in globals.channel_list:
@@ -354,19 +510,19 @@ def main_ui(stdscr):
draw_node_list()
draw_channel_list()
draw_messages_window()
draw_messages_window(True)
else:
elif len(input_text) > 0:
# Enter key pressed, send user input as message
send_message(input_text, channel=globals.selected_channel)
draw_messages_window()
draw_messages_window(True)
# Clear entry window and reset input text
input_text = ""
entry_win.clear()
# entry_win.refresh()
elif char == curses.KEY_BACKSPACE or char == 127:
elif char in (curses.KEY_BACKSPACE, chr(127)):
if input_text:
input_text = input_text[:-1]
y, x = entry_win.getyx()
@@ -375,20 +531,25 @@ def main_ui(stdscr):
entry_win.move(y, x - 1)
entry_win.refresh()
elif char == 96: # ` Launch the settings interface
elif char == "`": # ` Launch the settings interface
curses.curs_set(0)
settings_menu(stdscr, globals.interface)
curses.curs_set(1)
elif char == 47:
elif char == chr(16):
# Display packet log
if globals.display_log is False:
globals.display_log = True
draw_messages_window()
draw_messages_window(True)
else:
globals.display_log = False
packetlog_win.clear()
draw_messages_window()
draw_messages_window(True)
else:
# Append typed character to input text
input_text += chr(char)
if(isinstance(char, str)):
input_text += char
else:
input_text += chr(char)

View File

@@ -61,7 +61,8 @@ def generate_menu_from_protobuf(interface):
menu_structure["Main Menu"]["User Settings"] = {
"longName": (None, current_user_config.get("longName", "Not Set")),
"shortName": (None, current_user_config.get("shortName", "Not Set"))
"shortName": (None, current_user_config.get("shortName", "Not Set")),
"isLicensed": (None, current_user_config.get("isLicensed", "False"))
}
else:

View File

@@ -1,6 +1,5 @@
import meshtastic.serial_interface
import meshtastic.tcp_interface
import meshtastic.ble_interface
import logging
import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface
import globals
def initialize_interface(args):
@@ -12,6 +11,6 @@ def initialize_interface(args):
try:
return meshtastic.serial_interface.SerialInterface(args.port)
except PermissionError as ex:
print("You probably need to add yourself to the `dialout` group to use a serial connection.")
logging.error("You probably need to add yourself to the `dialout` group to use a serial connection.")
if globals.interface.devPath is None:
return meshtastic.tcp_interface.TCPInterface("meshtastic.local")

View File

@@ -57,17 +57,15 @@ def convert_to_camel_case(string):
def get_name_from_number(number, type='long'):
name = ""
for node in globals.interface.nodes.values():
nodes_snapshot = list(globals.interface.nodes.values())
for node in nodes_snapshot:
if number == node['num']:
if type == 'long':
name = node['user']['longName']
return name
return node['user']['longName']
elif type == 'short':
name = node['user']['shortName']
return name
return node['user']['shortName']
else:
pass
else:
name = str(decimal_to_hex(number)) # If long name not found, use the ID as string
return name
# If no match is found, use the ID as a string
return str(decimal_to_hex(number))