franka/robot/target_control/runner.rs
1//! The realtime side of [target control](super), generic over the interface: anchoring, the
2//! slot, the generator under the three rules, the deviation guard, and the stop path --
3//! land, hold the echo of the last command for the settle window, finish on one more of it.
4
5use std::sync::atomic::Ordering;
6use std::sync::mpsc::SyncSender;
7use std::sync::Arc;
8
9use super::{LiveTuning, Settle, Shared, TargetSlot, DEVIATION_MESSAGE, STOP_TIMEOUT_CYCLES};
10use crate::error::{ControlException, FrankaError, FrankaResult};
11use crate::otg::{MultiOtg, OtgLimits};
12use crate::rate_limiting::DELTA_T;
13use crate::robot_state::RobotState;
14
15/// A generator within [`Settle::tolerance`] of its target moving slower than this, per axis,
16/// counts as landed (m/s, rad/s). The hold then freezes a velocity step of at most this in
17/// one cycle, a jerk of 100 per second cubed, which the joint side of a Cartesian command
18/// amplifies threefold (1 mm/s freezes as about 3840 rad/s^3 on joint 2 in the simulator,
19/// over its 3750). Not smaller: re-anchoring on a float32 echo (FCI v10) keeps a landed
20/// generator in micro-profiles of a few 1e-8 that peak at about 2e-5 per second.
21pub const REST_VELOCITY: f64 = 1e-4;
22/// ... and accelerating less than this (m/s^2, rad/s^2); the micro-profiles above peak at
23/// about 0.01.
24pub const REST_ACCELERATION: f64 = 0.05;
25/// An *arm* whose every joint moves slower than this, rad/s, is at rest: what a stop in torque
26/// mode waits for before `motion_finished`, the hold having settled only the generator.
27pub const REST_JOINT_VELOCITY: f64 = 0.01;
28
29/// Maps the slot's target and the robot's echo (`S` values each) to where the generator is
30/// re-anchored and where it is to go (`N` values each) for one cycle.
31pub(super) type Chart<const N: usize, const S: usize> =
32 fn(target: &[f64; S], commanded: &[f64; S]) -> ([f64; N], [f64; N]);
33
34/// What one cycle produced.
35pub(super) struct Step<const N: usize, const S: usize> {
36 pub position: [f64; N],
37 pub velocity: [f64; N],
38 pub acceleration: [f64; N],
39 /// The slot's target the generator was planning towards.
40 pub target: [f64; S],
41 /// The command is being held bit-identical; the backstop must not touch it.
42 pub hold: bool,
43 pub finished: bool,
44}
45
46/// The stop's final phase: the same position, sent `sent` times so far.
47struct Hold<const N: usize> {
48 position: [f64; N],
49 sent: u32,
50}
51
52/// See the [module documentation](self).
53pub(super) struct Runner<const N: usize, const S: usize> {
54 shared: Arc<Shared<S>>,
55 started: Option<SyncSender<()>>,
56 otg: MultiOtg<N>,
57 settle: Settle,
58 chart: Chart<N, S>,
59 target: [f64; S],
60 anchored: bool,
61 deviated: bool,
62 stop_cycles: u32,
63 hold: Option<Hold<N>>,
64}
65
66/// `acceleration` per axis where it brakes a generator restarted at `velocity` (against it, or
67/// `velocity` zero), zero where it would drive it faster: a goal held at the cap sent no
68/// acceleration toward more speed, so a stop never plans from one, and a braking one is kept, so
69/// a generator that meets the cap cycle after cycle still slows at its jerk, not by one cycle's
70/// jerk each time.
71pub(super) fn braking<const N: usize>(velocity: &[f64; N], acceleration: &[f64; N]) -> [f64; N] {
72 std::array::from_fn(|i| {
73 if acceleration[i] * velocity[i] > 0.0 {
74 0.0
75 } else {
76 acceleration[i]
77 }
78 })
79}
80
81/// `acceleration` per axis only where it brakes a generator restarted at `velocity`, strictly: at
82/// a position limit a generator restarted at rest keeps no acceleration that would wind it up
83/// against the limit cycle after cycle.
84pub(super) fn strictly_braking<const N: usize>(
85 velocity: &[f64; N],
86 acceleration: &[f64; N],
87) -> [f64; N] {
88 std::array::from_fn(|i| {
89 if acceleration[i] * velocity[i] < 0.0 {
90 acceleration[i]
91 } else {
92 0.0
93 }
94 })
95}
96
97/// The chart of an interface whose slot and generator share their coordinates.
98pub(super) fn identity<const N: usize>(
99 target: &[f64; N],
100 commanded: &[f64; N],
101) -> ([f64; N], [f64; N]) {
102 (*commanded, *target)
103}
104
105impl<const N: usize, const S: usize> Runner<N, S> {
106 pub(super) fn new(
107 shared: Arc<Shared<S>>,
108 started: SyncSender<()>,
109 limits: [OtgLimits; N],
110 settle: Settle,
111 chart: Chart<N, S>,
112 ) -> FrankaResult<Self> {
113 Ok(Runner {
114 shared,
115 started: Some(started),
116 // Synchronised axes: a diagonal target moves along a straight line, and a pose
117 // target's translation and rotation arrive together.
118 otg: MultiOtg::with_limits([0.0; N], limits, true)?,
119 settle,
120 chart,
121 target: [0.0; S],
122 anchored: false,
123 deviated: false,
124 stop_cycles: 0,
125 hold: None,
126 })
127 }
128
129 /// One cycle: `commanded` is the robot's echo of the last command, `strayed` whether
130 /// the deviation guard's threshold is crossed (`false` until anchored), and `blocked`
131 /// whether the goal is held at a joint limit as near the target as it gets: a stop counts
132 /// that as landed while the target is the same, since the rest is out of reach.
133 pub(super) fn cycle(
134 &mut self,
135 state: &RobotState,
136 commanded: [f64; S],
137 strayed: bool,
138 blocked: bool,
139 ) -> Step<N, S> {
140 if let Ok(mut latest) = self.shared.state.try_lock() {
141 *latest = *state;
142 }
143 if let Some(hold) = &mut self.hold {
144 // Neither re-anchored nor stepped: the very same command again, and
145 // `motion_finished` on the one after the settle window.
146 hold.sent += 1;
147 return Step {
148 position: hold.position,
149 velocity: [0.0; N],
150 acceleration: [0.0; N],
151 target: self.target,
152 hold: true,
153 finished: hold.sent > self.settle.cycles,
154 };
155 }
156 // Read before the slot: a target published before `stop()` is then never missed.
157 let stopping = self.deviated || self.shared.stop.load(Ordering::SeqCst);
158 let previous = self.target;
159 if !self.anchored {
160 self.anchored = true;
161 self.target = commanded;
162 self.otg.reset((self.chart)(&commanded, &commanded).0);
163 self.shared.slot.publish(commanded);
164 if let Some(started) = self.started.take() {
165 let _ = started.try_send(());
166 }
167 } else if !self.deviated && strayed {
168 self.deviated = true;
169 self.target = commanded;
170 } else if !self.deviated {
171 // A torn read keeps the previous target for this one cycle; a stop, which no
172 // publish can follow, waits for the final one.
173 while !self.shared.slot.load(&mut self.target) && stopping {}
174 }
175 // The echo and the target are finite, so neither setter fails.
176 let (anchor, goal) = (self.chart)(&self.target, &commanded);
177 let _ = self.otg.set_position(anchor);
178 let _ = self.otg.set_target(goal);
179 let position = self.otg.step(DELTA_T);
180 let axes = self.otg.axes();
181 let velocity = std::array::from_fn(|i| axes[i].velocity());
182 let acceleration = std::array::from_fn(|i| axes[i].acceleration());
183
184 if stopping || self.deviated {
185 self.stop_cycles = self.stop_cycles.saturating_add(1);
186 // `blocked` is the last cycle's, against the last target.
187 let landed = (blocked && self.target == previous)
188 || axes.iter().all(|a| {
189 (a.position() - a.target()).abs() < self.settle.tolerance
190 && a.velocity().abs() < REST_VELOCITY
191 && a.acceleration().abs() < REST_ACCELERATION
192 });
193 // Landed, or out of patience: hold the robot's echo of the last command, which
194 // is continuous with what the robot already has by construction (the generator's
195 // own position may differ from it by whatever the backstop took off the last
196 // command), and this is the first of the identical ones.
197 if landed || self.stop_cycles >= STOP_TIMEOUT_CYCLES {
198 let held = (self.chart)(&commanded, &commanded).0;
199 self.hold = Some(Hold {
200 position: held,
201 sent: 1,
202 });
203 return Step {
204 position: held,
205 velocity: [0.0; N],
206 acceleration: [0.0; N],
207 target: self.target,
208 hold: true,
209 finished: false,
210 };
211 }
212 }
213 Step {
214 position,
215 velocity,
216 acceleration,
217 target: self.target,
218 hold: false,
219 finished: false,
220 }
221 }
222
223 /// The generator's axes, for a test that watches what a retuned budget does to them.
224 #[cfg(test)]
225 pub(super) fn axes(&self) -> &[crate::otg::Otg; N] {
226 self.otg.axes()
227 }
228
229 /// The session's live-tuning slot, for the loop that owns this runner. Read-only from here:
230 /// the runner publishes the anchoring cycle's target into `slot`, never into this one.
231 pub(super) fn tuning(&self) -> &TargetSlot<{ LiveTuning::WORDS }> {
232 &self.shared.tuning
233 }
234
235 /// Hands the generator new per-axis limits, for a live retune of the budget. Every other
236 /// part of its state is kept, and the next [`cycle`](Self::cycle) plans under them.
237 ///
238 /// [`Otg::set_state`](crate::otg::Otg::set_state), which `cycle`'s `set_position` runs, and
239 /// the end of every step both clamp the stored velocity and acceleration into whatever
240 /// limits are in force. What holds that clamp to one cycle of the next order rather than an
241 /// impulse is that the caller walks a lowered limit down at that order's rate
242 /// ([`TuningPolicy::StepUpGateDown`](super::TuningPolicy::StepUpGateDown)) -- the rate, not
243 /// the moment this is called: a limit written a cycle later is clamped against a velocity
244 /// that is a cycle older, and the bound is the same one cycle on.
245 ///
246 /// A limit that is not finite and positive leaves the generator as it was, and none is:
247 /// the session's own budget was validated before the loop was built, and a tuned one is
248 /// between that and a target [`LiveTuning::BOUNDS`] holds positive.
249 pub(super) fn set_limits(&mut self, limits: [OtgLimits; N]) {
250 let _ = self.otg.set_limits(limits);
251 }
252
253 /// Restarts the generator at `velocity` and `acceleration`, keeping its position: the torque
254 /// backend re-anchors it on what a cut goal actually carried, as the third OTG rule
255 /// re-anchors the position. Both are end-of-cycle states, as
256 /// [`Otg::set_state`](crate::otg::Otg::set_state) requires: the generator's own, cut, never
257 /// the cut step's mean. A non-finite state leaves the generator as it was.
258 pub(super) fn restart(&mut self, velocity: [f64; N], acceleration: [f64; N]) {
259 let _ = self
260 .otg
261 .set_state(self.otg.position(), velocity, acceleration);
262 }
263
264 /// The loop's result: a regular end after the deviation guard fired is the error it is.
265 pub(super) fn finish(&self, result: FrankaResult<()>) -> FrankaResult<()> {
266 match result {
267 Ok(()) if self.deviated => Err(FrankaError::Control(ControlException::new(
268 DEVIATION_MESSAGE,
269 ))),
270 other => other,
271 }
272 }
273}