96 lines
5.5 KiB
Docker

# SanadR1 — Gemini voice + dashboard for the Unitree R1 EDU backpack.
#
# BUILD THIS ON THE BACKPACK (native aarch64/arm64). Building on an x86 host
# needs `docker buildx` + qemu. Needs internet at build time (CycloneDDS clone,
# pip). Run with `--network host` (see docker-compose.yml) — the robot's DDS and
# the on-board mic multicast (239.168.123.161:5555) do not traverse a NAT bridge.
# Preferred launch: docker/build_and_run.sh (it also disables the old conda service).
FROM arm64v8/python:3.10-slim-bookworm
# ── System deps ──────────────────────────────────────────────────────────────
# build-essential/cmake → compile the CycloneDDS *python binding* (no
# aarch64 wheel) + PyAudio if it falls back to sdist
# cyclonedds-dev/-tools → PREBUILT CycloneDDS 0.10.2 C lib + idlc for arm64
# (Debian bookworm ships 0.10.2-2 — exact robot
# match; avoids the source compile that segfaults
# under qemu cross-build and is slow even native)
# portaudio19-dev/libasound2 → PyAudio must import (AudioManager needs it even
# with no sound cards; audio itself is DDS/UDP)
# iproute2 → REQUIRED: BuiltinMic._find_g1_local_ip() shells
# `ip -4 -o addr` unguarded; missing `ip` = no mic
# libgomp1 → provides libgomp.so.1 for the LD_PRELOAD below
# curl → HEALTHCHECK
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake \
cyclonedds-dev cyclonedds-tools \
portaudio19-dev libasound2 iproute2 libgomp1 \
ffmpeg \
pulseaudio pulseaudio-utils alsa-utils \
curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# aarch64 "cannot allocate memory in static TLS block" guard on numpy/opencv
# import (start_sanad.sh calls this a mandatory Jetson fix). Soname form is
# resolved via ldconfig, so it can't point at a missing path (libgomp1 above).
ENV LD_PRELOAD=libgomp.so.1
# cyclonedds' find helper (buildhelp/cyclone_search.py) hardcodes
# $CYCLONEDDS_HOME/lib/libddsc.so, but Debian installs into the multiarch dir
# /usr/lib/aarch64-linux-gnu. Symlink the two libs it checks into /usr/lib so
# good_directory(/usr) passes. (Runtime loading uses the multiarch path already
# on the default loader search, so no LD_LIBRARY_PATH is needed.)
ENV CYCLONEDDS_HOME=/usr
RUN ln -sf /usr/lib/aarch64-linux-gnu/libddsc.so /usr/lib/libddsc.so \
&& ln -sf /usr/lib/aarch64-linux-gnu/libcycloneddsidl.so /usr/lib/libcycloneddsidl.so
# ── Python dependencies (mask removed → no bleak) ────────────────────────────
COPY docker/requirements.docker.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# CycloneDDS python binding, compiled against the C lib above (no aarch64 wheel).
# cyclonedds 0.10.2's build helper imports wheel.bdist_wheel (removed in
# wheel>=0.46), so pin the build toolchain before the --no-build-isolation build.
RUN pip install --no-cache-dir "setuptools<80" "wheel<0.46" \
&& pip install --no-cache-dir --no-build-isolation cyclonedds==0.10.2
# ── Unitree SDK (vendored wheel — not on PyPI) + native crc lib (wheel omits it)
COPY docker/vendor/unitree_sdk2py-*.whl /tmp/
RUN pip install --no-cache-dir --no-deps /tmp/unitree_sdk2py-*.whl
COPY docker/vendor/crc_aarch64.so docker/vendor/crc_amd64.so \
/usr/local/lib/python3.10/site-packages/unitree_sdk2py/utils/lib/
# ── Application code ─────────────────────────────────────────────────────────
# main.py's deployed-layout shim imports THIS folder by its own name and aliases
# Project.Sanad → it, inserting the folder's PARENT (/opt) into sys.path. What
# matters: a non-'Project' parent + an __init__.py (both hold here). The name
# 'SanadR1' is what the running instance is called, so keep it for clarity.
WORKDIR /opt
COPY . /opt/SanadR1
WORKDIR /opt/SanadR1
# Non-root app user for USB audio: Sanad refuses to touch audio as root, and
# PulseAudio needs a per-user session. uid 1000 mirrors the host 'unitree'; put it
# in the 'audio' group (Debian gid 29 == the host's) so it can reach /dev/snd.
# The entrypoint (still root) chowns the mounted volumes, then drops to this user.
RUN groupmod -g 29 audio 2>/dev/null || groupadd -g 29 audio ; \
useradd -m -u 1000 -o -s /bin/bash sanad \
&& usermod -aG audio sanad \
&& mkdir -p /run/user/1000 && chown sanad:sanad /run/user/1000 && chmod 700 /run/user/1000 \
&& chown -R sanad:sanad /opt/SanadR1
# ── Runtime config (audio + DDS are network-based; no local sound hardware) ──
ENV PYTHONUNBUFFERED=1 \
SANAD_DDS_INTERFACE=eth10 \
SANAD_DASHBOARD_HOST=0.0.0.0 \
SANAD_NAV_TOOLS=0 \
SANAD_AUDIO_PROFILE=builtin \
SANAD_VOICE_BRAIN=gemini \
PORT=8001
# SANAD_GEMINI_API_KEY is supplied at run time (compose env_file / -e), never baked.
EXPOSE 8001
HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \
CMD curl -fsS "http://127.0.0.1:${PORT:-8001}/" >/dev/null || exit 1
# entrypoint waits for eth10 (robot link) before launching, mirroring start_sanad.sh
ENTRYPOINT ["/opt/SanadR1/docker/entrypoint.sh"]