orbita3d_controller/
lib.rs

1//! Orbita3dController
2//!
3//! This crate provides a controller for the Orbita3d actuator.
4//!
5//! The main Class is `Orbita3dController` which allows to control the Orbita3d actuator.
6//!
7//! # Examples
8//! ```rust
9//! use orbita3d_controller::Orbita3dController;
10//!
11//! let mut orbita3d = Orbita3dController::with_config("./config/fake.yaml").unwrap();
12//!
13//! let orientation = orbita3d.get_current_orientation().unwrap();
14//! println!("Current orientation: {:?}", orientation);
15//!
16//! orbita3d.enable_torque(true).unwrap();
17//!
18//! let target = [0.0, 0.0, 0.7, 0.7];
19//! orbita3d.set_target_orientation(target).unwrap();
20//!
21//! let orientation = orbita3d.get_current_orientation().unwrap();
22//! println!("Current orientation: {:?}", orientation);
23//! ```
24
25pub mod io;
26
27use io::Orbita3dIOConfig;
28
29#[cfg(feature = "build_ethercat")]
30use crate::io::EthercatPoulpeController;
31#[cfg(feature = "build_dynamixel")]
32use io::{
33    CachedDynamixelPoulpeController, CachedDynamixelSerialController, DynamixelPoulpeController,
34    DynamixelSerialController,
35};
36
37use motor_toolbox_rs::{FakeMotorsController, MotorsController, Result, PID};
38
39use orbita3d_kinematics::{conversion, Orbita3dKinematicsModel};
40use serde::{Deserialize, Serialize};
41use std::{thread, time::Duration};
42
43#[derive(Debug, Deserialize, Serialize)]
44/// Orbita3d Config
45pub struct Orbita3dConfig {
46    /// IO specific config
47    pub io: Orbita3dIOConfig,
48    /// Disks specific config
49    pub disks: DisksConfig,
50    /// Kinematics model config
51    pub kinematics_model: Orbita3dKinematicsModel,
52    /// Should we invert some axis? (in roll/pitch/yaw)
53    pub inverted_axes: [Option<bool>; 3],
54    pub motor_gearbox_params: Option<MotorGearboxConfig>,
55    pub default_mode: Option<u8>,
56}
57
58#[derive(Debug, Deserialize, Serialize)]
59/// Motor/gearbox characteristics
60pub struct MotorGearboxConfig {
61    /// motor and gearbox characteristics for current/torque conversion
62    pub motor_gearbox_ratio: f64,
63    pub motor_nominal_current: f64,
64    pub motor_nominal_torque: f64,
65    pub motor_efficiency: f64,
66    pub motor_gearbox_efficiency: f64,
67}
68
69#[derive(Debug, Deserialize, Serialize)]
70/// Disks Config
71pub struct DisksConfig {
72    /// Zeros of each disk (in rad), used as an offset
73    pub zeros: ZeroType,
74    /// Reduction between the motor gearbox and the disk
75    pub reduction: f64,
76}
77
78#[derive(Debug, Deserialize, Serialize)]
79/// Zero type config
80/// This is used to configure the zero of each disk
81pub enum ZeroType {
82    /// ApproximateHardwareZero config
83    ApproximateHardwareZero(ApproximateHardwareZero),
84    /// ZeroStartup config
85    ZeroStartup(ZeroStartup),
86    /// HallZero config
87    HallZero(HallZero),
88    /// FirmwareZero config
89    FirmwareZero(FirmwareZero),
90}
91
92#[derive(Debug, Deserialize, Serialize)]
93/// Firmwarezero config
94pub struct FirmwareZero;
95
96#[derive(Debug, Deserialize, Serialize)]
97/// ApproximateHardwareZero config
98pub struct ApproximateHardwareZero {
99    /// Hardware zero of each disk (in rad)
100    pub hardware_zero: [f64; 3],
101}
102
103#[derive(Debug, Deserialize, Serialize)]
104/// HallZero config
105pub struct HallZero {
106    /// Hardware zero of each disk (in rad)
107    pub hardware_zero: [f64; 3],
108
109    /// Top/Middle/Bottom Hall active for the zero position
110    pub hall_indice: [u8; 3],
111}
112
113#[derive(Debug, Deserialize, Serialize)]
114/// ZeroStartup config
115pub struct ZeroStartup;
116
117/// Orbita3d Controller
118pub struct Orbita3dController {
119    inner: Box<dyn MotorsController<3> + Send>,
120    pub kinematics: Orbita3dKinematicsModel,
121}
122
123#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
124/// Feedback struct
125pub struct Orbita3dFeedback {
126    pub orientation: [f64; 4],
127}
128
129impl Orbita3dController {
130    /// Creates a new Orbita3dController using the given configuration file.
131    pub fn with_config(configfile: &str) -> Result<Self> {
132        log::info!("Loading config file: {}", configfile);
133
134        let f = match std::fs::File::open(configfile) {
135            Ok(f) => f,
136            Err(e) => {
137                log::error!("Error opening config file: {}", e);
138                return Err(e.into());
139            }
140        };
141        let config: Orbita3dConfig = serde_yaml::from_reader(f)?;
142
143        let controller: Box<dyn MotorsController<3> + Send> = match config.io {
144            // This is a legacy mode, not maintained anymore
145            #[cfg(feature = "build_dynamixel")]
146            Orbita3dIOConfig::DynamixelSerial(dxl_config) => match dxl_config.use_cache {
147                true => {
148                    let controller = CachedDynamixelSerialController::new(
149                        &dxl_config.serial_port,
150                        dxl_config.id,
151                        config.disks.zeros,
152                        config.disks.reduction,
153                    )?;
154
155                    log::info!("Using cached dynamixel controller {:?}", controller);
156
157                    Box::new(controller)
158                }
159                false => {
160                    let controller = DynamixelSerialController::new(
161                        &dxl_config.serial_port,
162                        dxl_config.id,
163                        config.disks.zeros,
164                        config.disks.reduction,
165                    )?;
166
167                    log::info!("Using dynamixel controller {:?}", controller);
168
169                    Box::new(controller)
170                }
171            },
172            // This is a legacy mode, not maintained anymore
173            #[cfg(feature = "build_dynamixel")]
174            Orbita3dIOConfig::DynamixelPoulpe(dxl_config) => match dxl_config.use_cache {
175                true => {
176                    let controller = CachedDynamixelPoulpeController::new(
177                        &dxl_config.serial_port,
178                        dxl_config.id,
179                        config.disks.zeros,
180                        config.disks.reduction,
181                    )?;
182
183                    log::info!("Using cached poulpe dynamixel controller {:?}", controller);
184
185                    Box::new(controller)
186                }
187                false => {
188                    let controller = DynamixelPoulpeController::new(
189                        &dxl_config.serial_port,
190                        dxl_config.id,
191                        config.disks.zeros,
192                        config.disks.reduction,
193                    )?;
194
195                    log::info!("Using poulpe dynamixel controller {:?}", controller);
196
197                    Box::new(controller)
198                }
199            },
200            // The fake mode is heavily used for testing and debugging, it allows to run everything without any hardware
201            Orbita3dIOConfig::FakeMotors(_) => {
202                let mut controller = FakeMotorsController::<3>::new();
203
204                let offsets = match config.disks.zeros {
205                    ZeroType::ApproximateHardwareZero(zero) => zero.hardware_zero,
206                    ZeroType::ZeroStartup(_) => controller.get_current_position()?,
207                    ZeroType::HallZero(zero) => zero.hardware_zero,
208                    ZeroType::FirmwareZero(_) => [0.0, 0.0, 0.0],
209                };
210
211                let controller = controller
212                    .with_offsets(offsets.map(Some))
213                    .with_reduction([Some(config.disks.reduction); 3])
214                    .with_inverted_axes(config.inverted_axes);
215
216                log::info!("Using fake motors controller {:?}", controller);
217
218                Box::new(controller)
219            }
220            // The ethercat mode with the "Poulpe" electronics
221            #[cfg(feature = "build_ethercat")]
222            Orbita3dIOConfig::PoulpeEthercat(ethercat_config) => {
223                let controller = EthercatPoulpeController::new(
224                    &ethercat_config.url,
225                    ethercat_config.id,
226                    ethercat_config.name,
227                    config.disks.zeros,
228                    config.disks.reduction,
229                    config.inverted_axes,
230                    config.motor_gearbox_params,
231                    config.default_mode,
232                )?;
233                log::info!("Using poulpe ethercat controller {:?}", controller);
234
235                Box::new(controller)
236            }
237        };
238
239        Ok(Self {
240            inner: controller,
241            kinematics: config.kinematics_model,
242        })
243    }
244}
245
246impl Orbita3dController {
247    /// Check if the torque is ON or OFF
248    pub fn is_torque_on(&mut self) -> Result<bool> {
249        let torques = self.inner.is_torque_on()?;
250        assert!(torques.iter().all(|&t| t == torques[0]));
251        Ok(torques[0])
252    }
253    /// Enable the torque
254    ///
255    /// # Arguments
256    /// * reset_target: if true, the target position will be reset to the current position
257    pub fn enable_torque(&mut self, reset_target: bool) -> Result<()> {
258        if !self.is_torque_on()? && reset_target {
259            let thetas = self.inner.get_current_position()?;
260            thread::sleep(Duration::from_millis(1));
261            self.inner.set_target_position(thetas)?;
262            thread::sleep(Duration::from_millis(1));
263        }
264        self.inner.set_torque([true; 3])
265    }
266    /// Disable the torque
267    pub fn disable_torque(&mut self) -> Result<()> {
268        self.inner.set_torque([false; 3])
269    }
270
271    /// Get the current orientation (as quaternion (qx, qy, qz, qw))
272    pub fn get_current_orientation(&mut self) -> Result<[f64; 4]> {
273        let thetas = self.inner.get_current_position()?;
274        let mut rot = self.kinematics.compute_forward_kinematics(thetas);
275
276        // apply axis inversion
277        // TODO: this is not the best way to do it, we should apply the inversion to the quaternion
278        let inverted_axes = self.inner.output_inverted_axes();
279        let mut euler = conversion::matrix_to_intrinsic_roll_pitch_yaw(rot);
280        for i in 0..3 {
281            if let Some(inverted) = inverted_axes[i] {
282                if inverted {
283                    euler[i] = -euler[i];
284                }
285            }
286        }
287        rot = conversion::intrinsic_roll_pitch_yaw_to_matrix(euler[0], euler[1], euler[2]);
288        Ok(conversion::rotation_matrix_to_quaternion(rot))
289    }
290
291    /// Get the current orientation (as intrinsic rpy with multiturn)
292    pub fn get_current_rpy_orientation(&mut self) -> Result<[f64; 3]> {
293        let thetas = self.inner.get_current_position()?;
294        let inverted_axes = self.inner.output_inverted_axes();
295        let mut rpy = self
296            .kinematics
297            .compute_forward_kinematics_rpy_multiturn(thetas)?;
298
299        for i in 0..3 {
300            if let Some(inverted) = inverted_axes[i] {
301                if inverted {
302                    rpy[i] = -rpy[i];
303                }
304            }
305        }
306
307        Ok(rpy)
308    }
309
310    /// Get the current velocity $\omega$ (as a velocity pseudo vector (wx, wy, wz) which defines the instantaneous axis of rotation and with the norm represents the velocity)
311    pub fn get_current_velocity(&mut self) -> Result<[f64; 3]> {
312        let thetas = self.inner.get_current_position()?;
313        let input_velocity = self.inner.get_current_velocity()?;
314
315        let mut vel = self
316            .kinematics
317            .compute_output_velocity(thetas, input_velocity);
318
319        // apply axis inversion
320        // TODO: the inversion is applied to the angle-axis representation of the torque
321        // although it is not the best way to do it, it is the only way to do it with the current implementation
322        // we should decide which representation to invert and do it consistently
323        let inverted_axes = self.inner.output_inverted_axes();
324        for i in 0..3 {
325            if let Some(inverted) = inverted_axes[i] {
326                if inverted {
327                    vel[i] = -vel[i];
328                }
329            }
330        }
331
332        Ok(vel.into())
333    }
334    /// Get the current torque (as pseudo vector)
335    pub fn get_current_torque(&mut self) -> Result<[f64; 3]> {
336        let thetas = self.inner.get_current_position()?;
337        let input_torque = self.inner.get_current_torque()?;
338
339        let mut torque = self.kinematics.compute_output_torque(thetas, input_torque);
340
341        // apply axis inversion
342        // TODO: the inversion is applied to the angle-axis representation of the torque
343        // although it is not the best way to do it, it is the only way to do it with the current implementation
344        // we should decide which representation to invert and do it consistently
345        let inverted_axes = self.inner.output_inverted_axes();
346        for i in 0..3 {
347            if let Some(inverted) = inverted_axes[i] {
348                if inverted {
349                    torque[i] = -torque[i];
350                }
351            }
352        }
353
354        // If parameters are known, convert to Nm
355        if let Some(ratio) = self.inner.torque_current_ratio() {
356            torque.iter_mut().for_each(|t| *t *= ratio);
357            Ok(torque.into())
358        } else {
359            Ok(torque.into())
360        }
361    }
362
363    /// Get the target orientation (as quaternion (qx, qy, qz, qw))
364    pub fn get_target_orientation(&mut self) -> Result<[f64; 4]> {
365        let thetas = self.inner.get_target_position()?;
366        let mut rot = self.kinematics.compute_forward_kinematics(thetas);
367        // apply axis inversion
368        // TODO: this is not the best way to do it, we should apply the inversion to the quaternion
369        let inverted_axes = self.inner.output_inverted_axes();
370        let mut euler = conversion::matrix_to_intrinsic_roll_pitch_yaw(rot);
371        for i in 0..3 {
372            if let Some(inverted) = inverted_axes[i] {
373                if inverted {
374                    euler[i] = -euler[i];
375                }
376            }
377        }
378        rot = conversion::intrinsic_roll_pitch_yaw_to_matrix(euler[0], euler[1], euler[2]);
379        Ok(conversion::rotation_matrix_to_quaternion(rot))
380    }
381
382    /// Get the target orientation (as intrinsic rpy with multiturn)
383    pub fn get_target_rpy_orientation(&mut self) -> Result<[f64; 3]> {
384        let thetas = self.inner.get_target_position()?;
385        let mut rpy = self
386            .kinematics
387            .compute_forward_kinematics_rpy_multiturn(thetas)?;
388
389        // apply the output axis inversion
390        let inverted_axes = self.inner.output_inverted_axes();
391        for i in 0..3 {
392            if let Some(inverted) = inverted_axes[i] {
393                if inverted {
394                    rpy[i] = -rpy[i];
395                }
396            }
397        }
398        Ok(rpy)
399    }
400
401    /// Set the target orientation (as quaternion (qx, qy, qz, qw))
402    pub fn set_target_orientation(&mut self, target: [f64; 4]) -> Result<()> {
403        let mut rot =
404            conversion::quaternion_to_rotation_matrix(target[0], target[1], target[2], target[3]);
405
406        // apply axis inversion
407        // TODO: this is not the best way to do it, we should apply the inversion to the quaternion
408        let inverted_axes = self.inner.output_inverted_axes();
409        let mut euler = conversion::matrix_to_intrinsic_roll_pitch_yaw(rot);
410        for i in 0..3 {
411            if let Some(inverted) = inverted_axes[i] {
412                if inverted {
413                    euler[i] = -euler[i];
414                }
415            }
416        }
417        rot = conversion::intrinsic_roll_pitch_yaw_to_matrix(euler[0], euler[1], euler[2]);
418
419        let thetas = self.kinematics.compute_inverse_kinematics(rot)?;
420        self.inner.set_target_position(thetas)
421    }
422
423    /// Set the target orientation fro the roll pitch yaw intrinsic angles (taking multi turn into account)
424    pub fn set_target_rpy_orientation(&mut self, target: [f64; 3]) -> Result<()> {
425        let inverted_axes = self.inner.output_inverted_axes();
426        let mut rpy_target = target;
427
428        for i in 0..3 {
429            if let Some(inverted) = inverted_axes[i] {
430                if inverted {
431                    rpy_target[i] = -rpy_target[i];
432                }
433            }
434        }
435
436        let thetas = self
437            .kinematics
438            .compute_inverse_kinematics_rpy_multiturn(rpy_target)?;
439
440        self.inner.set_target_position(thetas)
441    }
442
443    /// Set the target orientation (as quaternion (qx, qy, qz, qw)) with feedback
444    pub fn set_target_orientation_fb(&mut self, target: [f64; 4]) -> Result<Orbita3dFeedback> {
445        let rot =
446            conversion::quaternion_to_rotation_matrix(target[0], target[1], target[2], target[3]);
447
448        let thetas = self.kinematics.compute_inverse_kinematics(rot)?;
449
450        let fb: Result<[f64; 3]> = self.inner.set_target_position_fb(thetas);
451        match fb {
452            Ok(fb) => {
453                let rot = self
454                    .kinematics
455                    .compute_forward_kinematics([fb[0], fb[1], fb[2]]); //Why the f*ck can't I use slice here?
456                                                                        // let vel = self
457                                                                        //     .kinematics
458                                                                        //     .compute_output_velocity(thetas, [fb[3], fb[4], fb[5]]);
459                                                                        // let torque = self
460                                                                        //     .kinematics
461                                                                        //     .compute_output_torque(thetas, [fb[6], fb[7], fb[8]]);
462
463                Ok(Orbita3dFeedback {
464                    orientation: conversion::rotation_matrix_to_quaternion(rot),
465                    // velocity: [vel[0], vel[1], vel[2]],
466                    // torque: [torque[0], torque[1], torque[2]],
467                })
468            }
469            Err(e) => Err(e),
470        }
471    }
472
473    /// Set the target orientation from roll pitch yaw (accepts yaw>180°) intrinsic angles with feedback => returns feedback rpy
474    pub fn set_target_rpy_orientation_fb(&mut self, target: [f64; 3]) -> Result<[f64; 3]> {
475        let inverted_axes = self.inner.output_inverted_axes();
476        let mut rpy_target = target;
477
478        for i in 0..3 {
479            if let Some(inverted) = inverted_axes[i] {
480                if inverted {
481                    rpy_target[i] = -rpy_target[i];
482                }
483            }
484        }
485
486        let thetas = self
487            .kinematics
488            .compute_inverse_kinematics_rpy_multiturn(rpy_target)?;
489        let fb: Result<[f64; 3]> = self.inner.set_target_position_fb(thetas);
490
491        match fb {
492            Ok(fb) => {
493                let mut rpy = self
494                    .kinematics
495                    .compute_forward_kinematics_rpy_multiturn([fb[0], fb[1], fb[2]])?;
496                for i in 0..3 {
497                    if let Some(inverted) = inverted_axes[i] {
498                        if inverted {
499                            rpy[i] = -rpy[i];
500                        }
501                    }
502                }
503                Ok(rpy)
504            }
505
506            Err(e) => Err(e),
507        }
508    }
509
510    /// Get the position of each raw motor (in rad)
511    /// caution: this is the raw value used by the motors used inside the actuator, not the orbita3d orientation!
512    pub fn get_raw_motors_positions(&mut self) -> Result<[f64; 3]> {
513        let red = self.inner.reduction();
514        match self.inner.get_current_position() {
515            Ok(p) => {
516                let mut pos = p;
517                for i in 0..3 {
518                    pos[i] *= red[i].unwrap();
519                }
520                Ok(pos)
521            }
522            Err(e) => Err(e),
523        }
524    }
525
526    /// Get the velocity limit of each raw motor (in rad/s)
527    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
528    pub fn get_raw_motors_velocity_limit(&mut self) -> Result<[f64; 3]> {
529        self.inner.get_velocity_limit()
530    }
531    /// Set the velocity limit of each raw motor (in rad/s)
532    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
533    pub fn set_raw_motors_velocity_limit(&mut self, limit: [f64; 3]) -> Result<()> {
534        self.inner.set_velocity_limit(limit)
535    }
536    /// Get the torque limit of each raw motor (in N.m)
537    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
538    pub fn get_raw_motors_torque_limit(&mut self) -> Result<[f64; 3]> {
539        self.inner.get_torque_limit()
540    }
541    /// Set the torque limit of each raw motor (in N.m)
542    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
543    pub fn set_raw_motors_torque_limit(&mut self, limit: [f64; 3]) -> Result<()> {
544        self.inner.set_torque_limit(limit)
545    }
546    /// Get the pid gains of each raw motor
547    /// caution: this is the raw value used by the motors used inside the actuator, not on orbita3d orientation!
548    pub fn get_raw_motors_pid_gains(&mut self) -> Result<[PID; 3]> {
549        self.inner.get_pid_gains()
550    }
551    /// Set the pid gains of each raw motor
552    /// caution: this is the raw value used by the motors used inside the actuator, not on orbita3d orientation!
553    pub fn set_raw_motors_pid_gains(&mut self, gains: [PID; 3]) -> Result<()> {
554        self.inner.set_pid_gains(gains)
555    }
556
557    /// Get the raw motors velocity in rad/s (top, middle, bottom)
558    pub fn get_raw_motors_velocity(&mut self) -> Result<[f64; 3]> {
559        let red = self.inner.reduction();
560        match self.inner.get_current_velocity() {
561            Ok(v) => {
562                let mut vel = v;
563                for i in 0..3 {
564                    vel[i] *= red[i].unwrap();
565                }
566                Ok(vel)
567            }
568            Err(e) => Err(e),
569        }
570    }
571    /// Get the raw motors current in mA (top, middle, bottom)
572    pub fn get_raw_motors_current(&mut self) -> Result<[f64; 3]> {
573        let red = self.inner.reduction();
574        match self.inner.get_current_torque() {
575            Ok(t) => {
576                let mut tor = t;
577                for i in 0..3 {
578                    tor[i] /= red[i].unwrap();
579                }
580                Ok(tor)
581            }
582            Err(e) => Err(e),
583        }
584    }
585
586    /// Get the axis sensors values (gearbox mounted absolute magnetic encoder)
587    pub fn get_axis_sensors(&mut self) -> Result<[f64; 3]> {
588        let red = self.inner.reduction();
589        match self.inner.get_axis_sensors() {
590            Ok(a) => {
591                let mut ax = a;
592                for i in 0..3 {
593                    ax[i] *= red[i].unwrap();
594                }
595                Ok(ax)
596            }
597            Err(e) => Err(e),
598        }
599    }
600
601    /// Get the axis sensor zeros values (random offset from factory)
602    pub fn get_axis_sensor_zeros(&mut self) -> Result<[f64; 3]> {
603        self.inner.get_axis_sensor_zeros()
604    }
605
606    /// Get the error codes from the motors
607    pub fn get_error_codes(&mut self) -> Result<[i32; 3]> {
608        self.inner.get_error_codes()
609    }
610
611    /// Get the temperature for each motor in °C from a NTC sensor in contact with the motor body (top, middle, bottom)
612    pub fn get_motor_temperatures(&mut self) -> Result<[f64; 3]> {
613        self.inner.get_motor_temperatures()
614    }
615
616    /// Get the temperature for each H-Gate in °C (top, middle, bottom)
617    pub fn get_board_temperatures(&mut self) -> Result<[f64; 3]> {
618        self.inner.get_board_temperatures()
619    }
620
621    /// Get the board state register
622    pub fn get_board_state(&mut self) -> Result<u8> {
623        self.inner.get_board_state()
624    }
625    /// Get the board state register
626    pub fn set_board_state(&mut self, state: u8) -> Result<()> {
627        self.inner.set_board_state(state)
628    }
629
630    /// Get the current mode of operation (cf. firmware_poulpe documentation)
631    pub fn get_control_mode(&mut self) -> Result<[u8; 3]> {
632        self.inner.get_control_mode()
633    }
634
635    /// Set the current mode of operation (cf. firmware_poulpe documentation). Currently valid: 0=NoMode, 1=ProfilePositionMode, 3=ProfileVelocityMode, 4=ProfileTorqueMode
636    pub fn set_control_mode(&mut self, mode: [u8; 3]) -> Result<()> {
637        self.inner.set_control_mode(mode)
638    }
639
640    /// Triggers and emergency stop
641    pub fn emergency_stop(&mut self) {
642        self.inner.emergency_stop()
643    }
644
645    /// Set target velocity axis-angle representaiton in rad/s
646    ///
647    /// # Arguments
648    /// * target: axis-angle representation of the target velocity (rad/s)
649    pub fn set_target_velocity(&mut self, target: [f64; 3]) -> Result<()> {
650        let mut target_vel = target;
651        // apply the axis inversion
652        let inverted_axes = self.inner.output_inverted_axes();
653        for i in 0..3 {
654            if let Some(inverted) = inverted_axes[i] {
655                if inverted {
656                    target_vel[i] = -target_vel[i];
657                }
658            }
659        }
660
661        // calculate the velocity kinematics
662        let thetas = self.inner.get_current_position()?;
663        // input velocity - velocity of the motors
664        let mut theta_vel = self
665            .kinematics
666            .compute_input_velocity(thetas, target_vel.into());
667
668        // apply the reduction
669        let red = self.inner.reduction();
670        for i in 0..3 {
671            theta_vel[i] *= red[i].unwrap();
672        }
673
674        self.inner.set_target_velocity(theta_vel)
675    }
676
677    // pub fn get_target_velocity(&mut self) -> Result<[f64; 3]> {
678    //     let mut theta_vel = self.inner.get_target_velocity()?;
679    //     // calculate the velocity kinematics
680    //     let thetas = self.inner.get_current_position()?;
681    //     // input velocity - velocity of the motors
682    //     let mut target_vel = self
683    //         .kinematics
684    //         .compute_output_velocity(thetas, theta_vel.into());
685
686    //     let inverted_axes = self.inner.output_inverted_axes();
687    //     for i in 0..3 {
688    //         if let Some(inverted) = inverted_axes[i] {
689    //             if inverted {
690    //                 target_vel[i] = -target_vel[i];
691    //             }
692    //         }
693    //     }
694    //     // apply the reduction
695    //     let red = self.inner.reduction();
696    //     for i in 0..3 {
697    //         target_vel[i] /= red[i].unwrap();
698    //     }
699    //     Ok(target_vel)
700    // }
701
702    /// Set target torque axis-angle representaiton in N.m
703    ///
704    /// # Arguments
705    /// * target: axis-angle representation of the target torque (N.m)
706    pub fn set_target_torque(&mut self, target: [f64; 3]) -> Result<()> {
707        let mut target_torque = target;
708        // apply the axis inversion
709        let inverted_axes = self.inner.output_inverted_axes();
710        for i in 0..3 {
711            if let Some(inverted) = inverted_axes[i] {
712                if inverted {
713                    target_torque[i] = -target_torque[i];
714                }
715            }
716        }
717        // calculate the torque kinematics
718        let thetas = self.inner.get_current_position()?;
719        // input torque - torque of the motors
720        let mut theta_torque = self
721            .kinematics
722            .compute_input_torque(thetas, target_torque.into());
723        // aplly the reduction
724        let red = self.inner.reduction();
725        for i in 0..3 {
726            theta_torque[i] /= red[i].unwrap();
727        }
728
729        self.inner.set_target_torque(theta_torque)
730    }
731
732    // pub fn get_target_torque(&mut self) -> Result<[f64; 3]> {
733    //     let mut theta_torque = self.inner.get_target_torque()?;
734    //     // calculate the torque kinematics
735    //     let thetas = self.inner.get_current_position()?;
736    //     // input torque - torque of the motors
737    //     let mut target_torque = self
738    //         .kinematics
739    //         .compute_output_torque(thetas, theta_torque.into());
740
741    //     let inverted_axes = self.inner.output_inverted_axes();
742    //     for i in 0..3 {
743    //         if let Some(inverted) = inverted_axes[i] {
744    //             if inverted {
745    //                 target_torque[i] = -target_torque[i];
746    //             }
747    //         }
748    //     }
749    //     // aplly the reduction
750    //     let red = self.inner.reduction();
751    //     for i in 0..3 {
752    //         target_torque[i] /= red[i].unwrap();
753    //     }
754    //     Ok(target_torque)
755    // }
756}