FrankaController
The controller API, which NativeFrankaController inherits. FrankaController itself runs
the 1 kHz loop in Python, on the event loop: that loop is legacy, so use
NativeFrankaController (see NativeFrankaController).
- class aiofranka.controller.FrankaController(robot)[source]
Bases:
objectHigh-level asyncio controller for Franka robots with multiple control modes.
Legacy: its 1 kHz loop runs in Python, on the event loop, where anything else that runs (a planner, garbage collection, a thread holding the GIL) delays the next torque command. Use NativeFrankaController, which has this API and runs the loop in C++; FrankaController keeps working for existing code, and for a subclass’s step().
This controller runs a 1kHz torque control loop in the background using asyncio, while allowing you to send high-level commands asynchronously. Supports three control modes:
Impedance Control: Joint-space spring-damper control
Operational Space Control (OSC): Task-space control with null-space
Direct Torque Control: Raw torque commands
The controller automatically handles: - Background control loop at 1kHz - Torque rate limiting for safety - Thread-safe state updates - Rate-limited command updates - Smooth trajectory generation
- Parameters:
robot (RobotInterface)
- robot
Robot interface instance
- Type:
- # Impedance control gains
- kp
Joint position stiffness [Nm/rad] (7,)
- Type:
np.ndarray
- kd
Joint damping [Nm⋅s/rad] (7,)
- Type:
np.ndarray
- # OSC gains
- ee_kp
EE stiffness as an acceleration gain [1/s²] (6,): the OSC commands the wrench Λ·(ee_kp·e − ee_kd·v), with Λ the task-space inertia, so ee_kp = ω² and ee_kd = 2ζω for a natural frequency ω and damping ratio ζ
- Type:
np.ndarray
- ee_kd
EE damping as an acceleration gain [1/s] (6,)
- Type:
np.ndarray
- null_kp
Null-space stiffness [Nm/rad] (7,)
- Type:
np.ndarray
- null_kd
Null-space damping [Nm⋅s/rad] (7,)
- Type:
np.ndarray
- # Target states
- q_desired
Desired joint positions [rad] (7,)
- Type:
np.ndarray
- ee_desired
Desired pose of the OSC control frame as 4x4 transform
- Type:
np.ndarray
- torque
Direct torque command [Nm] (7,)
- Type:
np.ndarray
- control_transform
Tool center point that the OSC controls, in the flange frame, 4x4 (default: identity, i.e. the flange); see set_tcp()
- Type:
np.ndarray
- last_command
Last torque sent to the robot, after rate limiting and clipping [Nm] (7,)
- Type:
np.ndarray
- # Safety
Examples
- Basic usage:
>>> robot = RobotInterface("172.16.0.2") >>> controller = FrankaController(robot) >>> await controller.start() >>> controller.switch("impedance") >>> controller.set_freq(50) >>> await controller.set("q_desired", target_joints) >>> await controller.stop()
- OSC control:
>>> controller.switch("osc") >>> controller.ee_kp = np.array([300, 300, 300, 1000, 1000, 1000]) >>> controller.set_freq(50) >>> desired_ee = np.eye(4) >>> desired_ee[:3, 3] = [0.4, 0.0, 0.5] >>> await controller.set("ee_desired", desired_ee)
- Direct torque:
>>> controller.switch("torque") >>> controller.torque = np.zeros(7) # Zero torques
- Caveats:
Must await controller.start() before sending commands
Use set_freq() before set() to enforce timing
High gains can cause instability or safety triggers
Switching controllers resets initial state
State access is thread-safe but copy if modifying
Control loop must run continuously at ~1kHz
- __init__(robot)[source]
Initialize controller with robot interface.
- Parameters:
robot (RobotInterface) – Initialized robot interface
Note
Controller is initialized in “impedance” mode with conservative gains. Call start() to begin the control loop, then switch() to change modes.
- Default Gains:
Impedance: kp=80, kd=4 (per joint)
OSC: ee_kp=[100]*6, ee_kd=[4]*6
Null-space: null_kp=1, null_kd=1
- async test_connection()[source]
Test control loop timing and diagnose connection quality.
Runs for 5 seconds and prints statistics about control loop performance: - Actual frequency (should be ~1000 Hz) - Mean/std/min/max loop time - Jitter (max - min)
Use this to verify your setup is working correctly before running experiments.
- Example Output:
- Control loop stats (last 1000 iterations):
Frequency: 1000.2 Hz (target: 1000 Hz) Mean dt: 1.000 ms, Std: 0.015 ms Min dt: 0.985 ms, Max dt: 1.025 ms Jitter (max-min): 0.040 ms
- Caveats:
High jitter (>0.5ms) indicates system load or network issues
Frequency <990 Hz suggests performance problems
Run on a dedicated realtime system for best results
- set_freq(freq)[source]
Set the update frequency for rate-limited set() calls.
This enforces strict timing for subsequent set() calls, automatically sleeping to maintain the specified frequency. Prevents sending commands too fast and ensures smooth, consistent control.
- Parameters:
freq (float) – Desired update frequency in Hz (typically 10-100 Hz)
Note
Must be called BEFORE the first set() call to take effect. Each attribute tracked by set() has independent timing.
Examples
>>> controller.set_freq(50) # 50 Hz updates >>> for i in range(100): ... await controller.set("q_desired", compute_target()) ... # Automatically sleeps to maintain 50 Hz
- Caveats:
Don’t set freq > 200 Hz (unnecessary and may cause timing issues)
Lower freq = smoother motion but slower response
Higher freq = faster response but requires more computation
Timing is per-attribute (q_desired and ee_desired tracked separately)
- async set(attr, value)[source]
Rate-limited setter that enforces strict timing for control updates.
This method ensures updates to controller attributes happen at the frequency specified by set_freq(). It compensates for drift by tracking the target time for each update, guaranteeing consistent timing even if your computation time varies.
- Parameters:
attr (str) – Attribute name to set. Common values: - “q_desired”: Joint position target (impedance mode) - “ee_desired”: End-effector pose target (OSC mode) - “torque”: Direct torque command (torque mode)
value – Value to set. Type depends on attr: - q_desired: np.ndarray (7,) [rad] - ee_desired: np.ndarray (4, 4) homogeneous transform - torque: np.ndarray (7,) [Nm]
Note
Automatically sleeps to maintain frequency set by set_freq()
First call for an attribute initializes timing
Each attribute has independent timing tracking
Thread-safe update of shared state
Examples
- Impedance control:
>>> controller.set_freq(50) >>> for i in range(100): ... target = initial_q + np.sin(i / 50.0 * np.pi) * 0.1 ... await controller.set("q_desired", target)
- OSC control:
>>> controller.set_freq(100) >>> desired_ee = np.eye(4) >>> desired_ee[:3, 3] = [0.5, 0.0, 0.3] >>> await controller.set("ee_desired", desired_ee)
- Caveats:
Must call set_freq() before first set() call
If computation takes longer than 1/freq, timing will slip
Don’t mix set() and direct attribute assignment
Don’t call set() faster than the specified frequency
- async start()[source]
Start the 1kHz background control loop.
This creates an asyncio task that runs the control loop continuously at ~1000 Hz. The loop reads robot state, computes control torques based on the current controller type, and sends torque commands.
- Returns:
The background control loop task
- Return type:
Note
Blocks for 1 second to ensure loop starts successfully
Starts robot torque control mode automatically
Control loop runs until stop() is called
You can send commands via set() while loop is running
Example
>>> await controller.start() >>> # Control loop now running in background >>> controller.set_freq(50) >>> await controller.set("q_desired", target) >>> await controller.stop()
- Caveats:
Must be awaited (async function)
Don’t call start() multiple times without stop()
If loop crashes, robot will trigger safety stop
Check terminal for error messages if robot stops unexpectedly
- async stop()[source]
Stop the background control loop and robot.
Gracefully terminates the control loop task and stops robot torque control. Robot will hold position briefly then release brakes.
Note
Blocks for 1 second to ensure clean shutdown
Cancels asyncio control loop task
Stops robot torque control mode
Safe to call multiple times
Example
>>> await controller.start() >>> # ... do control ... >>> await controller.stop()
- Caveat:
Ensure robot is in a safe configuration before stopping. Robot will briefly hold position then release brakes.
- switch(controller_type)[source]
Switch between control modes at runtime.
Changes the active controller without stopping the control loop. Resets the initial state (initial_qpos, initial_ee) to current robot state and clears rate-limiting timing.
- Parameters:
controller_type (str) – Controller type to switch to: - “impedance”: Joint-space impedance control - “pid”: Joint-space PID control with integral term - “osc”: Operational space control (task space) - “torque”: Direct torque control
Example
>>> controller.switch("impedance") >>> controller.kp = np.ones(7) * 80 >>> # ... run impedance control ... >>> >>> controller.switch("osc") >>> controller.ee_kp = np.array([300, 300, 300, 1000, 1000, 1000]) >>> # ... run OSC control ...
Note
Can be called while control loop is running
Resets q_desired to current position
Resets ee_desired to current pose of the control frame
Clears timing state from previous set() calls
Resets integral term when switching to/from PID
- Caveats:
Switching causes brief discontinuity in control
Adjust gains after switching for smooth transition
Don’t switch rapidly (< 1 Hz) as it resets state
- set_tcp(transform)[source]
Set the tool center point (TCP) that the OSC controls, relative to the flange.
The OSC then tracks ee_desired with this frame instead of the flange. Express it in the flange frame (the attachment_site of the MuJoCo model): its origin is the center of the flange face, z points out of the flange, and the box on the side of link 7 points between +x and +y. The docs show it on the robot (Controllers > Operational Space Control > Tool Center Point). ee_desired becomes the current pose of the new TCP, so the arm holds still.
The TCP of the end-effector profile in Desk is not used here.
- Parameters:
transform (array-like) – Pose of the TCP in the flange frame as a 4x4 transform, or its translation [m] (3,) for the flange’s orientation
- Raises:
ValueError – If transform is not a translation or a 4x4 pose
Example
>>> controller.switch("osc") >>> controller.set_tcp([0, 0, 0.1034]) # Franka Hand fingertips >>> target = controller.ee_desired.copy() >>> target[2, 3] -= 0.05 # 5 cm down, in the base frame >>> await controller.set("ee_desired", target)
- check_tool(config)[source]
Check that the tool a configuration needs is the end-effector profile active in Desk.
The profile is the one RobotInterface read from Desk when it connected. A configuration without a tool, and MuJoCo, which has no Desk, pass.
- Parameters:
config (str | Path | dict) – Controller configuration (see aiofranka.config)
- Raises:
RuntimeError – If another tool is active, or Desk could not be read
- activate(config, check_tool=True, check_null_target=True)[source]
Apply a controller configuration: its mode, gains and policy rate.
For impedance it sets kp and kd; for osc the TCP, ee_kp, ee_kd, null_kp, null_kd and the null-space target. It then sets the rate of set() to the configuration’s frequency. Like switch(), it holds the current joint positions (impedance) or TCP pose (osc). See aiofranka.config for the file format.
An OSC configuration without a null_target keeps the joint positions at activation as the null-space target, as switch() does. One with a null_target (e.g. a policy’s) is only activated with the arm already there, every joint within arrival_tolerance, the criterion move() stops at; otherwise the null space would swing the arm toward it at once. Move there first:
>>> config = aiofranka.load_config("configs/pocky/lv1_osc.yaml") >>> await controller.move(config["null_target"]) >>> controller.activate(config)
Reading a file takes a few milliseconds, which delays the 1 kHz loop that long; while it runs, pass a configuration read before with aiofranka.load_config().
- Parameters:
check_tool (bool) – Refuse a configuration whose tool is not the end effector active in Desk (see check_tool())
check_null_target (bool) – Refuse an OSC configuration whose null_target the arm is not at. Only skip it where the null space is known to be at rest, e.g. at a pose where the TCP Jacobian’s null space holds no error toward the target.
- Returns:
The configuration, as aiofranka.load_config() reads it
- Return type:
- Raises:
ValueError – If the configuration is not valid
RuntimeError – If its tool is not the one active in Desk, or the arm is not at its null_target
Example
>>> controller.activate("configs/osc.yaml") >>> target = controller.ee_desired.copy() >>> target[2, 3] -= 0.05 >>> await controller.set("ee_desired", target) # at the configured frequency
- async move(qpos=[0, 0, 0.0, -1.57079, 0, 1.57079, -0.7853])[source]
Move robot to target joint position using smooth trajectory.
Generates a time-optimal, jerk-limited trajectory using Ruckig online trajectory generation, then executes it at 50 Hz. Automatically switches to impedance mode if not already active.
- Parameters:
qpos (list | np.ndarray) – Target joint positions [rad] (7,) Default: Home position
Note
Uses Ruckig for smooth, time-optimal trajectories
Respects velocity, acceleration, and jerk limits
Switches to impedance control automatically
Executes trajectory at 50 Hz (20ms updates)
Duration depends on distance and limits
- Trajectory Limits:
Max velocity: 10 rad/s per joint
Max acceleration: 5 rad/s² per joint
Max jerk: 1 rad/s³ per joint
Examples
- Move to home position:
>>> await controller.move()
- Move to custom position:
>>> target = [0, -0.785, 0, -2.356, 0, 1.571, 0.785] >>> await controller.move(target)
- Move to current position + offset:
>>> current = controller.state['qpos'] >>> await controller.move(current + np.array([0.1, 0, 0, 0, 0, 0, 0]))
- Caveats:
Large motions take longer (trajectory is time-optimal)
Don’t call while other control is active
Blocks until motion completes: until every joint is within arrival_tolerance (0.03 rad by default) of the target, or 3 s after the trajectory if friction keeps it farther (it then prints how far)
Switches to impedance mode (resets controller state)
May fail if target is at joint limits or in collision
- async identify_payload(tool_length=0.2, tool_radius=0.1, floor=0.0, **kwargs)[source]
Identify the mass and center of mass of the tool on the flange.
Moves through poses around the current one, measures the joint torques at rest that the robot does not compensate, and fits the tool to them. It only measures: save the result with aiofranka.save_tool() and activate it with aiofranka.load_tool(). See aiofranka.payload.identify_payload() for the details and further arguments.
- Parameters:
tool_length (float) – Length of the tool from the flange [m], for the collision checks (default: 0.2)
tool_radius (float) – Radius of the tool [m] (default: 0.1)
floor (float) – Height of the floor or table in the base frame [m] (default: 0.0)
**kwargs – Further arguments of aiofranka.payload.identify_payload()
- Returns:
Estimated tool, with standard errors
- Return type:
Example
>>> estimate = await controller.identify_payload(tool_length=0.25) >>> aiofranka.save_tool("gripper", estimate.mass, estimate.com) >>> aiofranka.load_tool("gripper")