Skip to main content

franka/robot/target_control/
cartesian.rs

1//! The Cartesian interface of [target control](super): a pose target in the base frame. The
2//! slot holds the position and a unit quaternion `[x, y, z, w]`; one synchronised six-axis
3//! generator runs on the position and on the base-frame rotation vector of the orientation
4//! error (see [`chart`]). [`Backend::Impedance`] follows the generator's pose with the
5//! differential IK and sends the impedance law's torques; [`Backend::RobotController`]
6//! streams the pose through [`Robot::control_cartesian_pose`].
7
8use std::sync::mpsc::SyncSender;
9use std::sync::Arc;
10
11use nalgebra::Matrix3;
12
13use super::position::VelocityLimit;
14use super::rotation::{
15    angle_between, checked_pose, distance, exp, from_quaternion, log, pose_from, rotation_of,
16    to_quaternion, translation_of, unit_quaternion,
17};
18use super::runner::Step;
19use super::torque::{PoseTracker, TorqueLoop};
20use super::{
21    check_posture, joint_position_limits, spawn, Backend, FieldBound, Handle, ImpedanceOptions,
22    LiveTuning, Runner, Shared, TargetControlOptions, TuningUpdate, DERIVED_GAINS_MESSAGE,
23    NO_TUNING_MESSAGE,
24};
25use crate::control_types::CartesianPose;
26use crate::error::{FrankaError, FrankaResult};
27use crate::lowpass_filter::MAX_CUTOFF_FREQUENCY;
28use crate::math_utils::orthonormalized_rotation;
29use crate::model::Model;
30use crate::otg::OtgLimits;
31use crate::rate_limiting::{
32    limit_rate_cartesian_pose, DELTA_T, FACTOR_CARTESIAN_ROTATION_POSE_INTERFACE,
33};
34use crate::robot::Robot;
35use crate::robot_state::RobotState;
36use crate::wire::robot::codec::FciVersion;
37
38/// What one cycle sent, for the observer.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct CartesianSent {
41    /// The pose that went to the robot (after the backstop), or with [`Backend::Impedance`]
42    /// the desired pose the IK follows; column-major. On a cycle the joint velocity cap cut,
43    /// the desired pose before the cut: `q_goal` and `dq_goal` are what was commanded.
44    pub pose: [f64; 16],
45    /// The orientation of `pose` as a unit quaternion, `[x, y, z, w]`.
46    pub orientation: [f64; 4],
47    /// The target position the generator was planning towards, base frame, m.
48    pub target: [f64; 3],
49    /// The target orientation the generator was planning towards, `[x, y, z, w]`.
50    pub target_orientation: [f64; 4],
51    /// The generator's velocity at the end of the cycle, m/s.
52    pub velocity: [f64; 3],
53    /// The generator's acceleration at the end of the cycle, m/s^2.
54    pub acceleration: [f64; 3],
55    /// The generator's angular velocity at the end of the cycle, base frame, rad/s.
56    pub angular_velocity: [f64; 3],
57    /// The generator's angular acceleration at the end of the cycle, base frame, rad/s^2.
58    pub angular_acceleration: [f64; 3],
59    /// How far the backstop moved the generator's position, m: zero unless the generator
60    /// outran the budget (or without `limit_rate`, or with [`Backend::Impedance`]).
61    pub backstop_alteration: f64,
62    /// How far the backstop turned the generator's orientation, rad; as above.
63    pub backstop_angular_alteration: f64,
64    /// The joint target of the impedance law, rad; zeros with [`Backend::RobotController`].
65    pub q_goal: [f64; 7],
66    /// The joint goal's velocity, rad/s: the finite difference of `q_goal` the law feeds
67    /// forward with [`ImpedanceOptions::velocity_feedforward`] on, under the joint velocity cap
68    /// and after [`ImpedanceOptions::velocity_feedforward_cutoff`]'s low-pass if one is set;
69    /// zeros on the first cycle, while holding and with [`Backend::RobotController`]. This is
70    /// the value the law and the recorder both see, not the raw difference.
71    pub dq_goal: [f64; 7],
72    /// The fraction of the generator's step the goal carried on a cycle it fell short and the
73    /// generator was restarted from the goal: held at a joint position limit, cut to
74    /// [`ImpedanceOptions::joint_velocity_fraction`](super::ImpedanceOptions::joint_velocity_fraction)
75    /// of the joint velocity limits, or both. Exactly 1 on every other cycle, and with
76    /// [`Backend::RobotController`].
77    pub cap_scale: f64,
78    /// Per joint, the bound of the IK's box the goal was held on: 0 none, -1 / +1 the lower /
79    /// upper position bound (the margin, or the braking toward it), -2 / +2 the velocity bound;
80    /// zeros with [`Backend::RobotController`].
81    pub pinned: [i8; 7],
82    /// The torques sent, Nm, clamped to the torque limits; zeros with
83    /// [`Backend::RobotController`].
84    pub tau: [f64; 7],
85    /// The velocity envelope's share of `tau`, Nm, before the clamp: the barrier opposing every
86    /// joint measured faster than
87    /// [`ImpedanceOptions::velocity_barrier_fraction`](super::ImpedanceOptions::velocity_barrier_fraction)
88    /// of its limit, less the law's torque along a joint's motion faded out above the fade's
89    /// start ([`FADE_BAND`](super::FADE_BAND)); zeros below it and with
90    /// [`Backend::RobotController`].
91    pub tau_envelope: [f64; 7],
92    /// The position envelope's share of `tau`, Nm, before the clamp: the spring pushing a joint
93    /// measured inside the position barrier's onset out, less the law's torque toward a limit
94    /// faded out inside
95    /// [`ImpedanceOptions::joint_position_margin`](super::ImpedanceOptions::joint_position_margin);
96    /// `tau` is the clamped sum of the law, `tau_envelope` and this. Zeros away from the limits
97    /// and with [`Backend::RobotController`].
98    pub tau_position: [f64; 7],
99    /// The IK's residual toward `pose` after this cycle's iterations, the norm of the position
100    /// error, m, and the orientation error weighted by
101    /// [`IkOptions::rotation_weight`](super::IkOptions::rotation_weight); 0 with
102    /// [`Backend::RobotController`].
103    pub ik_error: f64,
104    /// The IK's active-set passes this cycle, over its iterations; 0 with
105    /// [`Backend::RobotController`].
106    pub ik_passes: u32,
107    /// How hard the residual pushes on the goal's position pins, in the weighted task units of
108    /// `ik_error` (m) per cycle: the largest residual along a pinned joint's unit Jacobian
109    /// column, counted only into its bound; 0 with [`Backend::RobotController`].
110    pub stall_pressure: f64,
111    /// Whether the IK's goal is stalled at a joint position limit, the flag on
112    /// `stall_pressure` with its hysteresis; while it or a position pin after it holds, the
113    /// generator is restarted from the goal every cycle without its velocity into the limit.
114    /// Always false with [`Backend::RobotController`].
115    pub stalled: bool,
116    /// The largest raw step the cycle's IK solves asked for, rad: its norm with the nullspace
117    /// bias in and before the per-joint box clip. While `ik_blend` is above 0 each iteration
118    /// solves twice, and a position stage's norm is its own, before the blend scales it down.
119    /// 0 with [`Backend::RobotController`].
120    pub ik_step: f64,
121    /// How much of that same solve's `ik_step` the box took off, rad; 0 with
122    /// [`Backend::RobotController`].
123    pub ik_step_clipped: f64,
124    /// The IK's priority blend `β` this cycle: 0 the weighted solve, 1 position first (see
125    /// `stalled`); 0 with [`Backend::RobotController`].
126    pub ik_blend: f64,
127    /// Whether the generator was held at a wall this cycle: the IK stalled, or a wall still
128    /// within its life. Always false with [`Backend::RobotController`].
129    pub held: bool,
130    /// Per block (translation, rotation), cycles since that block was last pushed on a wall,
131    /// and -1 when it claims no wall; `[-1, -1]` with [`Backend::RobotController`].
132    ///
133    /// A block takes a wall only when the free joints' remainder in it exceeds the stall's wall
134    /// share of what the pins push, and keeps it for a while after. On this path, where
135    /// position first drives the translation residual down to micrometres, the translation age
136    /// normally stays -1 and only the rotation's varies.
137    pub wall_age: [i8; 2],
138    /// How far, m, the leash pulled the generator's anchor back from the previous desired
139    /// position toward the measured one: zero while the arm follows, positive while it is
140    /// held back ([`Leash`](super::Leash)); 0 with [`Backend::RobotController`].
141    ///
142    /// What the leash took off, not the lead itself: 0 whenever the pose is inside the leash --
143    /// the common case -- so a recording of nothing but zeros says only that the arm kept up.
144    /// The leash scales both blocks together, so an orientation beyond its own leash pulls the
145    /// position back with it and this reads positive inside the translation leash.
146    pub leash_alteration: f64,
147    /// The same for the orientation, rad, and zero on the same terms.
148    pub leash_angular_alteration: f64,
149}
150
151/// The observer's type: called every cycle on the realtime thread, so it must not allocate
152/// or block.
153pub type CartesianObserver = Box<dyn FnMut(&RobotState, &CartesianSent) + Send>;
154
155/// The handle of a running Cartesian target control; see the [module documentation](super).
156/// The target is a pose; orientations are unit quaternions in **`[x, y, z, w]` order** (the
157/// scalar part last) or the rotation block of a column-major pose as in `O_T_EE`.
158pub struct CartesianTargetControl {
159    inner: Handle<7>,
160    /// Why this session has no live tuning, `None` when it has: the reason is known at
161    /// [`start`] and nowhere else, since an unseeded slot cannot say which of the two it was.
162    no_tuning: Option<&'static str>,
163}
164
165impl CartesianTargetControl {
166    /// Sets the target position, absolute, in the base frame, m; the target orientation
167    /// stays what it is. Callable from any thread at any rate; only the latest target counts.
168    ///
169    /// # Errors
170    /// [`crate::error::FrankaError::InvalidArgument`] if a coordinate is not finite,
171    /// [`crate::error::FrankaError::InvalidOperation`] with [`super::ENDED_MESSAGE`] once the
172    /// loop has ended for any reason.
173    pub fn set_position(&self, position_in_base: [f64; 3]) -> FrankaResult<()> {
174        self.inner
175            .modify_target(|target| target[..3].copy_from_slice(&position_in_base))
176    }
177
178    /// Sets the target orientation, absolute, in the base frame, as a unit quaternion in
179    /// **`[x, y, z, w]` order**; the target position stays what it is.
180    ///
181    /// # Errors
182    /// [`crate::error::FrankaError::InvalidArgument`] if a component is not finite or the norm
183    /// is further than [`super::UNIT_QUATERNION_TOLERANCE`] from one (within it, the
184    /// quaternion is normalised), [`crate::error::FrankaError::InvalidOperation`] with
185    /// [`super::ENDED_MESSAGE`] once the loop has ended.
186    pub fn set_orientation(&self, orientation_xyzw: [f64; 4]) -> FrankaResult<()> {
187        let orientation = unit_quaternion(orientation_xyzw)?;
188        self.inner
189            .modify_target(|target| target[3..].copy_from_slice(&orientation))
190    }
191
192    /// Sets the target position, m, and orientation, a unit quaternion in **`[x, y, z, w]`
193    /// order** (the scalar part `w` last), both absolute in the base frame.
194    ///
195    /// # Errors
196    /// As [`set_position`](Self::set_position) and [`set_orientation`](Self::set_orientation).
197    pub fn set_target(
198        &self,
199        position_in_base: [f64; 3],
200        orientation_xyzw: [f64; 4],
201    ) -> FrankaResult<()> {
202        let orientation = unit_quaternion(orientation_xyzw)?;
203        self.inner.set_target(join(&position_in_base, &orientation))
204    }
205
206    /// Sets the target pose, a column-major 4x4 transform in the base frame as `O_T_EE`.
207    ///
208    /// # Errors
209    /// [`crate::error::FrankaError::InvalidArgument`] if an entry is not finite, the last row
210    /// is not `[0, 0, 0, 1]` or the rotation block is further than
211    /// [`super::ORTHONORMAL_TOLERANCE`] from orthonormal (within it, it is
212    /// re-orthonormalised), [`crate::error::FrankaError::InvalidOperation`] with
213    /// [`super::ENDED_MESSAGE`] once the loop has ended.
214    pub fn set_pose(&self, pose: &[f64; 16]) -> FrankaResult<()> {
215        let (position, rotation) = checked_pose(pose)?;
216        self.set_target(position, to_quaternion(&rotation))
217    }
218
219    /// The latest target position (the start until something sets it).
220    pub fn target(&self) -> [f64; 3] {
221        split(&self.inner.target()).0
222    }
223
224    /// The latest target orientation, `[x, y, z, w]`.
225    pub fn target_orientation(&self) -> [f64; 4] {
226        split(&self.inner.target()).1
227    }
228
229    /// The latest target pose, column-major as `O_T_EE`.
230    pub fn target_pose(&self) -> [f64; 16] {
231        let (position, orientation) = split(&self.inner.target());
232        pose_from(&from_quaternion(&orientation), &position)
233    }
234
235    /// The latest robot state the loop received, copied out.
236    pub fn state(&self) -> RobotState {
237        self.inner.state()
238    }
239
240    /// Moves the law's parameters while the loop runs: applies every field `update` carries and
241    /// returns the [`bounds`](FieldBound) that had to clamp one, so a caller can show a slider
242    /// snapping. Callable from any thread at any rate; the loop reads the result on its next
243    /// cycle and crosses to it under each field's [`policy`](FieldBound::policy).
244    ///
245    /// The update is all or nothing and then clamped into [`LiveTuning::BOUNDS`]; what it does
246    /// not carry, it does not touch. A change is never a step in the torque: the fields that
247    /// multiply a nonzero state cross over [`SLEW_TAU`](super::SLEW_TAU), and
248    /// [`TuningPolicy::remaining`](super::TuningPolicy::remaining) says how much of a crossing
249    /// is still to come.
250    ///
251    /// Nor is it ever a step in the command. [`budget`](LiveTuning::budget) and
252    /// [`rotation_budget`](LiveTuning::rotation_budget) reach the generator, whose velocity and
253    /// acceleration are raised on the cycle the update arrives and lowered as a ramp at the next
254    /// order's limit, because a narrower budget clamps the generator's stored state rather than
255    /// re-planning it ([`TuningPolicy::StepUpGateDown`](super::TuningPolicy::StepUpGateDown)).
256    /// So a lowered budget takes `(current - target) / rate` seconds to be wholly in force; the
257    /// jerks step, either way.
258    ///
259    /// The command never steps, but lowering the acceleration or jerk while the arm moves
260    /// lengthens the stop (toward `v² / 2a`, plus the jerk's ramp), so a near goal is overshot and
261    /// returned to. For example, at the default 0.3 m/s (0.17 m/s per axis), an acceleration
262    /// dragged from 0.5 to 0.1 m/s² with the goal 3 cm ahead overshoots it by about 20 cm; faster
263    /// motion or a lower acceleration overshoots further. Lower the velocity first, apply it, then
264    /// lower the acceleration or jerk.
265    ///
266    /// # Errors
267    /// [`crate::error::FrankaError::InvalidArgument`] naming the field if a value is not finite
268    /// or is a zero that means something other than "softer", with nothing written;
269    /// [`crate::error::FrankaError::InvalidOperation`] with [`super::ENDED_MESSAGE`] once the
270    /// loop has ended, with [`super::NO_TUNING_MESSAGE`] on a [`Backend::RobotController`]
271    /// session, whose tracking is the robot's own, or with [`super::DERIVED_GAINS_MESSAGE`] on
272    /// one whose gains a single Cartesian stiffness cannot rebuild.
273    pub fn tune(&self, update: &TuningUpdate) -> FrankaResult<Vec<&'static FieldBound>> {
274        self.tunable()?;
275        self.inner.tune(update)
276    }
277
278    /// The tuning targets in force, the session's own options until something tunes them.
279    ///
280    /// # Errors
281    /// [`crate::error::FrankaError::InvalidOperation`] with [`super::NO_TUNING_MESSAGE`] or
282    /// [`super::DERIVED_GAINS_MESSAGE`] on a session that has no live tuning, as
283    /// [`tune`](Self::tune) describes.
284    pub fn tuning(&self) -> FrankaResult<LiveTuning> {
285        self.tunable()?;
286        self.inner.tuning()
287    }
288
289    /// Whether this session has live tuning at all, with why not if it has not.
290    fn tunable(&self) -> FrankaResult<()> {
291        match self.no_tuning {
292            Some(why) => Err(FrankaError::InvalidOperation(why.to_string())),
293            None => Ok(()),
294        }
295    }
296
297    /// Whether the loop is still running; `false` after it ended for any reason.
298    pub fn is_running(&self) -> bool {
299        self.inner.is_running()
300    }
301
302    /// Settles at the current target, finishes the motion, joins the thread and returns the
303    /// loop's result: `Ok` for a regular end, [`crate::error::FrankaError::Control`] if the
304    /// robot aborted the motion or a deviation guard fired.
305    pub fn stop(self) -> FrankaResult<()> {
306        self.inner.stop()
307    }
308}
309
310#[cfg(test)]
311impl CartesianTargetControl {
312    /// The handle `start` builds on `shared` for a session whose seed came back as `seed`.
313    pub(super) fn on(shared: Arc<Shared<7>>, seed: Result<LiveTuning, &'static str>) -> Self {
314        CartesianTargetControl {
315            inner: Handle {
316                shared,
317                thread: None,
318            },
319            no_tuning: seed.err(),
320        }
321    }
322}
323
324/// A position and a rotation: an echo, a measured pose or a desired one.
325pub(super) type Placement = ([f64; 3], Matrix3<f64>);
326
327/// The Cartesian [`Chart`](super::runner::Chart): the position, re-anchored on the echo's,
328/// and the base-frame rotation vector `log(R_target * R_echo^T)` from zero; the runner's
329/// rotational step is composed back as `exp(step) * R_echo`.
330pub(super) fn chart(target: &[f64; 7], commanded: &[f64; 7]) -> ([f64; 6], [f64; 6]) {
331    let ([tx, ty, tz], target_orientation) = split(target);
332    let ([x, y, z], orientation) = split(commanded);
333    let r_target = from_quaternion(&target_orientation);
334    let r_commanded = from_quaternion(&orientation);
335    let [a, b, c] = log(&(r_target * r_commanded.transpose()));
336    ([x, y, z, 0.0, 0.0, 0.0], [tx, ty, tz, a, b, c])
337}
338
339/// The translation and the orthonormalised rotation of a pose (float32 on FCI v10:
340/// orthonormal to 1e-7 only, and composed into the next command).
341pub(super) fn placement(pose: &[f64; 16]) -> Placement {
342    (
343        translation_of(pose),
344        orthonormalized_rotation(&rotation_of(pose)),
345    )
346}
347
348/// The column-major pose of a placement.
349pub(super) fn pose_of(placement: &Placement) -> [f64; 16] {
350    pose_from(&placement.1, &placement.0)
351}
352
353/// The pose a step lands on: its position, and its rotational step composed onto `rotation`,
354/// the one the runner was anchored on.
355pub(super) fn compose(step: &Step<6, 7>, rotation: &Matrix3<f64>) -> Placement {
356    let [x, y, z, a, b, c] = step.position;
357    ([x, y, z], exp(&[a, b, c]) * rotation)
358}
359
360/// Whether the measured pose is further from `start` than either guard allows.
361pub(super) fn strayed(
362    state: &RobotState,
363    start: &Placement,
364    max_deviation: f64,
365    max_angular: f64,
366) -> bool {
367    distance(&translation_of(&state.O_T_EE), &start.0) > max_deviation
368        || angle_between(&start.1, &rotation_of(&state.O_T_EE)) > max_angular
369}
370
371/// The observer's record of a step with `pose` as sent; the backend's own fields stay zero.
372pub(super) fn sent(step: &Step<6, 7>, pose: [f64; 16]) -> CartesianSent {
373    let [vx, vy, vz, wx, wy, wz] = step.velocity;
374    let [ax, ay, az, bx, by, bz] = step.acceleration;
375    let (target, target_orientation) = split(&step.target);
376    CartesianSent {
377        pose,
378        orientation: to_quaternion(&rotation_of(&pose)),
379        target,
380        target_orientation,
381        velocity: [vx, vy, vz],
382        acceleration: [ax, ay, az],
383        angular_velocity: [wx, wy, wz],
384        angular_acceleration: [bx, by, bz],
385        backstop_alteration: 0.0,
386        backstop_angular_alteration: 0.0,
387        q_goal: [0.0; 7],
388        dq_goal: [0.0; 7],
389        cap_scale: 1.0,
390        pinned: [0; 7],
391        tau: [0.0; 7],
392        tau_envelope: [0.0; 7],
393        tau_position: [0.0; 7],
394        ik_error: 0.0,
395        ik_passes: 0,
396        stall_pressure: 0.0,
397        stalled: false,
398        ik_step: 0.0,
399        ik_step_clipped: 0.0,
400        ik_blend: 0.0,
401        held: false,
402        wall_age: [-1, -1],
403        leash_alteration: 0.0,
404        leash_angular_alteration: 0.0,
405    }
406}
407
408/// The translation and the base-frame rotation vector from the echo to `pose`.
409fn increment(pose: &[f64; 16], echo: &Placement) -> [f64; 6] {
410    let [x, y, z] = translation_of(pose);
411    let [a, b, c] = log(&(rotation_of(pose) * echo.1.transpose()));
412    [x - echo.0[0], y - echo.0[1], z - echo.0[2], a, b, c]
413}
414
415/// The seven slot values of a pose: its translation and the quaternion of its rotation.
416pub(super) fn slot_values(translation: &[f64; 3], rotation: &Matrix3<f64>) -> [f64; 7] {
417    join(translation, &to_quaternion(rotation))
418}
419
420fn join(position: &[f64; 3], orientation: &[f64; 4]) -> [f64; 7] {
421    let mut slot = [0.0; 7];
422    slot[..3].copy_from_slice(position);
423    slot[3..].copy_from_slice(orientation);
424    slot
425}
426
427fn split(slot: &[f64; 7]) -> ([f64; 3], [f64; 4]) {
428    let [x, y, z, a, b, c, d] = *slot;
429    ([x, y, z], [a, b, c, d])
430}
431
432/// The per-axis limits of the six generator axes; the rotational ones carry the backstop's
433/// 0.99 factor, or it binds on every synchronised jerk and the two orbit instead of landing.
434pub(super) fn axis_limits(limits: OtgLimits, rotation_limits: OtgLimits) -> [OtgLimits; 6] {
435    let translational = limits.per_axis_for_norm(3);
436    let rotational = rotation_limits
437        .scaled(FACTOR_CARTESIAN_ROTATION_POSE_INTERFACE)
438        .per_axis_for_norm(3);
439    [
440        translational,
441        translational,
442        translational,
443        rotational,
444        rotational,
445        rotational,
446    ]
447}
448
449const THREAD: &str = "franka-cartesian-target";
450
451pub(super) fn start(
452    robot: &Arc<Robot>,
453    options: TargetControlOptions,
454) -> FrankaResult<CartesianTargetControl> {
455    options.validate()?;
456    check_posture(
457        &options.backend,
458        &joint_position_limits(robot.fci_version()),
459    )?;
460    let shared = Arc::new(Shared::<7>::default());
461    let loop_shared = Arc::clone(&shared);
462    let scheduling = options.scheduling();
463    let tuning = match &options.backend {
464        Backend::Impedance(impedance) => seed(&options, impedance),
465        Backend::RobotController => Err(NO_TUNING_MESSAGE),
466    };
467    let inner = match options.backend {
468        Backend::RobotController => spawn(
469            THREAD,
470            robot,
471            shared,
472            scheduling,
473            move |robot: &Robot, started| pose_loop(robot, started, options, loop_shared),
474        )?,
475        Backend::Impedance(impedance) => {
476            let model = Arc::new(robot.load_model()?);
477            // Before the thread, so the loop's first read of the slot is the seed the loop was
478            // built with and no cycle runs with anything else. The user thread is the slot's
479            // only writer for the session's whole life; the loop never writes it. A seed that
480            // is not faithful is not published at all, and the session then has no live tuning.
481            if let Ok(seed) = tuning {
482                shared.tuning.publish(seed.to_words());
483            }
484            spawn(
485                THREAD,
486                robot,
487                shared,
488                scheduling,
489                move |robot: &Robot, started| {
490                    let limit_rate = options.limit_rate;
491                    let version = robot.fci_version();
492                    torque_loop(options, impedance, model, version, loop_shared, started)?
493                        .run(robot, limit_rate)
494                },
495            )?
496        }
497    };
498    Ok(CartesianTargetControl {
499        inner,
500        no_tuning: tuning.err(),
501    })
502}
503
504/// What the session's live tuning starts at -- the law as `impedance` has it and the plan's two
505/// budgets -- or why it has none. The loop and the slot are seeded from this one place, so the
506/// loop's first read of the slot finds exactly what it already holds and nothing is applied
507/// until something is tuned.
508///
509/// A seed is refused when [`LiveTuning::gains`] does not rebuild `impedance`'s own gains. The
510/// operator's Cartesian stiffness is one number and the law's are twelve, derived from it by
511/// [`ImpedanceGains::scaled_cartesian`](super::ImpedanceGains::scaled_cartesian), and the apply
512/// step rebuilds all twelve whenever *any* field moves. On a gains set that derivation cannot
513/// reproduce -- [`DROID`](super::ImpedanceGains::DROID), whose translational damping is 37
514/// against the scaled preset's 50 and 90 -- the first retune of an unrelated field would rewrite
515/// them in a single cycle, and the slew could not soften it, because the stiffness it watches
516/// never moved. A session `LiveTuning` cannot represent is left with no live tuning rather than
517/// quietly retuned into one it can.
518pub(super) fn seed(
519    options: &TargetControlOptions,
520    impedance: &ImpedanceOptions,
521) -> Result<LiveTuning, &'static str> {
522    let seed = LiveTuning::from_options(impedance, options.limits, options.rotation_limits);
523    (seed.gains() == impedance.gains)
524        .then_some(seed)
525        .ok_or(DERIVED_GAINS_MESSAGE)
526}
527
528/// The [`Backend::Impedance`] loop of this interface: a [`PoseTracker`] on the six-axis
529/// runner, over the negotiated version's joint limits.
530pub(super) fn torque_loop(
531    options: TargetControlOptions,
532    impedance: ImpedanceOptions,
533    model: Arc<Model>,
534    version: FciVersion,
535    shared: Arc<Shared<7>>,
536    started: SyncSender<()>,
537) -> FrankaResult<TorqueLoop<6, 7, PoseTracker>> {
538    let runner = Runner::new(
539        shared,
540        started,
541        axis_limits(options.limits, options.rotation_limits),
542        options.settle,
543        chart,
544    )?;
545    let velocity = VelocityLimit::of(version);
546    let tracker = PoseTracker::new(
547        &options,
548        &impedance,
549        Arc::clone(&model),
550        joint_position_limits(version),
551        velocity,
552    );
553    // The loop reads the slot only where `start` seeded it, and the two agree because both ask
554    // the same question of the same options.
555    let tuning = seed(&options, &impedance).ok();
556    Ok(TorqueLoop::new(
557        runner,
558        model,
559        impedance,
560        tracker,
561        options.observer,
562        tuning,
563    ))
564}
565
566/// [`Backend::RobotController`]: the pose stream, re-anchored on the robot's echo every cycle
567/// with the rate limiter as the backstop; a stop holds the echo bit for bit.
568fn pose_loop(
569    robot: &Robot,
570    started: SyncSender<()>,
571    options: TargetControlOptions,
572    shared: Arc<Shared<7>>,
573) -> FrankaResult<()> {
574    let TargetControlOptions {
575        limits,
576        rotation_limits,
577        controller_mode,
578        max_deviation,
579        max_angular_deviation,
580        settle,
581        limit_rate,
582        mut observer,
583        ..
584    } = options;
585    let mut runner = Runner::new(
586        shared,
587        started,
588        axis_limits(limits, rotation_limits),
589        settle,
590        chart,
591    )?;
592    let mut start: Option<Placement> = None;
593    let mut held: Option<[f64; 16]> = None;
594    // The backstop's reference: the twist and acceleration of what it sent, not the echoed
595    // ones, whose float32 rounding is worth 200 rad/s^3 of jerk on FCI v10.
596    let (mut last_twist, mut last_acceleration) = ([0.0; 6], [0.0; 6]);
597    let result = robot.control_cartesian_pose(
598        |state: &RobotState, _period| {
599            let echo = placement(&state.O_T_EE_c);
600            let start = *start.get_or_insert(echo);
601            let strayed = strayed(state, &start, max_deviation, max_angular_deviation);
602            let step = runner.cycle(state, slot_values(&echo.0, &echo.1), strayed, false);
603
604            let (mut backstop_alteration, mut backstop_angular_alteration) = (0.0, 0.0);
605            let pose = if step.hold {
606                // The robot's echo of the last command, bit for bit, for the whole hold.
607                *held.get_or_insert(state.O_T_EE_c)
608            } else {
609                let (position, rotation) = compose(&step, &echo.1);
610                let mut pose = pose_from(&rotation, &position);
611                if limit_rate {
612                    // It only fails on non-finite input; the echo is then the safe command.
613                    #[rustfmt::skip]
614                    let limited = limit_rate_cartesian_pose(
615                        limits.max_velocity, limits.max_acceleration, limits.max_jerk,
616                        rotation_limits.max_velocity, rotation_limits.max_acceleration,
617                        rotation_limits.max_jerk,
618                        &pose, &state.O_T_EE_c, &last_twist, &last_acceleration,
619                    ).unwrap_or(state.O_T_EE_c);
620                    backstop_alteration = distance(&translation_of(&limited), &position);
621                    backstop_angular_alteration = angle_between(&rotation, &rotation_of(&limited));
622                    pose = limited;
623                }
624                pose
625            };
626            let twist = increment(&pose, &echo).map(|d| d / DELTA_T);
627            last_acceleration = std::array::from_fn(|i| (twist[i] - last_twist[i]) / DELTA_T);
628            last_twist = twist;
629            if let Some(observe) = observer.as_mut() {
630                observe(
631                    state,
632                    &CartesianSent {
633                        backstop_alteration,
634                        backstop_angular_alteration,
635                        ..sent(&step, pose)
636                    },
637                );
638            }
639            let mut output = CartesianPose::new(pose);
640            output.motion_finished = step.finished;
641            output
642        },
643        controller_mode,
644        limit_rate,
645        MAX_CUTOFF_FREQUENCY,
646    );
647    runner.finish(result)
648}