Usage#

Basic control#

Three steps: power on the arm, bring the controller up on the NUC, then drive the arm from the PC. Every command below runs inside the frankatwin conda env of that machine (Installation). Step 2 shows four usage examples (each script takes --config robot.yaml; the two arm controllers they use are described in Architecture).

Step 0 · Power on and prepare the arm#

Robot — power on the arm and activate FCI mode in Desk.

Step 1 · Start the daemon#

NUC

conda activate frankatwin
python -m frankatwin.daemon -v

The daemon launches osc_shm — the arm now holds its current pose under impedance control — and keeps it alive. Leave the terminal open; the banner should show RT = SCHED_FIFO, tau_rate = 800 Nm/s, the payload and collision settings, then daemon ready. All flags (--config, payload overrides), the banner line by line, what it logs while running, how to stop it and when to restart it: Daemon.

Step 2 · Drive the arm from the PC#

PC — run the examples below from the root of the frankatwin repository:

conda activate frankatwin
cd frankatwin

Example 1 · Reset the arm#

Script: examples/move_to.py

What the robot does. The arm moves under stiff position control to a target named one of two ways:

  • --target-joints default — seven joint angles. libfranka’s MotionGenerator gives each joint a cubic ramp up, a constant-velocity cruise and a cubic ramp down, the seven synchronized to finish together, so the end effector sweeps an arc.

  • --target-ee — a Cartesian pose. libfranka solves the IK and the end effector follows a straight 5th-order path.

Safety

The arm does not yield on contact. Clear its path, keep the user stop in hand, and use --q-max-speed 0.1 the first time you try a new target.

# Home: q = [0, -π/4, 0, -3π/4, 0, π/2, π/4] rad
python examples/move_to.py

# A joint configuration [rad], paced slowly
python examples/move_to.py --target-joints 0 -0.4 0 -2.0 0 1.6 0.785 --q-max-speed 0.5

# An end-effector pose: x y z [m] + quaternion wxyz, tool pointing down
python examples/move_to.py --target-ee 0.4 0.0 0.3  0 1 0 0  --q-max-speed 0.5

flag

meaning

--target-joints J1 … J7

joint angles [rad]

--target-ee x y z qw qx qy qz

TCP position [m] and quaternion (wxyz) in the base frame; 0 1 0 0 = tool down

--q-max-speed

per-joint velocity cap [rad/s] for both modes, in (0, 1.25]; default reset.q_max_speed (0.5)

--q-max-speed is approximate for --target-ee. libfranka solves the IK internally, so the cap is only estimated — from the Jacobian at the start pose; for --target-joints it is exact.

Example 2 · Track scripted trajectories#

Script: examples/cart_impedance.py

Streams a Cartesian reference to osc_shm at 50 Hz, then prints tracking error and torque headroom.

What the robot does. Under impedance control the end effector tracks a reference anchored at the pose it starts from, in one of three shapes:

  • sine default — ±5 cm along z at 0.5 Hz for 4 s, ending back at the start pose.

  • multiband — x, y, z, yaw and roll, each driven at a low and a high frequency at once (0.15–1.1 Hz), for 12 s. Roughly ±12 cm in x and y, ±10 cm in z and ~0.5 rad of combined yaw/roll, peaking at 0.3 m/s; it ends away from the start pose. --profile heldout is the 6-DOF variant used as the sysid validation run: it also tilts the tool (up to 20°), adds a third frequency up to 2 Hz, and fades back to the start pose — roughly ±10 cm and ~0.5 rad, peaking at 0.45 m/s and 1.5 rad/s.

  • chirp — all six axes swept from --f0 to --f1 (--band low: 0.1 → 0.7 Hz; --band high: 0.7 → 3 Hz with the amplitude ∝ (0.7/f)^0.5 and a 1 s fade-out, sized for kp 200/20; the low band is the fit run, the high band an optional held-out run), for 8 s. Roughly ±10 cm in x and y, ±15 cm in z and ~0.6 rad of rotation, peaking at 0.46 m/s and 1.5 rad/s. This is the excitation the sysid fit uses.

The multiband and chirp targets are plotted axis by axis in SysID details › Target trajectories.

Safety

multiband and chirp are fast sysid motions, up to ±15 cm and ±0.5 rad. Clear the space around the robot before running them.

# Go home first -- before every run: the reference is anchored at the pose the
# arm is in, and a run does not bring the joints back to where it started
python examples/move_to.py

# Default: ±5 cm along z at 0.5 Hz for 4 s
python examples/cart_impedance.py

# The sysid runs (docs/sysid.md): the low chirp band for the fit, the heldout multiband for validation,
# the high chirp band as an optional second held-out run
python examples/move_to.py
python examples/cart_impedance.py --mode chirp --band low  --kp-pos 200 --kp-ori 20 --log-1khz data/sysid/chirp_low_fit.csv
python examples/move_to.py
python examples/cart_impedance.py --mode multiband --profile heldout --kp-pos 500 --kp-ori 30 \
    --log-1khz data/sysid/multiband_heldout.csv
python examples/move_to.py
python examples/cart_impedance.py --mode chirp --band high --kp-pos 200 --kp-ori 20 --log-1khz data/sysid/chirp_high_heldout.csv

