Merge pull request #21 from pdxlocations/database

This commit is contained in:
pdxlocations
2025-01-14 06:34:48 -08:00
committed by GitHub
8 changed files with 463 additions and 168 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
.venv/
__pycache__/
__pycache__/
client.db
.DS_Store
+229
View File
@@ -0,0 +1,229 @@
import sqlite3
import globals
import time
from utilities.utils import get_nodeNum, get_name_from_number
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:
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
# Ensure the table exists
create_table_query = f'''
CREATE TABLE IF NOT EXISTS {quoted_table_name} (
user_id TEXT,
message_text TEXT,
timestamp INTEGER
)
'''
db_cursor.execute(create_table_query)
# Insert the message
insert_query = f'''
INSERT INTO {quoted_table_name} (user_id, message_text, timestamp)
VALUES (?, ?, ?)
'''
db_cursor.execute(insert_query, (user_id, message_text, int(time.time())))
db_connection.commit()
except sqlite3.Error as e:
print(f"SQLite error in save_message_to_db: {e}")
except Exception as e:
print(f"Unexpected error in save_message_to_db: {e}")
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:
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",))
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}"'
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
# Extract the channel name from the table name
channel = table_name.split("_")[1]
# Convert the channel to an integer if it's numeric, otherwise keep it as a string
channel = int(channel) if channel.isdigit() else channel
# Add the channel to globals.channel_list if not already present
if channel not in globals.channel_list:
globals.channel_list.append(channel)
# Ensure the channel exists in globals.all_messages
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.message_prefix} Sent: ", 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)
except sqlite3.Error as e:
print(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}")
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:
db_cursor = db_connection.cursor()
# Table name construction
table_name = f"{str(get_nodeNum())}_nodedb"
nodeinfo_table = f'"{table_name}"' # Quote the table name because it might begin with numerics
# Create the table if it doesn't exist
create_table_query = f'''
CREATE TABLE IF NOT EXISTS {nodeinfo_table} (
user_id TEXT PRIMARY KEY,
long_name TEXT,
short_name TEXT,
hw_model TEXT,
is_licensed TEXT,
role TEXT,
public_key TEXT
)
'''
db_cursor.execute(create_table_query)
# Iterate over nodes and insert them into the database
if globals.interface.nodes:
for node in globals.interface.nodes.values():
role = node['user'].get('role', 'CLIENT')
is_licensed = node['user'].get('isLicensed', '0')
public_key = node['user'].get('publicKey', '')
insert_query = f'''
INSERT OR IGNORE INTO {nodeinfo_table} (user_id, long_name, short_name, hw_model, is_licensed, role, public_key)
VALUES (?, ?, ?, ?, ?, ?, ?)
'''
db_cursor.execute(insert_query, (
node['num'],
node['user']['longName'],
node['user']['shortName'],
node['user']['hwModel'],
is_licensed,
role,
public_key
))
db_connection.commit()
except sqlite3.Error as e:
print(f"SQLite error in init_nodedb: {e}")
except Exception as e:
print(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:
table_name = f"{str(get_nodeNum())}_nodedb"
nodeinfo_table = f'"{table_name}"' # Quote the table name becuase we might begin with numerics
db_cursor = db_connection.cursor()
# Check if a record with the same user_id already exists
existing_record = db_cursor.execute(f'SELECT * FROM {nodeinfo_table} WHERE user_id=?', (packet['from'],)).fetchone()
if existing_record is None:
role = packet['decoded']['user'].get('role', 'CLIENT')
is_licensed = packet['decoded']['user'].get('isLicensed', '0')
public_key = packet['decoded']['user'].get('publicKey', '')
# No existing record, insert the new record
insert_query = f'''
INSERT INTO {nodeinfo_table} (user_id, long_name, short_name, hw_model, is_licensed, role, public_key)
VALUES (?, ?, ?, ?, ?, ?, ?)
'''
db_cursor.execute(insert_query, (
packet['from'],
packet['decoded']['user']['longName'],
packet['decoded']['user']['shortName'],
packet['decoded']['user']['hwModel'],
is_licensed,
role,
public_key
))
db_connection.commit()
else:
# Check if values are different, update if necessary
# Extract existing values
existing_long_name = existing_record[1]
existing_short_name = existing_record[2]
existing_is_licensed = existing_record[4]
existing_role = existing_record[5]
existing_public_key = existing_record[6]
# Extract new values from the packet
new_long_name = packet['decoded']['user']['longName']
new_short_name = packet['decoded']['user']['shortName']
new_is_licensed = packet['decoded']['user'].get('isLicensed', '0')
new_role = packet['decoded']['user'].get('role', 'CLIENT')
new_public_key = packet['decoded']['user'].get('publicKey', '')
# Check for any differences
if (
existing_long_name != new_long_name or
existing_short_name != new_short_name or
existing_is_licensed != new_is_licensed or
existing_role != new_role or
existing_public_key != new_public_key
):
# Perform necessary updates
update_query = f'''
UPDATE {nodeinfo_table}
SET long_name = ?, short_name = ?, is_licensed = ?, role = ?, public_key = ?
WHERE user_id = ?
'''
db_cursor.execute(update_query, (
new_long_name,
new_short_name,
new_is_licensed,
new_role,
new_public_key,
packet['from']
))
db_connection.commit()
# TODO display new node name in nodelist
except sqlite3.Error as e:
print(f"SQLite error in maybe_store_nodeinfo_in_db: {e}")
finally:
db_connection.close()
+8 -2
View File
@@ -1,9 +1,15 @@
all_messages = {}
channel_list = []
notifications = set()
packet_buffer = []
myNodeNum = 0
selected_channel = 0
selected_message = 0
selected_node = 0
direct_message = False
current_window = 0
interface = None
display_log = False
display_log = False
db_file_path = "client.db"
message_prefix = ">>"
sent_message_prefix =">> Sent:"
notification_symbol = "*"
+5 -1
View File
@@ -3,7 +3,7 @@
'''
Curses Client for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 0.2.0
V 0.3.0
'''
import curses
@@ -15,6 +15,7 @@ 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 db_handler import init_nodedb, load_messages_from_db
import globals
# Set environment variables for ncurses compatibility
@@ -29,7 +30,10 @@ def main(stdscr):
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)
if __name__ == "__main__":
curses.wrapper(main)
+16 -9
View File
@@ -1,10 +1,13 @@
from meshtastic import BROADCAST_NUM
from utilities.utils import get_node_list, decimal_to_hex, get_nodeNum
import globals
from ui.curses_ui import update_packetlog_win, draw_node_list, update_messages_window, draw_channel_list, add_notification
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
def on_receive(packet):
global nodes_win
# update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
@@ -12,11 +15,13 @@ def on_receive(packet):
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
update_packetlog_win()
draw_packetlog_win()
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'NODEINFO_APP':
get_node_list()
draw_node_list()
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':
message_bytes = packet['decoded']['payload']
@@ -44,17 +49,19 @@ def on_receive(packet):
message_from_string = ""
for node in globals.interface.nodes.values():
if message_from_id == node['num']:
message_from_string = node["user"]["longName"] # Get the long name using the node ID
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">> {message_from_string} ", message_string))
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">> {message_from_string} ", message_string)]
draw_channel_list()
update_messages_window()
globals.all_messages[globals.channel_list[channel_number]] = [(f"{globals.message_prefix} {message_from_string} ", message_string)]
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}")
+9 -2
View File
@@ -1,7 +1,12 @@
from meshtastic import BROADCAST_NUM
from db_handler import save_message_to_db
from utilities.utils import get_nodeNum
import globals
def send_message(message, destination=BROADCAST_NUM, channel=0):
myid = get_nodeNum()
send_on_channel = 0
if isinstance(globals.channel_list[channel], int):
send_on_channel = 0
@@ -20,6 +25,8 @@ def send_message(message, destination=BROADCAST_NUM, channel=0):
# Add sent message to the messages dictionary
if globals.channel_list[channel] in globals.all_messages:
globals.all_messages[globals.channel_list[channel]].append((">> Sent: ", message))
globals.all_messages[globals.channel_list[channel]].append((globals.sent_message_prefix + " ", message))
else:
globals.all_messages[globals.channel_list[channel]] = [(">> Sent: ", message)]
globals.all_messages[globals.channel_list[channel]] = [(globals.sent_message_prefix + " ", message)]
save_message_to_db(globals.channel_list[channel], myid, message)
+164 -145
View File
@@ -5,108 +5,21 @@ 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
def handle_notification(channel_number, add=True):
global channel_win
_, win_width = channel_win.getmaxyx() # Get the width of the channel window
# Get the channel name
if isinstance(globals.channel_list[channel_number], str): # Channels
channel_name = globals.channel_list[channel_number]
elif isinstance(globals.channel_list[channel_number], int): # DM's
channel_name = get_name_from_number(globals.channel_list[channel_number])
else:
return
# Truncate the channel name if it's too long to fit in the window
truncated_channel_name = channel_name[:win_width - 5] + '-' if len(channel_name) > win_width - 5 else channel_name
# Add or remove the notification indicator
notification = " *" if add else " "
channel_win.addstr(channel_number + 1, len(truncated_channel_name) + 1, notification, curses.color_pair(4))
channel_win.refresh()
def add_notification(channel_number):
handle_notification(channel_number, add=True)
def remove_notification(channel_number):
handle_notification(channel_number, add=False)
channel_win.box()
def update_messages_window():
global messages_win
messages_win.clear()
# Calculate how many messages can fit in the window
max_messages = messages_win.getmaxyx()[0] - 2 # Subtract 2 for the top and bottom border
# Determine the starting index for displaying messages
if globals.channel_list[globals.selected_channel] in globals.all_messages:
start_index = max(0, len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - max_messages)
def handle_notification(channel_number, add=True):
if add:
globals.notifications.add(channel_number) # Add the channel to the notification tracker
else:
# Handle the case where selected_channel does not exist
start_index = 0 # Set start_index to 0 or any other appropriate value
# Display messages starting from the calculated start index
# Check if selected_channel exists in all_messages before accessing it
if globals.channel_list[globals.selected_channel] in globals.all_messages:
row = 1
for _, (prefix, message) in enumerate(globals.all_messages[globals.channel_list[globals.selected_channel]][start_index:], start=1):
full_message = f"{prefix}{message}"
wrapped_messages = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
for wrapped_message in wrapped_messages:
messages_win.addstr(row, 1, wrapped_message, curses.color_pair(1) if prefix.startswith(">> Sent:") else curses.color_pair(2))
row += 1
messages_win.box()
messages_win.refresh()
update_packetlog_win()
def update_packetlog_win():
if globals.display_log:
packetlog_win.clear()
packetlog_win.box()
# Get the dimensions of the packet log window
height, width = packetlog_win.getmaxyx()
columns = [10,10,15,30]
span = 0
for column in columns[:-1]:
span += column
# 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
for i, packet in enumerate(reversed(globals.packet_buffer)):
if i >= height - 3: # Skip if exceeds the window height
break
# Format each field
from_id = get_name_from_number(packet['from'], 'short').ljust(columns[0])
to_id = (
"BROADCAST".ljust(columns[1]) if str(packet['to']) == "4294967295"
else get_name_from_number(packet['to'], 'short').ljust(columns[1])
)
if 'decoded' in packet:
port = packet['decoded']['portnum'].ljust(columns[2])
payload = (packet['decoded']['payload']).ljust(columns[3])
else:
port = "NO KEY".ljust(columns[2])
payload = "NO KEY".ljust(columns[3])
# Combine and truncate if necessary
logString = f"{from_id} {to_id} {port} {payload}"
logString = logString[:width - 3]
# Add to the window
packetlog_win.addstr(i + 2, 1, logString)
packetlog_win.refresh()
globals.notifications.discard(channel_number) # Remove the channel from the notification tracker
def draw_text_field(win, text):
# win.clear()
win.border()
win.addstr(1, 1, text)
@@ -114,10 +27,13 @@ def draw_centered_text_field(win, text, y_offset = 0):
height, width = win.getmaxyx()
x = (width - len(text)) // 2
y = (height // 2) + y_offset
win.addstr(y, x, text)
win.refresh()
def draw_debug(value):
function_win.addstr(1, 1, f"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
@@ -140,35 +56,85 @@ def draw_splash(stdscr):
stdscr.box()
stdscr.refresh()
def draw_channel_list():
# Get the dimensions of the channel window
_, win_width = channel_win.getmaxyx()
for i, (channel, message_list) in enumerate(globals.all_messages.items()):
def draw_channel_list():
channel_win.clear()
win_height, win_width = channel_win.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):
# 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 ""
# 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 globals.selected_channel == i and not globals.direct_message:
channel_win.addstr(i + 1, 1, truncated_channel, curses.color_pair(3))
remove_notification(globals.selected_channel)
else:
channel_win.addstr(i + 1, 1, truncated_channel, curses.color_pair(4))
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))
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()
def draw_messages_window():
"""Update the messages window based on the selected channel and scroll position."""
messages_win.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
# 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):
full_message = f"{prefix}{message}"
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
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
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)
row += 1
messages_win.box()
messages_win.refresh()
draw_packetlog_win()
def draw_node_list():
nodes_win.clear()
height, width = nodes_win.getmaxyx()
start_index = max(0, globals.selected_node - (height - 3)) # Calculate starting index based on selected node and window height
win_height = nodes_win.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 < height - 1 : # Check if there is enough space in the window
if globals.selected_node + 1 == start_index + i and globals.direct_message:
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))
@@ -176,9 +142,6 @@ def draw_node_list():
nodes_win.box()
nodes_win.refresh()
def draw_debug(value):
function_win.addstr(1, 100, f"debug: {value} ")
function_win.refresh()
def select_channels(direction):
channel_list_length = len(globals.channel_list)
@@ -190,7 +153,19 @@ def select_channels(direction):
globals.selected_channel = 0
draw_channel_list()
update_messages_window()
draw_messages_window()
def select_messages(direction):
messages_length = len(globals.all_messages[globals.channel_list[globals.selected_channel]])
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
draw_messages_window()
def select_nodes(direction):
node_list_length = len(get_node_list())
@@ -203,6 +178,51 @@ def select_nodes(direction):
draw_node_list()
def draw_packetlog_win():
columns = [10,10,15,30]
span = 0
if globals.display_log:
packetlog_win.clear()
height, width = packetlog_win.getmaxyx()
for column in columns[:-1]:
span += column
# 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
for i, packet in enumerate(reversed(globals.packet_buffer)):
if i >= height - 3: # Skip if exceeds the window height
break
# Format each field
from_id = get_name_from_number(packet['from'], 'short').ljust(columns[0])
to_id = (
"BROADCAST".ljust(columns[1]) if str(packet['to']) == "4294967295"
else get_name_from_number(packet['to'], 'short').ljust(columns[1])
)
if 'decoded' in packet:
port = packet['decoded']['portnum'].ljust(columns[2])
payload = (packet['decoded']['payload']).ljust(columns[3])
else:
port = "NO KEY".ljust(columns[2])
payload = "NO KEY".ljust(columns[3])
# Combine and truncate if necessary
logString = f"{from_id} {to_id} {port} {payload}"
logString = logString[:width - 3]
# Add to the window
packetlog_win.addstr(i + 2, 1, logString)
packetlog_win.box()
packetlog_win.refresh()
def main_ui(stdscr):
global messages_win, nodes_win, channel_win, function_win, packetlog_win
stdscr.keypad(True)
@@ -238,9 +258,9 @@ def main_ui(stdscr):
nodes_win.scrollok(True)
channel_win.scrollok(True)
channel_win.refresh()
draw_channel_list()
draw_node_list()
draw_messages_window()
# Draw boxes around windows
channel_win.box()
@@ -257,7 +277,6 @@ def main_ui(stdscr):
function_win.refresh()
input_text = ""
globals.direct_message = False
entry_win.keypad(True)
curses.curs_set(1)
@@ -272,40 +291,41 @@ def main_ui(stdscr):
# draw_debug(f"Keypress: {char}")
if char == curses.KEY_UP:
if globals.direct_message:
draw_channel_list()
select_nodes(-1)
else:
if globals.current_window == 0:
select_channels(-1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
elif globals.current_window == 1:
select_messages(-1)
elif globals.current_window == 2:
select_nodes(-1)
elif char == curses.KEY_DOWN:
if globals.direct_message:
draw_channel_list()
select_nodes(1)
else:
if globals.current_window == 0:
select_channels(1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
elif globals.current_window == 1:
select_messages(1)
elif globals.current_window == 2:
select_nodes(1)
elif char == curses.KEY_LEFT:
if globals.direct_message == False:
pass
else:
globals.direct_message = False
draw_channel_list()
draw_node_list()
globals.current_window = (globals.current_window - 1) % 3
draw_channel_list()
draw_node_list()
draw_messages_window()
elif char == curses.KEY_RIGHT:
if globals.direct_message == False:
globals.direct_message = True
draw_channel_list()
draw_node_list()
else:
pass
globals.current_window = (globals.current_window + 1) % 3
draw_channel_list()
draw_node_list()
draw_messages_window()
# Check for Esc
elif char == 27:
break
elif char == curses.KEY_ENTER or char == 10 or char == 13:
if globals.direct_message:
if globals.current_window == 2:
node_list = get_node_list()
if node_list[globals.selected_node] not in globals.channel_list:
globals.channel_list.append(node_list[globals.selected_node])
@@ -313,21 +333,21 @@ def main_ui(stdscr):
globals.selected_channel = globals.channel_list.index(node_list[globals.selected_node])
globals.selected_node = 0
globals.direct_message = False
globals.current_window = 0
draw_node_list()
draw_channel_list()
update_messages_window()
draw_messages_window()
else:
# Enter key pressed, send user input as message
send_message(input_text, channel=globals.selected_channel)
update_messages_window()
messages_win.refresh()
draw_messages_window()
# Clear entry window and reset input text
input_text = ""
entry_win.clear()
entry_win.refresh()
# entry_win.refresh()
elif char == curses.KEY_BACKSPACE or char == 127:
if input_text:
@@ -347,12 +367,11 @@ def main_ui(stdscr):
# Display packet log
if globals.display_log is False:
globals.display_log = True
update_messages_window()
draw_messages_window()
else:
globals.display_log = False
packetlog_win.clear()
update_messages_window()
draw_messages_window()
else:
# Append typed character to input text
input_text += chr(char)
+29 -8
View File
@@ -1,26 +1,37 @@
import globals
from meshtastic.protobuf import config_pb2
import re
def get_channels():
"""Retrieve channels from the node and update globals.channel_list and globals.all_messages."""
node = globals.interface.getNode('^local')
device_channels = node.channels
channel_output = []
# Clear and rebuild channel list
# globals.channel_list = []
for device_channel in device_channels:
if device_channel.role:
# Use the channel name if available, otherwise use the modem preset
if device_channel.settings.name:
channel_output.append(device_channel.settings.name)
globals.all_messages[device_channel.settings.name] = []
channel_name = device_channel.settings.name
else:
# If channel name is blank, use the modem preset
lora_config = node.localConfig.lora
modem_preset_enum = lora_config.modem_preset
modem_preset_string = config_pb2._CONFIG_LORACONFIG_MODEMPRESET.values_by_number[modem_preset_enum].name
channel_output.append(convert_to_camel_case(modem_preset_string))
globals.all_messages[convert_to_camel_case(modem_preset_string)] = []
channel_name = convert_to_camel_case(modem_preset_string)
return list(globals.all_messages.keys())
# Add channel to globals.channel_list if not already present
if channel_name not in globals.channel_list:
globals.channel_list.append(channel_name)
# Initialize globals.all_messages[channel_name] if it doesn't exist
if channel_name not in globals.all_messages:
globals.all_messages[channel_name] = []
return globals.channel_list
def get_node_list():
node_list = []
@@ -57,4 +68,14 @@ def get_name_from_number(number, type='long'):
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