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 measured velocity, or with ImpedanceOptions::velocity_feedforward on the velocity error dq_goal - dq, 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. The goal never moves a joint faster than ImpedanceOptions::joint_velocity_fraction of the arm’s limit (max_joint_velocity, which narrows toward the position limits on the FR3): a faster step is scaled down as a whole and the generator re-anchored on what went out. A joint measured faster than the cap, or than FADE_BAND of its limit under the barrier’s onset if that is lower, keeps less and less of the law’s torque along its motion, none from ImpedanceOptions::velocity_barrier_fraction of its limit, where it meets a damping of VELOCITY_BARRIER_GAIN on the excess, added before the clamp. Toward a joint position limit the goal brakes to stop ImpedanceOptions::joint_position_margin inside it; a joint measured inside the margin keeps less of the law’s torque toward the limit over POSITION_FADE_BAND, then meets a spring (POSITION_BARRIER_STIFFNESS). 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: 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; when they name a cpu, it also pins itself to that core, whether or not the raise succeeded. A failure of either 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("172.16.0.2", 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.
FieldBound
One scalar of LiveTuning: what it is called, what it means, and how far it may go.
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 inside the cycle’s joint box, with a posture bias through the nullspace.
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. The leash bounds the force, not the release: the arm closes the gap it was held back by under the spring alone, which can outrun the 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.
LiveTuning
The targets a running session will accept. These are targets, not the values in force: the loop keeps its own current values and moves them here under each field’s policy.
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.
SlewGains
How far one cycle moves each word of a LiveTuning toward its target: the fraction of the remaining gap a slewed word closes, and the cycle time a gated word’s descent is measured in.
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.
TuningUpdate
A partial change to a LiveTuning: every field left None keeps the value it has.

Enums§

Backend
What tracks the target: the robot’s controller or the crate’s torques.
TuningDanger
How much care moving a FieldBound wants. Advice for the operator, enforced by whoever publishes the table – this crate clamps, and has no opinion beyond the clamp.
TuningPolicy
How the loop is to move an accepted value into effect. Carried here rather than in the loop so that a published schema can explain a slider’s behaviour without a second table.

Constants§

DEFAULT_LIMIT_FRACTION
The fraction of the robot’s joint limits JointTargetControlOptions::default budgets.
DERIVED_GAINS_MESSAGE
The message of the FrankaError::InvalidOperation a live-tuning call returns on a Cartesian impedance session whose gains LiveTuning::gains cannot rebuild from one Cartesian stiffness – ImpedanceGains::DROID is the preset this is true of. Such a session keeps the gains it was started with and is given no tuning slot, rather than having the six derived dampings rewritten in one cycle by the first change to any other field.
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.
FADE_BAND
The narrowest band, as a fraction of a joint’s velocity limit, over which the law’s push along the joint’s motion fades out under the barrier’s onset: the fade starts at the cap or this far under the onset, whichever is lower, and never below rest.
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. Backend::Impedance refuses one inside ImpedanceOptions::joint_position_margin where that is larger.
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.
MIN_FEEDFORWARD_CUTOFF
The lowest cutoff, Hz, ImpedanceOptions::velocity_feedforward_cutoff accepts. A first-order low-pass at 1 Hz still settles inside a second; below it the filter is a near-integrator holding a velocity the goal no longer has for longer than a session runs.
MIN_JOINT_DAMPING_RATIO
The least joint_damping[i] / sqrt(joint_stiffness[i]) a running session is left at. The shipped presets sit between 0.32 (ImpedanceGains::CARTESIAN’s wrist) and 2.12 (JOINT’s), so this is under every one of them with room to spare: it forbids the undamped corner and nothing else. Like every number in LiveTuning::BOUNDS it is a derivation on paper until it is swept.
NO_TUNING_MESSAGE
The message of the FrankaError::InvalidOperation a live-tuning call returns on a session whose interface or backend has none: LiveTuning describes the impedance law of the Cartesian interface, and nothing else runs one.
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.
POSITION_BARRIER_STIFFNESS
1/rad: the position barrier’s stiffness as a multiple of the joint’s torque clamp, K_i = POSITION_BARRIER_STIFFNESS × torque_limits_i: 1075 Nm/rad at the 86 Nm clamp, 144 at 11.5.
POSITION_FADE_BAND
rad: the band inside the margin over which the law’s torque toward a joint limit fades out, from full at the margin to none at the position barrier’s onset, margin − POSITION_FADE_BAND from the limit. Inside that onset a spring pushes the joint out.
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 freezes as about 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.
SLEW_TAU
The slew time constant of every slewed field, s.
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.
VELOCITY_BARRIER_GAIN
Nm per rad/s of the velocity barrier: the damping a joint meets beyond ImpedanceOptions::velocity_barrier_fraction of its limit. Stable at 1 kHz behind the 100 Hz low-pass while the law’s own joint damping plus this stays under the joint’s effective inertia per millisecond; see The impedance backend in the book.

Functions§

impedance_torques
The torque backend’s law, offline:
joint_position_limits
The joint position limits (lower, upper), rad, of the arm speaking version: the FER’s or the FR3’s JOINT_POSITION_LIMITS.
max_joint_velocity
The joint velocity limits, rad/s, of the arm speaking version away from its joint position limits: the FER’s MAX_JOINT_VELOCITY or the FR3’s caps. The FR3’s limit narrows toward the position limits, which the impedance backend follows; the FER’s is flat. Its cap and barrier (ImpedanceOptions::joint_velocity_fraction, ImpedanceOptions::velocity_barrier_fraction) and JointTargetControlOptions::scaled_limits are fractions of these.

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.