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)]
12pub struct PoulpeEthercatConfig {
14 pub url: String,
16 pub id: Option<u8>,
18 pub name: Option<String>,
20}
21
22#[derive(Debug)]
23pub struct EthercatPoulpeController {
25 io: PoulpeRemoteClient,
26 id: u16,
27
28 offsets: [Option<f64>; 3],
29 reduction: [Option<f64>; 3],
30 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 #[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 let mut trials = 20; 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 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()); } else {
126 io.set_mode_of_operation(id as u16, 1); }
128 } else {
129 io.set_mode_of_operation(id as u16, 1); }
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 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, ¤t_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 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 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, ¤t_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 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 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 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 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 for (i, s) in sensor.iter_mut().enumerate() {
401 *s *= 1.0 / self.reduction[i].unwrap() as f32;
403 if self.axis_sensor_zeros[i].is_some() {
405 *s -= self.axis_sensor_zeros[i].unwrap() as f32;
406 }
407 if self.offsets[i].is_some() {
409 *s -= self.offsets[i].unwrap() as f32;
410 }
411 *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]), 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 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 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; let diff_gear = current_position * reduction - hardware_zero * reduction;
540 let shortest_diff_gear = angle_diff(current_position * reduction, hardware_zero * reduction); let shortest_to_zero = angle_diff(0.0, hardware_zero * reduction);
542
543 let pos = (current_position * reduction) % TAU; 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 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[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 }
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 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
650fn 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!(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}