Skip to main content

franka/robot/target_control/
mod.rs

1//! Target control: a 1 kHz loop on its own thread that follows targets a low-rate commander
2//! sets whenever it likes.
3//!
4//! Most programs that want to move a Franka are not 1 kHz programs: a planner, a vision
5//! loop, a script over a socket, a hand on a keyboard. They produce *targets* -- at 10 Hz,
6//! in bursts, with pauses -- each a step the robot must never see as a step. This is the
7//! fourth way to control the robot, next to the callbacks, `ActiveControl` and the
8//! read-only stream: [`Robot::start_cartesian_target_control`] and
9//! [`Robot::start_joint_target_control`] spawn a named thread that runs the crate's own
10//! control loop and hand back a handle whose `set_*` can be called from any thread, at any
11//! rate, with any target. The handle's [`stop`](CartesianTargetControl::stop) brings the
12//! command to rest on the last target, finishes the motion and returns the loop's result.
13//!
14//! # Two backends
15//! The [`Backend`] of the options decides what the generator's output becomes. The default,
16//! [`Backend::Impedance`], sends **torques**: the hybrid joint and Cartesian impedance law of
17//! [`ImpedanceGains`] (`Kp = J^T Kx J + Kq`, `Kd = J^T Kxd J + Kqd`, the damping on the
18//! velocity error `dq_goal - dq` with the goal's own velocity as the feedforward, plus the
19//! Coriolis term, clamped to the torque limits) tracks a joint target `q_goal` -- the
20//! generator's own output on the joint interface, the solution of a differential inverse
21//! kinematics following the generator's pose one cycle at a time on the Cartesian one
22//! ([`IkOptions`]) -- through [`Robot::control_torques`] with the crate's low-pass filter
23//! ([`ImpedanceOptions::cutoff_frequency`]) and torque rate limiter. There is no echo of a
24//! torque command, so the loop anchors on the *measured* configuration in its first cycle
25//! (the Cartesian interface on the model's pose of it, where the IK's residual is zero), and
26//! then, every cycle, on the measured state pulled toward the previous desired by at most the
27//! [`Leash`]: exactly the previous desired while the arm follows, so the generator runs from
28//! its own last output and its limits are the whole budget; a bounded distance ahead of an arm
29//! that is held back, so the spring force is bounded by the stiffness times the leash and the
30//! generator resumes from where the arm is once it is let go. [`Backend::RobotController`]
31//! instead streams the generator's output as a pose or joint-position command to the robot's
32//! own impedance controller (`controller_mode`); the rest of this page describes that path
33//! where the two differ.
34//!
35//! # What the loop does every cycle
36//! The three rules of the [`otg`](crate::otg) module, learnt on a real arm: the generator's
37//! limits are **per axis** (a Cartesian budget is a norm, so it gets
38//! [`OtgLimits::per_axis_for_norm`](crate::otg::OtgLimits::per_axis_for_norm)`(3)`), it steps **one nominal cycle** per command
39//! ([`DELTA_T`](crate::rate_limiting::DELTA_T)) whatever the measured period, and it is
40//! **re-anchored on the last command** -- the robot's echo of it (`O_T_EE_c`, `q_d`) with
41//! [`Backend::RobotController`], the leashed previous output with [`Backend::Impedance`] --
42//! with `set_position` before every re-plan. With [`Backend::RobotController`] the
43//! rate limiter under the same budget then runs as the backstop that must never bind
44//! (`limit_rate_cartesian_pose`, `limit_rate_joint_positions`), and the loop's own libfranka
45//! limiter stays on behind it; the observer is told by how much the backstop moved the
46//! command. The first setpoint of the motion is always the anchor itself
47//! -- on FCI v10 the first command is its own filter reference
48//! and would otherwise go out as a jump -- and the start returns only once that cycle has
49//! run, so [`target`](CartesianTargetControl::target) and
50//! [`state`](CartesianTargetControl::state) are valid from the first call.
51//!
52//! The Cartesian target is a pose: three more axes of the same synchronised generator run
53//! on the base-frame rotation vector of the orientation error, `log(R_target * R_echo^T)`,
54//! re-anchored at zero every cycle and composed back as `exp(step) * R_echo`, under a
55//! rotational norm budget of their own; translation and rotation arrive together.
56//!
57//! Two guards. If the *measured* pose strays more than `max_deviation` (m) or
58//! `max_angular_deviation` (rad) from the start, the target freezes where the command is,
59//! the generator brings it to rest, and the loop ends with [`FrankaError::Control`]. And a
60//! stop never finishes on a moving command: once every axis of the generator has landed
61//! (within [`Settle::tolerance`] of the target, slower than [`REST_VELOCITY`], accelerating
62//! less than [`REST_ACCELERATION`] -- the orientation included, in rad) the loop stops
63//! stepping it and holds the last command -- with [`Backend::RobotController`] the robot's
64//! echo of it, continuous with what the robot has by construction, whatever the backstop took
65//! off that command, sent bit for bit and past the backstop; with [`Backend::Impedance`] the
66//! landed desired pose or joint goal, whose torques are those of rest -- for
67//! [`Settle::cycles`] cycles, then
68//! sets `motion_finished` on one more of it: the sequence a robot accepts as "finished at
69//! rest". If the generator has not landed within [`STOP_TIMEOUT_CYCLES`] the same hold starts
70//! from wherever the command is. That hold settles the generator, not the arm: in torque mode
71//! an arm still closing its lag would be handed to the robot's controller short of the goal,
72//! so [`Backend::Impedance`] finishes only once every joint moves slower than
73//! [`REST_JOINT_VELOCITY`], or after [`STOP_TIMEOUT_CYCLES`] more cycles, the law kept on the
74//! held goal meanwhile.
75//!
76//! # Threads
77//! The commander side is a single-writer seqlock ([`TargetSlot`]) the loop polls without
78//! blocking; `set_*` serialises its callers with a mutex on the user side only. The latest
79//! [`RobotState`] is published with `Mutex::try_lock` from the realtime side and read with
80//! `lock` on the user side. Nothing allocates on the realtime thread after the start. The
81//! optional observer is the exception to "nothing of yours runs at 1 kHz": it is called on
82//! the realtime thread every cycle with the state and what was sent, and must not allocate
83//! or block -- copying into a preallocated ring, as `franka_rerun::Recorder::push` does, is
84//! what it is for.
85//!
86//! The loop thread is raised to `SCHED_FIFO` like `Robot::new` raises its caller: to the
87//! highest priority, or to `realtime_priority` when the options name one; a failure is fatal
88//! under [`RealtimeConfig::Enforce`] and ignored under [`RealtimeConfig::Ignore`].
89//!
90//! Only one control or read operation may run on a `Robot` at a time, so while a target
91//! control runs, `robot.read()` and the other loops fail with
92//! [`FrankaError::InvalidOperation`], exactly as with a callback loop on another thread;
93//! `robot.stop()` preempts it, and the handle's `stop` then returns the preemption as a
94//! [`FrankaError::Control`].
95//!
96//! ```no_run
97//! use std::sync::Arc;
98//! use franka::{RealtimeConfig, Robot, TargetControlOptions};
99//!
100//! # fn main() -> franka::FrankaResult<()> {
101//! let robot = Arc::new(Robot::new("192.168.0.1", RealtimeConfig::Enforce)?);
102//! let control = robot.start_cartesian_target_control(TargetControlOptions::default())?;
103//! let start = control.target();
104//! for step in 1..=5 {
105//!     // A planner, a socket, a keyboard: any thread, any rate, any target.
106//!     control.set_position([start[0] + 0.01 * f64::from(step), start[1], start[2]])?;
107//!     std::thread::sleep(std::time::Duration::from_millis(300));
108//! }
109//! // An orientation too: a unit quaternion in [x, y, z, w] order, or a pose as O_T_EE.
110//! let yaw = std::f64::consts::FRAC_PI_8;
111//! let orientation = [0.0, 0.0, (yaw / 2.0).sin(), (yaw / 2.0).cos()];
112//! control.set_target(start, orientation)?;
113//! control.stop()?; // settles on the last target, finishes the motion, joins the thread
114//! # Ok(())
115//! # }
116//! ```
117
118mod cartesian;
119mod ik;
120mod impedance;
121mod joint;
122mod options;
123mod rotation;
124mod runner;
125mod slot;
126mod torque;
127
128pub use cartesian::{CartesianObserver, CartesianSent, CartesianTargetControl};
129pub use ik::{IkOptions, MAX_POSTURE_RATE};
130pub use impedance::{
131    impedance_torques, Backend, ImpedanceGains, ImpedanceOptions, Leash, RATED_TORQUES,
132};
133pub use joint::{JointObserver, JointSent, JointTargetControl};
134pub use options::{JointTargetControlOptions, TargetControlOptions, DEFAULT_LIMIT_FRACTION};
135pub use rotation::{ORTHONORMAL_TOLERANCE, UNIT_QUATERNION_TOLERANCE};
136pub use slot::TargetSlot;
137
138use runner::Runner;
139pub use runner::{REST_ACCELERATION, REST_JOINT_VELOCITY, REST_VELOCITY};
140
141use std::sync::atomic::{AtomicBool, Ordering};
142use std::sync::mpsc::{self, SyncSender};
143use std::sync::{Arc, Mutex, PoisonError};
144use std::thread::JoinHandle;
145
146use crate::error::{ControlException, FrankaError, FrankaResult};
147use crate::rate_limiting;
148use crate::realtime::{
149    set_current_thread_scheduler_priority, set_current_thread_to_highest_scheduler_priority,
150    RealtimeConfig,
151};
152use crate::robot::Robot;
153use crate::robot_state::RobotState;
154use crate::wire::robot::codec::FciVersion;
155
156/// Cycles a stop waits for the generator to land before holding and finishing from wherever
157/// the command is: five seconds.
158pub const STOP_TIMEOUT_CYCLES: u32 = 5000;
159
160/// The message of the [`FrankaError::InvalidOperation`] a `set_*` returns once the loop has
161/// ended, for whatever reason; `stop()` has the reason.
162pub const ENDED_MESSAGE: &str =
163    "franka target control: the control loop has ended; stop() returns its result.";
164
165/// The message of the [`FrankaError::Control`] the loop ends with after the deviation guard
166/// froze the target.
167pub const DEVIATION_MESSAGE: &str = "franka target control: the measured pose strayed \
168     further than max_deviation (or max_angular_deviation) from the start; the target was \
169     frozen where the command was and the motion finished from rest.";
170
171/// How a stop ends: once the generator is within `tolerance` of the target on every axis and
172/// at rest ([`REST_VELOCITY`], [`REST_ACCELERATION`]), the echo of the last command is held
173/// for `cycles` cycles and `motion_finished` set on the next one.
174#[derive(Debug, Clone, Copy, PartialEq)]
175pub struct Settle {
176    /// Per-axis tolerance, in the interface's unit: m for a position, rad for a joint or for
177    /// the orientation error of a pose. Default 1e-3.
178    pub tolerance: f64,
179    /// Cycles the landed command is held, identical, before `motion_finished`. Default 250.
180    pub cycles: u32,
181}
182
183impl Default for Settle {
184    fn default() -> Self {
185        Settle {
186            tolerance: 1e-3,
187            cycles: 250,
188        }
189    }
190}
191
192/// Checks the options both interfaces share.
193fn validate_common(
194    max_deviation: f64,
195    settle: Settle,
196    realtime_priority: Option<i32>,
197    backend: &Backend,
198) -> FrankaResult<()> {
199    if let Backend::Impedance(impedance) = backend {
200        impedance.validate()?;
201    }
202    let positive = |x: f64| x.is_finite() && x > 0.0;
203    if !positive(max_deviation) {
204        return Err(FrankaError::InvalidArgument(format!(
205            "target control: max_deviation must be finite and positive, got {max_deviation}"
206        )));
207    }
208    if !positive(settle.tolerance) || settle.cycles == 0 {
209        return Err(FrankaError::InvalidArgument(format!(
210            "target control: settle needs a finite, positive tolerance and at least one \
211             cycle, got {settle:?}"
212        )));
213    }
214    if let Some(priority) = realtime_priority {
215        if !(1..=99).contains(&priority) {
216            return Err(FrankaError::InvalidArgument(format!(
217                "target control: realtime_priority must be within 1..=99, got {priority}"
218            )));
219        }
220    }
221    Ok(())
222}
223
224/// The joint position limits (lower, upper) of the negotiated version's arm.
225fn joint_position_limits(version: FciVersion) -> ([f64; 7], [f64; 7]) {
226    match version {
227        FciVersion::V5 => rate_limiting::fer::JOINT_POSITION_LIMITS,
228        FciVersion::V10 => rate_limiting::JOINT_POSITION_LIMITS,
229    }
230}
231
232/// How far, rad, inside the negotiated version's joint position limits a joint target and an
233/// impedance posture must lie: [`JointTargetControl::set_joints`] and both `start`s refuse a
234/// configuration outside.
235pub const JOINT_LIMIT_INSET: f64 = 0.02;
236
237/// Refuses a `q` outside `limits` inset by [`JOINT_LIMIT_INSET`], naming the joint and `what`.
238fn check_joint_limits(q: &[f64; 7], limits: &([f64; 7], [f64; 7]), what: &str) -> FrankaResult<()> {
239    for (i, value) in q.iter().enumerate() {
240        let lower = limits.0[i] + JOINT_LIMIT_INSET;
241        let upper = limits.1[i] - JOINT_LIMIT_INSET;
242        if !(lower..=upper).contains(value) {
243            return Err(FrankaError::InvalidArgument(format!(
244                "target control: the {what} puts joint {} at {value} rad, outside \
245                 [{lower}, {upper}] ({JOINT_LIMIT_INSET} rad inside the arm's limits)",
246                i + 1
247            )));
248        }
249    }
250    Ok(())
251}
252
253/// The posture of an impedance backend, checked against the arm's limits at `start`.
254fn check_posture(backend: &Backend, limits: &([f64; 7], [f64; 7])) -> FrankaResult<()> {
255    match backend {
256        Backend::Impedance(ImpedanceOptions {
257            posture: Some(posture),
258            ..
259        }) => check_joint_limits(posture, limits, "posture"),
260        _ => Ok(()),
261    }
262}
263
264/// What the user thread and the loop thread share.
265struct Shared<const N: usize> {
266    slot: TargetSlot<N>,
267    stop: AtomicBool,
268    running: AtomicBool,
269    state: Mutex<RobotState>,
270    /// Serialises the writers of `slot`.
271    writer: Mutex<()>,
272}
273
274impl<const N: usize> Default for Shared<N> {
275    fn default() -> Self {
276        Shared {
277            slot: TargetSlot::default(),
278            stop: AtomicBool::new(false),
279            running: AtomicBool::new(false),
280            state: Mutex::new(RobotState::default()),
281            writer: Mutex::new(()),
282        }
283    }
284}
285
286/// The user side, generic over the interface; the public handles wrap it.
287struct Handle<const N: usize> {
288    shared: Arc<Shared<N>>,
289    thread: Option<JoinHandle<FrankaResult<()>>>,
290}
291
292impl<const N: usize> Handle<N> {
293    fn set_target(&self, target: [f64; N]) -> FrankaResult<()> {
294        self.modify_target(|current| *current = target)
295    }
296
297    /// Publishes `modify` of the latest target, under the writer lock so that two callers
298    /// changing different parts of it never lose each other's part.
299    fn modify_target(&self, modify: impl FnOnce(&mut [f64; N])) -> FrankaResult<()> {
300        let _writer = self
301            .shared
302            .writer
303            .lock()
304            .unwrap_or_else(PoisonError::into_inner);
305        if !self.is_running() {
306            return Err(FrankaError::InvalidOperation(ENDED_MESSAGE.to_string()));
307        }
308        let mut target = self.target();
309        modify(&mut target);
310        if target.iter().any(|v| !v.is_finite()) {
311            return Err(FrankaError::InvalidArgument(format!(
312                "target control: the target must be finite, got {target:?}"
313            )));
314        }
315        self.shared.slot.publish(target);
316        Ok(())
317    }
318
319    fn target(&self) -> [f64; N] {
320        let mut target = [0.0; N];
321        // Only a writer mid-update can make a load fail, and a write is a handful of stores.
322        for _ in 0..1000 {
323            if self.shared.slot.load(&mut target) {
324                break;
325            }
326            std::hint::spin_loop();
327        }
328        target
329    }
330
331    fn state(&self) -> RobotState {
332        *self
333            .shared
334            .state
335            .lock()
336            .unwrap_or_else(PoisonError::into_inner)
337    }
338
339    fn is_running(&self) -> bool {
340        self.shared.running.load(Ordering::SeqCst)
341    }
342
343    fn stop(mut self) -> FrankaResult<()> {
344        self.shared.stop.store(true, Ordering::SeqCst);
345        match self.thread.take() {
346            Some(thread) => join(thread),
347            None => Ok(()),
348        }
349    }
350}
351
352impl<const N: usize> Drop for Handle<N> {
353    /// Requests the stop and detaches: the loop settles and finishes on its own, holding its
354    /// `Arc<Robot>` until it has.
355    fn drop(&mut self) {
356        if self.thread.is_some() {
357            self.shared.stop.store(true, Ordering::SeqCst);
358        }
359    }
360}
361
362fn join(thread: JoinHandle<FrankaResult<()>>) -> FrankaResult<()> {
363    thread.join().unwrap_or_else(|_| {
364        Err(FrankaError::Control(ControlException::new(
365            "franka target control: the control thread panicked",
366        )))
367    })
368}
369
370/// `SCHED_FIFO` for the loop thread, the way `Robot::new` does it for its caller.
371fn raise_priority(config: RealtimeConfig, priority: Option<i32>) -> FrankaResult<()> {
372    let result = match priority {
373        None => set_current_thread_to_highest_scheduler_priority(),
374        Some(priority) => set_current_thread_scheduler_priority(priority),
375    };
376    match result {
377        Err(message) if config == RealtimeConfig::Enforce => Err(FrankaError::Realtime(message)),
378        _ => Ok(()),
379    }
380}
381
382/// Spawns the loop thread and waits for its first cycle. `body` runs the crate's control
383/// loop and must send on the channel from the first cycle; if it returns before that, its
384/// error is what the start returns.
385fn spawn<const N: usize, F>(
386    name: &str,
387    robot: &Arc<Robot>,
388    shared: Arc<Shared<N>>,
389    priority: Option<i32>,
390    body: F,
391) -> FrankaResult<Handle<N>>
392where
393    F: FnOnce(&Robot, SyncSender<()>) -> FrankaResult<()> + Send + 'static,
394{
395    let (started, first_cycle) = mpsc::sync_channel::<()>(1);
396    let robot = Arc::clone(robot);
397    let config = robot.robot.realtime_config();
398    shared.running.store(true, Ordering::SeqCst);
399    let thread_shared = Arc::clone(&shared);
400    let thread = std::thread::Builder::new()
401        .name(name.to_string())
402        .spawn(move || {
403            let result = raise_priority(config, priority).and_then(|()| body(&robot, started));
404            thread_shared.running.store(false, Ordering::SeqCst);
405            result
406        })
407        .map_err(|e| {
408            FrankaError::InvalidOperation(format!(
409                "franka target control: cannot spawn the control thread: {e}"
410            ))
411        })?;
412    match first_cycle.recv() {
413        Ok(()) => Ok(Handle {
414            shared,
415            thread: Some(thread),
416        }),
417        Err(_) => Err(join(thread).err().unwrap_or_else(|| {
418            FrankaError::Control(ControlException::new(
419                "franka target control: the control loop ended before its first cycle",
420            ))
421        })),
422    }
423}
424
425impl Robot {
426    /// Starts a Cartesian target control loop on its own thread and returns once its first
427    /// cycle has anchored on the current pose (the model's pose of the measured configuration
428    /// with [`Backend::Impedance`], the commanded one with [`Backend::RobotController`]); see
429    /// the [module documentation](self).
430    ///
431    /// # Errors
432    /// [`FrankaError::InvalidArgument`] if the options are invalid,
433    /// [`FrankaError::Realtime`] if the loop thread cannot be raised to `SCHED_FIFO` under
434    /// [`RealtimeConfig::Enforce`], whatever [`Robot::load_model`] fails with under
435    /// [`Backend::Impedance`], and whatever [`Robot::control_torques`] or
436    /// [`Robot::control_cartesian_pose`] fails with before its first cycle,
437    /// [`FrankaError::InvalidOperation`] if another control or read operation is running
438    /// among them.
439    pub fn start_cartesian_target_control(
440        self: &Arc<Self>,
441        options: TargetControlOptions,
442    ) -> FrankaResult<CartesianTargetControl> {
443        cartesian::start(self, options)
444    }
445
446    /// Starts a joint target control loop on its own thread and returns once its first cycle
447    /// has anchored on the current joint positions; see the [module documentation](self).
448    ///
449    /// # Errors
450    /// As [`Robot::start_cartesian_target_control`], with
451    /// [`Robot::control_joint_positions`] as the [`Backend::RobotController`] loop.
452    pub fn start_joint_target_control(
453        self: &Arc<Self>,
454        options: JointTargetControlOptions,
455    ) -> FrankaResult<JointTargetControl> {
456        joint::start(self, options)
457    }
458}
459
460#[cfg(test)]
461mod tests;