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    pub inverted_axes: [Option<bool>; 3],
53}
54
55#[derive(Debug, Deserialize, Serialize)]
56/// Disks Config
57pub struct DisksConfig {
58    /// Zeros of each disk (in rad), used as an offset
59    pub zeros: ZeroType,
60    /// Reduction between the motor gearbox and the disk
61    pub reduction: f64,
62}
63
64#[derive(Debug, Deserialize, Serialize)]
65/// Zero type config
66/// This is used to configure the zero of each disk
67pub enum ZeroType {
68    /// ApproximateHardwareZero config
69    ApproximateHardwareZero(ApproximateHardwareZero),
70    /// ZeroStartup config
71    ZeroStartup(ZeroStartup),
72    /// HallZero config
73    HallZero(HallZero),
74    /// FirmwareZero config
75    FirmwareZero(FirmwareZero),
76}
77
78#[derive(Debug, Deserialize, Serialize)]
79/// Firmwarezero config
80pub struct FirmwareZero;
81
82#[derive(Debug, Deserialize, Serialize)]
83/// ApproximateHardwareZero config
84pub struct ApproximateHardwareZero {
85    /// Hardware zero of each disk (in rad)
86    pub hardware_zero: [f64; 3],
87}
88
89#[derive(Debug, Deserialize, Serialize)]
90/// HallZero config
91pub struct HallZero {
92    /// Hardware zero of each disk (in rad)
93    pub hardware_zero: [f64; 3],
94
95    /// Top/Middle/Bottom Hall active for the zero position
96    pub hall_indice: [u8; 3],
97}
98
99#[derive(Debug, Deserialize, Serialize)]
100/// ZeroStartup config
101pub struct ZeroStartup;
102
103/// Orbita3d Controller
104pub struct Orbita3dController {
105    inner: Box<dyn MotorsController<3> + Send>,
106    pub kinematics: Orbita3dKinematicsModel,
107}
108
109#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
110/// Feedback struct
111pub struct Orbita3dFeedback {
112    pub orientation: [f64; 4],
113}
114
115impl Orbita3dController {
116    /// Creates a new Orbita3dController using the given configuration file.
117    pub fn with_config(configfile: &str) -> Result<Self> {
118        log::info!("Loading config file: {}", configfile);
119
120        let f = match std::fs::File::open(configfile) {
121            Ok(f) => f,
122            Err(e) => {
123                log::error!("Error opening config file: {}", e);
124                return Err(e.into());
125            }
126        };
127        let config: Orbita3dConfig = serde_yaml::from_reader(f)?;
128
129        let controller: Box<dyn MotorsController<3> + Send> = match config.io {
130            // This is a legacy mode, not maintained anymore
131            #[cfg(feature = "build_dynamixel")]
132            Orbita3dIOConfig::DynamixelSerial(dxl_config) => match dxl_config.use_cache {
133                true => {
134                    let controller = CachedDynamixelSerialController::new(
135                        &dxl_config.serial_port,
136                        dxl_config.id,
137                        config.disks.zeros,
138                        config.disks.reduction,
139                    )?;
140
141                    log::info!("Using cached dynamixel controller {:?}", controller);
142
143                    Box::new(controller)
144                }
145                false => {
146                    let controller = DynamixelSerialController::new(
147                        &dxl_config.serial_port,
148                        dxl_config.id,
149                        config.disks.zeros,
150                        config.disks.reduction,
151                    )?;
152
153                    log::info!("Using dynamixel controller {:?}", controller);
154
155                    Box::new(controller)
156                }
157            },
158            // This is a legacy mode, not maintained anymore
159            #[cfg(feature = "build_dynamixel")]
160            Orbita3dIOConfig::DynamixelPoulpe(dxl_config) => match dxl_config.use_cache {
161                true => {
162                    let controller = CachedDynamixelPoulpeController::new(
163                        &dxl_config.serial_port,
164                        dxl_config.id,
165                        config.disks.zeros,
166                        config.disks.reduction,
167                    )?;
168
169                    log::info!("Using cached poulpe dynamixel controller {:?}", controller);
170
171                    Box::new(controller)
172                }
173                false => {
174                    let controller = DynamixelPoulpeController::new(
175                        &dxl_config.serial_port,
176                        dxl_config.id,
177                        config.disks.zeros,
178                        config.disks.reduction,
179                    )?;
180
181                    log::info!("Using poulpe dynamixel controller {:?}", controller);
182
183                    Box::new(controller)
184                }
185            },
186            // The fake mode is heavily used for testing and debugging, it allows to run everything without any hardware
187            Orbita3dIOConfig::FakeMotors(_) => {
188                let mut controller = FakeMotorsController::<3>::new();
189
190                let offsets = match config.disks.zeros {
191                    ZeroType::ApproximateHardwareZero(zero) => zero.hardware_zero,
192                    ZeroType::ZeroStartup(_) => controller.get_current_position()?,
193                    ZeroType::HallZero(zero) => zero.hardware_zero,
194                    ZeroType::FirmwareZero(_) => [0.0, 0.0, 0.0],
195                };
196
197                let controller = controller
198                    .with_offsets(offsets.map(Some))
199                    .with_reduction([Some(config.disks.reduction); 3])
200                    .with_inverted_axes(config.inverted_axes);
201
202                log::info!("Using fake motors controller {:?}", controller);
203
204                Box::new(controller)
205            }
206            // The ethercat mode with the "Poulpe" electronics
207            #[cfg(feature = "build_ethercat")]
208            Orbita3dIOConfig::PoulpeEthercat(ethercat_config) => {
209                let controller = EthercatPoulpeController::new(
210                    &ethercat_config.url,
211                    ethercat_config.id,
212                    ethercat_config.name,
213                    config.disks.zeros,
214                    config.disks.reduction,
215                    config.inverted_axes,
216                )?;
217                log::info!("Using poulpe ethercat controller {:?}", controller);
218
219                Box::new(controller)
220            }
221        };
222
223        Ok(Self {
224            inner: controller,
225            kinematics: config.kinematics_model,
226        })
227    }
228}
229
230impl Orbita3dController {
231    /// Check if the torque is ON or OFF
232    pub fn is_torque_on(&mut self) -> Result<bool> {
233        let torques = self.inner.is_torque_on()?;
234        assert!(torques.iter().all(|&t| t == torques[0]));
235        Ok(torques[0])
236    }
237    /// Enable the torque
238    ///
239    /// # Arguments
240    /// * reset_target: if true, the target position will be reset to the current position
241    pub fn enable_torque(&mut self, reset_target: bool) -> Result<()> {
242        if !self.is_torque_on()? && reset_target {
243            let thetas = self.inner.get_current_position()?;
244            thread::sleep(Duration::from_millis(1));
245            self.inner.set_target_position(thetas)?;
246            thread::sleep(Duration::from_millis(1));
247        }
248        self.inner.set_torque([true; 3])
249    }
250    /// Disable the torque
251    pub fn disable_torque(&mut self) -> Result<()> {
252        self.inner.set_torque([false; 3])
253    }
254
255    /// Get the current orientation (as quaternion (qx, qy, qz, qw))
256    pub fn get_current_orientation(&mut self) -> Result<[f64; 4]> {
257        let thetas = self.inner.get_current_position()?;
258        let rot = self.kinematics.compute_forward_kinematics(thetas);
259        Ok(conversion::rotation_matrix_to_quaternion(rot))
260    }
261
262    /// Get the current orientation (as intrinsic rpy with multiturn)
263    pub fn get_current_rpy_orientation(&mut self) -> Result<[f64; 3]> {
264        let thetas = self.inner.get_current_position()?;
265        let inverted_axes = self.inner.output_inverted_axes();
266        let mut rpy = self
267            .kinematics
268            .compute_forward_kinematics_rpy_multiturn(thetas)?;
269
270        for i in 0..3 {
271            if let Some(inverted) = inverted_axes[i] {
272                if inverted {
273                    rpy[i] = -rpy[i];
274                }
275            }
276        }
277
278        Ok(rpy)
279    }
280
281    /// 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)
282    pub fn get_current_velocity(&mut self) -> Result<[f64; 3]> {
283        let thetas = self.inner.get_current_position()?;
284        let input_velocity = self.inner.get_current_velocity()?;
285
286        let rot = self
287            .kinematics
288            .compute_output_velocity(thetas, input_velocity);
289        Ok(rot.into())
290    }
291    /// Get the current torque (as pseudo vector)
292    pub fn get_current_torque(&mut self) -> Result<[f64; 3]> {
293        let thetas = self.inner.get_current_position()?;
294        let input_torque = self.inner.get_current_torque()?;
295
296        Ok(self
297            .kinematics
298            .compute_output_torque(thetas, input_torque)
299            .into())
300    }
301
302    /// Get the target orientation (as quaternion (qx, qy, qz, qw))
303    pub fn get_target_orientation(&mut self) -> Result<[f64; 4]> {
304        let thetas = self.inner.get_target_position()?;
305        let rot = self.kinematics.compute_forward_kinematics(thetas);
306        Ok(conversion::rotation_matrix_to_quaternion(rot))
307    }
308
309    /// Get the target orientation (as intrinsic rpy with multiturn)
310    pub fn get_target_rpy_orientation(&mut self) -> Result<[f64; 3]> {
311        let thetas = self.inner.get_target_position()?;
312        let rpy = self
313            .kinematics
314            .compute_forward_kinematics_rpy_multiturn(thetas)?;
315        Ok(rpy)
316    }
317
318    /// Set the target orientation (as quaternion (qx, qy, qz, qw))
319    pub fn set_target_orientation(&mut self, target: [f64; 4]) -> Result<()> {
320        let rot =
321            conversion::quaternion_to_rotation_matrix(target[0], target[1], target[2], target[3]);
322        let thetas = self.kinematics.compute_inverse_kinematics(rot)?;
323        self.inner.set_target_position(thetas)
324    }
325
326    /// Set the target orientation fro the roll pitch yaw intrinsic angles (taking multi turn into account)
327    pub fn set_target_rpy_orientation(&mut self, target: [f64; 3]) -> Result<()> {
328        let inverted_axes = self.inner.output_inverted_axes();
329        let mut rpy_target = target;
330
331        for i in 0..3 {
332            if let Some(inverted) = inverted_axes[i] {
333                if inverted {
334                    rpy_target[i] = -rpy_target[i];
335                }
336            }
337        }
338
339        let thetas = self
340            .kinematics
341            .compute_inverse_kinematics_rpy_multiturn(rpy_target)?;
342
343        self.inner.set_target_position(thetas)
344    }
345
346    /// Set the target orientation (as quaternion (qx, qy, qz, qw)) with feedback
347    pub fn set_target_orientation_fb(&mut self, target: [f64; 4]) -> Result<Orbita3dFeedback> {
348        let rot =
349            conversion::quaternion_to_rotation_matrix(target[0], target[1], target[2], target[3]);
350
351        let thetas = self.kinematics.compute_inverse_kinematics(rot)?;
352
353        let fb: Result<[f64; 3]> = self.inner.set_target_position_fb(thetas);
354        match fb {
355            Ok(fb) => {
356                let rot = self
357                    .kinematics
358                    .compute_forward_kinematics([fb[0], fb[1], fb[2]]); //Why the f*ck can't I use slice here?
359                                                                        // let vel = self
360                                                                        //     .kinematics
361                                                                        //     .compute_output_velocity(thetas, [fb[3], fb[4], fb[5]]);
362                                                                        // let torque = self
363                                                                        //     .kinematics
364                                                                        //     .compute_output_torque(thetas, [fb[6], fb[7], fb[8]]);
365
366                Ok(Orbita3dFeedback {
367                    orientation: conversion::rotation_matrix_to_quaternion(rot),
368                    // velocity: [vel[0], vel[1], vel[2]],
369                    // torque: [torque[0], torque[1], torque[2]],
370                })
371            }
372            Err(e) => Err(e),
373        }
374    }
375
376    /// Set the target orientation from roll pitch yaw (accepts yaw>180°) intrinsic angles with feedback => returns feedback rpy
377    pub fn set_target_rpy_orientation_fb(&mut self, target: [f64; 3]) -> Result<[f64; 3]> {
378        let inverted_axes = self.inner.output_inverted_axes();
379        let mut rpy_target = target;
380
381        for i in 0..3 {
382            if let Some(inverted) = inverted_axes[i] {
383                if inverted {
384                    rpy_target[i] = -rpy_target[i];
385                }
386            }
387        }
388
389        let thetas = self
390            .kinematics
391            .compute_inverse_kinematics_rpy_multiturn(rpy_target)?;
392        let fb: Result<[f64; 3]> = self.inner.set_target_position_fb(thetas);
393
394        match fb {
395            Ok(fb) => {
396                let mut rpy = self
397                    .kinematics
398                    .compute_forward_kinematics_rpy_multiturn([fb[0], fb[1], fb[2]])?;
399                for i in 0..3 {
400                    if let Some(inverted) = inverted_axes[i] {
401                        if inverted {
402                            rpy[i] = -rpy[i];
403                        }
404                    }
405                }
406                Ok(rpy)
407            }
408
409            Err(e) => Err(e),
410        }
411    }
412
413    /// Get the position of each raw motor (in rad)
414    /// caution: this is the raw value used by the motors used inside the actuator, not the orbita3d orientation!
415    pub fn get_raw_motors_positions(&mut self) -> Result<[f64; 3]> {
416        let red = self.inner.reduction();
417        match self.inner.get_current_position() {
418            Ok(p) => {
419                let mut pos = p;
420                for i in 0..3 {
421                    pos[i] *= red[i].unwrap();
422                }
423                Ok(pos)
424            }
425            Err(e) => Err(e),
426        }
427    }
428
429    /// Get the velocity limit of each raw motor (in rad/s)
430    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
431    pub fn get_raw_motors_velocity_limit(&mut self) -> Result<[f64; 3]> {
432        self.inner.get_velocity_limit()
433    }
434    /// Set the velocity limit of each raw motor (in rad/s)
435    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
436    pub fn set_raw_motors_velocity_limit(&mut self, limit: [f64; 3]) -> Result<()> {
437        self.inner.set_velocity_limit(limit)
438    }
439    /// Get the torque limit of each raw motor (in N.m)
440    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
441    pub fn get_raw_motors_torque_limit(&mut self) -> Result<[f64; 3]> {
442        self.inner.get_torque_limit()
443    }
444    /// Set the torque limit of each raw motor (in N.m)
445    /// caution: this is the raw value used by the motors used inside the actuator, not a limit to orbita3d orientation!
446    pub fn set_raw_motors_torque_limit(&mut self, limit: [f64; 3]) -> Result<()> {
447        self.inner.set_torque_limit(limit)
448    }
449    /// Get the pid gains of each raw motor
450    /// caution: this is the raw value used by the motors used inside the actuator, not on orbita3d orientation!
451    pub fn get_raw_motors_pid_gains(&mut self) -> Result<[PID; 3]> {
452        self.inner.get_pid_gains()
453    }
454    /// Set the pid gains of each raw motor
455    /// caution: this is the raw value used by the motors used inside the actuator, not on orbita3d orientation!
456    pub fn set_raw_motors_pid_gains(&mut self, gains: [PID; 3]) -> Result<()> {
457        self.inner.set_pid_gains(gains)
458    }
459
460    /// Get the raw motors velocity in rad/s (top, middle, bottom)
461    pub fn get_raw_motors_velocity(&mut self) -> Result<[f64; 3]> {
462        let red = self.inner.reduction();
463        match self.inner.get_current_velocity() {
464            Ok(v) => {
465                let mut vel = v;
466                for i in 0..3 {
467                    vel[i] *= red[i].unwrap();
468                }
469                Ok(vel)
470            }
471            Err(e) => Err(e),
472        }
473    }
474    /// Get the raw motors current in mA (top, middle, bottom)
475    pub fn get_raw_motors_current(&mut self) -> Result<[f64; 3]> {
476        let red = self.inner.reduction();
477        match self.inner.get_current_torque() {
478            Ok(t) => {
479                let mut tor = t;
480                for i in 0..3 {
481                    tor[i] /= red[i].unwrap();
482                }
483                Ok(tor)
484            }
485            Err(e) => Err(e),
486        }
487    }
488
489    /// Get the axis sensors values (gearbox mounted absolute magnetic encoder)
490    pub fn get_axis_sensors(&mut self) -> Result<[f64; 3]> {
491        let red = self.inner.reduction();
492        match self.inner.get_axis_sensors() {
493            Ok(a) => {
494                let mut ax = a;
495                for i in 0..3 {
496                    ax[i] *= red[i].unwrap();
497                }
498                Ok(ax)
499            }
500            Err(e) => Err(e),
501        }
502    }
503
504    /// Get the axis sensor zeros values (random offset from factory)
505    pub fn get_axis_sensor_zeros(&mut self) -> Result<[f64; 3]> {
506        self.inner.get_axis_sensor_zeros()
507    }
508
509    /// Get the error codes from the motors
510    pub fn get_error_codes(&mut self) -> Result<[i32; 3]> {
511        self.inner.get_error_codes()
512    }
513
514    /// Get the temperature for each motor in °C from a NTC sensor in contact with the motor body (top, middle, bottom)
515    pub fn get_motor_temperatures(&mut self) -> Result<[f64; 3]> {
516        self.inner.get_motor_temperatures()
517    }
518
519    /// Get the temperature for each H-Gate in °C (top, middle, bottom)
520    pub fn get_board_temperatures(&mut self) -> Result<[f64; 3]> {
521        self.inner.get_board_temperatures()
522    }
523
524    /// Get the board state register
525    pub fn get_board_state(&mut self) -> Result<u8> {
526        self.inner.get_board_state()
527    }
528    /// Get the board state register
529    pub fn set_board_state(&mut self, state: u8) -> Result<()> {
530        self.inner.set_board_state(state)
531    }
532
533    /// Get the current mode of operation (cf. firmware_poulpe documentation)
534    pub fn get_control_mode(&mut self) -> Result<[u8; 3]> {
535        self.inner.get_control_mode()
536    }
537
538    /// Set the current mode of operation (cf. firmware_poulpe documentation). Currently valid: 0=NoMode, 1=ProfilePositionMode, 3=ProfileVelocityMode, 4=ProfileTorqueMode
539    pub fn set_control_mode(&mut self, mode: [u8; 3]) -> Result<()> {
540        self.inner.set_control_mode(mode)
541    }
542
543    /// Triggers and emergency stop
544    pub fn emergency_stop(&mut self) {
545        self.inner.emergency_stop()
546    }
547}