Command from a low-rate program
At the end of this page you will have moved the end effector to targets set from an ordinary thread, at whatever rate your program runs, with the crate’s realtime thread doing the 1 kHz work. This is the interface for planners, policies, teleoperation and scripts, and the one the Python bindings use.
Prerequisites: First program works against your
robot or the simulator, the arm is at the ready pose (move_to_ready), and the space around
the end effector is free.
extern crate franka;
use std::sync::Arc;
use franka::{RealtimeConfig, Robot, TargetControlOptions};
fn main() -> franka::FrankaResult<()> {
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(); // the start position, base frame, metres
control.set_position([start[0] + 0.05, start[1], start[2]])?; // any thread, any rate
std::thread::sleep(std::time::Duration::from_secs(1));
let state = control.state(); // the latest RobotState, copied out
assert!(control.is_running());
control.stop()?; // settle, finish the motion, join: the loop's result
let _ = state; Ok(()) }
start_cartesian_target_control returns once the loop’s first cycle has run, so target()
and state() are valid at once. A 5 cm step along one axis under the default budget becomes
an S-curve that peaks at about 0.12 m/s and lands after about 0.85 s (the budget is a norm
and each axis gets 1/√3 of it).
The API
impl Robot {
pub fn start_cartesian_target_control(self: &Arc<Self>, options: TargetControlOptions)
-> FrankaResult<CartesianTargetControl>;
pub fn start_joint_target_control(self: &Arc<Self>, options: JointTargetControlOptions)
-> FrankaResult<JointTargetControl>;
}
impl CartesianTargetControl {
pub fn set_position(&self, position_in_base: [f64; 3]) -> FrankaResult<()>;
pub fn set_orientation(&self, orientation_xyzw: [f64; 4]) -> FrankaResult<()>;
pub fn set_target(&self, position_in_base: [f64; 3], orientation_xyzw: [f64; 4])
-> FrankaResult<()>;
pub fn set_pose(&self, pose: &[f64; 16]) -> FrankaResult<()>; // column-major, as O_T_EE
pub fn target(&self) -> [f64; 3];
pub fn target_orientation(&self) -> [f64; 4];
pub fn target_pose(&self) -> [f64; 16];
pub fn state(&self) -> RobotState;
pub fn tune(&self, update: &TuningUpdate) -> FrankaResult<Vec<&'static FieldBound>>;
pub fn tuning(&self) -> FrankaResult<LiveTuning>;
pub fn is_running(&self) -> bool;
pub fn stop(self) -> FrankaResult<()>;
}
// JointTargetControl is the same with set_joints([f64; 7]) and target() -> [f64; 7],
// and without tune / tuning (below).
The Cartesian target is a pose, absolute in the base frame. set_position moves its
position and keeps its orientation (the start orientation until something sets it),
set_orientation the other way round, set_target and set_pose set both. Orientations
are unit quaternions in [x, y, z, w] order, the scalar part last, or the rotation
block of a column-major pose in the convention of O_T_EE. A quaternion or rotation block
within 1e-3 of unit or orthonormal is normalised on the way in; one further off is refused.
set_joints takes the seven joint positions in radians. Only the latest target counts.
Every setter returns FrankaError::InvalidArgument for a non-finite or malformed value and
FrankaError::InvalidOperation once the loop has ended for any reason; is_running() is
false then, and stop() has the reason. target() is the latest target, state() the
latest RobotState the loop received. While the loop runs it holds the robot’s control
lock, so robot.read() and the other loops fail with InvalidOperation.
stop() lands the command on the last target, holds it for 250 cycles (the robot’s echo
of it in the robot-controller backend; the loop’s own last setpoint in the impedance backend),
sets motion_finished on one more, joins the thread and returns the loop’s result. In the
impedance backend the finish also waits for the arm: motion_finished goes out only once
every joint moves slower than REST_JOINT_VELOCITY (0.01 rad/s), or after the 5 s timeout,
the law kept on the held goal meanwhile, so that an arm still closing its lag is not handed
to the robot’s controller short of the target. The result is
Ok for a regular end, FrankaError::Control if the robot aborted the motion or the
deviation guard fired. A target published just before stop() is not lost. Dropping the
handle without stop() requests the stop and detaches: the loop settles and finishes on its
own, holding its Arc<Robot> until it has. robot.stop() from elsewhere preempts the loop,
and the handle’s stop() then returns the preemption as FrankaError::Control.
Options
TargetControlOptions (Cartesian) | default | JointTargetControlOptions | default |
|---|---|---|---|
limits: OtgLimits, a norm budget | 0.3 m/s, 0.5 m/s², 20 m/s³ | limits: Option<[OtgLimits; 7]> | None: 20 % of the negotiated version’s joint limits |
rotation_limits: OtgLimits, a norm budget | 0.5 rad/s, 1.0 rad/s², 20 rad/s³ | ||
backend: Backend | Impedance(ImpedanceOptions::cartesian()) | backend | Impedance(ImpedanceOptions::joint()) |
controller_mode (robot-controller backend only) | CartesianImpedance | controller_mode | JointImpedance |
max_deviation | 0.30 m | max_deviation | 1.0 rad |
max_angular_deviation | 0.5 rad | ||
settle: Settle (landing tolerance, hold cycles) | 1 mm, 250 cycles | settle | 1 mrad, 250 cycles |
limit_rate | true | limit_rate | true |
realtime_priority: Option<i32> | None (highest) | same | same |
cpu: Option<usize> | None (not pinned) | same | same |
observer | none | observer | none |
Every field is public and has a with_* builder; validate() checks the options without
starting anything. A Cartesian budget is a norm and each axis gets 1/√3 of it. With the
robot’s controller tracking, an FER’s joint-space continuity check refuses 2.5 m/s² near the
ready pose and its collision threshold trips above about 1 m/s², so the default 0.5 m/s² sits
below both. The continuity check applies to a pose stream, so to the
robot-controller backend only; the collision thresholds apply to both. The joint default is
slow on purpose; raise it with JointTargetControlOptions::scaled_limits(version, fraction)
or explicit limits.
The observer is FnMut(&RobotState, &CartesianSent) (&JointSent for joints), called
every cycle on the realtime thread with the state and what was sent: the pose or q
after the backstop, the target, the generator’s velocity and acceleration (angular too for a
pose), and by how much the backstop altered the command. In the impedance backend pose /
q is the loop’s setpoint, the backstop alterations are 0, and the record also carries the
joint goal q_goal, its velocity dq_goal, the fraction of the generator’s step the goal
carried when a joint limit cut it (cap_scale, exactly 1 when none did) and the joints held on a
bound (pinned), the clamped torques tau and the velocity and position envelopes’ shares of
them (tau_envelope, tau_position), what the leash took off the desired state this
cycle (leash_alteration, m for a pose and rad for joints, plus leash_angular_alteration,
rad, for a pose; 0 while the arm follows) and, for a pose, the inverse kinematics residual
ik_error (the norm of the position error, m, and the orientation error times
rotation_weight), its active-set passes ik_passes and the stall at a joint limit
(stall_pressure, stalled). It must not allocate or block; copying into a preallocated
ring is what it is for, and how franka_rerun::Recorder::push and the commander example’s
CSV log hook in.
realtime_priority sets the loop thread’s SCHED_FIFO priority; None is the highest,
as Robot::new uses for its caller. A program with other realtime threads gives the loop a
lower one. A failure to raise it is fatal under RealtimeConfig::Enforce and ignored under
Ignore, so the simulator runs the loop on an ordinary kernel.
Pinning the loop thread. with_cpu(Some(n)) pins the loop thread to core n with
sched_setaffinity right after the priority is raised
(franka::realtime::pin_current_thread_to_cpu is the call); None, the default, leaves the
placement to the scheduler. On a host that keeps a core free for the loop (isolcpus= on
the kernel command line) the loop never migrates and never shares its core with the rest of
the program; two arms in one process can each be given a core. A failure to pin is treated
like a failure to raise the priority: fatal under Enforce, ignored under Ignore.
Backends
The generator produces a setpoint stream; backend decides who tracks it.
Backend::Impedance(ImpedanceOptions), the default, runs the loop through
control_torques and sends, every cycle, the torques of the hybrid joint impedance law that
DROID’s controller
(polymetis
HybridJointImpedanceControl) runs:
Kp = Jᵀ Kx J + diag(Kq)
Kd = Jᵀ Kxd J + diag(Kqd)
tau = Kp (q_goal − q) + Kd (g dq_goal − dq) + coriolis(q, dq)
clamped to ±torque_limits, then low-pass filtered at cutoff_frequency
J is the zero Jacobian at the end-effector frame for the measured q, so the Cartesian
gains act at the frame O_T_EE targets are in; q_goal is the generator’s output on the
joint interface and, on the Cartesian interface, the joint configuration a differential
inverse kinematics finds for the generator’s pose each cycle; dq_goal is that goal’s
velocity. Gravity is the robot’s, as in every torque loop. g is 0 by default, DROID’s form:
the damping acts on the velocity, and a goal moving at v is tracked Kd v / Kp behind.
velocity_feedforward = true sets g to velocity_feedforward_gain (default 1), damping the
velocity error instead. It is off because real arms vibrated with it on: dq_goal is a
finite difference (of the IK solution on the Cartesian interface), and Kd carries its noise
into the torque. velocity_feedforward_cutoff, a first-order low-pass on dq_goal, is the
knob to try against that; no value has been validated on hardware. What differs from DROID’s
law by default is that the generator is leashed to the arm (below).
ImpedanceOptions | Cartesian interface (::cartesian()) | joint interface (::joint()) |
|---|---|---|
gains.cartesian_stiffness Kx (N/m, Nm/rad) | 750, 750, 750, 15, 15, 15 | 0 |
gains.cartesian_damping Kxd (Ns/m, Nms/rad) | 50, 50, 90, 2, 2, 2 | 0 |
gains.joint_stiffness Kq (Nm/rad) | 40, 30, 50, 25, 35, 25, 10 | 600, 600, 600, 600, 250, 150, 50 |
gains.joint_damping Kqd (Nms/rad) | 4, 6, 5, 5, 3, 2, 1 | 50, 50, 50, 50, 30, 25, 15 |
torque_limits (Nm) | 86, 86, 86, 86, 11.5, 11.5, 11.5 | same |
cutoff_frequency (Hz) | 100 | 100 |
velocity_feedforward | false | false |
velocity_feedforward_gain g, in [0, 1] | 1 | 1 |
velocity_feedforward_cutoff (Hz) | 1000 (MAX_CUTOFF_FREQUENCY: off) | same |
leash: Leash | 0.025 m, 0.15 rad | 0.1 rad per joint (the torque clamp, not the leash, bounds the torque: 600 × 0.1 = 60 Nm on joints 1 to 4, under their 86 Nm clamp; on joints 5 and 6 the 11.5 Nm clamp binds first) |
project_joint_gains | false | false |
posture (IK nullspace reference) | None: the start configuration | not used |
ik: IkOptions | λ 0.05, nullspace gain 1.0 /s, 3 iterations, tolerance 1e-6 (weighted), rotation weight 0.1 m/rad | not used |
joint_velocity_fraction (of max_joint_velocity(version)) | 0.7 | 0.7 |
velocity_barrier_fraction | 0.85 | 0.85 |
joint_position_margin (rad) | 0.05 | 0.05 |
The Cartesian column, ImpedanceGains::CARTESIAN, is DROID’s preset with the translational
damping raised from 37 to 50, 50, 90 Ns/m: a damping ratio of about 0.8 from the arm’s
apparent masses at the ready pose (0.94 kg along x and y, 3.9 kg along z, computed from the
model), where 37 leaves z at 0.34 and ringing. ImpedanceGains::DROID is the preset as DROID
ran it, and with velocity_feedforward off it is the polymetis law for replaying policies
trained on it. The joint column is the fer_joint_impedance example’s gains. What each knob does:
cartesian_stiffness: how hard the arm pulls back toward the target, per metre and per radian of end-effector error; the arm is a spring of this stiffness to anything that pushes it.cartesian_damping: the resistance to end-effector velocity error, per m/s and rad/s.joint_stiffness,joint_damping: the same in joint space, on every joint including the one direction the Cartesian term cannot see (the elbow’s swing); on the Cartesian interface they regularise the nullspace and, unprojected, also stiffen the end effector (below); on the joint interface they are the whole law.torque_limits: the per-joint clamp on the command, before the filter.cutoff_frequency: the low-pass filter on the torques;MAX_CUTOFF_FREQUENCYturns it off.velocity_feedforward:Kd (g dq_goal − dq)when on,−Kd dqwhen off, which is the default because real arms vibrated with it on.velocity_feedforward_gain:g, the weight of the goal velocity, when the feedforward is on. At the default 1 the damping acts on the velocity error, so a goal at steady speed needs no lag; lower values trade that lag,(Kqd / Kq) vat 0, against how much of the goal’s own ripple the law forwards.velocity_feedforward_cutoff: a first-order low-pass ondq_goalbefore it is fed forward. The default 1000 Hz isMAX_CUTOFF_FREQUENCY, the filter off; the floorvalidate()accepts isMIN_FEEDFORWARD_CUTOFF, 1 Hz. No value has been validated on hardware.leash: how far the desired state may run ahead of the measured one. Every cycle the generator is anchored on the measured pose (the model’s, for the measuredq) pulled toward the previous desired by at most the leash, the torque-mode form of the third generator rule: while the arm follows, that is exactly the previous desired and nothing changes; held back by a hand, an obstacle or an unreachable target, the desired stays within the leash of the arm, so the spring force on whoever holds it is limited by the leashed position error. On release the generator resumes from where the arm is, under its budget. The leash keeps acting during the stop’s hold. On the joint interface each joint’s goal stays withinleash.jointof the measured joint; there the torque clamp bounds the torque (theJOINTpreset’s 600 Nm/rad × 0.1 rad is 60 Nm on joints 1 to 4, under their 86 Nm clamp; on joints 5 and 6 the 11.5 Nm clamp binds before the leash does).project_joint_gains: confines the joint gains to the Jacobian’s nullspace (N Kq N,N = I − J⁺ J), so that the stiffness you set is the stiffness felt at the end effector, to within the damping of the projector and away from singularities. Unprojected, the joint springs are felt throughJon top ofKx: at the ready pose the default 750 N/m is felt as about 990 to 1180 N/m in translation (computed on the FER model), and two to three timesKxin rotation. With the projection on, zero Cartesian gains would leave the end effector free.posture: the joint configuration the inverse kinematics drifts toward in the nullspace, at most 0.5 rad/s;Noneis the configuration the loop started in. A posture insidejoint_position_marginof a joint limit is refused withInvalidArgument, as is a joint target there.joint_velocity_fraction: no joint of the goal moves faster than this fraction of the arm’s velocity limit, which narrows toward the joint position limits on the FR3 and is flat on the FER (target_control::max_joint_velocityaway from them: the FER’sMAX_JOINT_VELOCITY, the published 2.175 and 2.61 rad/s less libfranka’s tolerance, so 2.13 to 2.15 rad/s on joints 1 to 4 and 2.564 / 2.549 / 2.549 on 5 to 7, or the FR3’s 2.62, 5.26, 4.18, 5.26). A faster step is scaled down as a whole, so the goal keeps its direction and an unreachable or singular pose is approached at a bounded rate; the generator is then re-anchored on what went out, so it never plans from a velocity the arm was not sent. In (0, 1].velocity_barrier_fraction: a joint measured faster than this fraction of its limit meetsVELOCITY_BARRIER_GAIN(20 Nm per rad/s) of damping on the excess, at most its torque limit, added before the clamp. It catches what the command cap cannot, a spring-back or an arm overshooting its goal. Below it the law’s torque along a joint’s motion fades out linearly, frommin(cap, onset − FADE_BAND × limit)(FADE_BAND0.15) of measured speed to none at the onset, so a joint catching up on its lag cannot cancel the barrier. At leastjoint_velocity_fraction, at most 1.joint_position_margin: how far the goal keeps from each joint position limit. Toward a limit the goal brakes to rest on the margin; a joint measured inside it keeps less and less of the law’s torque toward the limit and, 0.02 rad further in, meets a spring pushing it out. On the Cartesian interface the other joints make up what a joint held at its margin cannot, position before orientation (ik.rotation_weight; while the goal is stalled position comes first, to within micrometres): with several joints held, the tool stays put and the turn gets as far as the wall allows, and under a speed limit the orientation lags more. In [0.035, 0.5]. See The joint position limit guard.
Every gain must be finite and non-negative, the leash finite and positive (validate()).
The law itself is public as franka::impedance_torques. The options are set through
builders:
use franka::{Backend, ImpedanceGains, ImpedanceOptions, TargetControlOptions};
let gains = ImpedanceGains {
cartesian_stiffness: [400.0, 400.0, 400.0, 15.0, 15.0, 15.0],
..ImpedanceGains::CARTESIAN
};
let options = TargetControlOptions::default()
.with_backend(Backend::Impedance(ImpedanceOptions::cartesian().with_gains(gains)));
// DROID's law as it ran, for replaying policies trained on it: the feedforward the preset
// needs off is off by default.
let parity = ImpedanceOptions::cartesian().with_gains(ImpedanceGains::DROID);
Collision thresholds. Target control does not set collision thresholds;
set_collision_behavior configures the robot’s reflexes. Choose thresholds for the task
and payload. The leash limits position error, not total contact force: stiffness and
velocity-dependent damping both contribute to the commanded torque. On the joint
interface, the JOINT preset reaches a 20 Nm threshold at 0.033 rad of error.
nonrealtime_commander takes --thresholds N for its collision thresholds.
Hand-guiding and the deviation guard. The guard measures the arm against the start
pose (max_deviation 0.30 m, max_angular_deviation 0.5 rad). Compliance lets a person
move the arm, and moving it past either bound ends the loop: the target freezes where the
command is, the loop finishes at rest, and the robot’s own controller holds the arm where
it was left, without a reflex. That is the right
default against a runaway commander; a session where the arm is meant to be moved by hand
raises both bounds.
Backend::RobotController sends the setpoint stream as poses (control_cartesian_pose)
or joint positions (control_joint_positions) and the robot’s own controller, selected by
controller_mode, tracks it: what target control did before the impedance backend existed.
let options = TargetControlOptions::default().with_backend(Backend::RobotController);
What changes between the two:
- Compliance. The impedance backend is a spring: push the arm and it gives way by
about force / stiffness (10 N against the default 750 N/m is about 1 cm, the unprojected
joint term making the arm somewhat stiffer than
Kx) and returns when released; the robot’s controller holds the pose stiffly. Hold the arm and the desired pose stops within the leash of it, limiting the position error that drives the spring. When released, the generator resumes from where the arm is. - No joint-side continuity refusals. The robot checks the inverse kinematics of a pose stream against its joint limits and refuses a stream whose joints would accelerate too fast (see Online trajectory generation). Torques are not checked that way, so the budget can be raised further in the impedance backend, within the collision thresholds.
- The deviation guard applies to both. The measured pose is compared with the start in
either backend; a compliant arm pushed past
max_deviationends the loop the same way. - Collision thresholds apply to both, and are yours to set. The robot’s contact and
collision reflexes watch the external torques and forces whatever commands them; a
compliant arm meeting an obstacle still trips them at the thresholds
set_collision_behaviorsets, and a spring reaches them by deflection (the paragraph above). - An unreachable or singular target lags in the impedance backend, because no joint of the
goal moves faster than
joint_velocity_fractionof its limit or closer to a limit thanjoint_position_margin, and the leash holds the desired pose near the arm; the robot’s controller refuses a pose stream it cannot follow. - The finish waits for the arm.
stop()in the impedance backend setsmotion_finishedonly once every joint is slower thanREST_JOINT_VELOCITY, or after the 5 s timeout.
Tune the law while it runs
A Cartesian session on the impedance backend takes changes to its gains, its inverse kinematics and its two budgets while the arm moves:
use franka::robot::target_control::TuningUpdate;
let mut update = TuningUpdate::default();
update.cartesian_stiffness = Some(400.0);
update.budget = Some([0.2, 0.4, 20.0]);
let clamped = control.tune(&update)?; // any thread, any rate; the bounds that had to clamp
let targets = control.tuning()?; // the targets in force
TuningUpdate is #[non_exhaustive], so start from Default and assign what the update
carries; a field it does not carry keeps its value and is not re-checked. The nine fields are
joint_stiffness[7], joint_damping[7], cartesian_stiffness, ik_damping,
ik_nullspace_gain, velocity_feedforward_gain, velocity_feedforward_cutoff, budget[3]
and rotation_budget[3]: 25 scalars, together a LiveTuning. One Cartesian stiffness stands
for all twelve Cartesian gains, the six stiffnesses and six dampings
ImpedanceGains::scaled_cartesian derives from it, which is what holds the preset’s damping
ratio. Only this kind of session has live tuning. tune and tuning return
InvalidOperation with NO_TUNING_MESSAGE on the robot-controller backend, and with
DERIVED_GAINS_MESSAGE on a Cartesian session whose gains one stiffness cannot rebuild
(ImpedanceGains::DROID is such a preset). tune alone also returns ENDED_MESSAGE once the
loop has ended; tuning has no such check and still reports the last published targets.
JointTargetControl has neither method: a joint session runs the options it was started with
for its whole life.
The bounds. LiveTuning::BOUNDS is one FieldBound per scalar: name, unit, min, max,
group, the transition policy and the presentation hints (log_slider, slider_max, off_at,
norm). It is a second and tighter gate than ImpedanceOptions::validate, which asks only
whether a session can be built at all and has to stay permissive for every caller of the
crate; BOUNDS asks how far a running session may be moved by hand. The table is the clamp
itself, so a caller that publishes it — the node serves its schema from it — cannot drift from
what is enforced.
An update is all or nothing and then clamped. A value that is not finite, or a zero where zero
means something other than “softer” (cartesian_stiffness, ik_damping), is refused with
InvalidArgument naming the field and nothing is written. Anything out of range is clamped
instead, and the bounds that had to clamp come back so that a caller can show the value it
will really get; read that value out of tuning(), not out of the bound. Raising a joint
spring also raises any joint damping below LiveTuning::joint_damping_floor of it (0.25 √K),
even one the update does not carry: the pair, not either half, is what has to stay damped.
FieldBound::danger is advice for whoever publishes the table, not a gate this crate applies
— TuningDanger::Advise on the fields that reach the torque directly, and
TuningDanger::ConfirmAbove(v) on the ones that widen what the arm may do above v, both
budgets and velocity_feedforward_gain above zero.
Reaching the loop. tune validates, clamps and publishes the whole set on the calling
thread, allocating as it goes, into a single-writer seqlock beside the target slot. The loop
takes a complete snapshot or keeps the previous one, never blocking and never seeing half an
update; the slot holds the latest value rather than a queue, so two updates between cycles
collapse into one. Live parameter protocol draws that
slot and defines the node’s JSON layer on top of it.
Crossing to it. What is published are targets, and the loop moves its own copy toward them
under each field’s TuningPolicy. A parameter that multiplies a generally-nonzero state would
be a step in the torque, so the gains, the IK fields and the feedforward gain slew over
SLEW_TAU, 0.3 s. The feedforward cutoff and both jerks step. A budget’s velocity and
acceleration are raised on the cycle they arrive and lowered as a ramp at the next order’s
limit (TuningPolicy::StepUpGateDown), because a narrower budget clamps the generator’s
stored state rather than re-planning it; the
fourth OTG rule is what Otg::set_limits owes a
running generator. TuningPolicy::remaining reads how much of a crossing is still to come
off the clock, so nothing has to be published back out of the realtime thread. The command
never steps, but lowering an acceleration or jerk while the arm moves lengthens the stop
toward v² / 2a: lower the velocity first, apply it, then the rest.
What the loop does every cycle
- Anchor. The first cycle takes the start, first target and first setpoint from the
robot’s echo of its commanded pose or joints (
O_T_EE_c,q_d) in the robot-controller backend, and from the measured state in the impedance backend, where no echo of the command exists: the measuredq, and for a pose the model’s pose of that configuration rather thanO_T_EE, so the inverse kinematics starts with a zero residual;q_goalstarts atq. Every later cycle of the impedance backend anchors on the measured state pulled toward the previous desired by at most the leash. - Read the slot. The latest target comes through a single-writer seqlock the loop polls without blocking; a torn read keeps the previous target for one cycle.
- Generate. One synchronised jerk-limited generator over all axes, per-axis limits, one nominal millisecond per command, re-anchored before every re-plan on the echo (robot-controller backend) or on the leashed anchor (impedance backend).
- Track. Impedance backend: the inverse kinematics step for a pose inside the joint
limits’ box, or the joint goal’s step scaled into it; the law with its torque along a fast
joint’s motion and toward a near limit faded, plus the velocity barrier and the position
spring, the clamp,
Torques. A goal that falls short restarts the generator from what went out. Robot-controller backend: the rate limiter under the same budget, then the loop’s own libfranka limiter; neither is meant to bind, and the observer sees when one does. - Guard. If the measured pose strays past
max_deviationormax_angular_deviationfrom the start, the target freezes and the loop ends withFrankaError::Control. - Land, hold, finish. After
stop(): run until every axis is withinsettle.toleranceof the target and at rest, hold the last command forsettle.cycles, thenmotion_finished; if the generator has not landed within five seconds, hold from where the command is. The impedance backend finishes only once the arm itself is at rest (REST_JOINT_VELOCITY), or after five more seconds.
Why each step is what it is, and what goes wrong without it, is in Online trajectory generation; the impedance law, its inverse kinematics and its provenance are in The impedance backend.
Examples and Python
examples/nonrealtime_commander.rs drives this loop from a scripted commander that steps
the target by ±5 cm with irregular holds, a 2 s stall and a burst of 20 targets in 100 ms
(--stdin reads x y z lines instead; --rotate adds a ±15° yaw sweep through
set_orientation; --log PATH writes one CSV row per cycle from the observer). Its --raw
mode sends the same steps to a bare control_cartesian_pose so the robot refuses the first
one, for contrast. crates/franka-rerun/examples/commander_live.rs is the same commander
streamed into a Rerun viewer as it runs. Python’s move_to, move_by and follow are this
loop; see From Python.
Target control works on the FR3 (FCI v10) and the FER (FCI v5) with either backend; the impedance backend is not yet validated on an FR3. See Backends.