Files
hermes-bot/CONTEXT.md
T
kyleandClaude Sonnet 5.5 69a93eeb2f Bring up the USB gadget link (package 0b, Pi side and talos side)
The Pi presents NCM Ethernet and ACM serial from a configfs gadget, started by
robot-usb-gadget.service before networking. usb0 gets 10.55.0.2/30 with no
gateway through netplan, matched by interface name: a MAC match turns into
PermanentMACAddress= in networkd, which a gadget interface does not have, so it
stayed unmanaged. talos reaches the Pi at 10.55.0.2 (ping ~2.4 ms, SSH works)
through a NetworkManager profile, host/omarchy/robot-usb-link.sh, matched by the
gadget's fixed host MAC.

Also clamp the face's tunable parameters (focus sets a blur radius, persistence
divides) and fps, after a review flagged unvalidated node parameters.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
2026-10-04 20:53:09 -07:00

7.6 KiB

Current state

Last updated 2026-10-04. HANDOFF.md is the brief; this file tracks where the work stands.

Hardware on hand

Raspberry Pi 4 (8 GB), BrainCraft HAT, Arducam UC-346 camera. Not yet: servos, USB-C splitter, printed enclosure, speakers. Work until those arrive is limited to what the Pi, HAT and camera can do.

The Pi (hermes-bot)

  • Ubuntu 24.04.5 server, kernel 6.8.0-1065-raspi (fully upgraded), flashed from talos. User hermes (key-only; passwordless sudo), SSH keys for kyle@vade (talos) and kyle@straylight.
  • On Wi-Fi at 192.168.88.152 (MAC d8:3a:dd:0d:66:f5); eth0 is not cabled. The owner says it has a static IP on the rack switch; that address is not recorded here yet. USB link: 10.55.0.2 (see package 0b below); ssh hermes@10.55.0.2 works from talos. avahi-daemon is installed, but mDNS is off in systemd-resolved on talos, so hermes-bot.local doesn't resolve there; use the IP.
  • cloud-init applies network-config and user-data on the first boot only. To change Wi-Fi later, edit /etc/netplan/50-cloud-init.yaml on the Pi, or bump instance_id in meta-data on system-boot. pi/50-cloud-init.yaml is a template for the Wi-Fi block; it is gitignored because it holds credentials.
  • Installed: ROS 2 Jazzy (ros-base, ros-dev-tools, colcon, robot_state_publisher, xacro, camera_ros); python3-numpy/pil/spidev/lgpio/rpi-lgpio; a venv ~/venv (system site packages visible) with adafruit-circuitpython-rgb-display and Blinka. Workspace: ~/ros_ws, source synced from ros/ with rsync -a --delete --exclude __pycache__ ros/ hermes@<ip>:~/ros_ws/src/, then colcon build --symlink-install.
  • CPU governor is ondemand; at full clock the face costs about half as much CPU. Set performance in the installer (package 5). It is not persistent yet.

