Skip to main content

franka/
control_types.rs

1//! Command types returned from control callbacks.
2//!
3//! Port of libfranka 0.21.2 `include/franka/control_types.h` / `src/control_types.cpp` plus the
4//! validation helpers of `include/franka/control_tools.h` (`checkFinite`, `checkMatrix`,
5//! `checkElbow`, `isValidElbow`, `isHomogeneousTransformation`).
6//!
7//! libfranka validates the commanded values inside the control loop
8//! (`control_loop.cpp` / `robot_impl.cpp` call `checkFinite`/`checkMatrix`/`checkElbow` on the
9//! values returned by the user callback) and throws `std::invalid_argument`. The Rust
10//! constructors are infallible; the equivalent checks live in [`Finishable`] implementors'
11//! `validate` methods, which the control loop calls for every command.
12
13use crate::error::{FrankaError, FrankaResult};
14
15/// Available controller modes for a `Robot`.
16///
17/// Port of `franka::ControllerMode`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ControllerMode {
20    /// Joint impedance controller.
21    JointImpedance,
22    /// Cartesian impedance controller.
23    CartesianImpedance,
24}
25
26/// Helper trait for control and motion generation loops.
27///
28/// Port of `franka::Finishable`, which in C++ is a base struct carrying the public
29/// `motion_finished` flag. In Rust the flag stays a public field on each command type and this
30/// trait gives the control loop uniform access to it.
31pub trait Finishable {
32    /// Whether the motion should terminate after this command has been processed.
33    fn is_finished(&self) -> bool;
34    /// Sets the "terminate after this command" flag.
35    fn set_finished(&mut self, finished: bool);
36}
37
38/// Which motion generator a command type drives.
39///
40/// The mapping onto the wire enum `research_interface::robot::Move::MotionGeneratorMode` is done
41/// by the robot layer; this enum keeps `control_types` independent of the protocol structs
42/// (libfranka does the same with `src/motion_generator_traits.h`).
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum MotionGeneratorKind {
45    /// Joint position motion generator.
46    JointPosition,
47    /// Joint velocity motion generator.
48    JointVelocity,
49    /// Cartesian pose motion generator.
50    CartesianPosition,
51    /// Cartesian velocity motion generator.
52    CartesianVelocity,
53}
54
55/// Implemented by the four motion generator command types.
56///
57/// Port of libfranka's `MotionGeneratorTraits<T>` (`src/motion_generator_traits.h`).
58/// [`Torques`] is a controller command and deliberately does *not* implement this trait.
59pub trait MotionGenerator: Finishable + Copy {
60    /// The motion generator this command type drives.
61    const KIND: MotionGeneratorKind;
62}
63
64/// Helper method to indicate that a motion should stop after processing the given command.
65///
66/// Port of `franka::MotionFinished`.
67pub fn motion_finished<T: Finishable>(mut command: T) -> T {
68    command.set_finished(true);
69    command
70}
71
72/// Determines whether the given elbow configuration is valid or not.
73///
74/// Port of `franka::isValidElbow`.
75pub fn is_valid_elbow(elbow: &[f64; 2]) -> bool {
76    elbow[1] == -1.0 || elbow[1] == 1.0
77}
78
79/// Determines whether the given array represents a valid homogeneous transformation matrix.
80///
81/// `transform` is a 4x4 matrix in column-major format. Port of
82/// `franka::isHomogeneousTransformation` (orthonormality threshold `1e-5`).
83pub fn is_homogeneous_transformation(transform: &[f64; 16]) -> bool {
84    const ORTHONORMAL_THRESHOLD: f64 = 1e-5;
85
86    if transform[3] != 0.0 || transform[7] != 0.0 || transform[11] != 0.0 || transform[15] != 1.0 {
87        return false;
88    }
89    for j in 0..3 {
90        // j .. column
91        let norm = (transform[j * 4].powi(2)
92            + transform[j * 4 + 1].powi(2)
93            + transform[j * 4 + 2].powi(2))
94        .sqrt();
95        if (norm - 1.0).abs() > ORTHONORMAL_THRESHOLD {
96            return false;
97        }
98    }
99    for i in 0..3 {
100        // i .. row
101        let norm =
102            (transform[i].powi(2) + transform[4 + i].powi(2) + transform[8 + i].powi(2)).sqrt();
103        if (norm - 1.0).abs() > ORTHONORMAL_THRESHOLD {
104            return false;
105        }
106    }
107    true
108}
109
110/// Checks that all elements of the array have a finite value.
111///
112/// Port of `franka::checkFinite`.
113pub fn check_finite(values: &[f64]) -> FrankaResult<()> {
114    if values.iter().all(|v| v.is_finite()) {
115        Ok(())
116    } else {
117        Err(FrankaError::InvalidArgument(
118            "Commanding value is infinite or NaN.".to_string(),
119        ))
120    }
121}
122
123/// Checks that the transformation matrix is finite and a homogeneous transformation.
124///
125/// Port of `franka::checkMatrix`.
126pub fn check_matrix(transform: &[f64; 16]) -> FrankaResult<()> {
127    check_finite(transform)?;
128    if !is_homogeneous_transformation(transform) {
129        return Err(FrankaError::InvalidArgument(
130            "libfranka: Attempt to set invalid transformation in motion generator. Has to be \
131             column major!"
132                .to_string(),
133        ));
134    }
135    Ok(())
136}
137
138/// Checks that the elbow vector is finite and that the elbow configuration is valid.
139///
140/// Port of `franka::checkElbow`.
141pub fn check_elbow(elbow: &[f64; 2]) -> FrankaResult<()> {
142    check_finite(elbow)?;
143    if !is_valid_elbow(elbow) {
144        return Err(FrankaError::InvalidArgument(
145            "Invalid elbow configuration given! Only +1 or -1 are allowed for the sign of the 4th \
146             joint."
147                .to_string(),
148        ));
149    }
150    Ok(())
151}
152
153macro_rules! impl_finishable {
154    ($t:ty) => {
155        impl Finishable for $t {
156            fn is_finished(&self) -> bool {
157                self.motion_finished
158            }
159            fn set_finished(&mut self, finished: bool) {
160                self.motion_finished = finished;
161            }
162        }
163    };
164}
165
166/// Stores joint-level torque commands without gravity and friction.
167///
168/// Port of `franka::Torques`.
169#[derive(Debug, Clone, Copy, PartialEq)]
170pub struct Torques {
171    /// Desired torques in \[Nm\].
172    pub tau_J: [f64; 7],
173    /// Determines whether to finish a currently running motion.
174    pub motion_finished: bool,
175}
176
177impl Torques {
178    /// Creates a new `Torques` instance from desired joint-level torques in \[Nm\].
179    pub fn new(tau_J: [f64; 7]) -> Self {
180        Torques {
181            tau_J,
182            motion_finished: false,
183        }
184    }
185
186    /// Validates the command like libfranka's `control_loop.cpp` does before sending it
187    /// (`checkFinite(control_command.tau_J_d)`).
188    pub fn validate(&self) -> FrankaResult<()> {
189        check_finite(&self.tau_J)
190    }
191}
192
193impl_finishable!(Torques);
194
195/// Stores values for joint position motion generation.
196///
197/// Port of `franka::JointPositions`.
198#[derive(Debug, Clone, Copy, PartialEq)]
199pub struct JointPositions {
200    /// Desired joint angles in \[rad\].
201    pub q: [f64; 7],
202    /// Determines whether to finish a currently running motion.
203    pub motion_finished: bool,
204}
205
206impl JointPositions {
207    /// Creates a new `JointPositions` instance from desired joint angles in \[rad\].
208    pub fn new(q: [f64; 7]) -> Self {
209        JointPositions {
210            q,
211            motion_finished: false,
212        }
213    }
214
215    /// Validates the command like libfranka's `control_loop.cpp` does before sending it
216    /// (`checkFinite(command.q_c)`).
217    pub fn validate(&self) -> FrankaResult<()> {
218        check_finite(&self.q)
219    }
220}
221
222impl_finishable!(JointPositions);
223
224impl MotionGenerator for JointPositions {
225    const KIND: MotionGeneratorKind = MotionGeneratorKind::JointPosition;
226}
227
228/// Stores values for joint velocity motion generation.
229///
230/// Port of `franka::JointVelocities`.
231#[derive(Debug, Clone, Copy, PartialEq)]
232pub struct JointVelocities {
233    /// Desired joint velocities in \[rad/s\].
234    pub dq: [f64; 7],
235    /// Determines whether to finish a currently running motion.
236    pub motion_finished: bool,
237}
238
239impl JointVelocities {
240    /// Creates a new `JointVelocities` instance from desired joint velocities in \[rad/s\].
241    pub fn new(dq: [f64; 7]) -> Self {
242        JointVelocities {
243            dq,
244            motion_finished: false,
245        }
246    }
247
248    /// Validates the command like libfranka's `control_loop.cpp` does before sending it
249    /// (`checkFinite(command.dq_c)`).
250    pub fn validate(&self) -> FrankaResult<()> {
251        check_finite(&self.dq)
252    }
253}
254
255impl_finishable!(JointVelocities);
256
257impl MotionGenerator for JointVelocities {
258    const KIND: MotionGeneratorKind = MotionGeneratorKind::JointVelocity;
259}
260
261/// Stores values for Cartesian pose motion generation.
262///
263/// Port of `franka::CartesianPose`.
264#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct CartesianPose {
266    /// Homogeneous transformation `O_T_EE_d`, column major, that transforms from the end effector
267    /// frame `EE` to base frame `O`.
268    pub O_T_EE: [f64; 16],
269    /// Elbow configuration: `elbow[0]` is the position of the 3rd joint in \[rad\],
270    /// `elbow[1]` the flip direction of the elbow (4th joint), +1 or -1.
271    pub elbow: [f64; 2],
272    /// Whether there is a stored elbow configuration.
273    ///
274    /// libfranka's `CartesianPose::hasElbow()` reports `elbow != {0, 0}`; the constructors below
275    /// reproduce that, so `with_elbow([.., ..], [0.0, 0.0])` yields `has_elbow == false`.
276    pub has_elbow: bool,
277    /// Determines whether to finish a currently running motion.
278    pub motion_finished: bool,
279}
280
281impl CartesianPose {
282    /// Creates a new `CartesianPose` without an elbow configuration.
283    ///
284    /// `O_T_EE` is a column-major homogeneous transformation matrix.
285    pub fn new(O_T_EE: [f64; 16]) -> Self {
286        CartesianPose {
287            O_T_EE,
288            elbow: [0.0; 2],
289            has_elbow: false,
290            motion_finished: false,
291        }
292    }
293
294    /// Creates a new `CartesianPose` with an elbow configuration.
295    pub fn with_elbow(O_T_EE: [f64; 16], elbow: [f64; 2]) -> Self {
296        CartesianPose {
297            O_T_EE,
298            elbow,
299            has_elbow: elbow != [0.0, 0.0],
300            motion_finished: false,
301        }
302    }
303
304    /// Validates the command like libfranka's `control_loop.cpp` does before sending it
305    /// (`checkMatrix(command.O_T_EE_c)` and, if an elbow is set, `checkElbow(command.elbow_c)`).
306    pub fn validate(&self) -> FrankaResult<()> {
307        check_matrix(&self.O_T_EE)?;
308        if self.has_elbow {
309            check_elbow(&self.elbow)?;
310        }
311        Ok(())
312    }
313}
314
315impl_finishable!(CartesianPose);
316
317impl MotionGenerator for CartesianPose {
318    const KIND: MotionGeneratorKind = MotionGeneratorKind::CartesianPosition;
319}
320
321/// Stores values for Cartesian velocity motion generation.
322///
323/// Port of `franka::CartesianVelocities`.
324#[derive(Debug, Clone, Copy, PartialEq)]
325pub struct CartesianVelocities {
326    /// Cartesian velocity with respect to the base frame `O`: `(dx, dy, dz)` in \[m/s\] and
327    /// `(omega_x, omega_y, omega_z)` in \[rad/s\].
328    pub O_dP_EE: [f64; 6],
329    /// Elbow configuration, see [`CartesianPose::elbow`].
330    pub elbow: [f64; 2],
331    /// Whether there is a stored elbow configuration, see [`CartesianPose::has_elbow`].
332    pub has_elbow: bool,
333    /// Determines whether to finish a currently running motion.
334    pub motion_finished: bool,
335}
336
337impl CartesianVelocities {
338    /// Creates a new `CartesianVelocities` without an elbow configuration.
339    pub fn new(O_dP_EE: [f64; 6]) -> Self {
340        CartesianVelocities {
341            O_dP_EE,
342            elbow: [0.0; 2],
343            has_elbow: false,
344            motion_finished: false,
345        }
346    }
347
348    /// Creates a new `CartesianVelocities` with an elbow configuration.
349    pub fn with_elbow(O_dP_EE: [f64; 6], elbow: [f64; 2]) -> Self {
350        CartesianVelocities {
351            O_dP_EE,
352            elbow,
353            has_elbow: elbow != [0.0, 0.0],
354            motion_finished: false,
355        }
356    }
357
358    /// Validates the command like libfranka's `control_loop.cpp` does before sending it
359    /// (`checkFinite(command.O_dP_EE_c)` and, if an elbow is set, `checkElbow(command.elbow_c)`).
360    pub fn validate(&self) -> FrankaResult<()> {
361        check_finite(&self.O_dP_EE)?;
362        if self.has_elbow {
363            check_elbow(&self.elbow)?;
364        }
365        Ok(())
366    }
367}
368
369impl_finishable!(CartesianVelocities);
370
371impl MotionGenerator for CartesianVelocities {
372    const KIND: MotionGeneratorKind = MotionGeneratorKind::CartesianVelocity;
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    const IDENTITY: [f64; 16] = [
380        1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
381    ];
382
383    fn message(result: FrankaResult<()>) -> String {
384        match result {
385            Err(FrankaError::InvalidArgument(msg)) => msg,
386            other => panic!("expected InvalidArgument, got {other:?}"),
387        }
388    }
389
390    // Port of TEST(Torques, CanConstructFromArray).
391    #[test]
392    fn torques_can_construct_from_array() {
393        let array = [0., 1., 2., 3., 4., 5., 6.];
394        let t = Torques::new(array);
395        assert_eq!(array, t.tau_J);
396        assert!(!t.motion_finished);
397        assert!(t.validate().is_ok());
398    }
399
400    // Port of TEST(JointPositions, CanConstructFromArray).
401    #[test]
402    fn joint_positions_can_construct_from_array() {
403        let array = [0., 1., 2., 3., 4., 5., 6.];
404        let jp = JointPositions::new(array);
405        assert_eq!(array, jp.q);
406        assert!(jp.validate().is_ok());
407    }
408
409    // Port of TEST(JointVelocities, CanConstructFromArray).
410    #[test]
411    fn joint_velocities_can_construct_from_array() {
412        let array = [0., 1., 2., 3., 4., 5., 6.];
413        let jv = JointVelocities::new(array);
414        assert_eq!(array, jv.dq);
415        assert!(jv.validate().is_ok());
416    }
417
418    // Port of TEST(CartesianPose, CanConstructFromArray).
419    #[test]
420    fn cartesian_pose_can_construct_from_array() {
421        let p = CartesianPose::new(IDENTITY);
422        assert_eq!(IDENTITY, p.O_T_EE);
423        assert!(!p.has_elbow);
424        assert!(p.validate().is_ok());
425    }
426
427    // Port of TEST(CartesianPose, CanConstructFromArrayWithElbow).
428    #[test]
429    fn cartesian_pose_can_construct_from_array_with_elbow() {
430        let elbow = [0., -1.];
431        let p = CartesianPose::with_elbow(IDENTITY, elbow);
432        assert_eq!(IDENTITY, p.O_T_EE);
433        assert_eq!(elbow, p.elbow);
434        assert!(p.has_elbow);
435        assert!(p.validate().is_ok());
436    }
437
438    // Port of TEST(CartesianVelocities, CanConstructFromArray).
439    #[test]
440    fn cartesian_velocities_can_construct_from_array() {
441        let array = [0., 1., 2., 3., 4., 5.];
442        let cv = CartesianVelocities::new(array);
443        assert_eq!(array, cv.O_dP_EE);
444        assert!(!cv.has_elbow);
445        assert!(cv.validate().is_ok());
446    }
447
448    // Port of TEST(CartesianVelocities, CanConstructFromArrayWithElbow).
449    #[test]
450    fn cartesian_velocities_can_construct_from_array_with_elbow() {
451        let array = [0., 1., 2., 3., 4., 5.];
452        let elbow = [0., 1.];
453        let cv = CartesianVelocities::with_elbow(array, elbow);
454        assert_eq!(array, cv.O_dP_EE);
455        assert_eq!(elbow, cv.elbow);
456        assert!(cv.has_elbow);
457        assert!(cv.validate().is_ok());
458    }
459
460    // libfranka's hasElbow() is `elbow != decltype(elbow)()`.
461    #[test]
462    fn zero_elbow_is_no_elbow() {
463        assert!(!CartesianPose::with_elbow(IDENTITY, [0., 0.]).has_elbow);
464        assert!(!CartesianVelocities::with_elbow([0.; 6], [0., 0.]).has_elbow);
465    }
466
467    #[test]
468    fn motion_finished_sets_the_flag() {
469        assert!(motion_finished(Torques::new([0.; 7])).motion_finished);
470        assert!(motion_finished(JointPositions::new([0.; 7])).motion_finished);
471        assert!(motion_finished(JointVelocities::new([0.; 7])).motion_finished);
472        assert!(motion_finished(CartesianPose::new(IDENTITY)).motion_finished);
473        assert!(motion_finished(CartesianVelocities::new([0.; 6])).motion_finished);
474    }
475
476    #[test]
477    fn motion_generator_kinds() {
478        assert_eq!(JointPositions::KIND, MotionGeneratorKind::JointPosition);
479        assert_eq!(JointVelocities::KIND, MotionGeneratorKind::JointVelocity);
480        assert_eq!(CartesianPose::KIND, MotionGeneratorKind::CartesianPosition);
481        assert_eq!(
482            CartesianVelocities::KIND,
483            MotionGeneratorKind::CartesianVelocity
484        );
485    }
486
487    // checkFinite
488    #[test]
489    fn non_finite_values_are_rejected() {
490        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
491            let mut q = [0.; 7];
492            q[3] = bad;
493            assert_eq!(
494                message(Torques::new(q).validate()),
495                "Commanding value is infinite or NaN."
496            );
497            assert_eq!(
498                message(JointPositions::new(q).validate()),
499                "Commanding value is infinite or NaN."
500            );
501            assert_eq!(
502                message(JointVelocities::new(q).validate()),
503                "Commanding value is infinite or NaN."
504            );
505            let mut dx = [0.; 6];
506            dx[2] = bad;
507            assert_eq!(
508                message(CartesianVelocities::new(dx).validate()),
509                "Commanding value is infinite or NaN."
510            );
511            let mut pose = IDENTITY;
512            pose[0] = bad;
513            assert_eq!(
514                message(CartesianPose::new(pose).validate()),
515                "Commanding value is infinite or NaN."
516            );
517        }
518    }
519
520    // checkMatrix / isHomogeneousTransformation
521    #[test]
522    fn invalid_transformation_is_rejected() {
523        assert!(is_homogeneous_transformation(&IDENTITY));
524
525        // Last row not (0, 0, 0, 1).
526        let mut pose = IDENTITY;
527        pose[3] = 1.0;
528        assert!(!is_homogeneous_transformation(&pose));
529        assert_eq!(
530            message(CartesianPose::new(pose).validate()),
531            "libfranka: Attempt to set invalid transformation in motion generator. Has to be \
532             column major!"
533        );
534
535        for idx in [7usize, 11] {
536            let mut pose = IDENTITY;
537            pose[idx] = 1.0;
538            assert!(!is_homogeneous_transformation(&pose));
539        }
540        let mut pose = IDENTITY;
541        pose[15] = 0.0;
542        assert!(!is_homogeneous_transformation(&pose));
543
544        // Non-unit column.
545        let mut pose = IDENTITY;
546        pose[0] = 2.0;
547        assert!(!is_homogeneous_transformation(&pose));
548
549        // Non-unit row (column norms stay 1, row 0 norm does not).
550        let pose = [
551            0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
552        ];
553        assert!(is_homogeneous_transformation(&pose));
554        let pose = [
555            0.6, 0.8, 0., 0., 0.8, -0.6, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
556        ];
557        assert!(is_homogeneous_transformation(&pose));
558
559        // Just inside / outside the 1e-5 orthonormality threshold.
560        let mut pose = IDENTITY;
561        pose[0] = 1.0 + 9e-6;
562        assert!(is_homogeneous_transformation(&pose));
563        pose[0] = 1.0 + 2e-5;
564        assert!(!is_homogeneous_transformation(&pose));
565    }
566
567    // checkElbow / isValidElbow
568    #[test]
569    fn invalid_elbow_is_rejected() {
570        assert!(is_valid_elbow(&[0.5, 1.0]));
571        assert!(is_valid_elbow(&[0.5, -1.0]));
572        assert!(!is_valid_elbow(&[0.5, 0.5]));
573
574        let expected = "Invalid elbow configuration given! Only +1 or -1 are allowed for the sign \
575                        of the 4th joint.";
576        assert_eq!(
577            message(CartesianPose::with_elbow(IDENTITY, [0.5, 0.5]).validate()),
578            expected
579        );
580        assert_eq!(
581            message(CartesianVelocities::with_elbow([0.; 6], [0.5, 0.5]).validate()),
582            expected
583        );
584        // Non-finite elbow is caught by checkFinite first.
585        assert_eq!(
586            message(CartesianPose::with_elbow(IDENTITY, [f64::NAN, 1.0]).validate()),
587            "Commanding value is infinite or NaN."
588        );
589        // Without an elbow configuration the elbow is not checked.
590        assert!(CartesianPose::new(IDENTITY).validate().is_ok());
591        assert!(CartesianVelocities::new([0.; 6]).validate().is_ok());
592    }
593}