Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions lucy_cli/lucy_cli/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@ def get_user_input(prompt: str, timeout: float = 1.0) -> str | None:
# Timeout occurred
return None

def display_control_status(state: dict):
"""Prints the control-status banner shown on every TUI screen.

Always states who holds control (us, another client, or nobody) and always
reminds the user of the 'c' key, so the shortcut stays discoverable from any
screen rather than only the main menu.

Args:
state: The current UI state; reads 'has_control' and 'active_controller'.
"""
if state.get('has_control'):
print('>> YOU ARE IN CONTROL of the robot <<')
print("Type 'c' to release control.")
return

controller = state.get('active_controller')
if controller:
print(f'!! READ-ONLY - CONTROLLED BY: {controller} !!')
else:
print('No client has control.')
print("Type 'c' to take control.")

def display_help_screen():
"""Displays a static help message and waits for user confirmation."""
clear_screen()
Expand Down Expand Up @@ -212,15 +234,7 @@ def display_main_menu(state: dict):
if state.get('autorefresh'):
print('[Auto-Refresh: ON]')

if state.get('has_control'):
print('>> YOU ARE IN CONTROL of the robot <<')
print("Type 'c' to release control.")
else:
if state.get('active_controller'):
print(f"!! CONTROLLED BY: {state['active_controller']} !!")
else:
print('No client has control.')
print("Type 'c' to take control.")
display_control_status(state)

print('\nSelect an actuator group:')
for i, name in enumerate(state.get('actuator_groups', [])):
Expand All @@ -236,10 +250,8 @@ def display_category_menu(state: dict, group_name: str):
group_name: The name of the board group being displayed.
"""
print(f'--- {group_name} ---\n')
if state.get('has_control'):
print('>> YOU ARE IN CONTROL of the robot <<\n')
else:
print(f"!! READ-ONLY (Controlled by {state.get('active_controller')}) !!\n")
display_control_status(state)
print()

print('1. Actuators')
print('2. Sensors')
Expand All @@ -254,10 +266,8 @@ def display_sensor_menu(state: dict, group_name: str):
group_name: The name of the board group being displayed.
"""
print(f'--- {group_name} (Sensors) ---')
if state.get('has_control'):
print('>> YOU ARE IN CONTROL of the robot <<\n')
else:
print(f"!! READ-ONLY (Controlled by {state.get('active_controller')}) !!\n")
display_control_status(state)
print()

sensors = state.get('sensors', {}).get(group_name, {}).get('sensors', [])
for i, sensor in enumerate(sensors):
Expand All @@ -277,10 +287,8 @@ def display_joint_menu(state: dict, group_name: str):
group_name: The name of the actuator group being displayed.
"""
print(f'--- {group_name} ---')
if state.get('has_control'):
print('>> YOU ARE IN CONTROL of the robot <<\n')
else:
print(f"!! READ-ONLY (Controlled by {state.get('active_controller')}) !!\n")
display_control_status(state)
print()

joints = state.get('actuators', {}).get(group_name, {}).get('joints', [])
for i, joint in enumerate(joints):
Expand Down
38 changes: 28 additions & 10 deletions lucy_cli/lucy_cli/tui_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .ros_interface import LucyROSInterface
from .tui import clear_screen
from .tui import display_category_menu
from .tui import display_control_status
from .tui import display_control_taken_popup
from .tui import display_help_screen
from .tui import display_joint_menu
Expand Down Expand Up @@ -111,6 +112,20 @@ def _prompt_choice(autorefresh: bool) -> str | None:
return None
return choice.lower()

def _toggle_control(ros: LucyROSInterface, has_control: bool):
"""Takes or releases control, whichever the current state calls for.

Shared by the menus and by the sensor monitor screen so that the 'c' key
advertised by tui.display_control_status() works everywhere it is shown.
"""
if has_control:
ros.release_control()
print('Releasing control...')
else:
ros.take_control()
print('Requesting control...')
time.sleep(0.5)

def _handle_common_command(ros: LucyROSInterface, choice: str, autorefresh: bool,
has_control: bool) -> tuple[bool, bool, bool]:
"""Handles keys shared by every menu (quit / help / auto-refresh / control).
Expand All @@ -129,13 +144,7 @@ def _handle_common_command(ros: LucyROSInterface, choice: str, autorefresh: bool
time.sleep(0.7)
return True, autorefresh, False
if choice == 'c':
if has_control:
ros.release_control()
print('Releasing control...')
else:
ros.take_control()
print('Requesting control...')
time.sleep(0.5)
_toggle_control(ros, has_control)
return True, autorefresh, False
return False, autorefresh, False

Expand Down Expand Up @@ -259,11 +268,12 @@ def handle_sensor_menu(ros: LucyROSInterface, sensors: dict, group_name: str,
continue

# Menu-specific: monitor a sensor's live value by number.
_monitor_sensor(ros, sensors, group_name, choice)
_monitor_sensor(ros, sensors, group_name, choice, autorefresh)

return autorefresh, False

def _monitor_sensor(ros: LucyROSInterface, sensors: dict, group_name: str, choice: str):
def _monitor_sensor(ros: LucyROSInterface, sensors: dict, group_name: str, choice: str,
autorefresh: bool):
"""Continuously displays one sensor's live value until the user goes back.

Read-only: unlike _edit_joint, there's nothing to write, so this loops
Expand All @@ -284,19 +294,27 @@ def _monitor_sensor(ros: LucyROSInterface, sensors: dict, group_name: str, choic
history = deque(maxlen=GRAPH_WIDTH)
while rclpy.ok():
clear_screen()
state = _build_sensor_state(ros, sensors, autorefresh)
value = ros.get_sensor_values().get(sensor['name'])
history.append(value)
value_str = f'{value:.3f}' if value is not None else 'N/A (no data yet)'
print(f"--- Monitoring: {sensor['name']} ---\n")
display_control_status(state)
print()
print(f"Type: {sensor['type']}")
print(f"Associated actuator: {sensor.get('associated_actuator') or 'N/A'}")
print(f'Value: {value_str}\n')
print(render_sensor_graph(history))
print("\nPress 'b' to go back.")

choice = get_user_input('> ', timeout=0.5)
if choice is not None and choice.lower() == 'b':
if choice is None:
continue
choice = choice.lower()
if choice == 'b':
return
if choice == 'c':
_toggle_control(ros, state['has_control'])

def handle_actuator_menu(ros: LucyROSInterface, actuators: dict, group_name: str,
autorefresh: bool) -> tuple[bool, bool]:
Expand Down
Loading