mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
* begin refactor * continue refactor - notifications not working * refactor - fix notif - chanels broken * refactor - settings broken * working refactor * continue refactor * remove unused import
801 lines
28 KiB
Python
801 lines
28 KiB
Python
import curses
|
|
import ipaddress
|
|
|
|
import meshtastic.serial_interface, meshtastic.tcp_interface
|
|
from meshtastic.protobuf import config_pb2, module_config_pb2, mesh_pb2, channel_pb2
|
|
import globals
|
|
|
|
def display_enum_menu(stdscr, enum_values, menu_item):
|
|
menu_height = len(enum_values) + 2
|
|
menu_width = max(len(option) for option in enum_values) + 4
|
|
y_start = (curses.LINES - menu_height) // 2
|
|
x_start = (curses.COLS - menu_width) // 2
|
|
|
|
# Maximum number of rows to display
|
|
max_rows = 10
|
|
|
|
# Calculate popup window dimensions and position
|
|
popup_height = min(len(enum_values), max_rows) + 2
|
|
popup_width = max(len(option) for option in enum_values) + 6
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
# Create the popup window
|
|
try:
|
|
popup_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
# Enable keypad mode
|
|
popup_win.keypad(True)
|
|
|
|
# Display enum values in the popup window
|
|
start_index = 0 # Starting index of displayed items
|
|
while True:
|
|
popup_win.clear()
|
|
popup_win.border()
|
|
|
|
# Calculate the starting index based on the menu item and window size
|
|
if menu_item >= start_index + max_rows:
|
|
start_index += 1
|
|
elif menu_item < start_index:
|
|
start_index -= 1
|
|
|
|
# Display enum values within the window height
|
|
for i in range(min(len(enum_values) - start_index, max_rows)):
|
|
option_index = start_index + i
|
|
if option_index == menu_item:
|
|
popup_win.addstr(i + 1, 2, enum_values[option_index], curses.A_REVERSE)
|
|
else:
|
|
popup_win.addstr(i + 1, 2, enum_values[option_index])
|
|
|
|
popup_win.refresh()
|
|
|
|
char = popup_win.getch()
|
|
if char == curses.KEY_DOWN:
|
|
if menu_item < len(enum_values) - 1:
|
|
menu_item += 1
|
|
elif char == curses.KEY_UP:
|
|
if menu_item > 0:
|
|
menu_item -= 1
|
|
elif char == ord('\n'):
|
|
selected_option = enum_values[menu_item]
|
|
popup_win.clear()
|
|
popup_win.refresh()
|
|
return selected_option, True
|
|
elif char == 27 or char == curses.KEY_LEFT: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
popup_win.refresh()
|
|
return None, False
|
|
|
|
def get_string_input(stdscr, setting_string):
|
|
popup_height = 5
|
|
popup_width = 40
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
try:
|
|
input_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
input_win.border()
|
|
input_win.keypad(True)
|
|
input_win.refresh()
|
|
|
|
input_win.addstr(1, 1, str(setting_string)) # Prepopulate input field with the setting value
|
|
input_win.refresh()
|
|
# Get user input
|
|
curses.curs_set(1)
|
|
input_text = ""
|
|
|
|
while True:
|
|
# Display the current input text
|
|
input_win.addstr(1, 1, input_text)
|
|
input_win.border()
|
|
input_win.refresh()
|
|
|
|
# Get a character from the user
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
|
curses.curs_set(0)
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
return input_text, True
|
|
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
|
# Delete the last character from input_text
|
|
input_text = input_text[:-1]
|
|
elif 32 <= key <= 126: # Printable ASCII characters
|
|
# Append the character to input_text
|
|
input_text += chr(key)
|
|
elif key == 27: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
input_win.refresh()
|
|
return None, False
|
|
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
|
|
|
|
def get_uint_input(stdscr, setting_string):
|
|
popup_height = 5
|
|
popup_width = 40
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
try:
|
|
input_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
input_win.border()
|
|
input_win.keypad(True)
|
|
input_win.refresh()
|
|
|
|
input_win.addstr(1, 1, str(setting_string)) # Prepopulate input field with the setting value
|
|
input_win.refresh()
|
|
curses.curs_set(1)
|
|
input_text = ""
|
|
|
|
while True:
|
|
# Display the current input text
|
|
input_win.addstr(1, 1, input_text)
|
|
input_win.border()
|
|
input_win.refresh()
|
|
|
|
# Get a character from the user
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
|
curses.curs_set(0)
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
return int(input_text), True
|
|
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
|
# Delete the last character from input_text
|
|
input_text = input_text[:-1]
|
|
elif 48 <= key <= 57: # Numbers(ASCII range)
|
|
# Append the character to input_text
|
|
input_text += chr(key)
|
|
elif key == 27 or key == curses.KEY_LEFT: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
input_win.refresh()
|
|
return None, False
|
|
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
|
|
|
|
def get_uint32_list_input(stdscr, setting_string):
|
|
setting_string = [str(num) for num in setting_string]
|
|
|
|
popup_height = 8 # Increased height to accommodate three lines
|
|
popup_width = 40
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
try:
|
|
input_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
input_win.border()
|
|
input_win.keypad(True)
|
|
input_win.refresh()
|
|
|
|
input_text = setting_string[:] # Copy the input strings
|
|
curses.curs_set(0)
|
|
|
|
while True:
|
|
# Display the current input text for each line
|
|
for i, line in enumerate(input_text):
|
|
input_win.addstr(1 + i, 1, line)
|
|
|
|
input_win.border()
|
|
input_win.refresh()
|
|
|
|
# Get a character from the user
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
return None, False # TODO allow setting this
|
|
# return input_text, True
|
|
# elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
|
# # Delete the last character from the current line's input_text
|
|
# current_y, current_x = input_win.getyx()
|
|
# input_text[current_y - 1] = input_text[current_y - 1][:-1]
|
|
# elif (48 <= key <= 57) or key == 44: # Numbers and comma (ASCII range)
|
|
# # Append the character to the current line's input_text
|
|
# current_y, current_x = input_win.getyx()
|
|
# input_text[current_y - 1] += chr(key)
|
|
elif key == 27 or key == curses.KEY_LEFT: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
input_win.refresh()
|
|
return None, False
|
|
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
|
|
def get_float_input(stdscr, setting_string):
|
|
popup_height = 5
|
|
popup_width = 40
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
try:
|
|
input_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
input_win.border()
|
|
input_win.keypad(True)
|
|
input_win.refresh()
|
|
|
|
input_win.addstr(1, 1, str(setting_string)) # Prepopulate input field with the setting value
|
|
input_win.refresh()
|
|
curses.curs_set(1)
|
|
input_text = ""
|
|
|
|
while True:
|
|
# Display the current input text
|
|
input_win.addstr(1, 1, input_text)
|
|
input_win.border()
|
|
input_win.refresh()
|
|
|
|
# Get a character from the user
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
|
curses.curs_set(0)
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
return float(input_text), True
|
|
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
|
# Delete the last character from input_text
|
|
input_text = input_text[:-1]
|
|
elif (48 <= key <= 57) or key == 46: # Numbers and decimal point (ASCII range)
|
|
# Append the character to input_text
|
|
input_text += chr(key)
|
|
elif key == 27 or key == curses.KEY_LEFT: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
input_win.refresh()
|
|
return None, False
|
|
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
|
|
|
|
def ip_to_fixed32(ip):
|
|
# Parse the IP address
|
|
ip_obj = ipaddress.ip_address(ip)
|
|
# Convert IP address to 32-bit integer
|
|
return int(ip_obj)
|
|
|
|
def fixed32_to_ip(fixed32):
|
|
# Convert 32-bit integer to IPv4Address object
|
|
ip_obj = ipaddress.IPv4Address(fixed32)
|
|
# Convert IPv4Address object to string representation
|
|
return str(ip_obj)
|
|
|
|
def get_fixed32_input(stdscr, setting_string):
|
|
popup_height = 5
|
|
popup_width = 40
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
try:
|
|
input_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
input_win.border()
|
|
input_win.keypad(True)
|
|
input_win.refresh()
|
|
|
|
input_win.addstr(1, 1, fixed32_to_ip(setting_string)) # Prepopulate input field with the setting value
|
|
input_win.refresh()
|
|
curses.curs_set(1)
|
|
input_text = ""
|
|
|
|
while True:
|
|
# Display the current input text
|
|
input_win.addstr(1, 1, input_text)
|
|
input_win.border()
|
|
input_win.refresh()
|
|
|
|
# Get a character from the user
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
|
|
curses.curs_set(0)
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
return ip_to_fixed32(input_text), True
|
|
elif key == curses.KEY_BACKSPACE or key == 127: # Backspace key
|
|
# Delete the last character from input_text
|
|
input_text = input_text[:-1]
|
|
elif 48 <= key <= 57 or key == 46: # Numbers + period (ASCII range)
|
|
# Append the character to input_text
|
|
input_text += chr(key)
|
|
elif key == 27 or key == curses.KEY_LEFT: # Check if escape key is pressed
|
|
curses.curs_set(0)
|
|
input_win.refresh()
|
|
return None, False
|
|
|
|
input_win.clear()
|
|
input_win.refresh()
|
|
|
|
|
|
def display_bool_menu(stdscr, setting_value):
|
|
bool_options = ["False", "True"]
|
|
return display_enum_menu(stdscr, bool_options, setting_value)
|
|
|
|
|
|
def generate_menu_from_protobuf(message_instance):
|
|
if not hasattr(message_instance, "DESCRIPTOR"):
|
|
return # This is not a protobuf message instance, exit
|
|
menu = {}
|
|
|
|
field_names = message_instance.DESCRIPTOR.fields_by_name.keys()
|
|
for field_name in field_names:
|
|
field_descriptor = message_instance.DESCRIPTOR.fields_by_name[field_name]
|
|
if field_descriptor is not None:
|
|
nested_message_instance = getattr(message_instance, field_name)
|
|
menu[field_name] = generate_menu_from_protobuf(nested_message_instance)
|
|
return menu
|
|
|
|
|
|
def change_setting(stdscr, menu_path):
|
|
node = globals.interface.localNode
|
|
field_descriptor = None
|
|
setting_value = 0
|
|
|
|
stdscr.clear()
|
|
stdscr.border()
|
|
stdscr.refresh()
|
|
menu_header(stdscr, f"{menu_path[-1]}")
|
|
|
|
# Determine the level of nesting based on the length of menu_path
|
|
|
|
if menu_path[1] == "User Settings":
|
|
n = globals.interface.getMyNodeInfo()
|
|
|
|
setting_string = n['user'].get(snake_to_camel(menu_path[2]), 0)
|
|
|
|
if menu_path[2] == "is_licensed":
|
|
setting_value, do_change_setting = display_bool_menu(stdscr, setting_string)
|
|
else:
|
|
setting_value, do_change_setting = get_string_input(stdscr, setting_string)
|
|
|
|
if not do_change_setting:
|
|
stdscr.clear()
|
|
stdscr.border()
|
|
menu_path.pop()
|
|
return
|
|
|
|
if menu_path[2] in ["long_name", "short_name"]:
|
|
if menu_path[2] == "short_name" and len(setting_value) > 4:
|
|
setting_value = setting_value[:4]
|
|
settings_set_owner(long_name=setting_value if menu_path[2] == "long_name" else None,
|
|
short_name=setting_value if menu_path[2] == "short_name" else None)
|
|
elif menu_path[2] == "is_licensed":
|
|
ln = n['user']['longName']
|
|
settings_set_owner(long_name=ln, is_licensed=setting_value)
|
|
|
|
stdscr.clear()
|
|
stdscr.border()
|
|
menu_path.pop()
|
|
return
|
|
|
|
if menu_path[1] == "Channels":
|
|
return
|
|
# n = interface.getChannelByChannelIndex()
|
|
# setting_string = getattr(getattr(n.channelSettings, str(menu_path[2])), menu_path[3])
|
|
# field_descriptor = getattr(n.channelSettings, menu_path[2]).DESCRIPTOR.fields_by_name[menu_path[3]]
|
|
|
|
|
|
|
|
if len(menu_path) == 4:
|
|
if menu_path[1] == "Radio Settings":
|
|
setting_string = getattr(getattr(node.localConfig, str(menu_path[2])), menu_path[3])
|
|
field_descriptor = getattr(node.localConfig, menu_path[2]).DESCRIPTOR.fields_by_name[menu_path[3]]
|
|
|
|
elif menu_path[1] == "Module Settings":
|
|
setting_string = getattr(getattr(node.moduleConfig, str(menu_path[2])), menu_path[3])
|
|
field_descriptor = getattr(node.moduleConfig, menu_path[2]).DESCRIPTOR.fields_by_name[menu_path[3]]
|
|
|
|
|
|
elif len(menu_path) == 5:
|
|
if menu_path[1] == "Radio Settings":
|
|
setting_string = getattr(getattr(getattr(node.localConfig, str(menu_path[2])), menu_path[3]), menu_path[4])
|
|
field_descriptor = getattr(getattr(node.localConfig, menu_path[2]), menu_path[3]).DESCRIPTOR.fields_by_name[menu_path[4]]
|
|
|
|
elif menu_path[1] == "Module Settings":
|
|
setting_string = getattr(getattr(getattr(node.moduleConfig, str(menu_path[2])), menu_path[3]), menu_path[4])
|
|
field_descriptor = getattr(getattr(node.moduleConfig, menu_path[2]), menu_path[3]).DESCRIPTOR.fields_by_name[menu_path[4]]
|
|
|
|
|
|
if field_descriptor.enum_type is not None:
|
|
enum_values = [enum_value.name for enum_value in field_descriptor.enum_type.values]
|
|
enum_option, do_change_setting = display_enum_menu(stdscr, enum_values, setting_string)
|
|
setting_value = enum_option
|
|
|
|
elif field_descriptor.type == 8: # Field type 8 corresponds to BOOL
|
|
setting_value, do_change_setting = display_bool_menu(stdscr, setting_string)
|
|
|
|
elif field_descriptor.type == 9: # Field type 9 corresponds to STRING
|
|
setting_value, do_change_setting = get_string_input(stdscr, setting_string)
|
|
|
|
elif field_descriptor.type == 2: # Field type 2 corresponds to FLOAT
|
|
setting_value, do_change_setting = get_float_input(stdscr, setting_string)
|
|
|
|
elif field_descriptor.type == 13: # Field type 13 corresponds to UINT32
|
|
if field_descriptor.label == field_descriptor.LABEL_REPEATED:
|
|
setting_value, do_change_setting = get_uint32_list_input(stdscr, setting_string)
|
|
else:
|
|
setting_value, do_change_setting = get_uint_input(stdscr, setting_string)
|
|
|
|
elif field_descriptor.type == 7: # Field type 7 corresponds to FIXED32
|
|
setting_value, do_change_setting = get_fixed32_input(stdscr, setting_string)
|
|
|
|
else:
|
|
menu_path.pop()
|
|
return
|
|
|
|
if not do_change_setting:
|
|
stdscr.clear()
|
|
stdscr.border()
|
|
menu_path.pop()
|
|
return
|
|
|
|
# formatted_text = f"{menu_path[2]}.{menu_path[3]} = {setting_value}"
|
|
# menu_header(stdscr,formatted_text,2)
|
|
|
|
ourNode = globals.interface.localNode
|
|
|
|
# Convert "true" to 1, "false" to 0, leave other values as they are
|
|
if setting_value == "True" or setting_value == "1":
|
|
setting_value_int = 1
|
|
elif setting_value == "False" or setting_value == "0":
|
|
setting_value_int = 0
|
|
else:
|
|
# If setting_value is not "true" or "false", keep it as it is
|
|
setting_value_int = setting_value
|
|
|
|
# if isinstance(setting_value_int, list):
|
|
# value_string = ', '.join(str(item) for item in setting_value_int)
|
|
# setting_value_int = value_string
|
|
|
|
try:
|
|
if len(menu_path) == 4:
|
|
if menu_path[1] == "Radio Settings":
|
|
setattr(getattr(ourNode.localConfig, menu_path[2]), menu_path[3], setting_value_int)
|
|
elif menu_path[1] == "Module Settings":
|
|
setattr(getattr(ourNode.moduleConfig, menu_path[2]), menu_path[3], setting_value_int)
|
|
|
|
elif len(menu_path) == 5:
|
|
if menu_path[1] == "Radio Settings":
|
|
setattr(getattr(getattr(ourNode.localConfig, menu_path[2]), menu_path[3]), menu_path[4], setting_value_int)
|
|
elif menu_path[1] == "Module Settings":
|
|
setattr(getattr(getattr(ourNode.moduleConfig, menu_path[2]), menu_path[3]), menu_path[4], setting_value_int)
|
|
|
|
except AttributeError as e:
|
|
print("Error setting attribute:", e)
|
|
|
|
|
|
ourNode.writeConfig(menu_path[2])
|
|
menu_path.pop()
|
|
|
|
def snake_to_camel(snake_str):
|
|
components = snake_str.split('_')
|
|
return components[0] + ''.join(x.title() for x in components[1:])
|
|
|
|
|
|
def display_values(stdscr, key_list, menu_path):
|
|
node = globals.interface.localNode
|
|
user_settings = ["long_name", "short_name", "is_licensed"]
|
|
for i, key in enumerate(key_list):
|
|
|
|
if len(menu_path) == 2:
|
|
if menu_path[1] == 'User Settings':
|
|
n = globals.interface.getMyNodeInfo()
|
|
try:
|
|
setting = n['user'][snake_to_camel(key_list[i])]
|
|
except:
|
|
setting = None
|
|
if key_list[i] in user_settings:
|
|
stdscr.addstr(i+3, 40, str(setting))
|
|
|
|
if len(menu_path) == 3:
|
|
if menu_path[1] == "Radio Settings":
|
|
setting = getattr(getattr(node.localConfig, menu_path[2]), key_list[i])
|
|
if menu_path[1] == "Module Settings":
|
|
setting = getattr(getattr(node.moduleConfig, menu_path[2]), key_list[i])
|
|
stdscr.addstr(i+3, 40, str(setting)[:14])
|
|
|
|
if len(menu_path) == 4:
|
|
if menu_path[1] == "Radio Settings":
|
|
setting = getattr(getattr(getattr(node.localConfig, menu_path[2]), menu_path[3]), key_list[i])
|
|
if menu_path[1] == "Module Settings":
|
|
setting = getattr(getattr(getattr(node.moduleConfig, menu_path[2]), menu_path[3]), key_list[i])
|
|
stdscr.addstr(i+3, 40, str(setting)[:14])
|
|
|
|
stdscr.refresh()
|
|
|
|
def menu_header(window, text, start_y=1):
|
|
window.clear()
|
|
window.box()
|
|
window.refresh()
|
|
_, window_width = window.getmaxyx()
|
|
start_x = (window_width - len(text)) // 2
|
|
formatted_text = text.replace('_', ' ').title()
|
|
window.addstr(start_y, start_x, formatted_text)
|
|
window.refresh()
|
|
|
|
def nested_menu(stdscr, menu):
|
|
menu_item = 0
|
|
current_menu = menu
|
|
prev_menu = []
|
|
menu_index = 0
|
|
next_key = None
|
|
|
|
key_list = []
|
|
menu_path = ["Main Menu"]
|
|
|
|
last_menu_level = False
|
|
|
|
while True:
|
|
|
|
if current_menu is not None:
|
|
menu_header(stdscr, f"{menu_path[menu_index]}")
|
|
|
|
# Display current menu
|
|
for i, key in enumerate(current_menu.keys(), start=0):
|
|
if i == menu_item:
|
|
if key in ["Reboot", "Reset NodeDB", "Shutdown", "Factory Reset"]:
|
|
stdscr.addstr(i+3, 1, key, curses.color_pair(5))
|
|
else:
|
|
stdscr.addstr(i+3, 1, key, curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(i+3, 1, key)
|
|
|
|
# Display current values
|
|
display_values(stdscr, key_list, menu_path)
|
|
|
|
char = stdscr.getch()
|
|
|
|
selected_key = list(current_menu.keys())[menu_item]
|
|
selected_value = current_menu[selected_key]
|
|
|
|
if char == curses.KEY_DOWN:
|
|
if last_menu_level == True:
|
|
last_menu_level = False
|
|
menu_item = min(len(current_menu) - 1, menu_item + 1)
|
|
|
|
elif char == curses.KEY_UP:
|
|
if last_menu_level == True:
|
|
last_menu_level = False
|
|
menu_item = max(0, menu_item - 1)
|
|
|
|
elif char == curses.KEY_RIGHT:
|
|
# if selected_key == "Region":
|
|
# settings_region()
|
|
# break
|
|
if selected_key == "Channels":
|
|
channels_editor(stdscr)
|
|
elif selected_key not in ["Reboot", "Reset NodeDB", "Shutdown", "Factory Reset"]:
|
|
menu_path.append(selected_key)
|
|
|
|
if isinstance(selected_value, dict):
|
|
# If the selected item is a submenu, navigate to it
|
|
prev_menu.append(current_menu)
|
|
menu_index += 1
|
|
current_menu = selected_value
|
|
menu_item = 0
|
|
last_menu_level = False
|
|
else:
|
|
last_menu_level = True
|
|
|
|
|
|
elif char == curses.KEY_LEFT:
|
|
if last_menu_level == True:
|
|
last_menu_level = False
|
|
if len(menu_path) > 1:
|
|
menu_path.pop()
|
|
current_menu = prev_menu[menu_index-1]
|
|
del prev_menu[menu_index-1]
|
|
menu_index -= 1
|
|
menu_item = 0
|
|
|
|
elif char == ord('\n'):
|
|
if selected_key == "Channels":
|
|
channels_editor(stdscr)
|
|
if selected_key == "Reboot":
|
|
settings_reboot()
|
|
elif selected_key == "Reset NodeDB":
|
|
settings_reset_nodedb()
|
|
elif selected_key == "Shutdown":
|
|
settings_shutdown()
|
|
elif selected_key == "Factory Reset":
|
|
settings_factory_reset()
|
|
|
|
elif selected_value is not None:
|
|
stdscr.refresh()
|
|
stdscr.getch()
|
|
|
|
elif char == 27: # escape to exit menu
|
|
break
|
|
|
|
if char:
|
|
stdscr.clear()
|
|
stdscr.border()
|
|
|
|
next_key = list(current_menu.keys())[menu_item]
|
|
key_list = list(current_menu.keys())
|
|
|
|
else:
|
|
break # Exit loop if current_menu is None
|
|
|
|
if last_menu_level == True:
|
|
if not isinstance(current_menu.get(next_key), dict):
|
|
change_setting(stdscr, menu_path)
|
|
|
|
|
|
def settings(stdscr):
|
|
popup_height = 22
|
|
popup_width = 60
|
|
popup_win = None
|
|
y_start = (curses.LINES - popup_height) // 2
|
|
x_start = (curses.COLS - popup_width) // 2
|
|
|
|
curses.curs_set(0)
|
|
try:
|
|
popup_win = curses.newwin(popup_height, popup_width, y_start, x_start)
|
|
except curses.error as e:
|
|
print("Error occurred while initializing curses window:", e)
|
|
|
|
popup_win.border()
|
|
popup_win.keypad(True)
|
|
|
|
# Generate menu from protobuf for both radio and module settings
|
|
|
|
|
|
user = mesh_pb2.User()
|
|
user_settings = ["long_name", "short_name", "is_licensed"]
|
|
user_config = generate_menu_from_protobuf(user)
|
|
user_config = {key: value for key, value in user_config.items() if key in user_settings}
|
|
|
|
channel = channel_pb2.ChannelSettings()
|
|
channel_config = generate_menu_from_protobuf(channel)
|
|
channel_config = [channel_config.copy() for i in range(8)]
|
|
|
|
|
|
radio = config_pb2.Config()
|
|
radio_config = generate_menu_from_protobuf(radio)
|
|
|
|
module = module_config_pb2.ModuleConfig()
|
|
module_config = generate_menu_from_protobuf(module)
|
|
|
|
# Add top-level menu items
|
|
top_level_menu = {
|
|
"User Settings": user_config,
|
|
"Channels": None,
|
|
"Radio Settings": radio_config,
|
|
"Module Settings": module_config,
|
|
"Reboot": None,
|
|
"Reset NodeDB": None,
|
|
"Shutdown": None,
|
|
"Factory Reset": None
|
|
}
|
|
|
|
# Call nested_menu function to display and handle the nested menu
|
|
nested_menu(popup_win, top_level_menu)
|
|
|
|
# Close the popup window
|
|
popup_win.clear()
|
|
popup_win.refresh()
|
|
|
|
# def settings_region():
|
|
# selected_option, do_set = set_region()
|
|
# if do_set:
|
|
# ourNode = interface.localNode
|
|
# setattr(ourNode.localConfig.lora, "region", selected_option)
|
|
# ourNode.writeConfig("lora")
|
|
|
|
def settings_reboot():
|
|
globals.interface.localNode.reboot()
|
|
|
|
def settings_reset_nodedb():
|
|
globals.interface.localNode.resetNodeDb()
|
|
|
|
def settings_shutdown():
|
|
globals.interface.localNode.shutdown()
|
|
|
|
def settings_factory_reset():
|
|
globals.interface.localNode.factoryReset()
|
|
|
|
def settings_set_owner(long_name=None, short_name=None, is_licensed=False):
|
|
if is_licensed == 'True':
|
|
is_licensed = True
|
|
elif is_licensed == 'False':
|
|
is_licensed = False
|
|
globals.interface.localNode.setOwner(long_name, short_name, is_licensed)
|
|
|
|
|
|
|
|
def channels_editor(stdscr):
|
|
# Define the list of channels
|
|
channels = [f"{i}" for i in range(8)]
|
|
|
|
# Initialize menu item index and selected channel
|
|
menu_item = 0
|
|
selected_channel = channels[menu_item]
|
|
|
|
while True:
|
|
# Display the list of channels in the curses window
|
|
menu_header(stdscr, "Channels")
|
|
|
|
# Fetch and print roles for each channel
|
|
for index, channel_index in enumerate(channels):
|
|
channel = globals.interface.localNode.getChannelByChannelIndex(index)
|
|
role = "DISABLED" if channel.role == 0 else "PRIMARY" if channel.role == 1 else "SECONDARY"
|
|
channel_settings = channel.settings
|
|
channel_name = channel_settings.name
|
|
if not channel_name and role != "DISABLED":
|
|
config = globals.interface.localNode.localConfig
|
|
channel_name_int = config.lora.modem_preset
|
|
channel_name = config_pb2.Config.LoRaConfig.ModemPreset.Name(channel_name_int)
|
|
|
|
channel_name_formatted = f"{channel_name:<13}" # Adjust channel name to be 13 characters wide
|
|
role_formatted = f"{role:<9}" # Adjust role to be 9 characters wide (SECONDARY = 9 chars)
|
|
|
|
if index == menu_item:
|
|
stdscr.addstr(index + 3, 1, f"{channel_index} ", curses.A_REVERSE)
|
|
stdscr.addstr(index + 3, 3, f"{channel_name_formatted}", curses.A_REVERSE)
|
|
stdscr.addstr(index + 3, 15, f"{role_formatted}", curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(index + 3, 1, f"{channel_index}")
|
|
stdscr.addstr(index + 3, 3, f"{channel_name_formatted}")
|
|
stdscr.addstr(index + 3, 15, f"{role_formatted}")
|
|
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_DOWN:
|
|
menu_item = min(len(channels) - 1, menu_item + 1)
|
|
elif key == curses.KEY_UP:
|
|
menu_item = max(0, menu_item - 1)
|
|
elif key == ord('\n'):
|
|
# Handle selection action
|
|
selected_channel = channels[menu_item]
|
|
# Perform action here, if needed
|
|
elif key == 27 or key == curses.KEY_LEFT: # escape to exit menu
|
|
break
|
|
|
|
selected_channel = channels[menu_item]
|
|
|
|
return selected_channel
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
globals.interface = meshtastic.serial_interface.SerialInterface()
|
|
|
|
# radio = config_pb2.Config()
|
|
# module = module_config_pb2.ModuleConfig()
|
|
# print(generate_menu_from_protobuf(radio, interface))
|
|
# print(generate_menu_from_protobuf(module, interface))
|
|
|
|
def main(stdscr):
|
|
stdscr.keypad(True)
|
|
while True:
|
|
settings(stdscr)
|
|
|
|
curses.wrapper(main) |