Skip to main content

Module target_control

Module target_control 

Source
Expand description

Target control: a 1 kHz loop on its own thread that follows targets a low-rate commander sets whenever it likes.

Most programs that want to move a Franka are not 1 kHz programs: a planner, a vision loop, a script over a socket, a hand on a keyboard. They produce targets – at 10 Hz, in bursts, with pauses – each a step the robot must never see as a step. This is the fourth way to control the robot, next to the callbacks, ActiveControl and the read-only stream: Robot::start_cartesian_target_control and Robot::start_joint_target_control spawn a named thread that runs the crate’s own control loop and hand back a handle whose set_* can be called from any thread, at any rate, with any target. The handle’s stop brings the command to rest on the last target, finishes the motion and returns the loop’s result.

§Two backends

The Backend of the options decides what the generator’s output becomes. The default, Backend::Impedance, sends torques: the hybrid joint and Cartesian impedance law of ImpedanceGains (Kp = J^T Kx J + Kq, Kd = J^T Kxd J + Kqd, the damping on the velocity error dq_goal - dq with the goal’s own velocity as the feedforward, plus the Coriolis term, clamped to the torque limits) tracks a joint target q_goal – the generator’s own output on the joint interface, the solution of a differential inverse kinematics following the generator’s pose one cycle at a time on the Cartesian one (IkOptions) – through Robot::control_torques with the crate’s low-pass filter (ImpedanceOptions::cutoff_frequency) and torque rate limiter. There is no echo of a torque command, so the loop anchors on the measured configuration in its first cycle (the Cartesian interface on the model’s pose of it, where the IK’s residual is zero), and then, every cycle, on the measured state pulled toward the previous desired by at most the Leash: exactly the previous desired while the arm follows, so the generator runs from its own last output and its limits are the whole budget; a bounded distance ahead of an arm that is held back, so the spring force is bounded by the stiffness times the leash and the generator resumes from where the arm is once it is let go. Backend::RobotController instead streams the generator’s output as a pose or joint-position command to the robot’s own impedance controller (controller_mode); the rest of this page describes that path where the two differ.

§What the loop does every cycle

The three rules of the otg module, learnt on a real arm: the generator’s limits are per axis (a Cartesian budget is a norm, so it gets OtgLimits::per_axis_for_norm(3)), it steps one nominal cycle per command (DELTA_T) whatever the measured period, and it is re-anchored on the last command – the robot’s echo of it (O_T_EE_c, q_d) with Backend::RobotController, the leashed previous output with Backend::Impedance – with set_position before every re-plan. With Backend::RobotController the rate limiter under the same budget then runs as the backstop that must never bind (limit_rate_cartesian_pose, limit_rate_joint_positions), and the loop’s own libfranka limiter stays on behind it; the observer is told by how much the backstop moved the command. The first setpoint of the motion is always the anchor itself – on FCI v10 the first command is its own filter reference and would otherwise go out as a jump – and the start returns only once that cycle has run, so target and state are valid from the first call.

The Cartesian target is a pose: three more axes of the same synchronised generator run on the base-frame rotation vector of the orientation error, log(R_target * R_echo^T), re-anchored at zero every cycle and composed back as exp(step) * R_echo, under a rotational norm budget of their own; translation and rotation arrive together.

Two guards. If the measured pose strays more than max_deviation (m) or max_angular_deviation (rad) from the start, the target freezes where the command is, the generator brings it to rest, and the loop ends with FrankaError::Control. And a stop never finishes on a moving command: once every axis of the generator has landed (within Settle::tolerance of the target, slower than REST_VELOCITY, accelerating less than REST_ACCELERATION – the orientation included, in rad) the loop stops stepping it and holds the last command – with Backend::RobotController the robot’s echo of it, continuous with what the robot has by construction, whatever the backstop took off that command, sent bit for bit and past the backstop; with Backend::Impedance the landed desired pose or joint goal, whose torques are those of rest – for Settle::cycles cycles, then sets motion_finished on one more of it: the sequence a robot accepts as “finished at rest”. If the generator has not landed within STOP_TIMEOUT_CYCLES the same hold starts from wherever the command is. That hold settles the generator, not the arm: in torque mode an arm still closing its lag would be handed to the robot’s controller short of the goal, so Backend::Impedance finishes only once every joint moves slower than REST_JOINT_VELOCITY, or after STOP_TIMEOUT_CYCLES more cycles, the law kept on the held goal meanwhile.

§Threads

The commander side is a single-writer seqlock (TargetSlot) the loop polls without blocking; set_* serialises its callers with a mutex on the user side only. The latest RobotState is published with Mutex::try_lock from the realtime side and read with lock on the user side. Nothing allocates on the realtime thread after the start. The optional observer is the exception to “nothing of yours runs at 1 kHz”: it is called on the realtime thread every cycle with the state and what was sent, and must not allocate or block – copying into a preallocated ring, as franka_rerun::Recorder::push does, is what it is for.

The loop thread is raised to SCHED_FIFO like Robot::new raises its caller: to the highest priority, or to realtime_priority when the options name one; a failure is fatal under RealtimeConfig::Enforce and ignored under RealtimeConfig::Ignore.

Only one control or read operation may run on a Robot at a time, so while a target control runs, robot.read() and the other loops fail with FrankaError::InvalidOperation, exactly as with a callback loop on another thread; robot.stop() preempts it, and the handle’s stop then returns the preemption as a FrankaError::Control.

use std::sync::Arc;
use franka::{RealtimeConfig, Robot, TargetControlOptions};

