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}