1
0
forked from iarv/contact

Compare commits

...

42 Commits

Author SHA1 Message Date
pdxlocations
49281f9106 works but not what i want 2025-02-21 18:13:30 -08:00
pdxlocations
f644b92356 use global interface in save 2025-02-20 22:32:17 -08:00
pdxlocations
3db44f4ae3 remove double writeconfig 2025-02-20 21:57:09 -08:00
pdxlocations
8c837e68a0 keep cursor in the input window (#130) 2025-02-18 22:30:42 -08:00
pdxlocations
5dd06624e3 do close interface after region set 2025-02-12 16:08:18 -08:00
pdxlocations
c83ccea4ef use protubuf number when setting region 2025-02-12 15:27:33 -08:00
pdxlocations
d7f0bee54c try setting region earlier 2025-02-12 15:22:02 -08:00
pdxlocations
fb60773ae6 don't close interface after region set 2025-02-12 15:18:58 -08:00
pdxlocations
47ab0a5b9a bump version 2025-02-09 22:09:59 -08:00
pdxlocations
989c3cf44e add region check at startup (#127) 2025-02-09 22:09:15 -08:00
pdxlocations
71aeae4f92 add note in draw_node_list 2025-02-09 20:49:49 -08:00
pdxlocations
34cd21b323 Fix Startup Error with Thread Lock (#126)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more

* grasping at straws

* more global

* back up

* db snapshot

* try a threading lock

* fix conflict

* lock it down

* sir locks a lot

* sir locks a lilttle less
2025-02-09 06:46:42 -08:00
pdxlocations
e69c51f9c3 Another attempt to fix startup errors (#125)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more

* grasping at straws

* more global

* back up

* db snapshot
2025-02-08 15:57:54 -08:00
pdxlocations
3c3bf0ad37 rename enum to list 2025-02-08 13:23:30 -08:00
pdxlocations
804f82cbe6 where have all the nodes_pad gone? 2025-02-08 10:03:51 -08:00
pdxlocations
57042d2050 Maybe fix startup error (again) (#124)
* more excuses

* none isn't better than nothing

* more checks

* typo

* refactor

* less is more
2025-02-08 09:14:25 -08:00
pdxlocations
8342753c51 Merge pull request #123 from pdxlocations/fix-startup-error
Maybe Fix Startup Error
2025-02-07 22:18:58 -08:00
pdxlocations
5690329b06 notes 2025-02-07 22:11:53 -08:00
pdxlocations
a080af3e84 add logging 2025-02-07 21:50:14 -08:00
pdxlocations
dd11932a53 make sure nodes_pad exists legitely 2025-02-07 21:44:17 -08:00
pdxlocations
dae71984bc make sure nodes pad is created 2025-02-07 21:01:06 -08:00
pdxlocations
3668d47119 define windows in resize 2025-02-07 20:49:38 -08:00
pdxlocations
fe3980bc5a ok I'll stop 2025-02-07 20:23:47 -08:00
pdxlocations
9c380c18fd maybe this 2025-02-07 20:19:26 -08:00
pdxlocations
30d14a6a9e big test 2025-02-07 18:08:03 -08:00
pdxlocations
bbfe361173 morer testing 2025-02-07 18:03:10 -08:00
pdxlocations
0d6f234191 more testing 2025-02-06 23:10:01 -08:00
pdxlocations
16c8e3032a fix startup error test 2025-02-06 23:06:19 -08:00
pdxlocations
611d59fefe Merge pull request #120 from rfschmid/fix-receiving-traceroute-from-archived-node 2025-02-05 17:31:01 -08:00
Russell Schmidt
651d381c78 Fix receiving traceroute from archived chat
They were invisible
2025-02-05 19:15:18 -06:00
pdxlocations
e7850b9204 add role to node details 2025-02-05 16:31:12 -08:00
pdxlocations
4306971871 switch locked emoji 2025-02-05 16:05:39 -08:00
pdxlocations
ba86108316 Merge pull request #119 from rfschmid/update-main-ui-screenshot
Update README.md main UI screenshot
2025-02-05 10:41:53 -08:00
Russell Schmidt
83393e2a25 Update README.md main UI screenshot 2025-02-05 12:25:20 -06:00
pdxlocations
9073da802d Update Settings Image 2025-02-05 08:16:15 -08:00
pdxlocations
5907807b71 Merge pull request #118 from pdxlocations/add-commands-to-readme
Update ReadMe with Commands
2025-02-05 08:11:22 -08:00
pdxlocations
cc7124b6f5 add commands 2025-02-05 08:10:43 -08:00
pdxlocations
353412be11 Merge pull request #117 from rfschmid:add-node-search-feature
Add channel and node search feature
2025-02-04 17:21:20 -08:00
Russell Schmidt
8382da07a3 Fix indexing if list changes while searching 2025-02-04 19:16:00 -06:00
pdxlocations
01cfe4c681 Merge pull request #116 from rfschmid/add-lock-icon-for-PSK 2025-02-04 16:02:31 -08:00
Russell Schmidt
1675b0a116 Add channel and node search feature
Press Ctrl + / while the nodes or channels window is highlighted to
start search

Type text to search as you type, first matching item will be selected,
starting at current selected index

Press Tab to find next match starting from the current index - search
wraps around if necessary

Press Esc or Enter to exit search mode
2025-02-04 17:34:41 -06:00
Russell Schmidt
b717d46441 Add lock/unlock icon for nodes with/without PSK 2025-02-04 17:23:44 -06:00
11 changed files with 713 additions and 564 deletions

View File

@@ -5,13 +5,37 @@
This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores.
<img width="846" alt="Screenshot_2024-03-29_at_4 00 29_PM" src="https://github.com/pdxlocations/meshtastic-curses-client/assets/117498748/e99533b7-5c0c-463d-8d5f-6e3cccaeced7">
<img width="846" alt="Contact - Main UI Screenshot" src="https://github.com/user-attachments/assets/d2996bfb-2c6d-46a8-b820-92a9143375f4">
<br><br>
Settings can be accessed within the client or can be run standalone
The settings dialogue can be accessed within the client or may be run standalone to configure your node by launching `settings.py`
<img width="509" alt="Screenshot 2024-04-15 at 3 39 12PM" src="https://github.com/pdxlocations/meshtastic-curses-client/assets/117498748/37bc57db-fe2d-4ba4-adc8-679b4cb642f9">
<img width="441" alt="Contact - Settings Dialogue" src="https://github.com/user-attachments/assets/dd47f52a-d4d8-4e40-8001-9ea53d87f816" />
## Message Persistence
All messages will saved in a SQLite DB and restored upon relaunch of the app. You may delete `client.db` if you wish to erase all stored messages and node data. If multiple nodes are used, each will independently store data in the database, but the data will not be shared or viewable between nodes.
## Client Configuration
By navigating to Settings -> App Settings, you may customize your UI's icons, colors, and more!
## Commands
- `↑→↓←` = Navigate around the UI.
- `ENTER` = Send a message typed in the Input Window, or with the Node List highlighted, select a node to DM
- `` ` `` = Open the Settings dialogue
- `CTRL` + `p` = Hide/show a log of raw received packets.
- `CTRL` + `t` = With the Node List highlighted, send a traceroute to the selected node
- `CTRL` + `d` = With the Channel List hightlighted, archive a chat to reduce UI clutter. Messages will be saved in the db and repopulate if you send or receive a DM from this user.
- `ESC` = Exit out of the Settings Dialogue, or Quit the application if settings are not displayed.
### Search
- Press `CTRL` + `/` while the nodes or channels window is highlighted to start search
- Type text to search as you type, first matching item will be selected, starting at current selected index
- Press Tab to find next match starting from the current index - search wraps around if necessary
- Press Esc or Enter to exit search mode
## Arguments
@@ -22,7 +46,7 @@ You can pass the following arguments to the client:
Optional arguments to specify a device to connect to and how.
- `--port`, `--serial`, `-s`: The port to connect to via serial, e.g. `/dev/ttyUSB0`.
- `--host`, `--tcp`, `-t`: The hostname or IP address to connect to using TCP.
- `--host`, `--tcp`, `-t`: The hostname or IP address to connect to using TCP, will default to localhost if no host is passed.
- `--ble`, `-b`: The BLE device MAC address or name to connect to.
If no connection arguments are specified, the client will attempt a serial connection and then a TCP connection to localhost.
@@ -33,3 +57,8 @@ If no connection arguments are specified, the client will attempt a serial conne
python main.py --port /dev/ttyUSB0
python main.py --host 192.168.1.1
python main.py --ble BlAddressOfDevice
```
To quickly connect to localhost, use:
```sh
python main.py -t
```

View File

@@ -144,14 +144,16 @@ def load_messages_from_db():
def init_nodedb():
"""Initialize the node database and update it with nodes from the interface."""
try:
if not globals.interface.nodes:
return # No nodes to initialize
ensure_node_table_exists() # Ensure the table exists before insertion
nodes_snapshot = list(globals.interface.nodes.values())
# Insert or update all nodes
for node in globals.interface.nodes.values():
for node in nodes_snapshot:
update_node_info_in_db(
user_id=node['num'],
long_name=node['user'].get('longName', ''),

View File

@@ -1,4 +1,5 @@
interface = None
lock = None
display_log = False
all_messages = {}
channel_list = []

View File

@@ -207,19 +207,19 @@ def get_list_input(prompt, current_option, list_options):
return current_option
def move_highlight(old_idx, new_idx, options, enum_win, enum_pad):
def move_highlight(old_idx, new_idx, options, list_win, list_pad):
if old_idx == new_idx:
return # no-op
enum_pad.chgat(old_idx, 0, enum_pad.getmaxyx()[1], get_color("settings_default"))
enum_pad.chgat(new_idx, 0, enum_pad.getmaxyx()[1], get_color("settings_default", reverse = True))
list_pad.chgat(old_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default"))
list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse = True))
enum_win.refresh()
list_win.refresh()
start_index = max(0, new_idx - (enum_win.getmaxyx()[0] - 4))
start_index = max(0, new_idx - (list_win.getmaxyx()[0] - 4))
enum_win.refresh()
enum_pad.refresh(start_index, 0,
enum_win.getbegyx()[0] + 3, enum_win.getbegyx()[1] + 4,
enum_win.getbegyx()[0] + enum_win.getmaxyx()[0] - 2, enum_win.getbegyx()[1] + 4 + enum_win.getmaxyx()[1] - 4)
list_win.refresh()
list_pad.refresh(start_index, 0,
list_win.getbegyx()[0] + 3, list_win.getbegyx()[1] + 4,
list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2, list_win.getbegyx()[1] + 4 + list_win.getmaxyx()[1] - 4)

33
main.py
View File

@@ -3,7 +3,7 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.2.0
V 1.2.1
'''
import curses
@@ -11,12 +11,15 @@ from pubsub import pub
import os
import logging
import traceback
import threading
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 input_handlers import get_list_input
from utilities.utils import get_channels, get_node_list, get_nodeNum
from settings import set_region
from db_handler import init_nodedb, load_messages_from_db
import default_config as config
import globals
@@ -38,6 +41,8 @@ logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(message)s"
)
globals.lock = threading.Lock()
def main(stdscr):
try:
draw_splash(stdscr)
@@ -45,15 +50,23 @@ def main(stdscr):
args = parser.parse_args()
logging.info("Initializing interface %s", args)
globals.interface = initialize_interface(args)
logging.info("Interface initialized")
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()
logging.info("Starting main UI")
with globals.lock:
globals.interface = initialize_interface(args)
if globals.interface.localNode.localConfig.lora.region == 0:
confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"])
if confirmation == "Yes":
set_region()
globals.interface.close()
globals.interface = initialize_interface(args)
logging.info("Interface initialized")
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()
logging.info("Starting main UI")
main_ui(stdscr)
except Exception as e:
logging.error("An error occurred: %s", e)

View File

@@ -12,93 +12,94 @@ from datetime import datetime
def on_receive(packet, interface):
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
draw_packetlog_win()
try:
if 'decoded' not in packet:
return
with globals.lock:
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
draw_packetlog_win()
try:
if 'decoded' not in packet:
return
# Assume any incoming packet could update the last seen time for a node
changed = refresh_node_list()
if(changed):
draw_node_list()
# Assume any incoming packet could update the last seen time for a node
changed = refresh_node_list()
if(changed):
draw_node_list()
if packet['decoded']['portnum'] == 'NODEINFO_APP':
if "user" in packet['decoded'] and "longName" in packet['decoded']["user"]:
maybe_store_nodeinfo_in_db(packet)
if packet['decoded']['portnum'] == 'NODEINFO_APP':
if "user" in packet['decoded'] and "longName" in packet['decoded']["user"]:
maybe_store_nodeinfo_in_db(packet)
elif packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
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
refresh_channels = False
refresh_messages = False
if packet.get('channel'):
channel_number = packet['channel']
else:
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
if packet.get('channel'):
channel_number = packet['channel']
else:
globals.channel_list.append(packet['from'])
if(packet['from'] not in globals.all_messages):
globals.all_messages[packet['from']] = []
update_node_info_in_db(packet['from'], chat_archived=False)
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
else:
globals.channel_list.append(packet['from'])
if(packet['from'] not in globals.all_messages):
globals.all_messages[packet['from']] = []
update_node_info_in_db(packet['from'], chat_archived=False)
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
channel_number = globals.channel_list.index(packet['from'])
# Add received message to the messages list
message_from_id = packet['from']
message_from_string = get_name_from_database(message_from_id, type='short') + ":"
if globals.channel_list[channel_number] != globals.channel_list[globals.selected_channel]:
add_notification(channel_number)
refresh_channels = True
else:
refresh_messages = True
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
# Add received message to the messages list
message_from_id = packet['from']
message_from_string = get_name_from_database(message_from_id, type='short') + ":"
# Timestamp handling
current_timestamp = time.time()
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
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
# 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
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} --", ""))
# 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))
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)
if refresh_channels:
draw_channel_list()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
except KeyError as e:
logging.error(f"Error processing packet: {e}")
except KeyError as e:
logging.error(f"Error processing packet: {e}")

View File

@@ -3,7 +3,7 @@ import google.protobuf.json_format
from meshtastic import BROADCAST_NUM
from meshtastic.protobuf import mesh_pb2, portnums_pb2
from db_handler import save_message_to_db, update_ack_nak, get_name_from_database
from db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db
import default_config as config
import globals
@@ -94,6 +94,9 @@ def on_response_traceroute(packet):
globals.channel_list.append(packet['from'])
refresh_channels = True
if(is_chat_archived(packet['from'])):
update_node_info_in_db(packet['from'], chat_archived=False)
channel_number = globals.channel_list.index(packet['from'])
if globals.channel_list[channel_number] == globals.channel_list[globals.selected_channel]:

View File

@@ -5,7 +5,7 @@ import base64
from db_handler import update_node_info_in_db
import globals
def save_changes(interface, menu_path, modified_settings):
def save_changes(menu_path, modified_settings):
"""
Save changes to the device based on modified settings.
:param interface: Meshtastic interface instance
@@ -17,7 +17,7 @@ def save_changes(interface, menu_path, modified_settings):
logging.info("No changes to save. modified_settings is empty.")
return
node = interface.getNode('^local')
node = globals.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
@@ -27,7 +27,7 @@ def save_changes(interface, menu_path, modified_settings):
lon = float(modified_settings.get('longitude', 0.0))
alt = int(modified_settings.get('altitude', 0))
interface.localNode.setFixedPosition(lat, lon, alt)
globals.interface.localNode.setFixedPosition(lat, lon, alt)
logging.info(f"Updated {config_category} with Latitude: {lat} and Longitude {lon} and Altitude {alt}")
return
@@ -122,10 +122,5 @@ def save_changes(interface, menu_path, modified_settings):
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}")
logging.error(f"Error saving changes: {e}")

View File

@@ -9,6 +9,7 @@ from ui.menus import generate_menu_from_protobuf
from ui.colors import setup_colors, get_color
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
from ui.dialog import dialog
from user_config import json_editor
import globals
@@ -151,7 +152,7 @@ def settings_menu(stdscr, interface):
menu_win.erase()
menu_win.refresh()
if show_save_option and selected_index == len(options):
save_changes(interface, menu_path, modified_settings)
save_changes(menu_path, modified_settings)
modified_settings.clear()
logging.info("Changes Saved")
@@ -348,6 +349,25 @@ def settings_menu(stdscr, interface):
menu_win.refresh()
break
def set_region():
node = globals.interface.getNode('^local')
device_config = node.localConfig
lora_descriptor = device_config.lora.DESCRIPTOR
# Get the enum mapping of region names to their numerical values
region_enum = lora_descriptor.fields_by_name["region"].enum_type
region_name_to_number = {v.name: v.number for v in region_enum.values}
regions = list(region_name_to_number.keys())
new_region_name = get_list_input('Select your region:', 'UNSET', regions)
# Convert region name to corresponding enum number
new_region_number = region_name_to_number.get(new_region_name, 0) # Default to 0 if not found
node.localConfig.lora.region = new_region_number
node.writeConfig("lora")
def main(stdscr):
logging.basicConfig( # Run `tail -f client.log` in another terminal to view live

File diff suppressed because it is too large Load Diff

View File

@@ -4,39 +4,59 @@ import logging
import base64
def extract_fields(message_instance, current_config=None):
FIELD_EXCLUSION_MAP = {
"global": {"sessionkey"},
"channel": {"channel_num", "id"},
"radio": {"ignore_incoming"},
"module": None
}
def extract_fields(message_instance, current_config=None, parent_context=None, exclusion_map=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 {}
if exclusion_map is None:
exclusion_map = FIELD_EXCLUSION_MAP # Use default if not provided
# Combine global exclusions with context-specific exclusions
global_exclusions = exclusion_map.get("global", set())
context_exclusions = exclusion_map.get(parent_context, set())
total_exclusions = global_exclusions.union(context_exclusions)
menu = {}
fields = message_instance.DESCRIPTOR.fields
for field in fields:
if field.name in {"sessionkey", "channel_num", "id", "ignore_incoming"}: # Skip certain fields
if field.name in total_exclusions:
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)
menu[field.name] = extract_fields(
nested_instance,
nested_config,
parent_context=parent_context,
exclusion_map=exclusion_map
)
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
if isinstance(current_value, int): # Map enum number to 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})"
current_value_name = f"{enum_value.name}" if enum_value else 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
menu[field.name] = (field, current_value)
else: # 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": {}}
@@ -68,9 +88,18 @@ def generate_menu_from_protobuf(interface):
for i in range(8):
current_channel = interface.localNode.getChannelByChannelIndex(i)
if current_channel:
channel_config = extract_fields(channel, current_channel.settings)
# Convert 'psk' field to Base64
channel_config["psk"] = (channel_config["psk"][0], base64.b64encode(channel_config["psk"][1]).decode('utf-8'))
# Apply 'channel' context here
channel_config = extract_fields(
channel,
current_channel.settings,
parent_context="channel", # Dynamic context
exclusion_map=FIELD_EXCLUSION_MAP # Pass exclusion map
)
# Convert 'psk' to Base64
channel_config["psk"] = (
channel_config["psk"][0],
base64.b64encode(channel_config["psk"][1]).decode('utf-8')
)
menu_structure["Main Menu"]["Channels"][f"Channel {i + 1}"] = channel_config
# Add Radio Settings
@@ -99,14 +128,23 @@ def generate_menu_from_protobuf(interface):
ordered_position_menu[key] = value
# Update the menu with the new order
menu_structure["Main Menu"]["Radio Settings"]["position"] = ordered_position_menu
menu_structure["Main Menu"]["Radio Settings"] = extract_fields(
radio,
current_radio_config,
parent_context="radio",
exclusion_map=FIELD_EXCLUSION_MAP
)
# 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)
menu_structure["Main Menu"]["Module Settings"] = extract_fields(
module,
current_module_config,
parent_context="module", # Apply 'module' context
exclusion_map=FIELD_EXCLUSION_MAP
)
# Add App Settings
menu_structure["Main Menu"]["App Settings"] = {"Open": "app_settings"}