The MCP server is now a ROS node and the only way in or out, per the owner's decision that ROS never leaves the robot. It serves MCP on the USB address only with a bearer token, keeps the stable tool name look() (which waits for the next camera frame, so it is never stale, and passes the camera's JPEG through), and registers turn_head/head_position only when a head is fitted, which today it is not. While an authenticated client is connected it republishes the agent heartbeat on /hermes/state, so the face wakes on connect and sleeps about 40 s after the client leaves; look() also makes the face squint. The token is generated on the Pi into /etc/robot-mcp.env (root:hermes, 640) and never printed. Verified end to end from talos with a real MCP client over the USB link: tools listed, a 157 KB JPEG back in 0.08 s, a wrong token refused, the Wi-Fi path dropped by the firewall. Host config snippets now point at 10.55.0.2. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
41 lines
1.9 KiB
Python
41 lines
1.9 KiB
Python
import os
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
pytest.importorskip('launch_ros')
|
|
|
|
HERE = os.path.dirname(__file__)
|
|
ROOT = os.path.dirname(HERE)
|
|
|
|
|
|
def test_launch_description_has_one_respawning_process_per_node():
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location('robot_launch', os.path.join(ROOT, 'launch', 'robot.launch.py'))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
from launch.actions import TimerAction
|
|
from launch_ros.actions import Node
|
|
ld = mod.generate_launch_description()
|
|
nodes = [e for e in ld.entities if isinstance(e, Node)]
|
|
delayed = [n for t in ld.entities if isinstance(t, TimerAction) for n in t.actions if isinstance(n, Node)]
|
|
assert len(nodes) + len(delayed) == len(mod.NODES) == 5
|
|
assert len(nodes) == 1 and 'face' in mod.NODES[1] and len(delayed) == 4 # the face starts at once
|
|
# respawn lives on a private attribute that differs between ROS releases, so check the source
|
|
src = open(os.path.join(ROOT, 'launch', 'robot.launch.py')).read()
|
|
assert 'respawn=True' in src and 'respawn_delay' in src
|
|
|
|
|
|
def test_every_node_in_the_launch_file_has_parameters_and_the_face_is_mounted_portrait():
|
|
cfg = yaml.safe_load(open(os.path.join(ROOT, 'config', 'robot.yaml')))
|
|
for name in ('camera', 'hermes_face', 'hermes_face_detector', 'hermes_audio_state', 'hermes_mcp'):
|
|
assert 'ros__parameters' in cfg[name], name
|
|
assert cfg['hermes_face']['ros__parameters']['rotation'] == 270
|
|
assert cfg['camera']['ros__parameters']['FrameDurationLimits'] == [66666, 66666]
|
|
|
|
|
|
def test_the_mcp_bridge_binds_the_usb_link_and_has_no_head_yet():
|
|
cfg = yaml.safe_load(open(os.path.join(ROOT, 'config', 'robot.yaml')))['hermes_mcp']['ros__parameters']
|
|
assert cfg['bind'] == '10.55.0.2' and cfg['port'] == 8765 and cfg['head'] is False
|
|
assert 'token' not in ' '.join(cfg) # the secret lives in an env file, not here
|