mirror of
https://github.com/pdxlocations/contact.git
synced 2026-07-06 01:41:15 +02:00
Merge pull request #21 from pdxlocations/database
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
client.db
|
||||
.DS_Store
|
||||
|
||||
+229
@@ -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
@@ -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 = "*"
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user