Hermes asked for structured detections instead of a bare JPEG. faces() returns the
faces in view with no photo: a persistent track id, bbox, detection confidence, how
long the person has been in view, and identity {name, similarity} or null, never a
made-up name. look() returns that same JSON as text plus the photo (send_image can
skip it); list_people() gives names and counts; robot://status and two prompts make
the server discoverable, where list_resources and list_prompts were empty. The
detector now tracks faces across frames (FaceTracker) and publishes the track id and
the identity as a second hypothesis.
Deliberately not built: embeddings in responses, age/gender attributes, auto-updating
face data, and a remote enroll tool (a prompt could make the robot call anyone "Kyle").
Checked end to end: a real Hermes turn on the local ornith model called faces() (0.03 s,
no image) and answered that Kyle was there at 0.78 similarity. Also: the camera is
calibrated (18 of 24 views, 0.296 px, outlier rejection added) and the calibration is
kept in pi/calibration and restored by the installer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
The face now treats /hermes/state as the agent's heartbeat: when nothing has been
heard for agent_timeout_s (10 s) it logs "agent gone" and sleeps, so "sleeping"
really means "no agent connected". It also logs every state change, which is how
a missed wake message was found: a one-off publish before discovery matches is
lost under volatile QoS.
The live log showed the face flickering between listening and idle about twice a
second, because the detector let go after 0.4 s. Hold is now 1.2 s so pauses
between words do not drop it, and the margin 7 dB over the adaptive floor (quiet
room noise peaks sit about 3 dB over it), with tests for both.
Confirmed by the owner on the real display: gaze directions, and a mouth that
follows their live voice proportionally.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
audio_state taps the microphone and the speaker monitor with parec and publishes
levels, a speaking/listening/quiet activity, and trigger-locked waveform windows;
audio itself stays out of ROS. The activity detector calibrates its noise floor
at start-up and absorbs steady noise, so a room at -43 dBFS does not read as
"listening" forever, and it ignores the mic while the robot speaks.
The face arbitrates the state it shows: sleeping (agent side only, so room sound
never wakes a face that means "no agent connected") > speaking > listening >
thinking > idle, and draws the mouth from the real samples while fresh scope data
exists, auto-scaled so a quiet voice is still visible.
Checked: a tone played to the sink shows as `speaking` with the mic ignored. The
microphone-to-mouth path with a live voice is not yet confirmed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
hermes_face now subscribes to /vision/faces and glances toward the best face (a
recognised one first, then the biggest), smoothed, holding 2.5 s and drifting
back to centre when none is seen. Sleeping ignores gaze. Image x flips on the
display: the camera looks the way the robot faces, so a person on the image's
right is on the robot's right, the viewer's left.
Fix the pan sign carried over from the prototype: positive pan is the robot's
left (the MCP contract), which is the viewer's right on the display.
robot-face.service starts the face at boot and restarts it on failure, with a
private working directory because lgpio drops a notification pipe in its cwd and
systemd's default / is not writable. robot-cpu-performance.service holds the CPU
at full clock, which halves the face's CPU cost.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs
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
The Pi (hermes-bot) is flashed, upgraded and running ROS 2 Jazzy. The ST7789
works through Blinka on lgpio: pass cs=None (the kernel owns CE0), and the glass
sits at y_offset=80 in the controller RAM for every rotation, since the driver
rotates in software. The mounted pose needs rotation=270.
hermes_face is a numpy port of the flat face from docs/face-prototype.html, with
phosphor and vector styles, plus a new `sleeping` pose for "no agent connected".
The renderer has no ROS or hardware imports. The tone map is a lookup table and
the glow runs at half resolution, so the face reaches ~28 fps on the panel at
24% of a core (20 fps) to 40% (30 fps) with the CPU at full clock.
CONTEXT.md now records the Pi, the cloud-init first-boot-only gotcha, and what
is left. pi/50-cloud-init.yaml is a gitignored Wi-Fi template.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFN8nRRCiDiMN7ChBpHJhs