Ships as a Docker image (libfranka 0.17.0, pinocchio 3.x, Eigen 3.4, gcc 13).
!!! warning "Host kernel must be PREEMPT_RT"
./fr3-stack build # one time (~5–10 min, cached after)
./fr3-stack up cart # cartesian impedance, anchored at current pose
./fr3-stack up hybrid --ft # hybrid force/position with Bota FT sensor
./fr3-stack up idle # gravity-comp + damping (hand-guidable)
./fr3-stack up cart --attach # foreground (default is detached)
./fr3-stack logs -f
./fr3-stack downMode is idle, cart, or hybrid. With a non-idle initial controller, the daemon anchors the spring at the arm's startup pose — the first RT tick produces zero net force, no jump.
| Flag | Default | Notes |
|---|---|---|
--robot <ip> |
required | FR3 IP |
--cmd-port |
5555 | workstation→NUC commands |
--state-port |
5556 | NUC→workstation state (~200 Hz) |
--initial-controller |
idle |
idle | cartesian_impedance | hybrid |
--ft-sensor-kind |
empty | currently only bota |
--ft-sensor-config |
empty | Bota: path to driver JSON |
Shorthands: --idle, --cart, --hybrid.
FR3_ROBOT_IP=192.168.1.11 \
FR3_INITIAL_CONTROLLER=cartesian_impedance \
FR3_FT_SENSOR_KIND=bota \
FR3_FT_SENSOR_CONFIG=/opt/bota/driver_config/bota_binary.json \
./fr3-stack up -dThe Bota EtherCAT driver is a git submodule:
git submodule update --init --recursive
./fr3-stack up hybrid -d --ftWhen publishing, State.wrench_ft is a length-6 vector; otherwise None. send_hybrid refuses to start without one unless require_ft_sensor=False.
pip install -e .Read from one of:
$FR3_CONFIG(explicit override)./fr3.yaml(per-project, gitignored)~/.config/fr3/config.yaml(user-global)
Template (examples/fr3.example.yaml):
nuc_host: 192.168.1.8 # where fr3-stack listens
robot_ip: 192.168.1.11 # FR3's own IP
desk:
user: dexlab
password: thedexlabEnv vars override file values: FR3_NUC_HOST, FR3_ROBOT_IP, FR3_DESK_USER, FR3_DESK_PASS.
| Class | Module | Use when |
|---|---|---|
Robot |
fr3_stack.robot |
Streaming wire — one send_* per command. Direct access to every wire field. |
Arm |
fr3_stack.client |
Pose-centric facade for inference / teleop loops. arm.robot is the escape hatch. |
from fr3_stack import Robot
import numpy as np
with Robot("nuc.local") as robot:
s = robot.wait_for_state()
robot.send_cartesian_impedance(
target_pos = s.pos + np.array([0, 0, 0.05]),
target_quat_xyzw = s.quat_xyzw,
K = [200, 200, 800, 30, 30, 30],
)
print(robot.state.pos, robot.state.wrench_ext)Per-call kwargs (K=, D=, target_wrench=, …) update an internal last-sent cache, so subsequent calls without those kwargs reuse the new values.
Stiffness is sticky — set once, then stream Pose targets:
from fr3_stack import Arm, Pose
with Arm("nuc.local") as arm:
arm.set_stiffness(K=[200, 200, 800, 30, 30, 30], damp_ratio=0.9)
arm.move_to(Pose([0.5, 0.0, 0.4], [0, 0, 0, 1]), duration=2.0)
obs = arm.observe()
target = Pose(obs.pose.pos + [0, 0, 0.05], obs.pose.quat)
arm.send(target)
arm.hold() # lock at current pose with cached gains
# arm.relax() # gravity-comp + damping (hand-guidable; sends idle)arm.move_to(target, duration=T) blocks for T seconds while the daemon's min-jerk generator drives the move; afterwards the controller holds at the goal. arm.send(...) is fire-and-forget streaming. Hybrid commands drop through to arm.robot.send_hybrid(...).
K/D/M defaults live in fr3_stack/configs/<name>.yaml, loaded when Robot is constructed. Profile variants (<name>.<profile>.yaml) let you keep multiple presets:
robot = Robot("nuc.local", profiles={"cartesian_impedance": "stiff"})
robot.set_profile("hybrid", "polish") # swap at runtimeIf you have a sensor mounted past the wrist, run payload identification before using hybrid:
./fr3-stack up hybrid -d --ft
# manual: hand-guide to ≥6 static poses with varied EE tilt
fr3-ft-calibrate 192.168.1.8
# or record once, replay automatically (CLEAR the workspace)
fr3-ft-calibrate-record 192.168.1.8
fr3-ft-calibrate-auto 192.168.1.8The solver writes fr3_stack/sensors/bota/config/ft_calibration.yaml, which docker-compose bind-mounts into the daemon. The daemon reads it once at startup — restart to pick up a new file:
./fr3-stack down
./fr3-stack up hybrid -d --ftAfter that, state.wrench_ft is compensated, state.wrench_ft_raw carries the raw stream, and state.ft_compensated == True.
robot.terminate() # sends idle with termination=True; daemon stops RT loop