Run Inspect Robots evals on real Franka FR3 and Panda arms with OpenPI DROID policies.
Note
This project is in early development. Pin a version before depending on its API.
Inspect Robots has two swappable inputs: a Policy and an Embodiment. This
package provides both sides of a Franka and OpenPI stack:
openpipolicy: a websocket client for Physical Intelligence OpenPI servers, with pi05-DROID velocity integration and gripper conversion.frankaembodiment: a lazy franky driver for one FR3 or Panda arm, its Franka Hand, and exterior and wrist cameras.
Both sides declare the same 8-D absolute joint_pos contract: seven arm joints
in radians followed by a normalized gripper, where 0 is closed and 1 is open.
The compatibility check passes with zero errors and zero warnings. For other
robot adapters, see
inspect-robots-yam and
inspect-robots-so101.
Create an environment and install the package, camera reader, and franky extra:
uv venv && source .venv/bin/activate
uv pip install "inspect-robots-franka[franka]"
uv pip install "openpi-client @ git+https://github.com/Physical-Intelligence/openpi.git#subdirectory=packages/openpi-client"openpi-client is intentionally installed from the Physical Intelligence git
repository. The unrelated openpi-client project on PyPI is not used.
Franky wheels bundle a particular libfranka version. The robot firmware decides which wheel is compatible. Check the franky installation table before connecting, and replace the version selected by the extra when the firmware requires a different wheel. Enable FCI in the Franka web interface.
Real-time control also needs the Franka host setup: a direct wired connection, the recommended PREEMPT_RT kernel, correct network settings, and a workstation that satisfies the libfranka real-time checks. Test the setup with libfranka examples before running a learned policy.
Install OpenPI from its source repository with submodules:
git clone --recurse-submodules https://github.com/Physical-Intelligence/openpi.git
cd openpi
GIT_LFS_SKIP_SMUDGE=1 uv sync
GIT_LFS_SKIP_SMUDGE=1 uv pip install -e .
uv run scripts/serve_policy.py --env=DROIDThe default DROID server loads pi05_droid from
gs://openpi-assets/checkpoints/pi05_droid and listens on port 8000. A specific
checkpoint can be served explicitly:
uv run scripts/serve_policy.py policy:checkpoint \
--policy.config=pi05_droid \
--policy.dir=gs://openpi-assets/checkpoints/pi05_droidPreflight constructs metadata only. It does not connect the robot, cameras, or policy server.
inspect-robots-franka-preflight
inspect-robots-franka-preflight --task cubepick-reach
inspect-robots-franka-preflight --dry-runA green report verifies action dimension, control mode, rotation representation, gripper kind, frame, cameras, state keys, and optional scene realizability. It cannot infer whether a checkpoint emits velocity or position actions.
The setup wizard interviews the two declared V4L2 camera slots:
inspect-robots setupThe Franka hostname is a network address rather than a discoverable framework
device slot. Add it with the other defaults in
~/.config/inspect-robots/config.ini:
[defaults]
policy = openpi
embodiment = franka
scorer = success_at_end
max_steps = 450
store_frames = true
[policy.args]
host = 192.168.10.20
port = 8000
actions_are_velocity = true
action_horizon = 15
replan_interval = 8
[embodiment.args]
hostname = 172.16.0.2
exterior_cam_device = /dev/v4l/by-id/YOUR-EXTERIOR-CAMERA
wrist_cam_device = /dev/v4l/by-id/YOUR-WRIST-CAMERAStable /dev/v4l/by-id/... or custom udev paths are safer than /dev/videoN,
which can change after a replug. With no injected Python camera reader, both
device fields are required. Setting exactly one raises ConfigError at
reset() before franky connects. An injected reader wins and ignores both
device fields.
Run a task or a direct instruction:
inspect-robots run --task cubepick-reach --policy openpi --embodiment franka
inspect-robots "pick up the red block" --policy openpi --embodiment frankaThe attended flow asks the operator to stand clear before homing, then asks for
scene readiness. Press Enter during execution to end the episode and answer y/N
to score it. FrankaConfig(unattended=True) skips all prompts and operator
polling. Unattended episodes run until the framework horizon unless another
component ends them.
The upstream websocket client has no inference timeout. A broken or unreachable
server can block infer(). Run the server and robot supervisor so a network
stall cannot leave an unsafe scene unattended.
Warning
Keep an operator at the e-stop for initial runs, after firmware changes, and whenever a new checkpoint or camera arrangement is introduced.
- Hard clamp: every
step()clips all eight values toFrankaConfig.joint_low/highinside the embodiment, independent of anyApprover. The defaults are the FR3 datasheet joint bounds pulled 0.05 rad inward on each side. Panda limits differ. Panda owners must override them. - Velocity semantics:
pi05_droidemits normalized joint velocities. The policy clips arm velocities to[-1, 1], cumulatively integrates them from the latest observed joints, and maps one unit to 0.2 rad per 15 Hz step. Position fine-tunes requireactions_are_velocity=false. Compatibility cannot detect a wrong setting. Use preflight,--dry-run, and one slow first jog. - Control rate: DROID checkpoints assume 15 Hz execution. Changing
FrankaConfig.control_hzchanges the physical velocity represented by every integrated policy step. - Gripper cadence: the Franka Hand does not accept a new tracking command at
every control tick. The embodiment sends a non-blocking move only after the
normalized target changes by more than
gripper_deadband, which defaults to 0.1. - Franky requirements: validate the firmware-specific wheel, FCI state, network, real-time kernel, collision thresholds, brakes, and e-stop before enabling learned motion.
The CLI's default DeltaLimitApprover derives a per-step limit of 5 percent of
each action range. That makes a full gripper stroke a 20-step ramp. The 0.1
embodiment deadband turns the ramp into a bounded command cadence. The same
derived defaults can clip legitimate arm motion. Joint 4 has about a 2.79 rad
configured range, so its default is about 0.14 rad per step, below the policy's
0.2 rad scale. The CLI accepts only a scalar --max-action-delta and has no
per-dimension approver override.
Python callers can set a per-dimension delta vector. A practical starting point allows the DROID arm scale and one complete gripper stroke:
import numpy as np
from inspect_robots import eval
from inspect_robots.approver import ChainApprover, ClampApprover, DeltaLimitApprover
from inspect_robots_franka import FrankaEmbodiment, OpenpiPolicy
embodiment = FrankaEmbodiment(
hostname="172.16.0.2",
exterior_cam_device="/dev/v4l/by-id/EXTERIOR",
wrist_cam_device="/dev/v4l/by-id/WRIST",
)
space = embodiment.info.action_space
approver = ChainApprover(
ClampApprover(space),
DeltaLimitApprover(space, max_delta=np.asarray([0.2] * 7 + [1.0])),
)
logs = eval("cubepick-reach", OpenpiPolicy(host="192.168.10.20"),
embodiment, approver=approver)The embodiment never sees DROID units. Conversion stays at the policy boundary:
| Location | 0 means | 1 means | Conversion |
|---|---|---|---|
| Franka package wire and embodiment | closed | open | identity |
| Franka Hand width | 0 m | gripper_max_width |
wire * max_width |
| DROID/OpenPI observation and action | open | closed | wire = 1 - droid |
For a first run, command asymmetric values such as 0.25 and 0.75 with the arm stationary. Confirm the observed width and physical motion before evaluating a policy.
| Slot | Label | Unit and meaning |
|---|---|---|
| 0 | joint1 |
absolute radians |
| 1 | joint2 |
absolute radians |
| 2 | joint3 |
absolute radians |
| 3 | joint4 |
absolute radians |
| 4 | joint5 |
absolute radians |
| 5 | joint6 |
absolute radians |
| 6 | joint7 |
absolute radians |
| 7 | gripper |
normalized, 0 closed and 1 open |
The shipped FR3 revolute bounds are:
datasheet low = (-2.7437, -1.7837, -2.9007, -3.0421, -2.8065, 0.5445, -3.0159)
datasheet high = ( 2.7437, 1.7837, 2.9007, -0.1518, 2.8065, 4.5169, 3.0159)
shipped low = datasheet low + 0.05 rad per arm slot
shipped high = datasheet high - 0.05 rad per arm slot
| Field | Default | Meaning |
|---|---|---|
hostname |
None |
FCI address, required at reset() |
control_hz |
15.0 |
Self-paced command rate |
joint_low, joint_high |
inset FR3 limits | Absolute hard-clamp bounds |
home_pose |
Franka ready pose | Mandatory reset target, gripper open |
rest_pose |
None |
Optional close-time park target; the gripper slot is ignored (arm-only park) |
relative_dynamics_factor |
0.15 |
Franky velocity, acceleration, and jerk scale |
gripper_max_width |
0.08 |
Physical width represented by wire value 1 |
gripper_speed |
0.05 |
Franka Hand speed in m/s |
gripper_deadband |
0.1 |
Minimum normalized change before a new hand command |
unattended |
False |
Skip operator readiness and verdict prompts |
exterior_cam_device, wrist_cam_device |
None |
Builtin OpenCV camera devices |
cam_height, cam_width |
480, 640 |
Declared and returned image resolution |
docs_extra |
empty | Rig-specific notes appended to embodiment docs |
| Field | Default | Meaning |
|---|---|---|
host, port |
127.0.0.1, 8000 |
OpenPI websocket server |
api_key |
None |
Optional websocket authentication, excluded from eval logs |
actions_are_velocity |
True |
Integrate DROID arm velocity slots |
velocity_action_scale |
0.2 |
Radians per step for one normalized velocity unit |
action_horizon |
15 |
pi05-DROID chunk length recorded in PolicyConfig |
replan_interval |
8 |
Actions consumed before framework re-inference |
name |
openpi |
Policy label in eval logs |
resize_px |
224 |
Default transport resize-with-pad size |
pi0_fast_droid uses an action horizon of 10. Released pi05-DROID uses 15.
Joint-position fine-tunes commonly use 16 and must also set
actions_are_velocity=false. A custom injected infer_fn owns its image
resizing; resize_px applies only to the default websocket transport.
Use the local cache path required by this workspace:
export UV_CACHE_DIR="$PWD/.uv-cache"
uv venv
uv pip install -e ".[dev]"
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest --covThe suite uses injected drivers, cameras, inference, clocks, and operator I/O. It requires no robot, server, camera, or stdin. Coverage is enforced at 100 percent with branch coverage enabled.
Dependency changes require uv lock. CI uses uv sync --locked; the weekly
canary resolves current allowed versions without the lockfile.
@software{inspect-robots-franka,
author = {Robocurve},
title = {Inspect Robots Franka: OpenPI adapters for Franka arms},
year = {2026},
url = {https://github.com/robocurve/inspect-robots-franka},
license = {MIT}
}See CITATION.cff for citation metadata.