96 lines
5.5 KiB
Docker
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"]
|