flag

meaning

--mode

sine default, multiband or chirp

--kp-pos, --kp-ori

impedance gains; default from robot.yaml

--log-1khz run.csv

record every controller tick (1 kHz), setpoints stamped by the tick they took effect, and write it on the PC at this path, with run_targets.csv and the JSON sidecar run.json next to it. The daemon records in memory; the rows are fetched after the run and nothing is written on the NUC. Enough on its own for a sysid run; the setpoint rate stays --rate. A relative path resolves against the directory the script is run from (format)

--log run50.csv

optional: the 50 Hz CSV of the PC-side state stream and its sidecar (format)

--dry-run

build the reference and print peak rates, no robot

Shape and duration flags are listed by --help; the multiband and chirp designs are explained in SysID details.

Example 3 · Run a policy closed-loop#

Script: examples/policy_loop.py

Runs a policy at a fixed rate under impedance control. Replace the stand-in demo_policy with your policy.

What the robot does. The default command below first sends the arm home (skip with --no-reset). The stand-in policy then raises the end effector about 15 cm over 2 s and lowers it about 15 cm over the next 2 s, four times (16 s), and the arm holds where it ends.

python examples/policy_loop.py                          # demo policy, 10 Hz, 16 s

flag

meaning

--hz, --duration

policy rate [Hz] (10) and run time [s] (16)

--pos-scale, --rot-scale

action → Δpos [m/step] (0.02) and Δrot [rad/step] (0.03)

--kp-pos, --kp-ori

impedance gains (500 / 30)

--err-delta-pos

delta translation action clamp, for safety — bounds how far the EE may lag its target [m]. Default 0: no clamp, pure impedance, as in sim

--no-reset

skip the initial move_to home

The whole loop of policy_loop.py:

def demo_policy(t, obs):                      # stand-in for a network; 6-D action in [-1, 1]
    a = np.zeros(6)                           # a[0:3] = Δxyz, a[3:6] = Δrot (axis-angle), base frame
    a[2] = 1.0 if t % 4.0 < 2.0 else -1.0     # up for 2 s, down for 2 s: ~15 cm as the impedance follows
    return a

with FrankaTwinClient(cfg) as robot:
    robot.move_to_q(cfg.robot.init_q)                         # 1. position control: reset to home
    robot.set_gains(kp_pos=500, kp_ori=30)                    # 2. impedance gains (Kd = 2*sqrt(Kp))
    s = robot.wait_for_state()
    t0 = time.monotonic()
    for k in range(int(16 * 10)):                             # 3. 16 s at 10 Hz
        obs = np.concatenate([s.q, s.dq, s.ee_pos, s.ee_quat, s.ee_linvel, s.ee_angvel])
        a = np.clip(demo_policy(k / 10, obs), -1, 1)
        pos  = s.ee_pos + 0.02 * a[:3]                        # 4. Δ on the measured pose (as in the sim task)
        quat = mul_wxyz(from_rotvec_wxyz(0.03 * a[3:]), s.ee_quat)
        robot.set_ee_target(pos, quat)                        # 5. non-blocking; osc_shm holds it at 1 kHz
        time.sleep(max(0.0, t0 + (k + 1) / 10 - time.monotonic()))   # 6. fixed-rate tick
        s = robot.get_state() or s                            # 7. newest frame of the 100 Hz stream

    robot.set_ee_target(s.ee_pos, s.ee_quat)                  # 8. hold where the run ended

Example 4 · Open and close the gripper#

Script: examples/gripper.py

What the robot does. Only the Franka Hand moves; the arm keeps holding its pose under impedance control. The hand is served on its own port (1338), so no osc_shm stop/restart is involved — you can open and close while a policy loop is running.

Safety

The jaws close at up to 0.1 m/s and squeeze with 30–70 N. Keep hands out of them; --homing sweeps the full stroke twice. The gripper runs on its own TCP endpoint, so none of the arm’s safety chain — the tracking abort, the collision reflex — applies to it.

# Once after power-up: calibrates the stroke
python examples/gripper.py --homing

# Fully open (80 mm)
python examples/gripper.py --open

# Open to 30 mm
python examples/gripper.py --open --width 0.03

# Grasp: squeeze at gripper.grasp_force (70 N)
python examples/gripper.py --close

# width / max_width / is_grasped / temperature
python examples/gripper.py --state

flag

meaning

--open [--width M]

open to a width in metres (default gripper.max_width, fully open)

--close [--force N] [--close-width M] [--eps M]

grasp; defaults 70 N, −0.01 m, 0.08 m

--homing / --stop / --state

calibrate / abort the motion / read the state

--speed

rate the width changes [m/s]; default 0.1 for both, which is the hardware ceiling

--no-wait

return once the daemon accepts the command

In a policy loop:

robot.gripper_open()                              # blocks < 2 s; the arm holds meanwhile
...
out = robot.gripper_close(force=30)               # returns when the fingers stalled
held = out["state"]["width"]                      # the object's width, real-side twin of the sim finger joint
robot.gripper_open(0.06, wait=False)              # or fire-and-forget, then robot.gripper_wait()