orbita3d_controller/io/
poulpe_ethercat.rs

1use crate::MotorGearboxConfig;
2use motor_toolbox_rs::{Limit, MotorsController, RawMotorsIO, Result, PID};
3use poulpe_ethercat_grpc::client::PoulpeRemoteClient;
4use serde::{Deserialize, Serialize};
5use std::{f64::consts::PI, f64::consts::TAU, thread, time::Duration};
6
7use log::{error, info};
8
9use crate::ZeroType;
10
11#[derive(Debug, Deserialize, Serialize)]
12/// EthercatPoulpeController Config
13pub struct PoulpeEthercatConfig {
14    /// url of the ethercat master grpc serverS
15    pub url: String,
16    /// Actuator id
17    pub id: Option<u8>,
18    /// Actuator name
19    pub name: Option<String>,
20}
21
22#[derive(Debug)]
23/// EthercatPoulpeController - wrapper around the three disks motors
24pub struct EthercatPoulpeController {
25    io: PoulpeRemoteClient,
26    id: u16,
27
28    offsets: [Option<f64>; 3],
29    reduction: [Option<f64>; 3],
30    // motor_reduction: [Option<f64>; 3],
31    limits: [Option<Limit>; 3],
32    inverted_axes: [Option<bool>; 3],
33    axis_sensor_zeros: [Option<f64>; 3],
34
35    motor_gearbox_params: Option<MotorGearboxConfig>,
36    #[allow(dead_code)]
37    default_mode: Option<u8>,
38}
39
40impl EthercatPoulpeController {
41    /// Creates a new EthercatPoulpeController
42    #[allow(clippy::too_many_arguments)]
43    pub fn new(
44        url: &str,
45        id: Option<u8>,
46        name: Option<String>,
47        zero: ZeroType,
48        reductions: f64,
49        inverted_axes: [Option<bool>; 3],
50        motor_gearbox_params: Option<MotorGearboxConfig>,
51        default_mode: Option<u8>,
52    ) -> Result<Self> {
53        let update_time = Duration::from_secs_f32(0.002);
54
55        let mut io = match (id, name) {
56            (_, Some(name)) => {
57                log::info!("Connecting to the slave with name: {}", name);
58
59                match PoulpeRemoteClient::connect_with_name(url.parse()?, vec![name], update_time) {
60                    Ok(client) => client,
61                    Err(e) => {
62                        error!(
63                            "Error while connecting to EthercatPoulpeController: {:?}",
64                            e
65                        );
66                        return Err("Error while connecting to EthercatPoulpeController".into());
67                    }
68                }
69            }
70            (Some(id), None) => {
71                log::info!("Connecting to the slave with id: {}", id);
72
73                match PoulpeRemoteClient::connect(url.parse()?, vec![id as u16], update_time) {
74                    Ok(client) => client,
75                    Err(e) => {
76                        error!(
77                            "Error while connecting to EthercatPoulpeController: {:?}",
78                            e
79                        );
80                        return Err("Error while connecting to EthercatPoulpeController".into());
81                    }
82                }
83            }
84            _ => {
85                log::error!("Invalid config file, make sure to provide either the id or the name!");
86                return Err("Invalid config file".into());
87            }
88        };
89        let id = io.ids[0];
90        let name = io.names[0].clone();
91
92        // wait for the connection to be established
93        let mut trials = 20; // 2s
94        while io.get_state(id as u16).is_err() {
95            thread::sleep(Duration::from_millis(100));
96            log::warn!(
97                "Waiting for connection to Orbita3d PoulpeRemoteClient with id {}",
98                id
99            );
100            if trials == 0 {
101                log::error!(
102                    "Error: Timeout while connecting to the Orbita3d PoulpeRemoteClient with id {}",
103                    id
104                );
105                return Err(
106                    "Error: Timeout while connecting to the Orbita3d  PoulpeRemoteClient".into(),
107                );
108            }
109            trials -= 1;
110        }
111        log::info!(
112            "Connected Orbita3d Client created for Slave {} (id: {}), sampling time: {:}ms",
113            name,
114            id,
115            update_time.as_millis()
116        );
117
118        // set the initial velocity and torque limit to 100%
119        io.set_velocity_limit(id as u16, [1.0; 3].to_vec());
120        io.set_torque_limit(id as u16, [1.0; 3].to_vec());
121
122        if let Some(mode) = default_mode {
123            if mode == 0 || mode == 1 || mode == 2 || mode == 3 {
124                io.set_mode_of_operation(id as u16, mode.into()); //0=NoMode, 1=ProfilePositionMode, 3=ProfileVelocityMode, 4=ProfileTorqueMode
125            } else {
126                io.set_mode_of_operation(id as u16, 1); //0=NoMode, 1=ProfilePositionMode, 3=ProfileVelocityMode, 4=ProfileTorqueMode
127            }
128        } else {
129            //We can only change the mode if torque=off, then we ensure we are ProfilePositionMode
130            io.set_mode_of_operation(id as u16, 1); //0=NoMode, 1=ProfilePositionMode, 3=ProfileVelocityMode, 4=ProfileTorqueMode
131        }
132        let mut poulpe_controller = EthercatPoulpeController {
133            io,
134            id: id as u16,
135            offsets: [None; 3],
136            reduction: [Some(reductions); 3],
137            limits: [None; 3],
138            inverted_axes,
139            axis_sensor_zeros: [None; 3],
140            motor_gearbox_params,
141            default_mode,
142        };
143
144        info!(
145            "Orbita3d EthercatPoulpeController:\n\t - url: {:?}\n\t - id: {:?}",
146            url, id
147        );
148
149        log::info!("Creacting controller");
150
151        match zero {
152            // deprecated
153            ZeroType::ApproximateHardwareZero(zero) => {
154                log::info!("ApproximateHardwarezero");
155
156                let current_pos = MotorsController::get_current_position(&mut poulpe_controller)?;
157                log::info!("Current position: {:?}", current_pos);
158
159                zero.hardware_zero
160                    .iter()
161                    .zip(current_pos.iter())
162                    .enumerate()
163                    .for_each(|(i, (&hardware_zero, &current_pos))| {
164                        poulpe_controller.offsets[i] = Some(find_closest_offset_to_zero(
165                            current_pos,
166                            hardware_zero,
167                            reductions,
168                        ));
169                    });
170            }
171            ZeroType::FirmwareZero(_) => {
172                log::info!(
173                    "FirmwareZero => zero has been done in firmware, no need to do it here."
174                );
175                //we only need to get these once, they are stored in the firmware
176                //beware offsets in config file are also used
177                let axis_sensor_zeros =
178                    MotorsController::get_axis_sensor_zeros(&mut poulpe_controller)?;
179                log::info!("Axis sensor zeros: {:?}", axis_sensor_zeros);
180                poulpe_controller.axis_sensor_zeros = [
181                    Some(axis_sensor_zeros[0]),
182                    Some(axis_sensor_zeros[1]),
183                    Some(axis_sensor_zeros[2]),
184                ];
185            }
186            //deprecated
187            ZeroType::ZeroStartup(_) => {
188                log::info!("ZeroStartup");
189
190                let current_pos = MotorsController::get_current_position(&mut poulpe_controller)?;
191
192                log::info!("Current position: {:?}", current_pos);
193
194                current_pos
195                    .iter()
196                    .enumerate()
197                    .for_each(|(i, &current_pos)| {
198                        poulpe_controller.offsets[i] = Some(current_pos);
199                    });
200            }
201            ZeroType::HallZero(_zero) => {
202                log::error!("HallZero Not supported with Ethercat!");
203            }
204        }
205
206        Ok(poulpe_controller)
207    }
208
209    pub fn get_rpy_inverted_axes(&self) -> [Option<bool>; 3] {
210        self.inverted_axes
211    }
212
213    pub fn id(&self) -> u8 {
214        self.id as u8
215    }
216}
217
218impl MotorsController<3> for EthercatPoulpeController {
219    fn io(&mut self) -> &mut dyn RawMotorsIO<3> {
220        self
221    }
222
223    fn offsets(&self) -> [Option<f64>; 3] {
224        self.offsets
225    }
226
227    fn reduction(&self) -> [Option<f64>; 3] {
228        let mut reduction = [None; 3];
229        reduction.iter_mut().enumerate().for_each(|(i, r)| {
230            *r = Some(self.reduction[i].unwrap());
231        });
232        reduction
233    }
234
235    fn limits(&self) -> [Option<motor_toolbox_rs::Limit>; 3] {
236        self.limits
237    }
238
239    // fn inverted_axes(&self) -> [Option<bool>; 3] {
240    //     // self.inverted_axes
241    //     [None, None, None] //For Orbita3d, we need to inverse axes in the roll/pitch/yaw space...
242    // }
243    fn output_inverted_axes(&self) -> [Option<bool>; 3] {
244        self.inverted_axes
245    }
246
247    fn torque_current_ratio(&self) -> Option<f64> {
248        if self.motor_gearbox_params.is_none() {
249            None
250        } else {
251            let params = self.motor_gearbox_params.as_ref().unwrap();
252            Some(
253                params.motor_nominal_torque
254                    * params.motor_efficiency
255                    * params.motor_gearbox_efficiency
256                    / params.motor_nominal_current
257                    * params.motor_gearbox_ratio,
258            )
259        }
260    }
261}
262
263impl RawMotorsIO<3> for EthercatPoulpeController {
264    fn name(&self) -> String {
265        "EthercatPoulpeController".to_string()
266    }
267
268    fn is_torque_on(&mut self) -> Result<[bool; 3]> {
269        match self.io.get_torque_state(self.id) {
270            Ok(state) => Ok([state, state, state]),
271            Err(_) => Err("Error while getting torque state".into()),
272        }
273    }
274
275    fn set_torque(&mut self, on: [bool; 3]) -> Result<()> {
276        assert!(on.iter().all(|&t| t == on[0]));
277        if on.iter().any(|&x| x) {
278            self.io.turn_on(self.id);
279        } else {
280            self.io.turn_off(self.id);
281        }
282        Ok(())
283    }
284
285    fn get_current_position(&mut self) -> Result<[f64; 3]> {
286        match self.io.get_position_actual_value(self.id) {
287            Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
288            Err(_) => Err("Error while getting position".into()),
289        }
290    }
291
292    fn get_current_velocity(&mut self) -> Result<[f64; 3]> {
293        match self.io.get_velocity_actual_value(self.id) {
294            Ok(velocity) => Ok([velocity[0] as f64, velocity[1] as f64, velocity[2] as f64]),
295            Err(_) => Err("Error while getting velocity".into()),
296        }
297    }
298
299    fn get_current_torque(&mut self) -> Result<[f64; 3]> {
300        match self.io.get_torque_actual_value(self.id) {
301            Ok(torque) => Ok([torque[0] as f64, torque[1] as f64, torque[2] as f64]),
302            Err(_) => Err("Error while getting torque".into()),
303        }
304    }
305
306    fn get_target_position(&mut self) -> Result<[f64; 3]> {
307        match self.io.get_target_position(self.id) {
308            Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
309            Err(_) => Err("Error while getting target position".into()),
310        }
311    }
312
313    fn set_target_position(&mut self, position: [f64; 3]) -> Result<()> {
314        let target_position = position.iter().map(|&x| x as f32).collect::<Vec<f32>>();
315        self.io.set_target_position(self.id, target_position);
316        Ok(())
317    }
318
319    //TODO add thit in poulpe_ethercat_controller
320    // fn get_target_velocity(&mut self) -> Result<[f64; 3]> {
321    //     match self.io.get_target_velocity(self.id) {
322    //         Ok(vel) => Ok([vel[0] as f64, vel[1] as f64, vel[2] as f64]),
323    //         Err(_) => Err("Error while getting target velocity".into()),
324    //     }
325    // }
326
327    fn set_target_velocity(&mut self, vel: [f64; 3]) -> Result<()> {
328        let target_velocity = vel.iter().map(|&x| x as f32).collect::<Vec<f32>>();
329        self.io.set_target_velocity(self.id, target_velocity);
330        Ok(())
331    }
332
333    //TODO add thit in poulpe_ethercat_controller
334    // fn get_target_torque(&mut self) -> Result<[f64; 3]> {
335    //     match self.io.get_target_torque(self.id) {
336    //         Ok(vel) => Ok([vel[0] as f64, vel[1] as f64, vel[2] as f64]),
337    //         Err(_) => Err("Error while getting target torque".into()),
338    //     }
339    // }
340
341    fn set_target_torque(&mut self, vel: [f64; 3]) -> Result<()> {
342        let target_torque = vel.iter().map(|&x| x as f32).collect::<Vec<f32>>();
343        self.io.set_target_torque(self.id, target_torque);
344        Ok(())
345    }
346
347    fn set_target_position_fb(&mut self, position: [f64; 3]) -> Result<[f64; 3]> {
348        let target_position = position.iter().map(|&x| x as f32).collect::<Vec<f32>>();
349        self.io.set_target_position(self.id, target_position);
350
351        match self.io.get_position_actual_value(self.id) {
352            Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
353            Err(_) => Err("Error while getting position".into()),
354        }
355    }
356
357    fn get_velocity_limit(&mut self) -> Result<[f64; 3]> {
358        match self.io.get_velocity_limit(self.id) {
359            Ok(limit) => Ok([limit[0] as f64, limit[1] as f64, limit[2] as f64]),
360            Err(_) => Err("Error while getting velocity limit".into()),
361        }
362    }
363
364    fn set_velocity_limit(&mut self, velocity: [f64; 3]) -> Result<()> {
365        let velocity_limit = velocity.iter().map(|&x| x as f32).collect::<Vec<f32>>();
366        self.io.set_velocity_limit(self.id, velocity_limit);
367        Ok(())
368    }
369
370    fn get_torque_limit(&mut self) -> Result<[f64; 3]> {
371        match self.io.get_torque_limit(self.id) {
372            Ok(limit) => Ok([limit[0] as f64, limit[1] as f64, limit[2] as f64]),
373            Err(_) => Err("Error while getting torque limit".into()),
374        }
375    }
376
377    fn set_torque_limit(&mut self, torque: [f64; 3]) -> Result<()> {
378        let torque_limit = torque.iter().map(|&x| x as f32).collect::<Vec<f32>>();
379        self.io.set_torque_limit(self.id, torque_limit);
380        Ok(())
381    }
382
383    //TODO
384    fn get_pid_gains(&mut self) -> Result<[PID; 3]> {
385        Ok([PID {
386            p: 0.0,
387            i: 0.0,
388            d: 0.0,
389        }; 3])
390    }
391
392    fn set_pid_gains(&mut self, _pid: [PID; 3]) -> Result<()> {
393        Ok(())
394    }
395    fn get_axis_sensors(&mut self) -> Result<[f64; 3]> {
396        match self.io.get_axis_sensors(self.id) {
397            Ok(mut sensor) => {
398                // substract the sensor zero and the axis offset
399                // FIXME: these are not the "output" angles. We need to compute kinematics and handle multiturn...
400                for (i, s) in sensor.iter_mut().enumerate() {
401                    // apply the gearing ratio first
402                    *s *= 1.0 / self.reduction[i].unwrap() as f32;
403                    // substract the zero and the offset
404                    if self.axis_sensor_zeros[i].is_some() {
405                        *s -= self.axis_sensor_zeros[i].unwrap() as f32;
406                    }
407                    // remove any offset
408                    if self.offsets[i].is_some() {
409                        *s -= self.offsets[i].unwrap() as f32;
410                    }
411                    // wrap to pi
412                    *s = wrap_to_pi(*s as f64) as f32;
413                }
414                Ok([sensor[0] as f64, sensor[1] as f64, sensor[2] as f64])
415            }
416
417            Err(_) => Err("Error while getting axis sensors".into()),
418        }
419    }
420
421    fn get_axis_sensor_zeros(&mut self) -> Result<[f64; 3]> {
422        match self.io.get_axis_sensor_zeros(self.id) {
423            Ok(sensor) => Ok([sensor[0] as f64, sensor[1] as f64, sensor[2] as f64]),
424            Err(_) => Err("Error while getting axis sensor zeros".into()),
425        }
426    }
427
428    fn get_board_state(&mut self) -> Result<u8> {
429        match self.io.get_state(self.id) {
430            Ok(state) => Ok(state as u8),
431            Err(_) => Err("Error while getting board state".into()),
432        }
433    }
434
435    fn get_error_codes(&mut self) -> Result<[i32; 3]> {
436        match self.io.get_error_codes(self.id) {
437            Ok(codes) => Ok([codes[0], codes[1], codes[2]]),
438            Err(_) => Err("Error while getting error codes".into()),
439        }
440    }
441
442    fn get_motor_temperatures(&mut self) -> Result<[f64; 3]> {
443        match self.io.get_motor_temperatures(self.id) {
444            Ok(temp) => Ok([temp[0] as f64, temp[1] as f64, temp[2] as f64]),
445            Err(_) => Err("Error while getting motor temperatures".into()),
446        }
447    }
448
449    fn get_board_temperatures(&mut self) -> Result<[f64; 3]> {
450        match self.io.get_board_temperatures(self.id) {
451            Ok(temp) => Ok([temp[0] as f64, temp[1] as f64, temp[2] as f64]),
452            Err(_) => Err("Error while getting board temperatures".into()),
453        }
454    }
455
456    fn set_board_state(&mut self, _state: u8) -> Result<()> {
457        Ok(())
458    }
459
460    fn get_control_mode(&mut self) -> Result<[u8; 3]> {
461        match self.io.get_mode_of_operation(self.id) {
462            Ok(mode) => Ok([mode as u8, mode as u8, mode as u8]), //It is in fact the same for each axis (TODO make it board level?)
463            Err(_) => Err("Error while getting mode of operation".into()),
464        }
465    }
466
467    fn set_control_mode(&mut self, _mode: [u8; 3]) -> Result<()> {
468        if !(_mode[0] == _mode[1] && _mode[1] == _mode[2]) {
469            return Err("Error, invalid control mode".into());
470        }
471        self.io.set_mode_of_operation(self.id, _mode[0] as u32);
472        Ok(())
473    }
474
475    fn emergency_stop(&mut self) {
476        self.io.emergency_stop(self.id);
477        error!("EMERCENCY STOP!");
478    }
479}
480
481fn find_closest_offset_to_zero(current_position: f64, hardware_zero: f64, reduction: f64) -> f64 {
482    //! Find the closest offset to zero
483    //!
484    //! The absolute encoder is placed before orbita reduction.
485    //! Thus, we do not have the real orbita disk absolute position.
486    //! But we only know where we are in a local dial of arc (2pi / reduction).
487    //!
488    //! To find the closest offset to our zero hardware, we assume that the current position is at maximum one dial from the hardware zero.
489    log::info!(
490        "find_closest_offset_to_zero: current_position: {}, hardware_zero: {}, reduction: {}",
491        current_position,
492        hardware_zero,
493        reduction
494    );
495
496    let dial_arc = 2.0 * PI / reduction;
497
498    let possibilities = [
499        hardware_zero - dial_arc,
500        hardware_zero,
501        hardware_zero + dial_arc,
502    ];
503    log::debug!("possibilities: {:?}", possibilities);
504
505    let best = possibilities
506        .iter()
507        .map(|&p| (p - current_position).abs())
508        .enumerate()
509        .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
510        .map(|(i, _)| possibilities[i])
511        .unwrap();
512
513    log::info!("best: {}", best);
514    best
515}
516
517#[allow(dead_code)]
518fn find_position_with_hall(
519    current_position: f64,
520    hardware_zero: f64,
521    hall_zero: f64,
522    hall_index: u8,
523    reduction: f64,
524) -> (f64, i16) {
525    //! Find the current position corrected by the hall sensor
526    //! There is 16 Hall sensors on the disk output and a ratio 'reduction' between the disk and the motor gearbox output
527    //! We knwow the 'hall_zero' index which correspond to the index of the Hall sensor closest to the disk zero position
528    //! We also know the 'hall_index' which is the index of the Hall sensor closest to the current position
529    //! Finally we know the 'hardware_zero' which is the position of the disk zero
530
531    const MAX_TURN: usize = 3;
532    let mut offset: [f64; MAX_TURN] = [0.0; MAX_TURN];
533    let mut offset_search: [f64; MAX_TURN] = [0.0; MAX_TURN];
534    let turn_offset = 2.0 * PI * reduction;
535    let hall_offset = 2.0 * PI / 16.0 * reduction; //22.5° disk for each Hall sensor
536
537    // let hall_diff = hall_diff(hall_index, hall_zero);
538
539    let diff_gear = current_position * reduction - hardware_zero * reduction;
540    let shortest_diff_gear = angle_diff(current_position * reduction, hardware_zero * reduction); //nul FIXME
541    let shortest_to_zero = angle_diff(0.0, hardware_zero * reduction);
542
543    let pos = (current_position * reduction) % TAU; //this should be the raw gearbox position
544    let _shortest_to_current = angle_diff(0.0, pos);
545    let _gearbox_turn = 0.0;
546
547    log::debug!(
548        "Diff: {:?} shortest diff: {:?} shortest_to_zero {:?} hall_zero_angle: {:?}",
549        diff_gear,
550        shortest_diff_gear,
551        shortest_to_zero,
552        hall_zero
553    );
554
555    for i in 0..offset.len() {
556        // theoretical position of the gearbox starting from the zero and moving toward detected hall
557
558        offset_search[i] = (hardware_zero * reduction) % TAU
559            + (hall_zero * reduction) % TAU
560            + ((i as f64 - (offset.len() / 2) as f64) * turn_offset) % TAU;
561        offset_search[i] %= TAU;
562
563        let residual = angle_diff(
564            pos,
565            (hardware_zero * reduction) % TAU + (hall_zero * reduction) % TAU,
566        ) / reduction;
567
568        // Offset to apply
569        offset[i] = current_position
570            - hall_zero
571            - residual
572            - (i as f64 - (offset.len() / 2) as f64)
573                * (turn_offset / reduction - TAU * (reduction - reduction.floor()) / reduction);
574
575        //in orbita ref
576    }
577
578    log::debug!(
579        "Residual (gearbox) {:?} (orbita) {:?}",
580        angle_diff(pos, (hall_zero * reduction) % TAU),
581        angle_diff(pos, (hall_zero * reduction) % TAU) / reduction
582    );
583    log::debug!("possible offset (orbita domain): {:?}", offset);
584    log::debug!("searching offset (gearbox domain): {:?}", offset_search);
585
586    log::debug!(
587        "current pos (gearbox): {:?} hardware_zero (gearbox): {:?} hall_idx: {:?} hall_zero: {:?} hall_offset: {:?} turn_offset: {:?}",
588        pos,
589        hardware_zero * reduction,
590        hall_index as f64,
591        hall_zero,
592        hall_offset,
593	turn_offset
594    );
595
596    let best = offset_search
597        .iter()
598        .map(|&p| {
599            let d = angle_diff(p, pos).abs();
600            log::debug!("Diff search: {:?}", d);
601            d
602        })
603        .enumerate()
604        .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
605        .map(|(i, _)| offset[i])
606        .unwrap();
607
608    let best_idx = offset.iter().position(|&x| x == best).unwrap();
609    log::debug!(
610        "best offset (orbita domain): {} gearbox domain: {:?}",
611        best,
612        offset_search[best_idx]
613    );
614    log::debug!(
615        "It corresponds to {} turn (orbita domain)",
616        best_idx as i16 - (offset.len() / 2) as i16
617    );
618
619    (best, best_idx as i16 - (offset.len() / 2) as i16)
620}
621
622#[allow(dead_code)]
623pub fn angle_diff(angle_a: f64, angle_b: f64) -> f64 {
624    let mut angle = angle_a - angle_b;
625    angle = (angle + PI) % TAU - PI;
626    if angle < -PI {
627        angle + TAU
628    } else {
629        angle
630    }
631}
632
633#[allow(dead_code)]
634pub fn hall_diff(hall_a: u8, hall_b: u8) -> f64 {
635    // shortest hall difference (16 discrete Hall)
636    let d: f64 = hall_a as f64 - hall_b as f64;
637    if d >= 0.0 {
638        if d >= 8.0 {
639            d - 16.0
640        } else {
641            d
642        }
643    } else if d >= -8.0 {
644        d
645    } else {
646        d + 16.0
647    }
648}
649
650// function wrapping an angle in radians to
651// the range [-pi, pi]
652fn wrap_to_pi(angle: f64) -> f64 {
653    (((angle + PI) % (2.0 * PI)) + (2.0 * PI)) % (2.0 * PI) - PI
654}
655
656#[cfg(test)]
657mod tests {
658    use rand::Rng;
659    use std::f64::consts::PI;
660
661    use crate::{
662        io::{poulpe_ethercat::find_closest_offset_to_zero, Orbita3dIOConfig},
663        Orbita3dConfig,
664    };
665
666    #[test]
667    fn parse_config_file() {
668        let f = std::fs::File::open("./config/ethercat_poulpe.yaml").unwrap();
669
670        let config: Result<Orbita3dConfig, _> = serde_yaml::from_reader(f);
671        assert!(config.is_ok());
672
673        let config = config.unwrap();
674
675        if let Orbita3dIOConfig::PoulpeEthercat(dxl_config) = config.io {
676            assert_eq!(config.kinematics_model.alpha, 54.0_f64.to_radians());
677            assert_eq!(config.kinematics_model.gamma_min, 40.0_f64.to_radians());
678            assert_eq!(config.kinematics_model.offset, 0.0);
679            assert_eq!(config.kinematics_model.beta, PI / 2.0);
680            assert_eq!(config.kinematics_model.gamma_max, PI);
681            assert!(config.kinematics_model.passiv_arms_direct);
682
683            if let crate::ZeroType::FirmwareZero(_) = config.disks.zeros {
684            } else {
685                panic!("Wrong config type");
686            }
687            assert_eq!(config.disks.reduction, 5.333_333_333_333_333);
688            // assert_eq!(config.disks.reduction, 4.2666667); //Old Orbita
689
690            assert_eq!(dxl_config.url, "http://127.0.0.1:50098");
691            assert_eq!(dxl_config.id, Some(0));
692        } else {
693            panic!("Wrong config type");
694        }
695    }
696
697    #[test]
698    fn test_approx() {
699        let mut rng = rand::thread_rng();
700
701        let pos = rng.gen_range(-PI..PI);
702        let reduction = rng.gen_range(0.5..2.0);
703        assert_eq!(find_closest_offset_to_zero(pos, pos, reduction), pos);
704
705        let pos = 0.0;
706        let hardware_zero = 0.25;
707        let reduction = 1.0;
708
709        assert_eq!(
710            find_closest_offset_to_zero(pos, hardware_zero, reduction),
711            0.25
712        );
713
714        let reduction = 100.0;
715        assert_eq!(
716            find_closest_offset_to_zero(pos, hardware_zero, reduction),
717            0.25 - 2.0 * PI / reduction
718        );
719    }
720}