droid-franka-client
June 19, 2026 · View on GitHub
Thin DROID / Franka FR3 eval client. It controls the robot (via the DROID stack on the NUC) and queries a remote galaxea-fm policy server over a websocket+msgpack protocol (PROTOCOL.md). It depends on the policy server only through that protocol — no shared Python code.
NUC (robot control, docker) ──zerorpc──► Franka FR3
▲ zerorpc
Laptop (this client, docker) ──ws:8000──► galaxea-fm policy server (GPU host)
+ ZED cameras ◄──actions───
What's in here
droid/ # git submodule → galaxea fork of droid-dataset/droid (Franka/ZED/docker patches)
# docker assets live in droid/.docker/{nuc,laptop}
openpi_client/ # vendored (flattened) openpi client; eval only uses image_tools
eval/ # main.py / main_ui.py — the actual eval loop + GalaxeaPolicyClient
docker/ # Dockerfile.eval + compose: BUILD the eval env from scratch (see below)
requirements.lock.txt # ground-truth pinned deps captured from the working env
start_client.sh # docker exec the eval loop in the eval container
sync_main.sh # copy eval scripts into the running eval container
.env.example # all machine-specific config (copy to .env)
PROTOCOL.md # the client↔server contract
LICENSE-G0.5 # license for Galaxea-maintained client code/docs
NOTICE # attribution and third-party component notes
CHANGES.md # public-release change notes
Reproducible environment. The eval runs in a py3.10 conda env (
droid_policy: droid + pyzed + openpi_client + msgpack/websockets/tyro/torch). That env is built fromdocker/Dockerfile.eval— it is NOT a hand-committed image. Anyone who clones this repo can rebuild it withdocker build, which is the whole point of the split.
Open-Source Notes
This repository is released under the G0.5 Community License Agreement (Non-Commercial + Limited Patent License). See LICENSE-G0.5 and NOTICE.
Component boundaries:
droid/is a Git submodule pinned to the public Galaxea DROID fork athttps://github.com/OpenGalaxea/droid.git. See the submodule's own README,CHANGES.md, andNOTICE.openpi_client/is a vendored, flattened subset of the OpenPI client package from Physical Intelligence. See licenses/LICENSE-OPENPI.docker/vendor/pyzed-4.0-cp310-cp310-linux_x86_64.whlis intentionally retained for reproducible Docker builds in restricted network environments. The wheel includes its own MIT license metadata; users must also comply with applicable Stereolabs ZED SDK / PyZED terms.
From-scratch setup (new machine)
Everything below assumes you cloned this repo and have Docker + the robot hardware (Franka FR3, NUC, two ZED cameras) wired up.
1. Clone with submodules
git clone git@github.com:OpenGalaxea/droid-franka-client.git && cd droid-franka-client
git submodule update --init --recursive # pulls droid + its fairo/oculus_reader submodules
The droid fork reads all machine config from environment variables, so you never edit files inside the submodule.
2. Configure your machine
cp .env.example .env
vim .env # set ROOT_DIR=<abs path to ./droid>, ROBOT_IP, NUC_IP, camera serials,
# SUDO_PASSWORD, POLICY_HOST/PORT, EVAL_OUTPUT_DIR ...
That's all — do not source it by hand. .env is plain KEY=value, so the same
file feeds three consumers automatically:
| consumer | how it reads .env |
|---|---|
docker compose (build/up) | auto-loads .env; we also pass --env-file ./.env explicitly below |
start_client.sh / sync_main.sh | source it themselves, and inject it into the container via docker exec --env-file |
parameters.py inside the container | gets the values because the container is launched / exec'd with them |
Why no host
source:main.pyandparameters.pyrun inside the docker container; exporting vars in your host shell never reaches the container. Config must be injected atdocker compose up/docker exectime — which the scripts do for you. The droid fork reads every machine value fromos.environ, so you never edit files inside the submodule.
3. Build & launch the NUC (robot control)
The NUC docker is owned by the droid fork (
droid/.docker/nuc/, comes in via the submodule). Run these on the NUC machine.
# build the image AND start the container (build alone won't create it):
docker compose --env-file ./.env -f droid/.docker/nuc/docker-compose-nuc.yaml up -d --build
# start the control services inside the running NUC container:
docker exec -d $NUC_CONTAINER bash /app/scripts/server/launch_server.sh # zerorpc :4242
docker exec -d $NUC_CONTAINER bash /app/droid/franka/launch_robot.sh # robot :50051
docker exec -d $NUC_CONTAINER bash /app/droid/franka/launch_gripper.sh # gripper :50052
sleep 5 && docker exec -d $NUC_CONTAINER bash /app/droid/franka/launch_gripper.sh # 1st gripper launch usually fails
4. Build & launch the laptop eval container
The eval container is built from docker/Dockerfile.eval (py3.10 env with droid +
pyzed + openpi_client + the pinned deps). The first build needs the upstream droid
laptop base image (ZED SDK + CUDA); pull it or build it from the submodule:
# (once) base image — pulls from ghcr, or build from the droid submodule:
docker pull ghcr.io/droid-dataset/droid_laptop:fr3 \
|| docker compose --env-file ./.env -f droid/.docker/laptop/docker-compose-laptop.yaml build
# build + start the reproducible eval container
docker compose --env-file ./.env -f docker/docker-compose.eval.yaml up -d --build
# verify cameras
lsusb | grep ZED # expect 4 lines (ZED-M wrist + ZED-2i external)
After a power cycle / USB re-plug, fix the ZED video device nodes (else the SDK won't see the cameras):
for dev in /dev/video*; do
maj=$(( 16#$(stat -c '%t' "$dev") )); min=$(( 16#$(stat -c '%T' "$dev") ))
docker exec $LAPTOP_CONTAINER bash -c "rm -f $dev && mknod $dev c $maj $min && chmod 666 $dev"
done
5. Start the policy server
Done in the server repo (galaxeafm). It must listen on $POLICY_PORT and
output joint_position. See that repo's experiments/droid/start_server.sh.
6. Run eval
bash sync_main.sh # push eval scripts into the laptop container (once / after edits)
bash start_client.sh # runs eval/main.py against $POLICY_HOST:$POLICY_PORT
# main_ui.py additionally shows subtask + bbox overlays
Interaction: type the task instruction once → robot executes (Ctrl+C aborts a
round) → after each round enter y (success) / n (fail) / q (quit). Stats print
each round; after 10 rounds it auto-resets and exits. Output → /eval_data
(mounted to $EVAL_OUTPUT_DIR).
Manual reset:
docker exec -it $LAPTOP_CONTAINER bash -c \
"/root/miniconda3/envs/droid_policy/bin/python -c \
'from droid.robot_env import RobotEnv; RobotEnv().reset()'"
Troubleshooting
Franka power-on: open https://$ROBOT_IP in Firefox (Chrome blocks the
self-signed cert) → if E-stop was pressed, twist to release & clear errors → click
the lock icon to release brakes (arm twitches, light turns blue) → Activate FCI.
grpc Robot context not valid / Net Exception: Firefox → Deactivate FCI → wait
2s → Activate FCI → restart launch_robot.sh.
Gripper receives signal but doesn't move: kill main.py in the laptop
container, then on the NUC re-run launch_gripper.sh a few times to reset gripper
state; retry main.py (a transient grpc error on first retry is normal).
Relationship to the policy server
The server (galaxeafm, repo foundationmodel/galaxeafm) is a separate codebase.
This client knows nothing about it beyond PROTOCOL.md. Swapping
policies requires zero changes here as long as the server honors that contract.