forked from iarv/contact
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0554ec1f6 | ||
|
|
bb623d149c | ||
|
|
4a92ad49ce | ||
|
|
9d0470d55b | ||
|
|
e96ea7ffef | ||
|
|
44b1a3071b | ||
|
|
702d20a011 | ||
|
|
fba4642ff8 | ||
|
|
916b0cfe53 | ||
|
|
e086814b83 | ||
|
|
92f08d020e | ||
|
|
86463f6f84 | ||
|
|
e58340fa65 | ||
|
|
c243daf253 | ||
|
|
70f1f5d4bf | ||
|
|
67f1bde217 | ||
|
|
ca17bbee31 | ||
|
|
659dad493c | ||
|
|
4359d37979 | ||
|
|
96612b3e1b | ||
|
|
84246aefd9 | ||
|
|
7cd98a39f8 | ||
|
|
978e2942cb | ||
|
|
92790ddca6 | ||
|
|
d5a6a0462f | ||
|
|
3816a3f166 | ||
|
|
c61dc19319 | ||
|
|
91d331af4f | ||
|
|
be92ac5de3 | ||
|
|
10179c4179 | ||
|
|
6e45caaac2 | ||
|
|
152555156e | ||
|
|
056b8b5f5f | ||
|
|
07f0721fd5 | ||
|
|
cf9276d399 | ||
|
|
482d158b15 | ||
|
|
11bd9c75ed | ||
|
|
1b2701e1a1 | ||
|
|
7fcc9abd76 | ||
|
|
9bfddb954d | ||
|
|
deb231fc63 | ||
|
|
bbe9e66fa5 | ||
|
|
aa7d98b1b0 | ||
|
|
8895784503 | ||
|
|
695b4949c0 | ||
|
|
404bac9133 | ||
|
|
4698b81a3f | ||
|
|
e7e9f24fe2 | ||
|
|
01f67ea8b5 | ||
|
|
c19684c119 | ||
|
|
0b0d8c482b | ||
|
|
f9774b2248 | ||
|
|
d6db1e1832 | ||
|
|
4c85aaecdf | ||
|
|
d8fc02b28a | ||
|
|
039673bb18 | ||
|
|
d81e694ee6 | ||
|
|
dfea291d21 | ||
|
|
41c60a49e9 | ||
|
|
fb3138883f | ||
|
|
767f0e2288 | ||
|
|
43680f8afb | ||
|
|
e15f625716 | ||
|
|
c090b3dd58 | ||
|
|
f472a3040c | ||
|
|
4a0c49b7d6 | ||
|
|
3034a1464a | ||
|
|
80fe10c050 | ||
|
|
0962c5b284 | ||
|
|
1b3abdebf2 | ||
|
|
a710374fe9 | ||
|
|
cb088c51d4 | ||
|
|
ccb46b8553 | ||
|
|
35748d071e | ||
|
|
7e85085b98 | ||
|
|
7493d21c1a | ||
|
|
d0af0e6af1 | ||
|
|
796c40b560 | ||
|
|
1c0704b940 | ||
|
|
6384777bb6 | ||
|
|
2fbaee5fc5 | ||
|
|
06e71331b6 | ||
|
|
fea705a09f | ||
|
|
a47a4a9b32 | ||
|
|
bc72c3b0b6 | ||
|
|
559618a229 | ||
|
|
2abdd763c1 | ||
|
|
89d8b7690f | ||
|
|
d267f41805 | ||
|
|
72939e5988 | ||
|
|
bd5d8aa6e4 | ||
|
|
5899f35833 | ||
|
|
550556df2b | ||
|
|
fac22aee91 | ||
|
|
87e68689f4 | ||
|
|
fe1eaacee9 | ||
|
|
94e7e8f628 | ||
|
|
54ec4009a1 | ||
|
|
9901e5b8a0 | ||
|
|
80a5edb6de | ||
|
|
93664397e8 | ||
|
|
5bd33ed786 | ||
|
|
fae3330bb0 | ||
|
|
5bd91bde25 | ||
|
|
2a7317e612 | ||
|
|
68f13585b6 | ||
|
|
ad81d34551 | ||
|
|
4c4c0d553e | ||
|
|
02c104dcd5 | ||
|
|
96493e5973 | ||
|
|
8a13b60d23 | ||
|
|
7ae4bb7c9d | ||
|
|
36ba9065a2 | ||
|
|
2ad2aa1faa | ||
|
|
342afce9fb | ||
|
|
205a0c547d | ||
|
|
daa94f57a6 | ||
|
|
5e17e8e7d3 | ||
|
|
455c3b10dd |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
client.db
|
||||
.DS_Store
|
||||
client.log
|
||||
settings.log
|
||||
config.json
|
||||
default_config.log
|
||||
|
||||
128
db_handler.py
128
db_handler.py
@@ -1,63 +1,105 @@
|
||||
import sqlite3
|
||||
import globals
|
||||
import time
|
||||
from utilities.utils import get_nodeNum, get_name_from_number
|
||||
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):
|
||||
# Construct the table name
|
||||
table_name = f"{str(globals.myNodeNum)}_{channel}_messages"
|
||||
quoted_table_name = f'"{table_name}"' # Quote the table name becuase we begin with numerics and contain spaces
|
||||
return quoted_table_name
|
||||
|
||||
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()
|
||||
|
||||
# Construct the table name
|
||||
table_name = f"{str(get_nodeNum())}_{channel}_messages"
|
||||
quoted_table_name = f'"{table_name}"' # Quote the table name becuase we begin with numerics and contain spaces
|
||||
quoted_table_name = get_table_name(channel)
|
||||
|
||||
# Ensure the table exists
|
||||
create_table_query = f'''
|
||||
CREATE TABLE IF NOT EXISTS {quoted_table_name} (
|
||||
user_id TEXT,
|
||||
message_text TEXT,
|
||||
timestamp INTEGER
|
||||
timestamp INTEGER,
|
||||
ack_type TEXT
|
||||
)
|
||||
'''
|
||||
|
||||
db_cursor.execute(create_table_query)
|
||||
|
||||
timestamp = int(time.time())
|
||||
|
||||
# Insert the message
|
||||
insert_query = f'''
|
||||
INSERT INTO {quoted_table_name} (user_id, message_text, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO {quoted_table_name} (user_id, message_text, timestamp, ack_type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
'''
|
||||
db_cursor.execute(insert_query, (user_id, message_text, int(time.time())))
|
||||
db_cursor.execute(insert_query, (user_id, message_text, timestamp, None))
|
||||
db_connection.commit()
|
||||
|
||||
return timestamp
|
||||
|
||||
except sqlite3.Error as e:
|
||||
logging.error(f"SQLite error in save_message_to_db: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error in save_message_to_db: {e}")
|
||||
|
||||
def update_ack_nak(channel, timestamp, message, ack):
|
||||
try:
|
||||
with sqlite3.connect(config.db_file_path) as db_connection:
|
||||
db_cursor = db_connection.cursor()
|
||||
update_query = f"""
|
||||
UPDATE {get_table_name(channel)}
|
||||
SET ack_type = ?
|
||||
WHERE user_id = ? AND
|
||||
timestamp = ? AND
|
||||
message_text = ?
|
||||
"""
|
||||
|
||||
db_cursor.execute(update_query, (ack, str(globals.myNodeNum), timestamp, message))
|
||||
db_connection.commit()
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"SQLite error in save_message_to_db: {e}")
|
||||
logging.error(f"SQLite error in update_ack_nak: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in save_message_to_db: {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
|
||||
query = "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ?"
|
||||
db_cursor.execute(query, (f"{str(get_nodeNum())}_%_messages",))
|
||||
db_cursor.execute(query, (f"{str(globals.myNodeNum)}_%_messages",))
|
||||
tables = [row[0] for row in db_cursor.fetchall()]
|
||||
|
||||
# Iterate through each table and fetch its messages
|
||||
for table_name in tables:
|
||||
query = f'SELECT user_id, message_text FROM "{table_name}"'
|
||||
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:
|
||||
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, timestamp, ack_type FROM {quoted_table_name}'
|
||||
|
||||
try:
|
||||
# Fetch all messages from the table
|
||||
db_cursor.execute(query)
|
||||
db_messages = [(row[0], row[1]) 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]
|
||||
@@ -73,31 +115,48 @@ 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 in db_messages:
|
||||
if user_id == str(get_nodeNum()):
|
||||
formatted_message = (f"{globals.sent_message_prefix}: ", 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)
|
||||
# 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
|
||||
|
||||
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
|
||||
table_name = f"{str(get_nodeNum())}_nodedb"
|
||||
table_name = f"{str(globals.myNodeNum)}_nodedb"
|
||||
nodeinfo_table = f'"{table_name}"' # Quote the table name because it might begin with numerics
|
||||
|
||||
# Create the table if it doesn't exist
|
||||
@@ -139,16 +198,16 @@ 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(get_nodeNum())}_nodedb"
|
||||
table_name = f"{str(globals.myNodeNum)}_nodedb"
|
||||
nodeinfo_table = f'"{table_name}"' # Quote the table name becuase we might begin with numerics
|
||||
db_cursor = db_connection.cursor()
|
||||
|
||||
@@ -223,7 +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
183
default_config.py
Normal 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}")
|
||||
15
globals.py
15
globals.py
@@ -1,19 +1,12 @@
|
||||
import os
|
||||
|
||||
app_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
db_file_path = os.path.join(app_directory, "client.db")
|
||||
|
||||
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"
|
||||
notification_symbol = "*"
|
||||
current_window = 0
|
||||
218
input_handlers.py
Normal file
218
input_handlers.py
Normal file
@@ -0,0 +1,218 @@
|
||||
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
|
||||
height = 7 # Fixed height for input prompt
|
||||
width = 60
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
|
||||
# Create a new window for user input
|
||||
input_win = curses.newwin(height, width, start_y, start_x)
|
||||
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, get_color("settings_default", bold=True))
|
||||
input_win.addstr(3, 2, "Enter value: ", get_color("settings_default"))
|
||||
input_win.refresh()
|
||||
|
||||
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.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), 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, get_color("settings_default"))
|
||||
|
||||
curses.curs_set(0)
|
||||
|
||||
# Clear the input window
|
||||
input_win.clear()
|
||||
input_win.refresh()
|
||||
return user_input
|
||||
|
||||
def get_bool_selection(message, current_value):
|
||||
message = "Select True or False:" if None else message
|
||||
cvalue = current_value
|
||||
options = ["True", "False"]
|
||||
selected_index = 0 if current_value == "True" else 1
|
||||
|
||||
height = 7
|
||||
width = 60
|
||||
start_y = (curses.LINES - height) // 2
|
||||
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, get_color("settings_default", bold=True))
|
||||
|
||||
for idx, option in enumerate(options):
|
||||
if idx == selected_index:
|
||||
bool_win.addstr(idx + 3, 4, option, get_color("settings_default", reverse=True))
|
||||
else:
|
||||
bool_win.addstr(idx + 3, 4, option, get_color("settings_default"))
|
||||
|
||||
bool_win.refresh()
|
||||
key = bool_win.getch()
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
selected_index = max(0, selected_index - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
selected_index = min(len(options) - 1, selected_index + 1)
|
||||
elif key == ord('\n'): # Enter key
|
||||
return options[selected_index]
|
||||
elif key == 27 or key == curses.KEY_LEFT: # ESC or Left Arrow
|
||||
return cvalue
|
||||
|
||||
def get_repeated_input(current_value):
|
||||
cvalue = current_value
|
||||
height = 10
|
||||
width = 60
|
||||
start_y = (curses.LINES - height) // 2
|
||||
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()
|
||||
curses.curs_set(1)
|
||||
user_input = ""
|
||||
|
||||
while True:
|
||||
repeated_win.clear()
|
||||
repeated_win.border()
|
||||
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()
|
||||
|
||||
if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
return cvalue # Return the current value without changes
|
||||
elif key == ord('\n'): # Enter key to save and return
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
return user_input.split(",") # Split the input into a list
|
||||
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
||||
user_input = user_input[:-1]
|
||||
else:
|
||||
try:
|
||||
user_input += chr(key) # Append valid character input
|
||||
except ValueError:
|
||||
pass # Ignore invalid character inputs
|
||||
|
||||
def get_enum_input(options, current_value):
|
||||
selected_index = options.index(current_value) if current_value in options else 0
|
||||
|
||||
height = min(len(options) + 4, curses.LINES - 2)
|
||||
width = 60
|
||||
start_y = (curses.LINES - height) // 2
|
||||
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:", get_color("settings_default", bold=True))
|
||||
|
||||
for idx, option in enumerate(options):
|
||||
if idx == selected_index:
|
||||
enum_win.addstr(idx + 2, 4, option, get_color("settings_default", reverse=True))
|
||||
else:
|
||||
enum_win.addstr(idx + 2, 4, option, get_color("settings_default"))
|
||||
|
||||
enum_win.refresh()
|
||||
key = enum_win.getch()
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
selected_index = max(0, selected_index - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
selected_index = min(len(options) - 1, selected_index + 1)
|
||||
elif key == ord('\n'): # Enter key
|
||||
return options[selected_index]
|
||||
elif key == 27 or key == curses.KEY_LEFT: # ESC or Left Arrow
|
||||
return current_value
|
||||
|
||||
|
||||
def get_fixed32_input(current_value):
|
||||
cvalue = current_value
|
||||
current_value = str(ipaddress.IPv4Address(current_value))
|
||||
height = 10
|
||||
width = 60
|
||||
start_y = (curses.LINES - height) // 2
|
||||
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()
|
||||
curses.curs_set(1)
|
||||
user_input = ""
|
||||
|
||||
while True:
|
||||
fixed32_win.clear()
|
||||
fixed32_win.border()
|
||||
fixed32_win.addstr(1, 2, "Enter an IP address (xxx.xxx.xxx.xxx):", curses.A_BOLD)
|
||||
fixed32_win.addstr(3, 2, f"Current: {current_value}")
|
||||
fixed32_win.addstr(5, 2, f"New value: {user_input}")
|
||||
fixed32_win.refresh()
|
||||
|
||||
key = fixed32_win.getch()
|
||||
|
||||
if key == 27 or key == curses.KEY_LEFT: # Escape or Left Arrow to cancel
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
return cvalue # Return the current value unchanged
|
||||
elif key == ord('\n'): # Enter key to validate and save
|
||||
# Validate IP address
|
||||
octets = user_input.split(".")
|
||||
if len(octets) == 4 and all(octet.isdigit() and 0 <= int(octet) <= 255 for octet in octets):
|
||||
curses.noecho()
|
||||
curses.curs_set(0)
|
||||
fixed32_address = ipaddress.ip_address(user_input)
|
||||
return int(fixed32_address) # Return the valid IP address
|
||||
else:
|
||||
fixed32_win.addstr(7, 2, "Invalid IP address. Try again.", curses.A_BOLD | curses.color_pair(5))
|
||||
fixed32_win.refresh()
|
||||
curses.napms(1500) # Wait for 1.5 seconds before refreshing
|
||||
user_input = "" # Clear invalid input
|
||||
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
||||
user_input = user_input[:-1]
|
||||
else:
|
||||
try:
|
||||
char = chr(key)
|
||||
if char.isdigit() or char == ".":
|
||||
user_input += char # Append only valid characters (digits or dots)
|
||||
except ValueError:
|
||||
pass # Ignore invalid inputs
|
||||
47
main.py
47
main.py
@@ -3,37 +3,58 @@
|
||||
'''
|
||||
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
|
||||
from pubsub import pub
|
||||
import os
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from utilities.arg_parser import setup_parser
|
||||
from utilities.interfaces import initialize_interface
|
||||
from message_handlers.rx_handler import on_receive
|
||||
from ui.curses_ui import main_ui, draw_splash
|
||||
from utilities.utils import get_channels
|
||||
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"
|
||||
os.environ["TERM"] = "screen"
|
||||
os.environ["LANG"] = "C.UTF-8"
|
||||
|
||||
def main(stdscr):
|
||||
draw_splash(stdscr)
|
||||
parser = setup_parser()
|
||||
args = parser.parse_args()
|
||||
globals.interface = initialize_interface(args)
|
||||
globals.channel_list = get_channels()
|
||||
pub.subscribe(on_receive, 'meshtastic.receive')
|
||||
init_nodedb()
|
||||
load_messages_from_db()
|
||||
main_ui(stdscr)
|
||||
# Configure logging
|
||||
# Run `tail -f client.log` in another terminal to view live
|
||||
logging.basicConfig(
|
||||
filename=config.log_file_path,
|
||||
level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
def main(stdscr):
|
||||
try:
|
||||
draw_splash(stdscr)
|
||||
parser = setup_parser()
|
||||
args = parser.parse_args()
|
||||
globals.interface = initialize_interface(args)
|
||||
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()
|
||||
main_ui(stdscr)
|
||||
except Exception as e:
|
||||
logging.error("An error occurred: %s", e)
|
||||
logging.error("Traceback: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
curses.wrapper(main)
|
||||
try:
|
||||
curses.wrapper(main)
|
||||
except Exception as e:
|
||||
logging.error("Fatal error in curses wrapper: %s", e)
|
||||
logging.error("Traceback: %s", traceback.format_exc())
|
||||
@@ -1,68 +1,104 @@
|
||||
import logging
|
||||
import time
|
||||
from meshtastic import BROADCAST_NUM
|
||||
from utilities.utils import get_node_list, decimal_to_hex, get_nodeNum
|
||||
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:
|
||||
draw_packetlog_win()
|
||||
try:
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'NODEINFO_APP':
|
||||
if 'decoded' not in packet:
|
||||
return
|
||||
|
||||
# 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:
|
||||
globals.node_list = new_node_list
|
||||
draw_node_list()
|
||||
|
||||
if packet['decoded']['portnum'] == 'NODEINFO_APP':
|
||||
if "user" in packet['decoded'] and "longName" in packet['decoded']["user"]:
|
||||
get_node_list()
|
||||
draw_node_list()
|
||||
maybe_store_nodeinfo_in_db(packet)
|
||||
|
||||
elif 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
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
|
||||
|
||||
if packet.get('channel'):
|
||||
channel_number = packet['channel']
|
||||
else:
|
||||
channel_number = 0
|
||||
myNodeNum = get_nodeNum()
|
||||
if packet['to'] == myNodeNum:
|
||||
|
||||
if packet['to'] == globals.myNodeNum:
|
||||
if packet['from'] in globals.channel_list:
|
||||
pass
|
||||
else:
|
||||
globals.channel_list.append(packet['from'])
|
||||
globals.all_messages[packet['from']] = []
|
||||
draw_channel_list()
|
||||
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
|
||||
|
||||
# Add received message to the messages list
|
||||
message_from_id = packet['from']
|
||||
message_from_string = ""
|
||||
for node in globals.interface.nodes.values():
|
||||
if message_from_id == node['num']:
|
||||
message_from_string = node["user"]["shortName"] + ":" # Get the name using the node ID
|
||||
break
|
||||
else:
|
||||
message_from_string = str(decimal_to_hex(message_from_id)) # If long name not found, use the ID as string
|
||||
|
||||
if globals.channel_list[channel_number] in globals.all_messages:
|
||||
globals.all_messages[globals.channel_list[channel_number]].append((f"{globals.message_prefix} {message_from_string} ", message_string))
|
||||
else:
|
||||
globals.all_messages[globals.channel_list[channel_number]] = [(f"{globals.message_prefix} {message_from_string} ", message_string)]
|
||||
message_from_string = get_name_from_number(message_from_id, type='short') + ":"
|
||||
|
||||
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
|
||||
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(True)
|
||||
|
||||
draw_channel_list()
|
||||
draw_messages_window()
|
||||
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}")
|
||||
@@ -1,7 +1,11 @@
|
||||
from datetime import datetime
|
||||
from meshtastic import BROADCAST_NUM
|
||||
from db_handler import save_message_to_db
|
||||
from utilities.utils import get_nodeNum
|
||||
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 = {}
|
||||
|
||||
@@ -17,21 +21,102 @@ def onAckNak(packet):
|
||||
message = globals.all_messages[acknak['channel']][acknak['messageIndex']][1]
|
||||
|
||||
confirm_string = " "
|
||||
ack_type = None
|
||||
if(packet['decoded']['routing']['errorReason'] == "NONE"):
|
||||
if(packet['from'] == get_nodeNum()): # Ack "from" ourself means implicit ACK
|
||||
confirm_string = "[◌]"
|
||||
if(packet['from'] == globals.myNodeNum): # Ack "from" ourself means implicit ACK
|
||||
confirm_string = config.ack_implicit_str
|
||||
ack_type = "Implicit"
|
||||
else:
|
||||
confirm_string = "[✓]"
|
||||
confirm_string = config.ack_str
|
||||
ack_type = "Ack"
|
||||
else:
|
||||
confirm_string = "[x]"
|
||||
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)
|
||||
|
||||
channel_number = globals.channel_list.index(acknak['channel'])
|
||||
if globals.channel_list[channel_number] == globals.channel_list[globals.selected_channel]:
|
||||
draw_messages_window()
|
||||
|
||||
def on_response_traceroute(packet):
|
||||
"""on response for trace route"""
|
||||
from ui.curses_ui import draw_channel_list, draw_messages_window, add_notification
|
||||
|
||||
refresh_channels = False
|
||||
refresh_messages = False
|
||||
|
||||
UNK_SNR = -128 # Value representing unknown SNR
|
||||
|
||||
route_discovery = mesh_pb2.RouteDiscovery()
|
||||
route_discovery.ParseFromString(packet["decoded"]["payload"])
|
||||
msg_dict = google.protobuf.json_format.MessageToDict(route_discovery)
|
||||
|
||||
msg_str = "Traceroute to:\n"
|
||||
|
||||
route_str = get_name_from_number(packet["to"], 'short') or f"{packet['to']:08x}" # Start with destination of response
|
||||
|
||||
# SNR list should have one more entry than the route, as the final destination adds its SNR also
|
||||
lenTowards = 0 if "route" not in msg_dict else len(msg_dict["route"])
|
||||
snrTowardsValid = "snrTowards" in msg_dict and len(msg_dict["snrTowards"]) == lenTowards + 1
|
||||
if lenTowards > 0: # Loop through hops in route and add SNR if available
|
||||
for idx, node_num in enumerate(msg_dict["route"]):
|
||||
route_str += " --> " + (get_name_from_number(node_num, 'short') or f"{node_num:08x}") \
|
||||
+ " (" + (str(msg_dict["snrTowards"][idx] / 4) if snrTowardsValid and msg_dict["snrTowards"][idx] != UNK_SNR else "?") + "dB)"
|
||||
|
||||
# End with origin of response
|
||||
route_str += " --> " + (get_name_from_number(packet["from"], 'short') or f"{packet['from']:08x}") \
|
||||
+ " (" + (str(msg_dict["snrTowards"][-1] / 4) if snrTowardsValid and msg_dict["snrTowards"][-1] != UNK_SNR else "?") + "dB)"
|
||||
|
||||
msg_str += route_str + "\n" # Print the route towards destination
|
||||
|
||||
# Only if hopStart is set and there is an SNR entry (for the origin) it's valid, even though route might be empty (direct connection)
|
||||
lenBack = 0 if "routeBack" not in msg_dict else len(msg_dict["routeBack"])
|
||||
backValid = "hopStart" in packet and "snrBack" in msg_dict and len(msg_dict["snrBack"]) == lenBack + 1
|
||||
if backValid:
|
||||
msg_str += "Back:\n"
|
||||
route_str = get_name_from_number(packet["from"], 'short') or f"{packet['from']:08x}" # Start with origin of response
|
||||
|
||||
if lenBack > 0: # Loop through hops in routeBack and add SNR if available
|
||||
for idx, node_num in enumerate(msg_dict["routeBack"]):
|
||||
route_str += " --> " + (get_name_from_number(node_num, 'short') or f"{node_num:08x}") \
|
||||
+ " (" + (str(msg_dict["snrBack"][idx] / 4) if msg_dict["snrBack"][idx] != UNK_SNR else "?") + "dB)"
|
||||
|
||||
# End with destination of response (us)
|
||||
route_str += " --> " + (get_name_from_number(packet["to"], 'short') or f"{packet['to']:08x}") \
|
||||
+ " (" + (str(msg_dict["snrBack"][-1] / 4) if msg_dict["snrBack"][-1] != UNK_SNR else "?") + "dB)"
|
||||
|
||||
msg_str += route_str + "\n" # Print the route back to us
|
||||
|
||||
if(packet['from'] not in globals.channel_list):
|
||||
globals.channel_list.append(packet['from'])
|
||||
refresh_channels = True
|
||||
|
||||
channel_number = globals.channel_list.index(packet['from'])
|
||||
|
||||
if globals.channel_list[channel_number] == globals.channel_list[globals.selected_channel]:
|
||||
refresh_messages = True
|
||||
else:
|
||||
add_notification(channel_number)
|
||||
refresh_channels = True
|
||||
|
||||
message_from_string = get_name_from_number(packet['from'], type='short') + ":\n"
|
||||
|
||||
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"{config.message_prefix} {message_from_string}", msg_str))
|
||||
|
||||
if refresh_channels:
|
||||
draw_channel_list()
|
||||
if refresh_messages:
|
||||
draw_messages_window(True)
|
||||
save_message_to_db(globals.channel_list[channel_number], packet['from'], msg_str)
|
||||
|
||||
draw_messages_window()
|
||||
|
||||
def send_message(message, destination=BROADCAST_NUM, channel=0):
|
||||
|
||||
myid = get_nodeNum()
|
||||
myid = globals.myNodeNum
|
||||
send_on_channel = 0
|
||||
channel_id = globals.channel_list[channel]
|
||||
if isinstance(channel_id, int):
|
||||
@@ -50,12 +135,44 @@ def send_message(message, destination=BROADCAST_NUM, channel=0):
|
||||
)
|
||||
|
||||
# Add sent message to the messages dictionary
|
||||
if channel_id in globals.all_messages:
|
||||
globals.all_messages[channel_id].append((globals.sent_message_prefix + "[…]: ", message))
|
||||
if channel_id not in globals.all_messages:
|
||||
globals.all_messages[channel_id] = []
|
||||
|
||||
# Handle timestamp logic
|
||||
current_timestamp = int(datetime.now().timestamp()) # Get current timestamp
|
||||
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
|
||||
|
||||
# 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:
|
||||
globals.all_messages[channel_id] = [(globals.sent_message_prefix + "[…]: ", message)]
|
||||
last_hour = None
|
||||
|
||||
ack_naks[sent_message_data.id] = {'channel' : channel_id, 'messageIndex' : len(globals.all_messages[channel_id]) - 1 }
|
||||
# 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))
|
||||
|
||||
save_message_to_db(channel_id, myid, message)
|
||||
timestamp = save_message_to_db(channel_id, myid, message)
|
||||
|
||||
ack_naks[sent_message_data.id] = {'channel': channel_id, 'messageIndex': len(globals.all_messages[channel_id]) - 1, 'timestamp': timestamp}
|
||||
|
||||
def send_traceroute():
|
||||
r = mesh_pb2.RouteDiscovery()
|
||||
globals.interface.sendData(
|
||||
r,
|
||||
destinationId=globals.node_list[globals.selected_node],
|
||||
portNum=portnums_pb2.PortNum.TRACEROUTE_APP,
|
||||
wantResponse=True,
|
||||
onResponse=on_response_traceroute,
|
||||
channelIndex=0,
|
||||
hopLimit=3,
|
||||
)
|
||||
|
||||
132
save_to_radio.py
Normal file
132
save_to_radio.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from meshtastic.protobuf import channel_pb2
|
||||
from google.protobuf.message import Message
|
||||
import logging
|
||||
import base64
|
||||
|
||||
def settings_reboot(interface):
|
||||
interface.localNode.reboot()
|
||||
|
||||
def settings_reset_nodedb(interface):
|
||||
interface.localNode.resetNodeDb()
|
||||
|
||||
def settings_shutdown(interface):
|
||||
interface.localNode.shutdown()
|
||||
|
||||
def settings_factory_reset(interface):
|
||||
interface.localNode.factoryReset()
|
||||
|
||||
def settings_set_owner(interface, long_name=None, short_name=None, is_licensed=False):
|
||||
if isinstance(is_licensed, str):
|
||||
is_licensed = is_licensed.lower() == 'true'
|
||||
interface.localNode.setOwner(long_name, short_name, is_licensed)
|
||||
|
||||
|
||||
def save_changes(interface, menu_path, modified_settings):
|
||||
"""
|
||||
Save changes to the device based on modified settings.
|
||||
:param interface: Meshtastic interface instance
|
||||
:param menu_path: Current menu path
|
||||
:param modified_settings: Dictionary of modified settings
|
||||
"""
|
||||
try:
|
||||
if not modified_settings:
|
||||
logging.info("No changes to save. modified_settings is empty.")
|
||||
return
|
||||
|
||||
node = interface.getNode('^local')
|
||||
|
||||
if menu_path[1] == "Radio Settings" or menu_path[1] == "Module Settings":
|
||||
config_category = menu_path[2].lower() # for radio and module configs
|
||||
|
||||
elif menu_path[1] == "User Settings": # for user configs
|
||||
config_category = "User Settings"
|
||||
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]) - 1
|
||||
except (IndexError, ValueError) as e:
|
||||
channel_num = None
|
||||
|
||||
channel = node.channels[channel_num]
|
||||
for key, value in modified_settings.items():
|
||||
if key == 'psk': # Special case: decode Base64 for psk
|
||||
channel.settings.psk = base64.b64decode(value)
|
||||
elif key == 'position_precision': # Special case: module_settings
|
||||
channel.settings.module_settings.position_precision = value
|
||||
else:
|
||||
setattr(channel.settings, key, value) # Use setattr for other fields
|
||||
|
||||
if channel_num == 0:
|
||||
channel.role = channel_pb2.Channel.Role.PRIMARY
|
||||
else:
|
||||
channel.role = channel_pb2.Channel.Role.SECONDARY
|
||||
|
||||
node.writeChannel(channel_num)
|
||||
|
||||
logging.info(f"Updated Channel {channel_num} in {config_category}")
|
||||
logging.info(node.channels)
|
||||
return
|
||||
|
||||
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):
|
||||
config_subcategory = getattr(node.localConfig, config_category)
|
||||
# Check if the category exists in moduleConfig
|
||||
elif hasattr(node.moduleConfig, config_category):
|
||||
config_subcategory = getattr(node.moduleConfig, config_category)
|
||||
else:
|
||||
logging.warning(f"Config category '{config_category}' not found in config.")
|
||||
continue
|
||||
|
||||
# Check if the config_item exists in the subcategory
|
||||
if hasattr(config_subcategory, config_item):
|
||||
field = getattr(config_subcategory, config_item)
|
||||
|
||||
try:
|
||||
if isinstance(field, (int, float, str, bool)): # Direct field types
|
||||
setattr(config_subcategory, config_item, new_value)
|
||||
logging.info(f"Updated {config_category}.{config_item} to {new_value}")
|
||||
elif isinstance(field, Message): # Handle protobuf sub-messages
|
||||
if isinstance(new_value, dict): # If new_value is a dictionary
|
||||
for sub_field, sub_value in new_value.items():
|
||||
if hasattr(field, sub_field):
|
||||
setattr(field, sub_field, sub_value)
|
||||
logging.info(f"Updated {config_category}.{config_item}.{sub_field} to {sub_value}")
|
||||
else:
|
||||
logging.warning(f"Sub-field '{sub_field}' not found in {config_category}.{config_item}")
|
||||
else:
|
||||
logging.warning(f"Invalid value for {config_category}.{config_item}. Expected dict.")
|
||||
else:
|
||||
logging.warning(f"Unsupported field type for {config_category}.{config_item}.")
|
||||
except AttributeError as e:
|
||||
logging.error(f"Failed to update {config_category}.{config_item}: {e}")
|
||||
else:
|
||||
logging.warning(f"Config item '{config_item}' not found in config category '{config_category}'.")
|
||||
|
||||
# Write the configuration changes to the node
|
||||
try:
|
||||
node.writeConfig(config_category)
|
||||
logging.info(f"Changes written to config category: {config_category}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to write configuration for category '{config_category}': {e}")
|
||||
|
||||
|
||||
node.writeConfig(config_category)
|
||||
|
||||
logging.info(f"Changes written to config category: {config_category}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving changes: {e}")
|
||||
948
settings.py
948
settings.py
File diff suppressed because it is too large
Load Diff
38
ui/colors.py
Normal file
38
ui/colors.py
Normal file
@@ -0,0 +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()
|
||||
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
|
||||
505
ui/curses_ui.py
505
ui/curses_ui.py
@@ -1,33 +1,85 @@
|
||||
import curses
|
||||
import textwrap
|
||||
import globals
|
||||
from utilities.utils import get_node_list, get_name_from_number, get_channels
|
||||
from settings import settings
|
||||
from message_handlers.tx_handler import send_message
|
||||
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, 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):
|
||||
@@ -35,11 +87,12 @@ def draw_debug(value):
|
||||
function_win.refresh()
|
||||
|
||||
def draw_splash(stdscr):
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) # Green text on black background
|
||||
setup_colors()
|
||||
curses.curs_set(0)
|
||||
|
||||
stdscr.clear()
|
||||
stdscr.bkgd(get_color("background"))
|
||||
|
||||
height, width = stdscr.getmaxyx()
|
||||
message_1 = "/ Λ"
|
||||
message_2 = "/ / \\"
|
||||
@@ -49,136 +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(1) | curses.A_BOLD)
|
||||
stdscr.addstr(start_y+1, start_x-1, message_2, curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(start_y+2, start_x-2, message_3, curses.color_pair(1) | 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(3))
|
||||
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(4))
|
||||
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))
|
||||
|
||||
# Display messages starting from the calculated start index
|
||||
row = 1
|
||||
for index, (prefix, message) in enumerate(messages[start_index:start_index + max_messages], 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)
|
||||
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.color_pair(3) # 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(1) if prefix.startswith(globals.sent_message_prefix) else curses.color_pair(2)
|
||||
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(get_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(3))
|
||||
else:
|
||||
nodes_win.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(4))
|
||||
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(get_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():
|
||||
|
||||
@@ -194,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
|
||||
@@ -218,25 +297,18 @@ 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()
|
||||
|
||||
# Initialize colors
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
||||
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
||||
curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
|
||||
# Calculate window max dimensions
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
@@ -246,88 +318,188 @@ 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_LEFT:
|
||||
globals.current_window = (globals.current_window - 1) % 3
|
||||
draw_channel_list()
|
||||
draw_node_list()
|
||||
draw_messages_window()
|
||||
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_RIGHT:
|
||||
globals.current_window = (globals.current_window + 1) % 3
|
||||
draw_channel_list()
|
||||
draw_node_list()
|
||||
draw_messages_window()
|
||||
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
|
||||
|
||||
old_window = globals.current_window
|
||||
globals.current_window = (globals.current_window + delta) % 3
|
||||
|
||||
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
|
||||
|
||||
elif char == curses.KEY_ENTER or char == 10 or char == 13:
|
||||
|
||||
# Check for Ctrl + t
|
||||
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 in (chr(curses.KEY_ENTER), chr(10), chr(13)):
|
||||
if globals.current_window == 2:
|
||||
node_list = get_node_list()
|
||||
node_list = globals.node_list
|
||||
if node_list[globals.selected_node] not in globals.channel_list:
|
||||
globals.channel_list.append(node_list[globals.selected_node])
|
||||
globals.all_messages[node_list[globals.selected_node]] = []
|
||||
@@ -338,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()
|
||||
@@ -359,20 +531,25 @@ def main_ui(stdscr):
|
||||
entry_win.move(y, x - 1)
|
||||
entry_win.refresh()
|
||||
|
||||
elif char == 96:
|
||||
curses.curs_set(0) # Hide cursor
|
||||
settings(stdscr)
|
||||
curses.curs_set(1) # Show cursor again
|
||||
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)
|
||||
|
||||
|
||||
|
||||
40
ui/dialog.py
Normal file
40
ui/dialog.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import curses
|
||||
|
||||
def dialog(stdscr, title, message):
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Calculate dialog dimensions
|
||||
max_line_lengh = 0
|
||||
message_lines = message.splitlines()
|
||||
for l in message_lines:
|
||||
max_line_length = max(len(l), max_line_lengh)
|
||||
dialog_width = max(len(title) + 4, max_line_length + 4)
|
||||
dialog_height = len(message_lines) + 4
|
||||
x = (width - dialog_width) // 2
|
||||
y = (height - dialog_height) // 2
|
||||
|
||||
# Create dialog window
|
||||
win = curses.newwin(dialog_height, dialog_width, y, x)
|
||||
win.border(0)
|
||||
|
||||
# Add title
|
||||
win.addstr(0, 2, title)
|
||||
|
||||
# Add message
|
||||
for i, l in enumerate(message_lines):
|
||||
win.addstr(2 + i, 2, l)
|
||||
|
||||
# Add button
|
||||
win.addstr(dialog_height - 2, (dialog_width - 4) // 2, " Ok ", curses.color_pair(1) | curses.A_REVERSE)
|
||||
|
||||
# Refresh dialog window
|
||||
win.refresh()
|
||||
|
||||
# Get user input
|
||||
while True:
|
||||
char = win.getch()
|
||||
# Close dialog with enter, space, or esc
|
||||
if char in(curses.KEY_ENTER, 10, 13, 32, 27):
|
||||
win.clear()
|
||||
win.refresh()
|
||||
return
|
||||
94
ui/menus.py
Normal file
94
ui/menus.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from meshtastic.protobuf import config_pb2, module_config_pb2, channel_pb2
|
||||
from save_to_radio import settings_reboot, settings_factory_reset, settings_reset_nodedb, settings_shutdown
|
||||
import logging, traceback
|
||||
|
||||
def extract_fields(message_instance, current_config=None):
|
||||
if isinstance(current_config, dict): # Handle dictionaries
|
||||
return {key: (None, current_config.get(key, "Not Set")) for key in current_config}
|
||||
|
||||
if not hasattr(message_instance, "DESCRIPTOR"):
|
||||
return {}
|
||||
|
||||
menu = {}
|
||||
fields = message_instance.DESCRIPTOR.fields
|
||||
for field in fields:
|
||||
if field.name in {"sessionkey", "channel_num", "id"}: # Skip certain fields
|
||||
continue
|
||||
|
||||
if field.message_type: # Nested message
|
||||
nested_instance = getattr(message_instance, field.name)
|
||||
nested_config = getattr(current_config, field.name, None) if current_config else None
|
||||
menu[field.name] = extract_fields(nested_instance, nested_config)
|
||||
elif field.enum_type: # Handle enum fields
|
||||
current_value = getattr(current_config, field.name, "Not Set") if current_config else "Not Set"
|
||||
if isinstance(current_value, int): # If the value is a number, map it to its name
|
||||
enum_value = field.enum_type.values_by_number.get(current_value)
|
||||
if enum_value: # Check if the enum value exists
|
||||
current_value_name = f"{enum_value.name}"
|
||||
else:
|
||||
current_value_name = f"Unknown ({current_value})"
|
||||
menu[field.name] = (field, current_value_name)
|
||||
else:
|
||||
menu[field.name] = (field, current_value) # Non-integer values
|
||||
else: # Handle other field types
|
||||
current_value = getattr(current_config, field.name, "Not Set") if current_config else "Not Set"
|
||||
menu[field.name] = (field, current_value)
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
def generate_menu_from_protobuf(interface):
|
||||
# Function to generate the menu structure from protobuf messages
|
||||
menu_structure = {"Main Menu": {}}
|
||||
|
||||
# Add Radio Settings
|
||||
radio = config_pb2.Config()
|
||||
current_radio_config = interface.localNode.localConfig if interface else None
|
||||
menu_structure["Main Menu"]["Radio Settings"] = extract_fields(radio, current_radio_config)
|
||||
|
||||
# Add Module Settings
|
||||
module = module_config_pb2.ModuleConfig()
|
||||
current_module_config = interface.localNode.moduleConfig if interface else None
|
||||
menu_structure["Main Menu"]["Module Settings"] = extract_fields(module, current_module_config)
|
||||
|
||||
# Add User Settings
|
||||
current_node_info = interface.getMyNodeInfo() if interface else None
|
||||
|
||||
if current_node_info:
|
||||
|
||||
current_user_config = current_node_info.get("user", None)
|
||||
if current_user_config and isinstance(current_user_config, dict):
|
||||
|
||||
menu_structure["Main Menu"]["User Settings"] = {
|
||||
"longName": (None, current_user_config.get("longName", "Not Set")),
|
||||
"shortName": (None, current_user_config.get("shortName", "Not Set")),
|
||||
"isLicensed": (None, current_user_config.get("isLicensed", "False"))
|
||||
}
|
||||
|
||||
else:
|
||||
logging.info("User settings not found in Node Info")
|
||||
menu_structure["Main Menu"]["User Settings"] = "No user settings available"
|
||||
else:
|
||||
logging.info("Node Info not available")
|
||||
menu_structure["Main Menu"]["User Settings"] = "Node Info not available"
|
||||
|
||||
# Add Channels
|
||||
channel = channel_pb2.ChannelSettings()
|
||||
menu_structure["Main Menu"]["Channels"] = {}
|
||||
if interface:
|
||||
for i in range(8):
|
||||
current_channel = interface.localNode.getChannelByChannelIndex(i)
|
||||
if current_channel:
|
||||
channel_config = extract_fields(channel, current_channel.settings)
|
||||
menu_structure["Main Menu"]["Channels"][f"Channel {i + 1}"] = channel_config
|
||||
|
||||
# Add additional settings options
|
||||
menu_structure["Main Menu"]["Reboot"] = settings_reboot
|
||||
menu_structure["Main Menu"]["Reset Node DB"] = settings_reset_nodedb
|
||||
menu_structure["Main Menu"]["Shutdown"] = settings_shutdown
|
||||
menu_structure["Main Menu"]["Factory Reset"] = settings_factory_reset
|
||||
|
||||
# Add Exit option
|
||||
menu_structure["Main Menu"]["Exit"] = None
|
||||
|
||||
return menu_structure
|
||||
@@ -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")
|
||||
@@ -34,11 +34,13 @@ def get_channels():
|
||||
return globals.channel_list
|
||||
|
||||
def get_node_list():
|
||||
node_list = []
|
||||
if globals.interface.nodes:
|
||||
for node in globals.interface.nodes.values():
|
||||
node_list.append(node['num'])
|
||||
return node_list
|
||||
sorted_nodes = sorted(
|
||||
globals.interface.nodes.values(),
|
||||
key = lambda node: (node['lastHeard'] if ('lastHeard' in node and isinstance(node['lastHeard'], int)) else 0),
|
||||
reverse = True)
|
||||
return [node['num'] for node in sorted_nodes]
|
||||
return []
|
||||
|
||||
def get_nodeNum():
|
||||
myinfo = globals.interface.getMyNodeInfo()
|
||||
@@ -55,27 +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
|
||||
|
||||
def sanitize_string(input_str: str) -> str:
|
||||
"""Check if the string starts with a letter (a-z, A-Z) or an underscore (_), and replace all non-alpha/numeric/underscore characters with underscores."""
|
||||
|
||||
if not re.match(r'^[a-zA-Z_]', input_str):
|
||||
# If not, add "_"
|
||||
input_str = '_' + input_str
|
||||
|
||||
# Replace special characters with underscores (for database tables)
|
||||
sanitized_str: str = re.sub(r'[^a-zA-Z0-9_]', '_', input_str)
|
||||
return sanitized_str
|
||||
# If no match is found, use the ID as a string
|
||||
return str(decimal_to_hex(number))
|
||||
Reference in New Issue
Block a user