mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
Compare commits
11 Commits
UIState
...
client-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f270b5ba5 | ||
|
|
4abe9611e3 | ||
|
|
4d20df17fe | ||
|
|
3bb57b9420 | ||
|
|
e305bb4464 | ||
|
|
636b27cf9b | ||
|
|
8e500cb305 | ||
|
|
0878937194 | ||
|
|
ac2016322b | ||
|
|
031d74a290 | ||
|
|
14913ce5ae |
@@ -9,9 +9,9 @@ This Python curses client for Meshtastic is a terminal-based client designed to
|
|||||||
<img width="846" alt="Contact - Main UI Screenshot" src="https://github.com/user-attachments/assets/d2996bfb-2c6d-46a8-b820-92a9143375f4">
|
<img width="846" alt="Contact - Main UI Screenshot" src="https://github.com/user-attachments/assets/d2996bfb-2c6d-46a8-b820-92a9143375f4">
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
The settings dialogue can be accessed within the client or may be run standalone to configure your node by launching `settings.py`
|
The settings dialogue can be accessed within the client or may be run standalone to configure your node by launching `contact --settings` or `contact -c`
|
||||||
|
|
||||||
<img width="441" alt="Contact - Settings Dialogue" src="https://github.com/user-attachments/assets/dd47f52a-d4d8-4e40-8001-9ea53d87f816" />
|
<img width="573" alt="Contact - Settings Dialogue" src="https://github.com/user-attachments/assets/dbe1287b-5558-407c-84b8-2a1bc913dec8" />
|
||||||
|
|
||||||
## Message Persistence
|
## Message Persistence
|
||||||
|
|
||||||
@@ -62,4 +62,4 @@ contact --ble BlAddressOfDevice
|
|||||||
To quickly connect to localhost, use:
|
To quickly connect to localhost, use:
|
||||||
```sh
|
```sh
|
||||||
contact -t
|
contact -t
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ dns, "IPv4 DNS server", ""
|
|||||||
rsyslog_server, "RSyslog server", ""
|
rsyslog_server, "RSyslog server", ""
|
||||||
enabled_protocols, "Enabled protocols", ""
|
enabled_protocols, "Enabled protocols", ""
|
||||||
|
|
||||||
|
[config.network.ipv4_config]
|
||||||
|
title, "IPv4 Config", ""
|
||||||
|
ip, "IP", ""
|
||||||
|
gateway, "Gateway", ""
|
||||||
|
subnet, "Subnet", ""
|
||||||
|
dns, "DNS", ""
|
||||||
|
|
||||||
[config.display]
|
[config.display]
|
||||||
title, "Display"
|
title, "Display"
|
||||||
screen_on_secs, "Screen on duration", "How long the screen remains on in seconds after the user button is pressed or messages are received."
|
screen_on_secs, "Screen on duration", "How long the screen remains on in seconds after the user button is pressed or messages are received."
|
||||||
@@ -105,6 +112,35 @@ theme, "Theme", ""
|
|||||||
alert_enabled, "Alert enabled", ""
|
alert_enabled, "Alert enabled", ""
|
||||||
banner_enabled, "Banner enabled", ""
|
banner_enabled, "Banner enabled", ""
|
||||||
ring_tone_id, "Ring tone ID", ""
|
ring_tone_id, "Ring tone ID", ""
|
||||||
|
language, "Language", ""
|
||||||
|
node_filter, "Node Filter", ""
|
||||||
|
node_highlight, "Node Highlight", ""
|
||||||
|
calibration_data, "Calibration Data", ""
|
||||||
|
map_data, "Map Data", ""
|
||||||
|
|
||||||
|
[config.device_ui.node_filter]
|
||||||
|
title, "Node Filter"
|
||||||
|
unknown_switch, "Unknown Switch", ""
|
||||||
|
offline_switch, "Offline Switch", ""
|
||||||
|
public_key_switch, "Public Key Switch", ""
|
||||||
|
hops_away, "Hops Away", ""
|
||||||
|
position_switch, "Position Switch", ""
|
||||||
|
node_name, "Node Name", ""
|
||||||
|
channel, "Channel", ""
|
||||||
|
|
||||||
|
[config.device_ui.node_highlight]
|
||||||
|
title, "Node Highlight"
|
||||||
|
chat_switch, "Chat Switch", ""
|
||||||
|
position_switch, "Position Switch", ""
|
||||||
|
telemetry_switch, "Telemetry Switch", ""
|
||||||
|
iaq_switch, "IAQ Switch", ""
|
||||||
|
node_name, "Node Name", ""
|
||||||
|
|
||||||
|
[config.device_ui.map_data]
|
||||||
|
title, "Map Data"
|
||||||
|
home, "Home", ""
|
||||||
|
style, "Style", ""
|
||||||
|
follow_gps, "Follow GPS", ""
|
||||||
|
|
||||||
[config.lora]
|
[config.lora]
|
||||||
title, "LoRa"
|
title, "LoRa"
|
||||||
|
|||||||
@@ -565,8 +565,11 @@ def settings_menu(stdscr, interface):
|
|||||||
state.start_index.pop()
|
state.start_index.pop()
|
||||||
|
|
||||||
elif field.type == 8: # Handle boolean type
|
elif field.type == 8: # Handle boolean type
|
||||||
new_value = get_list_input(human_readable_name, str(current_value), ["True", "False"])
|
new_value = get_list_input(human_readable_name, str(current_value), ["True", "False"])
|
||||||
new_value = new_value == "True" or new_value is True
|
if new_value == "Not Set":
|
||||||
|
pass # Leave it as-is
|
||||||
|
else:
|
||||||
|
new_value = new_value == "True" or new_value is True
|
||||||
state.start_index.pop()
|
state.start_index.pop()
|
||||||
|
|
||||||
elif field.label == field.LABEL_REPEATED: # Handle repeated field - Not currently used
|
elif field.label == field.LABEL_REPEATED: # Handle repeated field - Not currently used
|
||||||
|
|||||||
@@ -167,20 +167,20 @@ def display_menu(state):
|
|||||||
return menu_win, menu_pad, options
|
return menu_win, menu_pad, options
|
||||||
|
|
||||||
|
|
||||||
def move_highlight(old_idx, new_idx, options, menu_win, menu_pad, state):
|
def move_highlight(old_idx, options, menu_win, menu_pad, state):
|
||||||
if old_idx == new_idx: # No-op
|
if old_idx == state.selected_index: # No-op
|
||||||
return
|
return
|
||||||
|
|
||||||
max_index = len(options) + (1 if state.show_save_option else 0) - 1
|
max_index = len(options) + (1 if state.show_save_option else 0) - 1
|
||||||
visible_height = menu_win.getmaxyx()[0] - 5 - (2 if state.show_save_option else 0)
|
visible_height = menu_win.getmaxyx()[0] - 5 - (2 if state.show_save_option else 0)
|
||||||
|
|
||||||
# Adjust state.start_index only when moving out of visible range
|
# Adjust state.start_index only when moving out of visible range
|
||||||
if new_idx == max_index and state.show_save_option:
|
if state.selected_index == max_index and state.show_save_option:
|
||||||
pass
|
pass
|
||||||
elif new_idx < state.start_index[-1]: # Moving above the visible area
|
elif state.selected_index < state.start_index[-1]: # Moving above the visible area
|
||||||
state.start_index[-1] = new_idx
|
state.start_index[-1] = state.selected_index
|
||||||
elif new_idx >= state.start_index[-1] + visible_height: # Moving below the visible area
|
elif state.selected_index >= state.start_index[-1] + visible_height: # Moving below the visible area
|
||||||
state.start_index[-1] = new_idx - visible_height
|
state.start_index[-1] = state.selected_index - visible_height
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Ensure state.start_index is within bounds
|
# Ensure state.start_index is within bounds
|
||||||
@@ -193,10 +193,10 @@ def move_highlight(old_idx, new_idx, options, menu_win, menu_pad, state):
|
|||||||
menu_pad.chgat(old_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
menu_pad.chgat(old_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default"))
|
||||||
|
|
||||||
# Highlight new selection
|
# Highlight new selection
|
||||||
if state.show_save_option and new_idx == max_index:
|
if state.show_save_option and state.selected_index == max_index:
|
||||||
menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse=True))
|
menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse=True))
|
||||||
else:
|
else:
|
||||||
menu_pad.chgat(new_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[new_idx] in sensitive_settings else get_color("settings_default", reverse=True))
|
menu_pad.chgat(state.selected_index, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[state.selected_index] in sensitive_settings else get_color("settings_default", reverse=True))
|
||||||
|
|
||||||
menu_win.refresh()
|
menu_win.refresh()
|
||||||
|
|
||||||
@@ -264,18 +264,18 @@ def json_editor(stdscr, state):
|
|||||||
|
|
||||||
old_selected_index = state.selected_index
|
old_selected_index = state.selected_index
|
||||||
state.selected_index = max_index if state.selected_index == 0 else state.selected_index - 1
|
state.selected_index = max_index if state.selected_index == 0 else state.selected_index - 1
|
||||||
move_highlight(old_selected_index, state.selected_index, options, state.show_save_option, menu_win, menu_pad,state)
|
move_highlight(old_selected_index, options, menu_win, menu_pad, state)
|
||||||
|
|
||||||
elif key == curses.KEY_DOWN:
|
elif key == curses.KEY_DOWN:
|
||||||
|
|
||||||
old_selected_index = state.selected_index
|
old_selected_index = state.selected_index
|
||||||
state.selected_index = 0 if state.selected_index == max_index else state.selected_index + 1
|
state.selected_index = 0 if state.selected_index == max_index else state.selected_index + 1
|
||||||
move_highlight(old_selected_index, state.selected_index, options, menu_win, menu_pad, state)
|
move_highlight(old_selected_index, options, menu_win, menu_pad, state)
|
||||||
|
|
||||||
elif key == ord("\t") and state.show_save_option:
|
elif key == ord("\t") and state.show_save_option:
|
||||||
old_selected_index = state.selected_index
|
old_selected_index = state.selected_index
|
||||||
state.selected_index = max_index
|
state.selected_index = max_index
|
||||||
move_highlight(old_selected_index, state.selected_index, options, menu_win, menu_pad, state)
|
move_highlight(old_selected_index, options, menu_win, menu_pad, state)
|
||||||
|
|
||||||
elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return
|
elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ def initialize_interface(args):
|
|||||||
return meshtastic.tcp_interface.TCPInterface(args.host)
|
return meshtastic.tcp_interface.TCPInterface(args.host)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return meshtastic.serial_interface.SerialInterface(args.port)
|
client = meshtastic.serial_interface.SerialInterface(args.port)
|
||||||
except PermissionError as ex:
|
except PermissionError as ex:
|
||||||
logging.error(f"You probably need to add yourself to the `dialout` group to use a serial connection. {ex}")
|
logging.error(f"You probably need to add yourself to the `dialout` group to use a serial connection. {ex}")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(f"Unexpected error initializing interface: {ex}")
|
logging.error(f"Unexpected error initializing interface: {ex}")
|
||||||
if globals.interface.devPath is None:
|
if client.devPath is None:
|
||||||
return meshtastic.tcp_interface.TCPInterface("meshtastic.local")
|
client = meshtastic.tcp_interface.TCPInterface("localhost")
|
||||||
|
|
||||||
|
return client
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.critical(f"Fatal error initializing interface: {ex}")
|
logging.critical(f"Fatal error initializing interface: {ex}")
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "contact"
|
name = "contact"
|
||||||
version = "1.3.2"
|
version = "1.3.4"
|
||||||
description = "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."
|
description = "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."
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
|
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
|
||||||
|
|||||||
Reference in New Issue
Block a user