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;