Package 0a (platform proof on Ubuntu 24.04)

  • Step 1 done: host/check-pi-platform.sh passes.
  • Step 2 done: flashed, booted, upgraded; ROS installed; ros2 topic list works.
  • Step 5 (display) done. ST7789 works on lgpio through Blinka (rpi-lgpio, no RPi.GPIO). Two gotchas: pass cs=None (the kernel drives CE0; claiming GPIO8 raises "GPIO busy"), and the 240x240 glass sits at y_offset=80 in the controller's RAM, for every rotation (the driver rotates in software and leaves the scan direction fixed). Mounted pose (microSD end up, USB-C edge on the left) needs content turned 90 degrees clockwise: rotation=270. Full-frame push is ~23 fps through the driver; the face pushes raw RGB565 with _block and reaches ~27-28 fps.
  • Not done: audio (wm8960-soundcard overlay + mixer), camera (cam -l, camera_ros), PWM (waits for servos). Ask before rebooting the Pi.
  • Pi: dtoverlay=dwc2,dr_mode=peripheral; robot-usb-gadget.service builds a configfs gadget (NCM usb0 + ACM ttyGS0, host MAC 02:68:65:72:6d:01, robot MAC 02:68:65:72:6d:02) before networking; serial-getty@ttyGS0 is enabled; /etc/netplan/60-usb0.yaml gives usb0 10.55.0.2/30 with no gateway, so the default route stays on Wi-Fi. Installed by pi/install-usb-gadget.sh (a backup of config.txt is config.txt.bak-usbgadget). NCM works on the Pi 4 with kernel 1065; ECM was not needed.
  • netplan gotcha: match the gadget interface by name: usb0. A macaddress: match becomes PermanentMACAddress= in networkd, which a gadget interface lacks, so it stays unmanaged.
  • talos (Omarchy, NetworkManager): host/omarchy/robot-usb-link.sh (needs sudo) adds profile hermes-robot-usb: 10.55.0.1/30 on the interface with MAC 02:68:65:72:6d:01, priority 100, never-default. Verified from talos: ping ~2.4 ms, SSH over the link.
  • Not yet verified: serial login prompt (talos's /dev/ttyACM0 is root:uucp; use sudo screen /dev/ttyACM0 115200). The hermes account has no password, so the console shows a login prompt but cannot log in until the owner sets one; no autologin by design. Also not done: the nftables rewrite, the Pi booting with Wi-Fi off, replug and second-host recovery, get_throttled after a servo sweep on the splitter, the NixOS snippet, and straylight/other-host testing. Currently the Pi is powered from talos's USB port (bench test, no servos).

Face (ros/hermes_face)

  • render.py: numpy port of docs/face-prototype.html, flat face only (head and lowpoly not ported). Phosphor and vector styles. States: sleeping idle listening thinking looking speaking. sleeping (new) is the "no agent connected" pose: eyes closed to thin lines, mouth short, dim and slowly breathing. looking returns to the state it interrupted. Tone mapping is a lookup table (RGB and RGB565 tables), glow is computed at half resolution, rotation is a free np.rot90.
  • display.py: ST7789 driver plus an async pusher (render overlaps the SPI transfer).
  • face_node.py: node hermes_face; subscribes /hermes/state (std_msgs/String) and /joint_states (pan_joint, tilt_joint, radians); starts in initial_state (sleeping), so it runs with nothing else up. Parameters: rotation (270), x_offset (0), y_offset (80), baudrate, fps, output (st7789 or none), phosphor (P7), style, persistence_ms, focus, jitter, intensity, initial_state.
  • demo.py: no ROS. python -m hermes_face.demo --sequence cycles the states on the panel; --png-dir DIR writes a PNG per state without hardware.
  • Tests: python3 -m pytest test in ros/hermes_face (25 pass on the Pi). Renderer parameters (focus, persistence_ms, intensity, jitter) are clamped; fps is clamped to 1-60.
  • Measured on the Pi 4: ~9 ms per frame in isolation. With the CPU at full clock: 24% of a core at 20 fps and 40% at 30 fps (target was under 40% at 20 fps or more). Under ondemand the same load reads 40% at 20 fps and ~67% at 30 fps. No C++ rewrite was needed.
  • Not yet: audio levels for the mouth (/audio/levels), hermes_mcp publishing states, launch file in hermes_bringup, head/lowpoly shapes, DotStar LEDs, button mute.
  • Open: IPS black level. The backlight is on at full brightness, so black reads dark gray on camera. Dimming it needs a software PWM on GPIO26 (not a hardware PWM pin).

Next

  1. Finish package 0b: nftables rewrite (accept 4713/8765 only on usb0 from 10.55.0.1), Wi-Fi-off boot test, replug recovery, NixOS snippet in host/nixos/.
  2. Audio (package 1; needs the wm8960-soundcard overlay and a reboot), camera (package 3), then the rest of the packages in HANDOFF.md.

Repository

Branch master. .gitignore covers .remember/, caches, .venv/, *.orig, *.rej and pi/50-cloud-init.yaml. tools/check-pi-platform.sh duplicates host/check-pi-platform.sh and can probably go.

An automated review of the initial commit flagged three items. All stem from the code predating the USB-gadget design (HANDOFF.md package 0b); none is exposed until the Pi runs the services.

  • pi/robot_mcp_server.py: plain HTTP, ROBOT_MCP_BIND defaults to 0.0.0.0. Plan: default to 10.55.0.2, start after usb0.
  • pi/pipewire/50-network-server.conf: TCP 4713 on all interfaces, unrestricted. Plan: keep, comment that the firewall is the protection.
  • pi/nftables-robot.nft: still the Pi Zero version (single host IP, any interface). Package 0b rewrites it to accept only on the USB interface from 10.55.0.1.
  • pi/install.sh permissions looked fine (/etc/robot-mcp.env is root:$USER, 640).

The owner wants to confirm the real IPs on the link before changing any of this. If the USB link doesn't work, revisit: TLS or a Tailscale-only bind for MCP and audio.