hermes_thermal reads the CPU temperature and throttle flags, publishes /health/*, and at 72 / 77 / 82 C scales the camera, face and detector rates down through their live parameters, restoring them as it cools one level at a time with a margin and a dwell so it does not flap. The face and detector gained live parameter callbacks for this, the bridge's robot://status carries a health block (temperature, level, throttle flags now and since boot), and a bringup test asserts the guard's normal rates equal the real ones. Live testing on the robot (forcing 73 / 78 / 85 C and back) caught a bug the unit tests missed: one logging line whose severity changed between warn and info raised in rclpy and killed the guard on its first cooling step, and the respawn hid it. Fixed with separate call sites; the tick now survives any error; a full heat-then-cool test drives the real node, and a mutation check confirmed it fails on the original bug. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
48 lines
2.4 KiB
Python
48 lines
2.4 KiB
Python
"""Bring up the robot: camera, face, face detection, audio levels, the MCP bridge and the thermal guard.
|
|
|
|
ros2 launch hermes_bringup robot.launch.py [camera:=false] [face:=false] [vision:=false] [audio:=false] [mcp:=false] [thermal:=false]
|
|
[start_delay:=6.0]
|
|
|
|
Every node is its own process and is respawned if it exits, so one crash cannot take the others
|
|
down: the face keeps running when the camera does not. Parameters are in config/robot.yaml.
|
|
|
|
The face starts at once; the camera, vision, audio and MCP nodes wait start_delay seconds, so the four
|
|
Python processes do not fight over the CPU while the face imports and draws its first frame.
|
|
"""
|
|
import os
|
|
|
|
from ament_index_python.packages import get_package_share_directory
|
|
from launch import LaunchDescription
|
|
from launch.actions import DeclareLaunchArgument, TimerAction
|
|
from launch.conditions import IfCondition
|
|
from launch.substitutions import LaunchConfiguration
|
|
from launch_ros.actions import Node
|
|
|
|
# (launch argument, package, executable, node name in robot.yaml)
|
|
NODES = [
|
|
('camera', 'camera_ros', 'camera_node', 'camera'),
|
|
('face', 'hermes_face', 'face_node', 'hermes_face'),
|
|
('vision', 'hermes_vision', 'face_detector', 'hermes_face_detector'),
|
|
('audio', 'hermes_audio', 'audio_state', 'hermes_audio_state'),
|
|
('mcp', 'hermes_mcp', 'mcp_server', 'hermes_mcp'),
|
|
('thermal', 'hermes_thermal', 'thermal_guard', 'hermes_thermal'),
|
|
]
|
|
|
|
|
|
DELAYED = {'camera', 'vision', 'audio', 'mcp', 'thermal'}
|
|
|
|
|
|
def generate_launch_description():
|
|
params = os.path.join(get_package_share_directory('hermes_bringup'), 'config', 'robot.yaml')
|
|
actions = [DeclareLaunchArgument(arg, default_value='true', description=f'run the {name} node')
|
|
for arg, _, _, name in NODES]
|
|
actions.append(DeclareLaunchArgument('start_delay', default_value='6.0',
|
|
description='seconds the other nodes wait for the face'))
|
|
for arg, package, executable, name in NODES:
|
|
node = Node(package=package, executable=executable, name=name, output='screen',
|
|
parameters=[params], respawn=True, respawn_delay=2.0,
|
|
condition=IfCondition(LaunchConfiguration(arg)))
|
|
actions.append(TimerAction(period=LaunchConfiguration('start_delay'), actions=[node])
|
|
if arg in DELAYED else node)
|
|
return LaunchDescription(actions)
|