let robot = Arc::new(Robot::new("192.168.0.1", RealtimeConfig::Enforce)?);
let control = robot.start_cartesian_target_control(TargetControlOptions::default())?;
let start = control.target();
for step in 1..=5 {
    // A planner, a socket, a keyboard: any thread, any rate, any target.
    control.set_position([start[0] + 0.01 * f64::from(step), start[1], start[2]])?;
    std::thread::sleep(std::time::Duration::from_millis(300));
}
// An orientation too: a unit quaternion in [x, y, z, w] order, or a pose as O_T_EE.
let yaw = std::f64::consts::FRAC_PI_8;
let orientation = [0.0, 0.0, (yaw / 2.0).sin(), (yaw / 2.0).cos()];
control.set_target(start, orientation)?;
control.stop()?; // settles on the last target, finishes the motion, joins the thread

Structs§

CartesianSent
What one cycle sent, for the observer.
CartesianTargetControl
The handle of a running Cartesian target control; see the module documentation. The target is a pose; orientations are unit quaternions in [x, y, z, w] order (the scalar part last) or the rotation block of a column-major pose as in O_T_EE.
IkOptions
Options of the Cartesian backend’s differential inverse kinematics: every cycle, from the previous joint goal, up to iterations damped-least-squares steps toward the pose, a posture bias through the nullspace, the step capped at max_step and clamped inside the joint limits.
ImpedanceGains
The stiffness and damping of the law, all finite and non-negative.
ImpedanceOptions
Options of Backend::Impedance; cartesian and joint are the documented starting points and the with_* methods change one field each.
JointSent
What one cycle sent, for the observer.
JointTargetControl
The handle of a running joint target control; see the module documentation.
JointTargetControlOptions
Options of Robot::start_joint_target_control; Default is the documented starting point and the with_* methods change one field each.
Leash
How far the desired state may run ahead of the measured one. There is no echo of a torque command to re-anchor the generator on, so every cycle it is anchored on the measured state pulled toward the previous desired by at most this: while the arm follows, that is exactly the previous desired and nothing changes; held back (a hand, an obstacle, an unreachable target) the desired stays within the leash, the force on the arm is bounded by the stiffness times the leash, and on release the generator resumes from where the arm is under its budget. Once a stop holds, the leash pulls toward the held state instead, so an arm moved during the hold meets the same bound. The observer reports what the leash took off as leash_alteration.
Settle
How a stop ends: once the generator is within tolerance of the target on every axis and at rest (REST_VELOCITY, REST_ACCELERATION), the echo of the last command is held for cycles cycles and motion_finished set on the next one.
TargetControlOptions
Options of Robot::start_cartesian_target_control; Default is the documented starting point and the with_* methods change one field each.
TargetSlot
The latest target, N f64s, as a single-writer seqlock: the sequence number is odd while a write is in progress and changes with every write, so a reader that sees the same even number before and after loading the values has a consistent set. Reads never block and never spin unboundedly, which is what lets the realtime loop poll it every cycle; writes are wait-free too, but there must be only one writer at a time, which the handles serialise with a mutex on the user side.

Enums§

Backend
What tracks the target: the robot’s controller or the crate’s torques.

Constants§

DEFAULT_LIMIT_FRACTION
The fraction of the robot’s joint limits JointTargetControlOptions::default budgets.
DEVIATION_MESSAGE
The message of the FrankaError::Control the loop ends with after the deviation guard froze the target.
ENDED_MESSAGE
The message of the FrankaError::InvalidOperation a set_* returns once the loop has ended, for whatever reason; stop() has the reason.
JOINT_LIMIT_INSET
How far, rad, inside the negotiated version’s joint position limits a joint target and an impedance posture must lie: JointTargetControl::set_joints and both starts refuse a configuration outside.
MAX_POSTURE_RATE
The most, rad/s, the posture bias moves any joint: the pull nullspace_gain × distance toward a far posture is scaled down as a whole to this rate, so a posture 2 rad away is approached at 0.5 rad/s, not 2.
ORTHONORMAL_TOLERANCE
How far from orthonormal (the largest entry of |R^T R - I|, or |det R - 1|) the rotation block of a target pose may be: within this it is re-orthonormalised, beyond it refused.
RATED_TORQUES
The FR3’s and FER’s rated joint torques, Nm, the most torque_limits may allow.
REST_ACCELERATION
… and accelerating less than this (m/s^2, rad/s^2); the micro-profiles above peak at about 0.01.
REST_JOINT_VELOCITY
An arm whose every joint moves slower than this, rad/s, is at rest: what a stop in torque mode waits for before motion_finished, the hold having settled only the generator.
REST_VELOCITY
A generator within Settle::tolerance of its target moving slower than this, per axis, counts as landed (m/s, rad/s). The hold then freezes a velocity step of at most this in one cycle, a jerk of 100 per second cubed, which the joint side of a Cartesian command amplifies threefold (1 mm/s froze as 3840 rad/s^3 on joint 2 in the simulator, over its 3750). Not smaller: re-anchoring on a float32 echo (FCI v10) keeps a landed generator in micro-profiles of a few 1e-8 that peak at about 2e-5 per second.
STOP_TIMEOUT_CYCLES
Cycles a stop waits for the generator to land before holding and finishing from wherever the command is: five seconds.
UNIT_QUATERNION_TOLERANCE
How far from unit length a target quaternion may be before it is refused; within this it is normalised.

Functions§

impedance_torques
The torque backend’s law, offline:

Type Aliases§

CartesianObserver
The observer’s type: called every cycle on the realtime thread, so it must not allocate or block.
JointObserver
The observer’s type: called every cycle on the realtime thread, so it must not allocate or block.