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
173 lines
8.9 KiB
Bash
Executable File
173 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# Set up the robot's Raspberry Pi 4 from a fresh Ubuntu Server 24.04 (arm64) image.
|
|
#
|
|
# rsync -a ./ hermes@<pi>:~/hermes-bot/
|
|
# ssh hermes@<pi> 'cd hermes-bot && sudo ./pi/install.sh'
|
|
#
|
|
# Re-runnable: every step checks before it changes anything, and the heavy builds skip themselves if
|
|
# they are already done. Needs the internet (apt, git, pip, model downloads). The first run takes a
|
|
# while (the libcamera build alone is about 15 minutes). Reboot afterwards: it changes /boot/firmware/
|
|
# config.txt for the USB gadget and the audio codec.
|
|
#
|
|
# The user (default "hermes", set ROBOT_USER) must already exist: create it in cloud-init user-data.
|
|
# Order matters: the firewall goes in before PipeWire's network server, which listens on every
|
|
# interface and is protected only by that firewall.
|
|
set -euo pipefail
|
|
|
|
ROBOT_USER="${ROBOT_USER:-hermes}"
|
|
PI_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO="$(dirname "$PI_DIR")"
|
|
# Raspberry Pi's fork of libcamera, at a commit known to capture on this kernel. Upstream libcamera,
|
|
# Ubuntu's 0.2.0 and ROS's 0.7.2 all crash in the Pi image-processing plugin when capturing.
|
|
LIBCAMERA_REF=6c1dd9d55573010f710c9e190a73e7e76f0d9432
|
|
CAMERA_ROS_TAG=0.7.0
|
|
MODEL_BASE=https://github.com/opencv/opencv_zoo/raw/main/models # a plain raw URL returns a Git LFS pointer
|
|
|
|
export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a
|
|
REBOOT=0
|
|
|
|
[ "$(id -u)" = 0 ] || { echo "run with sudo" >&2; exit 1; }
|
|
HOME_DIR="$(getent passwd "$ROBOT_USER" | cut -d: -f6)"
|
|
[ -n "$HOME_DIR" ] || { echo "no user '$ROBOT_USER': create it in cloud-init user-data first" >&2; exit 1; }
|
|
grep -qs "Ubuntu 24.04" /etc/os-release || echo "warning: this is written for Ubuntu 24.04" >&2
|
|
|
|
step() { printf '\n==> %s\n' "$*"; }
|
|
as_user() { sudo -u "$ROBOT_USER" -H "$@"; }
|
|
cfg_sum() { sha256sum /boot/firmware/config.txt | cut -d' ' -f1; }
|
|
USER_UID="$(id -u "$ROBOT_USER")"
|
|
|
|
step "Base packages"
|
|
apt-get update -q
|
|
apt-get -y -q install git curl rsync software-properties-common openssl rfkill device-tree-compiler i2c-tools \
|
|
alsa-utils python3-numpy python3-pil python3-spidev python3-lgpio python3-rpi-lgpio python3-pip \
|
|
python3-venv pipewire pipewire-pulse wireplumber libspa-0.2-modules pulseaudio-utils \
|
|
meson ninja-build pkg-config python3-yaml python3-ply python3-jinja2 libyaml-dev libgnutls28-dev \
|
|
libssl-dev libudev-dev libevent-dev libdrm-dev libjpeg-dev libpng-dev libtiff-dev libboost-dev v4l-utils
|
|
|
|
step "Groups for hardware access"
|
|
for g in video audio i2c spi gpio dialout plugdev; do
|
|
getent group "$g" >/dev/null && usermod -aG "$g" "$ROBOT_USER" || echo " (no group $g on this image)"
|
|
done
|
|
|
|
step "ROS 2 Jazzy"
|
|
if ! dpkg -s ros-jazzy-ros-base >/dev/null 2>&1; then
|
|
add-apt-repository -y universe
|
|
. /etc/os-release
|
|
V="$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F tag_name | awk -F\" '{print $4}')"
|
|
curl -fL -o /tmp/ros2-apt-source.deb \
|
|
"https://github.com/ros-infrastructure/ros-apt-source/releases/download/${V}/ros2-apt-source_${V}.${UBUNTU_CODENAME:-$VERSION_CODENAME}_all.deb"
|
|
dpkg -i /tmp/ros2-apt-source.deb
|
|
apt-get update -q
|
|
fi
|
|
# ros-jazzy-camera-ros is installed for its dependencies; the camera driver actually used is built below
|
|
apt-get -y -q install ros-jazzy-ros-base ros-dev-tools python3-colcon-common-extensions \
|
|
ros-jazzy-robot-state-publisher ros-jazzy-xacro ros-jazzy-camera-ros ros-jazzy-vision-msgs
|
|
|
|
step "Raspberry Pi libcamera fork -> /opt/libcamera-rpi"
|
|
if [ -x /opt/libcamera-rpi/bin/cam ]; then
|
|
echo " already built"
|
|
else
|
|
rm -rf /var/tmp/libcamera-rpi
|
|
git clone -q --no-checkout https://github.com/raspberrypi/libcamera.git /var/tmp/libcamera-rpi
|
|
git -C /var/tmp/libcamera-rpi fetch -q --depth 1 origin "$LIBCAMERA_REF"
|
|
git -C /var/tmp/libcamera-rpi checkout -q FETCH_HEAD
|
|
(cd /var/tmp/libcamera-rpi \
|
|
&& meson setup build --buildtype=release -Dprefix=/opt/libcamera-rpi -Dpipelines=rpi/vc4 -Dipas=rpi/vc4 \
|
|
-Dcam=enabled -Dqcam=disabled -Dgstreamer=disabled -Dpycamera=disabled -Dlc-compliance=disabled \
|
|
-Dtest=false -Ddocumentation=disabled -Dv4l2=false \
|
|
&& ninja -C build -j4 && ninja -C build install)
|
|
fi
|
|
|
|
step "Python environment (~/venv: display, OpenCV 4.10 for YuNet, MCP SDK)"
|
|
# OpenCV's DNN needs 4.8+ for the YuNet model and apt has 4.6; numpy stays below 2 because the ROS
|
|
# Python packages are built against 1.x.
|
|
[ -d "$HOME_DIR/venv" ] || as_user python3 -m venv --system-site-packages "$HOME_DIR/venv"
|
|
as_user "$HOME_DIR/venv/bin/pip" install -q adafruit-circuitpython-rgb-display adafruit-blinka \
|
|
"opencv-python-headless==4.10.0.84" "numpy<2" "mcp>=2.3,<3"
|
|
|
|
step "Face models (~/models)"
|
|
as_user mkdir -p "$HOME_DIR/models"
|
|
fetch_model() { # name, path in the zoo
|
|
local dest="$HOME_DIR/models/$1"
|
|
if [ "$(stat -c %s "$dest" 2>/dev/null || echo 0)" -lt 100000 ]; then
|
|
as_user curl -fsSL -o "$dest" "$MODEL_BASE/$2/$1"
|
|
fi
|
|
[ "$(stat -c %s "$dest")" -gt 100000 ] || { echo " $1 looks like an LFS pointer, not the model" >&2; exit 1; }
|
|
}
|
|
fetch_model face_detection_yunet_2023mar.onnx face_detection_yunet
|
|
fetch_model face_recognition_sface_2021dec.onnx face_recognition_sface
|
|
|
|
step "camera_ros $CAMERA_ROS_TAG, built against the libcamera fork (~/camera_ws)"
|
|
if [ -d "$HOME_DIR/camera_ws/install/camera_ros" ]; then
|
|
echo " already built"
|
|
else
|
|
as_user mkdir -p "$HOME_DIR/camera_ws/src"
|
|
[ -d "$HOME_DIR/camera_ws/src/camera_ros" ] || as_user git clone -q --depth 1 --branch "$CAMERA_ROS_TAG" \
|
|
https://github.com/christianrauch/camera_ros.git "$HOME_DIR/camera_ws/src/camera_ros"
|
|
as_user bash -c "source /opt/ros/jazzy/setup.bash && export PKG_CONFIG_PATH=/opt/libcamera-rpi/lib/aarch64-linux-gnu/pkgconfig \
|
|
&& cd '$HOME_DIR/camera_ws' && colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release"
|
|
fi
|
|
|
|
step "Environment script and the robot's ROS workspace (~/ros_ws)"
|
|
install -o "$ROBOT_USER" -g "$ROBOT_USER" -m 644 "$PI_DIR/robot-env.sh" "$HOME_DIR/robot-env.sh"
|
|
as_user mkdir -p "$HOME_DIR/ros_ws/src"
|
|
rsync -a --delete --exclude __pycache__ "$REPO/ros/" "$HOME_DIR/ros_ws/src/"
|
|
chown -R "$ROBOT_USER:$ROBOT_USER" "$HOME_DIR/ros_ws/src"
|
|
as_user bash -c "source /opt/ros/jazzy/setup.bash && source '$HOME_DIR/camera_ws/install/setup.bash' \
|
|
&& cd '$HOME_DIR/ros_ws' && colcon build --symlink-install"
|
|
|
|
step "Camera calibration (this camera's, from pi/calibration; an existing file is never overwritten)"
|
|
as_user mkdir -p "$HOME_DIR/.ros/camera_info"
|
|
for f in "$PI_DIR"/calibration/*.yaml; do
|
|
[ -e "$f" ] || continue
|
|
[ -e "$HOME_DIR/.ros/camera_info/$(basename "$f")" ] || install -o "$ROBOT_USER" -g "$ROBOT_USER" -m 644 "$f" "$HOME_DIR/.ros/camera_info/"
|
|
done
|
|
|
|
step "USB gadget (NCM Ethernet + ACM serial) and the audio codec overlay"
|
|
before="$(cfg_sum)"
|
|
bash "$PI_DIR/install-usb-gadget.sh"
|
|
bash "$PI_DIR/install-audio-overlay.sh"
|
|
[ "$before" = "$(cfg_sum)" ] || REBOOT=1
|
|
|
|
step "Firewall (before anything listens on the network)"
|
|
bash "$PI_DIR/install-firewall.sh"
|
|
|
|
step "PipeWire for $ROBOT_USER (audio server, reachable only over the USB link)"
|
|
loginctl enable-linger "$ROBOT_USER"
|
|
as_user mkdir -p "$HOME_DIR/.config/pipewire/pipewire-pulse.conf.d"
|
|
install -o "$ROBOT_USER" -g "$ROBOT_USER" -m 644 "$PI_DIR/pipewire/50-network-server.conf" \
|
|
"$HOME_DIR/.config/pipewire/pipewire-pulse.conf.d/50-network-server.conf"
|
|
for _ in $(seq 20); do [ -S "/run/user/$USER_UID/bus" ] && break; sleep 1; done
|
|
as_user env XDG_RUNTIME_DIR="/run/user/$USER_UID" systemctl --user enable --now \
|
|
pipewire.socket pipewire-pulse.socket wireplumber.service \
|
|
|| echo " (the user session is not up yet; PipeWire starts at the next boot)"
|
|
|
|
step "Audio codec mixer and microphone gain"
|
|
if aplay -l 2>/dev/null | grep -q wm8960; then
|
|
alsactl -f "$PI_DIR/wm8960_asound.state" restore wm8960soundcard && alsactl store wm8960soundcard
|
|
# WirePlumber resets the codec's capture gain to +30 dB when it takes the device; 63% on PipeWire's
|
|
# cubic scale is -12 dB from that, about +17 dB.
|
|
as_user env XDG_RUNTIME_DIR="/run/user/$USER_UID" pactl set-source-volume @DEFAULT_SOURCE@ 63% \
|
|
|| echo " (could not set the source volume: run pactl set-source-volume @DEFAULT_SOURCE@ 63% later)"
|
|
else
|
|
echo " no WM8960 card yet: reboot, then run this script again"
|
|
REBOOT=1
|
|
fi
|
|
|
|
step "Services: the robot (face, camera, vision, audio, MCP bridge), the CPU governor, trimmed boot"
|
|
bash "$PI_DIR/install-robot-service.sh"
|
|
bash "$PI_DIR/trim-boot.sh"
|
|
|
|
cat <<EOF
|
|
|
|
Done.$([ "$REBOOT" = 1 ] && echo " REBOOT NOW: the USB gadget and the audio overlay only take effect then.")
|
|
|
|
Next:
|
|
- Slide the HAT's "Audio" switch (right edge) to ON, or every recording is silent.
|
|
- Plug the USB-C data cable into your computer, then on it: sudo host/omarchy/robot-usb-link.sh
|
|
- Hermes needs the MCP token. It is not printed here; read it with:
|
|
ssh $ROBOT_USER@10.55.0.2 sudo cat /etc/robot-mcp.env
|
|
then see host/omarchy/hermes-config-snippet.yaml.
|
|
- Check with: hermes mcp test robot
|
|
EOF
|