Add the agent heartbeat; retune listening so it stops flickering

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
This commit is contained in:
2026-10-04 23:43:14 -07:00
co-authored by Claude Sonnet 5.5
parent 79920ff6bc
commit 5d1ad1eff7
7 changed files with 95 additions and 15 deletions
+16 -4
View File
@@ -145,7 +145,15 @@ printed enclosure, speakers. Work until those arrive is limited to what the Pi,
glance is smoothed, held 2.5 s, then drifts back to centre. Sleeping ignores it. The pan sign in
the renderer was wrong (carried over from the prototype) and is fixed: positive pan = the robot's
left = the viewer's right, positive tilt = looks up, as the MCP `turn_head` tool documents.
Not yet confirmed by eye on the real display.
**Confirmed by the owner on the real display (2026-10-04):** the glance directions look right.
An earlier run showed nothing because the one-off wake message was lost (volatile QoS drops a
sample published before discovery matches); test publishers must republish, as `hermes_mcp`
will. The node logs every state change (`state: idle (agent=..., audio=...)`), so behaviour can be
checked from `journalctl -u robot-face` without watching the panel.
- **Agent heartbeat:** `/hermes/state` doubles as the agent's heartbeat. `hermes_mcp` must republish
its state about once a second; after `agent_timeout_s` (default 10, 0 disables) with none, the
face logs `agent gone, sleeping` and sleeps, so "sleeping" means "no agent connected". Verified:
woke on messages, slept 10 s after the last.
- 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
@@ -169,11 +177,15 @@ printed enclosure, speakers. Work until those arrive is limited to what the Pi,
scale is cubic: 63% is -12 dB from the +30 dB maximum, so ALSA `Capture` lands at 46, about +17
dB). WirePlumber remembers it; recheck after a reboot. The quiet-room floor at this gain is about
-43 dBFS (preamp and mic noise).
- **Confirmed by the owner:** the mouth draws their live voice and its size follows how close and
loud they are (proportional). The activity detector was retuned after the log showed the face
flickering between listening and idle about twice a second: margin 7 dB (quiet-room noise peaks
sit at about +3 dB over the floor), hold 1.2 s so pauses between words do not drop it. The
flicker fix is covered by unit tests but not yet re-confirmed live (the retest window was silent).
- Verified on the Pi: the `speaking` path (a tone played to the sink, with no speaker attached, shows
on the monitor at -13.5 dBFS, `speaking` for its length, mic ignored); levels and scope publish;
47 face tests, 8 audio tests. **Not yet verified:** the microphone-to-mouth path with a real voice
(one live attempt saw no speech: the cue and the talking did not overlap), and the audio node as a
service (it was run as a transient unit).
49 face tests, 10 audio tests. **Not yet verified:** the audio node as a service (it was run as a
transient unit).
## Face recognition (opt-in)