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 threadStructs§
- Cartesian
Sent - What one cycle sent, for the observer.
- Cartesian
Target Control - 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 inO_T_EE. - IkOptions
- Options of the Cartesian backend’s differential inverse kinematics: every cycle, from the
previous joint goal, up to
iterationsdamped-least-squares steps toward the pose, a posture bias through the nullspace, the step capped atmax_stepand clamped inside the joint limits. - Impedance
Gains - The stiffness and damping of the law, all finite and non-negative.
- Impedance
Options - Options of
Backend::Impedance;cartesianandjointare the documented starting points and thewith_*methods change one field each. - Joint
Sent - What one cycle sent, for the observer.
- Joint
Target Control - The handle of a running joint target control; see the module documentation.
- Joint
Target Control Options - Options of
Robot::start_joint_target_control;Defaultis the documented starting point and thewith_*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
toleranceof the target on every axis and at rest (REST_VELOCITY,REST_ACCELERATION), the echo of the last command is held forcyclescycles andmotion_finishedset on the next one. - Target
Control Options - Options of
Robot::start_cartesian_target_control;Defaultis the documented starting point and thewith_*methods change one field each. - Target
Slot - The latest target,
Nf64s, 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::defaultbudgets. - DEVIATION_
MESSAGE - The message of the
FrankaError::Controlthe loop ends with after the deviation guard froze the target. - ENDED_
MESSAGE - The message of the
FrankaError::InvalidOperationaset_*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_jointsand bothstarts refuse a configuration outside. - MAX_
POSTURE_ RATE - The most, rad/s, the posture bias moves any joint: the pull
nullspace_gain × distancetoward 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_limitsmay 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::toleranceof 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§
- Cartesian
Observer - The observer’s type: called every cycle on the realtime thread, so it must not allocate or block.
- Joint
Observer - The observer’s type: called every cycle on the realtime thread, so it must not allocate or block.