Skip to main content

franka/robot/target_control/
joint.rs

1//! The joint interface of [target control](super): seven joint position targets.
2//! [`Backend::Impedance`] sends the impedance law's torques towards the generator's output;
3//! [`Backend::RobotController`] streams it through [`Robot::control_joint_positions`].
4
5use std::sync::mpsc::SyncSender;
6use std::sync::Arc;
7
8use super::position::{JointLimits, VelocityLimit};
9use super::runner::{identity, Step};
10use super::torque::{JointTracker, TorqueLoop};
11use super::{
12    check_joint_limits, check_posture, joint_limit_inset, joint_position_limits, spawn, Backend,
13    Handle, ImpedanceOptions, JointTargetControlOptions, Runner, Shared, DEFAULT_LIMIT_FRACTION,
14};
15use crate::control_types::JointPositions;
16use crate::error::FrankaResult;
17use crate::lowpass_filter::MAX_CUTOFF_FREQUENCY;
18use crate::model::Model;
19use crate::otg::OtgLimits;
20use crate::rate_limiting::limit_rate_joint_positions;
21use crate::robot::Robot;
22use crate::robot_state::RobotState;
23use crate::wire::robot::codec::FciVersion;
24
25/// What one cycle sent, for the observer.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct JointSent {
28    /// The joint positions that went to the robot (after the backstop), or with
29    /// [`Backend::Impedance`] the generator's output the law tracks, rad.
30    pub q: [f64; 7],
31    /// The target the generator was planning towards, rad.
32    pub target: [f64; 7],
33    /// The generator's velocity at the end of the cycle, rad/s.
34    pub velocity: [f64; 7],
35    /// The generator's acceleration at the end of the cycle, rad/s^2.
36    pub acceleration: [f64; 7],
37    /// The most the backstop moved any joint of the generator's position, rad; zero without
38    /// `limit_rate` and with [`Backend::Impedance`]. Microradians are the noise of the
39    /// limiter's velocity and acceleration reference, the robot's float32 echo (FCI v10);
40    /// milliradians mean the generator is outrunning the limits.
41    pub backstop_alteration: f64,
42    /// The joint target of the impedance law, rad; zeros with [`Backend::RobotController`].
43    pub q_goal: [f64; 7],
44    /// The joint goal's velocity, rad/s: the finite difference of `q_goal` the law feeds
45    /// forward, under the joint velocity cap; zeros while holding and with
46    /// [`Backend::RobotController`].
47    pub dq_goal: [f64; 7],
48    /// The scale this cycle's goal step was cut by to stay in the joint limits' box: under
49    /// [`ImpedanceOptions::joint_velocity_fraction`](super::ImpedanceOptions::joint_velocity_fraction)
50    /// of the joint velocity limits, and braking to stop at
51    /// [`ImpedanceOptions::joint_position_margin`](super::ImpedanceOptions::joint_position_margin).
52    /// Exactly 1 when it was not, and with [`Backend::RobotController`].
53    pub cap_scale: f64,
54    /// Per joint, the bound that cut the step: 0 none, -1 / +1 the lower / upper position bound,
55    /// -2 / +2 the velocity bound; zeros with [`Backend::RobotController`].
56    pub pinned: [i8; 7],
57    /// The torques sent, Nm, clamped to the torque limits; zeros with
58    /// [`Backend::RobotController`].
59    pub tau: [f64; 7],
60    /// The velocity envelope's share of `tau`, Nm, before the clamp: the barrier opposing every
61    /// joint measured faster than
62    /// [`ImpedanceOptions::velocity_barrier_fraction`](super::ImpedanceOptions::velocity_barrier_fraction)
63    /// of its limit, less the law's torque along a joint's motion faded out above the fade's
64    /// start ([`FADE_BAND`](super::FADE_BAND)); zeros below it and with
65    /// [`Backend::RobotController`].
66    pub tau_envelope: [f64; 7],
67    /// The position envelope's share of `tau`, Nm, before the clamp; see
68    /// [`CartesianSent::tau_position`](super::CartesianSent::tau_position). Zeros with
69    /// [`Backend::RobotController`].
70    pub tau_position: [f64; 7],
71    /// The most, rad, the leash pulled any joint of the generator's anchor back from the
72    /// previous goal toward the measured position: zero while the arm follows, positive while
73    /// it is held back ([`Leash`](super::Leash)); 0 with [`Backend::RobotController`].
74    pub leash_alteration: f64,
75}
76
77/// The observer's type: called every cycle on the realtime thread, so it must not allocate
78/// or block.
79pub type JointObserver = Box<dyn FnMut(&RobotState, &JointSent) + Send>;
80
81/// The handle of a running joint target control; see the [module documentation](super).
82///
83/// It has no live tuning, and no `tune`: [`LiveTuning`](super::LiveTuning) describes the
84/// Cartesian interface's law and plan -- a Cartesian stiffness this interface has none of, and a
85/// budget as a norm, where this one plans per joint. A joint session therefore runs the options
86/// it was started with for its whole life.
87pub struct JointTargetControl {
88    inner: Handle<7>,
89    limits: ([f64; 7], [f64; 7]),
90    /// How far inside `limits` a target must lie.
91    inset: f64,
92}
93
94impl JointTargetControl {
95    /// Sets the target joint positions, rad. Callable from any thread at any rate; only the
96    /// latest target counts.
97    ///
98    /// # Errors
99    /// [`crate::error::FrankaError::InvalidArgument`] if a value is not finite or outside the
100    /// arm's joint position limits inset by [`JOINT_LIMIT_INSET`](super::JOINT_LIMIT_INSET) (with
101    /// [`Backend::Impedance`], by its
102    /// [`joint_position_margin`](ImpedanceOptions::joint_position_margin) where that is larger),
103    /// [`crate::error::FrankaError::InvalidOperation`] with [`super::ENDED_MESSAGE`] once the
104    /// loop has ended for any reason.
105    pub fn set_joints(&self, q: [f64; 7]) -> FrankaResult<()> {
106        if q.iter().all(|v| v.is_finite()) {
107            check_joint_limits(&q, &self.limits, self.inset, "target")?;
108        }
109        self.inner.set_target(q)
110    }
111
112    /// The latest target set (the start configuration until the first `set_joints`).
113    pub fn target(&self) -> [f64; 7] {
114        self.inner.target()
115    }
116
117    /// The latest robot state the loop received, copied out.
118    pub fn state(&self) -> RobotState {
119        self.inner.state()
120    }
121
122    /// Whether the loop is still running; `false` after it ended for any reason.
123    pub fn is_running(&self) -> bool {
124        self.inner.is_running()
125    }
126
127    /// Settles at the current target, finishes the motion, joins the thread and returns the
128    /// loop's result: `Ok` for a regular end, [`crate::error::FrankaError::Control`] if the
129    /// robot aborted the motion or the deviation guard fired.
130    pub fn stop(self) -> FrankaResult<()> {
131        self.inner.stop()
132    }
133}
134
135pub(super) fn max_abs_difference(a: &[f64; 7], b: &[f64; 7]) -> f64 {
136    a.iter()
137        .zip(b)
138        .map(|(x, y)| (x - y).abs())
139        .fold(0.0, f64::max)
140}
141
142/// The observer's record of a step with `q` as sent; the backend's own fields stay zero.
143pub(super) fn sent(step: &Step<7, 7>, q: [f64; 7]) -> JointSent {
144    JointSent {
145        q,
146        target: step.target,
147        velocity: step.velocity,
148        acceleration: step.acceleration,
149        backstop_alteration: 0.0,
150        q_goal: [0.0; 7],
151        dq_goal: [0.0; 7],
152        cap_scale: 1.0,
153        pinned: [0; 7],
154        tau: [0.0; 7],
155        tau_envelope: [0.0; 7],
156        tau_position: [0.0; 7],
157        leash_alteration: 0.0,
158    }
159}
160
161const THREAD: &str = "franka-joint-target";
162
163pub(super) fn start(
164    robot: &Arc<Robot>,
165    options: JointTargetControlOptions,
166) -> FrankaResult<JointTargetControl> {
167    options.validate()?;
168    let joint_limits = joint_position_limits(robot.fci_version());
169    check_posture(&options.backend, &joint_limits)?;
170    let inset = joint_limit_inset(&options.backend);
171    let limits = options.limits.unwrap_or_else(|| {
172        JointTargetControlOptions::scaled_limits(robot.fci_version(), DEFAULT_LIMIT_FRACTION)
173    });
174    let shared = Arc::new(Shared::<7>::default());
175    let loop_shared = Arc::clone(&shared);
176    let scheduling = options.scheduling();
177    let inner = match options.backend {
178        Backend::RobotController => spawn(
179            THREAD,
180            robot,
181            shared,
182            scheduling,
183            move |robot: &Robot, started| {
184                position_loop(robot, started, options, limits, loop_shared)
185            },
186        )?,
187        Backend::Impedance(impedance) => {
188            let model = Arc::new(robot.load_model()?);
189            spawn(
190                THREAD,
191                robot,
192                shared,
193                scheduling,
194                move |robot: &Robot, started| {
195                    let limit_rate = options.limit_rate;
196                    let version = robot.fci_version();
197                    torque_loop(
198                        options,
199                        limits,
200                        impedance,
201                        model,
202                        version,
203                        loop_shared,
204                        started,
205                    )?
206                    .run(robot, limit_rate)
207                },
208            )?
209        }
210    };
211    Ok(JointTargetControl {
212        inner,
213        limits: joint_limits,
214        inset,
215    })
216}
217
218/// The [`Backend::Impedance`] loop of this interface: a [`JointTracker`] on the seven-joint
219/// runner under `limits`, boxed in the version's joint limits.
220pub(super) fn torque_loop(
221    options: JointTargetControlOptions,
222    limits: [OtgLimits; 7],
223    impedance: ImpedanceOptions,
224    model: Arc<Model>,
225    version: FciVersion,
226    shared: Arc<Shared<7>>,
227    started: SyncSender<()>,
228) -> FrankaResult<TorqueLoop<7, 7, JointTracker>> {
229    let runner = Runner::new(shared, started, limits, options.settle, identity)?;
230    let velocity = VelocityLimit::of(version);
231    let limits = JointLimits {
232        position: joint_position_limits(version),
233        margin: impedance.joint_position_margin,
234        fraction: impedance.joint_velocity_fraction,
235        velocity,
236    };
237    let tracker = JointTracker::new(&options, limits);
238    // No live tuning: `LiveTuning`'s budget is a Cartesian norm, which this interface plans
239    // nothing with, and the fields of it this tracker would keep a copy of -- the IK's, the
240    // feedforward's filter -- it has none of. Its slot stays unseeded and its handle offers no
241    // setter, so the loop reads nothing per cycle.
242    Ok(TorqueLoop::new(
243        runner,
244        model,
245        impedance,
246        tracker,
247        options.observer,
248        None,
249    ))
250}
251
252/// [`Backend::RobotController`]: the position stream, re-anchored on the robot's echo every
253/// cycle with the rate limiter as the backstop; a stop holds the echo bit for bit.
254fn position_loop(
255    robot: &Robot,
256    started: SyncSender<()>,
257    options: JointTargetControlOptions,
258    limits: [OtgLimits; 7],
259    shared: Arc<Shared<7>>,
260) -> FrankaResult<()> {
261    let JointTargetControlOptions {
262        controller_mode,
263        max_deviation,
264        settle,
265        limit_rate,
266        mut observer,
267        ..
268    } = options;
269    let mut runner = Runner::new(shared, started, limits, settle, identity)?;
270    let velocity = limits.map(|l| l.max_velocity);
271    let acceleration = limits.map(|l| l.max_acceleration);
272    let jerk = limits.map(|l| l.max_jerk);
273    let mut start: Option<[f64; 7]> = None;
274    let result = robot.control_joint_positions(
275        |state: &RobotState, _period| {
276            let start = *start.get_or_insert(state.q_d);
277            let strayed = max_abs_difference(&state.q, &start) > max_deviation;
278            let step = runner.cycle(state, state.q_d, strayed, false);
279
280            let mut q = step.position;
281            let mut backstop_alteration = 0.0;
282            if limit_rate && !step.hold {
283                // The budget, tightened to the robot's own velocity envelope at `q`
284                // (position-dependent on an FR3), against the robot's echo; it only fails
285                // on non-finite input, and holding the echo is then the safe command.
286                // Skipped while the stop holds: that command must go out bit-identical.
287                let upper = robot.upper_joint_velocity_limits(&state.q);
288                let lower = robot.lower_joint_velocity_limits(&state.q);
289                let upper: [f64; 7] = std::array::from_fn(|i| upper[i].min(velocity[i]));
290                let lower: [f64; 7] = std::array::from_fn(|i| lower[i].max(-velocity[i]));
291                #[rustfmt::skip]
292                let limited = limit_rate_joint_positions(
293                    &upper, &lower, &acceleration, &jerk,
294                    &q, &state.q_d, &state.dq_d, &state.ddq_d,
295                ).unwrap_or(state.q_d);
296                backstop_alteration = max_abs_difference(&limited, &q);
297                q = limited;
298            }
299            if let Some(observe) = observer.as_mut() {
300                observe(
301                    state,
302                    &JointSent {
303                        backstop_alteration,
304                        ..sent(&step, q)
305                    },
306                );
307            }
308            let mut output = JointPositions::new(q);
309            output.motion_finished = step.finished;
310            output
311        },
312        controller_mode,
313        limit_rate,
314        MAX_CUTOFF_FREQUENCY,
315    );
316    runner.finish(result)
317}