Skip to main content

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}