1use motor_toolbox_rs::{Limit, MotorsController, RawMotorsIO, Result, PID};
2use poulpe_ethercat_grpc::client::PoulpeRemoteClient;
3use serde::{Deserialize, Serialize};
4use std::{f64::consts::PI, f64::consts::TAU, thread, time::Duration};
5
6use log::{error, info};
7
8use crate::ZeroType;
9
10#[derive(Debug, Deserialize, Serialize)]
11pub struct PoulpeEthercatConfig {
13 pub url: String,
15 pub id: Option<u8>,
17 pub name: Option<String>,
19}
20
21#[derive(Debug)]
22pub struct EthercatPoulpeController {
24 io: PoulpeRemoteClient,
25 id: u16,
26
27 offsets: [Option<f64>; 3],
28 reduction: [Option<f64>; 3],
29 limits: [Option<Limit>; 3],
31 inverted_axes: [Option<bool>; 3],
32 axis_sensor_zeros: [Option<f64>; 3],
33}
34
35impl EthercatPoulpeController {
36 pub fn new(
38 url: &str,
39 id: Option<u8>,
40 name: Option<String>,
41 zero: ZeroType,
42 reductions: f64,
43 inverted_axes: [Option<bool>; 3],
44 ) -> Result<Self> {
45 let update_time = Duration::from_secs_f32(0.002);
46
47 let mut io = match (id, name) {
48 (_, Some(name)) => {
49 log::info!("Connecting to the slave with name: {}", name);
50
51 match PoulpeRemoteClient::connect_with_name(url.parse()?, vec![name], update_time) {
52 Ok(client) => client,
53 Err(e) => {
54 error!(
55 "Error while connecting to EthercatPoulpeController: {:?}",
56 e
57 );
58 return Err("Error while connecting to EthercatPoulpeController".into());
59 }
60 }
61 }
62 (Some(id), None) => {
63 log::info!("Connecting to the slave with id: {}", id);
64
65 match PoulpeRemoteClient::connect(url.parse()?, vec![id as u16], update_time) {
66 Ok(client) => client,
67 Err(e) => {
68 error!(
69 "Error while connecting to EthercatPoulpeController: {:?}",
70 e
71 );
72 return Err("Error while connecting to EthercatPoulpeController".into());
73 }
74 }
75 }
76 _ => {
77 log::error!("Invalid config file, make sure to provide either the id or the name!");
78 return Err("Invalid config file".into());
79 }
80 };
81 let id = io.ids[0];
82 let name = io.names[0].clone();
83
84 let mut trials = 20; while io.get_state(id as u16).is_err() {
87 thread::sleep(Duration::from_millis(100));
88 log::warn!(
89 "Waiting for connection to Orbita3d PoulpeRemoteClient with id {}",
90 id
91 );
92 if trials == 0 {
93 log::error!(
94 "Error: Timeout while connecting to the Orbita3d PoulpeRemoteClient with id {}",
95 id
96 );
97 return Err(
98 "Error: Timeout while connecting to the Orbita3d PoulpeRemoteClient".into(),
99 );
100 }
101 trials -= 1;
102 }
103 log::info!(
104 "Connected Orbita3d Client created for Slave {} (id: {}), sampling time: {:}ms",
105 name,
106 id,
107 update_time.as_millis()
108 );
109
110 io.set_velocity_limit(id as u16, [1.0; 3].to_vec());
112 io.set_torque_limit(id as u16, [1.0; 3].to_vec());
113
114 io.set_mode_of_operation(id as u16, 1); let mut poulpe_controller = EthercatPoulpeController {
118 io,
119 id: id as u16,
120 offsets: [None; 3],
121 reduction: [Some(reductions); 3],
122 limits: [None; 3],
123 inverted_axes,
124 axis_sensor_zeros: [None; 3],
125 };
126
127 info!(
128 "Orbita3d EthercatPoulpeController:\n\t - url: {:?}\n\t - id: {:?}",
129 url, id
130 );
131
132 log::info!("Creacting controller");
133
134 match zero {
135 ZeroType::ApproximateHardwareZero(zero) => {
137 log::info!("ApproximateHardwarezero");
138
139 let current_pos = MotorsController::get_current_position(&mut poulpe_controller)?;
140 log::info!("Current position: {:?}", current_pos);
141
142 zero.hardware_zero
143 .iter()
144 .zip(current_pos.iter())
145 .enumerate()
146 .for_each(|(i, (&hardware_zero, ¤t_pos))| {
147 poulpe_controller.offsets[i] = Some(find_closest_offset_to_zero(
148 current_pos,
149 hardware_zero,
150 reductions,
151 ));
152 });
153 }
154 ZeroType::FirmwareZero(_) => {
155 log::info!(
156 "FirmwareZero => zero has been done in firmware, no need to do it here."
157 );
158 let axis_sensor_zeros =
161 MotorsController::get_axis_sensor_zeros(&mut poulpe_controller)?;
162 log::info!("Axis sensor zeros: {:?}", axis_sensor_zeros);
163 poulpe_controller.axis_sensor_zeros = [
164 Some(axis_sensor_zeros[0]),
165 Some(axis_sensor_zeros[1]),
166 Some(axis_sensor_zeros[2]),
167 ];
168 }
169 ZeroType::ZeroStartup(_) => {
171 log::info!("ZeroStartup");
172
173 let current_pos = MotorsController::get_current_position(&mut poulpe_controller)?;
174
175 log::info!("Current position: {:?}", current_pos);
176
177 current_pos
178 .iter()
179 .enumerate()
180 .for_each(|(i, ¤t_pos)| {
181 poulpe_controller.offsets[i] = Some(current_pos);
182 });
183 }
184 ZeroType::HallZero(_zero) => {
185 log::error!("HallZero Not supported with Ethercat!");
186 }
187 }
188
189 Ok(poulpe_controller)
190 }
191
192 pub fn get_rpy_inverted_axes(&self) -> [Option<bool>; 3] {
193 self.inverted_axes
194 }
195
196 pub fn id(&self) -> u8 {
197 self.id as u8
198 }
199}
200
201impl MotorsController<3> for EthercatPoulpeController {
202 fn io(&mut self) -> &mut dyn RawMotorsIO<3> {
203 self
204 }
205
206 fn offsets(&self) -> [Option<f64>; 3] {
207 self.offsets
208 }
209
210 fn reduction(&self) -> [Option<f64>; 3] {
211 let mut reduction = [None; 3];
212 reduction.iter_mut().enumerate().for_each(|(i, r)| {
213 *r = Some(self.reduction[i].unwrap());
214 });
215 reduction
216 }
217
218 fn limits(&self) -> [Option<motor_toolbox_rs::Limit>; 3] {
219 self.limits
220 }
221
222 fn output_inverted_axes(&self) -> [Option<bool>; 3] {
227 self.inverted_axes
228 }
229}
230
231impl RawMotorsIO<3> for EthercatPoulpeController {
232 fn name(&self) -> String {
233 "EthercatPoulpeController".to_string()
234 }
235
236 fn is_torque_on(&mut self) -> Result<[bool; 3]> {
237 match self.io.get_torque_state(self.id) {
238 Ok(state) => Ok([state, state, state]),
239 Err(_) => Err("Error while getting torque state".into()),
240 }
241 }
242
243 fn set_torque(&mut self, on: [bool; 3]) -> Result<()> {
244 assert!(on.iter().all(|&t| t == on[0]));
245 if on.iter().any(|&x| x) {
246 self.io.turn_on(self.id);
247 } else {
248 self.io.turn_off(self.id);
249 }
250 Ok(())
251 }
252
253 fn get_current_position(&mut self) -> Result<[f64; 3]> {
254 match self.io.get_position_actual_value(self.id) {
255 Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
256 Err(_) => Err("Error while getting position".into()),
257 }
258 }
259
260 fn get_current_velocity(&mut self) -> Result<[f64; 3]> {
261 match self.io.get_velocity_actual_value(self.id) {
262 Ok(velocity) => Ok([velocity[0] as f64, velocity[1] as f64, velocity[2] as f64]),
263 Err(_) => Err("Error while getting velocity".into()),
264 }
265 }
266
267 fn get_current_torque(&mut self) -> Result<[f64; 3]> {
268 match self.io.get_torque_actual_value(self.id) {
269 Ok(torque) => Ok([torque[0] as f64, torque[1] as f64, torque[2] as f64]),
270 Err(_) => Err("Error while getting torque".into()),
271 }
272 }
273
274 fn get_target_position(&mut self) -> Result<[f64; 3]> {
275 match self.io.get_target_position(self.id) {
276 Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
277 Err(_) => Err("Error while getting target position".into()),
278 }
279 }
280
281 fn set_target_position(&mut self, position: [f64; 3]) -> Result<()> {
282 let target_position = position.iter().map(|&x| x as f32).collect::<Vec<f32>>();
283 self.io.set_target_position(self.id, target_position);
284 Ok(())
285 }
286
287 fn set_target_velocity(&mut self, vel: [f64; 3]) -> Result<()> {
295 let target_velocity = vel.iter().map(|&x| x as f32).collect::<Vec<f32>>();
296 self.io.set_target_velocity(self.id, target_velocity);
297 Ok(())
298 }
299
300 fn set_target_torque(&mut self, vel: [f64; 3]) -> Result<()> {
308 let target_torque = vel.iter().map(|&x| x as f32).collect::<Vec<f32>>();
309 self.io.set_target_torque(self.id, target_torque);
310 Ok(())
311 }
312
313 fn set_target_position_fb(&mut self, position: [f64; 3]) -> Result<[f64; 3]> {
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
317 match self.io.get_position_actual_value(self.id) {
318 Ok(position) => Ok([position[0] as f64, position[1] as f64, position[2] as f64]),
319 Err(_) => Err("Error while getting position".into()),
320 }
321 }
322
323 fn get_velocity_limit(&mut self) -> Result<[f64; 3]> {
324 match self.io.get_velocity_limit(self.id) {
325 Ok(limit) => Ok([limit[0] as f64, limit[1] as f64, limit[2] as f64]),
326 Err(_) => Err("Error while getting velocity limit".into()),
327 }
328 }
329
330 fn set_velocity_limit(&mut self, velocity: [f64; 3]) -> Result<()> {
331 let velocity_limit = velocity.iter().map(|&x| x as f32).collect::<Vec<f32>>();
332 self.io.set_velocity_limit(self.id, velocity_limit);
333 Ok(())
334 }
335
336 fn get_torque_limit(&mut self) -> Result<[f64; 3]> {
337 match self.io.get_torque_limit(self.id) {
338 Ok(limit) => Ok([limit[0] as f64, limit[1] as f64, limit[2] as f64]),
339 Err(_) => Err("Error while getting torque limit".into()),
340 }
341 }
342
343 fn set_torque_limit(&mut self, torque: [f64; 3]) -> Result<()> {
344 let torque_limit = torque.iter().map(|&x| x as f32).collect::<Vec<f32>>();
345 self.io.set_torque_limit(self.id, torque_limit);
346 Ok(())
347 }
348
349 fn get_pid_gains(&mut self) -> Result<[PID; 3]> {
351 Ok([PID {
352 p: 0.0,
353 i: 0.0,
354 d: 0.0,
355 }; 3])
356 }
357
358 fn set_pid_gains(&mut self, _pid: [PID; 3]) -> Result<()> {
359 Ok(())
360 }
361 fn get_axis_sensors(&mut self) -> Result<[f64; 3]> {
362 match self.io.get_axis_sensors(self.id) {
363 Ok(mut sensor) => {
364 for (i, s) in sensor.iter_mut().enumerate() {
367 *s *= 1.0 / self.reduction[i].unwrap() as f32;
369 if self.axis_sensor_zeros[i].is_some() {
371 *s -= self.axis_sensor_zeros[i].unwrap() as f32;
372 }
373 if self.offsets[i].is_some() {
375 *s -= self.offsets[i].unwrap() as f32;
376 }
377 *s = wrap_to_pi(*s as f64) as f32;
379 }
380 Ok([sensor[0] as f64, sensor[1] as f64, sensor[2] as f64])
381 }
382
383 Err(_) => Err("Error while getting axis sensors".into()),
384 }
385 }
386
387 fn get_axis_sensor_zeros(&mut self) -> Result<[f64; 3]> {
388 match self.io.get_axis_sensor_zeros(self.id) {
389 Ok(sensor) => Ok([sensor[0] as f64, sensor[1] as f64, sensor[2] as f64]),
390 Err(_) => Err("Error while getting axis sensor zeros".into()),
391 }
392 }
393
394 fn get_board_state(&mut self) -> Result<u8> {
395 match self.io.get_state(self.id) {
396 Ok(state) => Ok(state as u8),
397 Err(_) => Err("Error while getting board state".into()),
398 }
399 }
400
401 fn get_error_codes(&mut self) -> Result<[i32; 3]> {
402 match self.io.get_error_codes(self.id) {
403 Ok(codes) => Ok([codes[0], codes[1], codes[2]]),
404 Err(_) => Err("Error while getting error codes".into()),
405 }
406 }
407
408 fn get_motor_temperatures(&mut self) -> Result<[f64; 3]> {
409 match self.io.get_motor_temperatures(self.id) {
410 Ok(temp) => Ok([temp[0] as f64, temp[1] as f64, temp[2] as f64]),
411 Err(_) => Err("Error while getting motor temperatures".into()),
412 }
413 }
414
415 fn get_board_temperatures(&mut self) -> Result<[f64; 3]> {
416 match self.io.get_board_temperatures(self.id) {
417 Ok(temp) => Ok([temp[0] as f64, temp[1] as f64, temp[2] as f64]),
418 Err(_) => Err("Error while getting board temperatures".into()),
419 }
420 }
421
422 fn set_board_state(&mut self, _state: u8) -> Result<()> {
423 Ok(())
424 }
425
426 fn get_control_mode(&mut self) -> Result<[u8; 3]> {
427 match self.io.get_mode_of_operation(self.id) {
428 Ok(mode) => Ok([mode as u8, mode as u8, mode as u8]), Err(_) => Err("Error while getting mode of operation".into()),
430 }
431 }
432
433 fn set_control_mode(&mut self, _mode: [u8; 3]) -> Result<()> {
434 if !(_mode[0] == _mode[1] && _mode[1] == _mode[2]) {
435 return Err("Error, invalid control mode".into());
436 }
437 self.io.set_mode_of_operation(self.id, _mode[0] as u32);
438 Ok(())
439 }
440
441 fn emergency_stop(&mut self) {
442 self.io.emergency_stop(self.id);
443 error!("EMERCENCY STOP!");
444 }
445}
446
447fn find_closest_offset_to_zero(current_position: f64, hardware_zero: f64, reduction: f64) -> f64 {
448 log::info!(
456 "find_closest_offset_to_zero: current_position: {}, hardware_zero: {}, reduction: {}",
457 current_position,
458 hardware_zero,
459 reduction
460 );
461
462 let dial_arc = 2.0 * PI / reduction;
463
464 let possibilities = [
465 hardware_zero - dial_arc,
466 hardware_zero,
467 hardware_zero + dial_arc,
468 ];
469 log::debug!("possibilities: {:?}", possibilities);
470
471 let best = possibilities
472 .iter()
473 .map(|&p| (p - current_position).abs())
474 .enumerate()
475 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
476 .map(|(i, _)| possibilities[i])
477 .unwrap();
478
479 log::info!("best: {}", best);
480 best
481}
482
483#[allow(dead_code)]
484fn find_position_with_hall(
485 current_position: f64,
486 hardware_zero: f64,
487 hall_zero: f64,
488 hall_index: u8,
489 reduction: f64,
490) -> (f64, i16) {
491 const MAX_TURN: usize = 3;
498 let mut offset: [f64; MAX_TURN] = [0.0; MAX_TURN];
499 let mut offset_search: [f64; MAX_TURN] = [0.0; MAX_TURN];
500 let turn_offset = 2.0 * PI * reduction;
501 let hall_offset = 2.0 * PI / 16.0 * reduction; let diff_gear = current_position * reduction - hardware_zero * reduction;
506 let shortest_diff_gear = angle_diff(current_position * reduction, hardware_zero * reduction); let shortest_to_zero = angle_diff(0.0, hardware_zero * reduction);
508
509 let pos = (current_position * reduction) % TAU; let _shortest_to_current = angle_diff(0.0, pos);
511 let _gearbox_turn = 0.0;
512
513 log::debug!(
514 "Diff: {:?} shortest diff: {:?} shortest_to_zero {:?} hall_zero_angle: {:?}",
515 diff_gear,
516 shortest_diff_gear,
517 shortest_to_zero,
518 hall_zero
519 );
520
521 for i in 0..offset.len() {
522 offset_search[i] = (hardware_zero * reduction) % TAU
525 + (hall_zero * reduction) % TAU
526 + ((i as f64 - (offset.len() / 2) as f64) * turn_offset) % TAU;
527 offset_search[i] %= TAU;
528
529 let residual = angle_diff(
530 pos,
531 (hardware_zero * reduction) % TAU + (hall_zero * reduction) % TAU,
532 ) / reduction;
533
534 offset[i] = current_position
536 - hall_zero
537 - residual
538 - (i as f64 - (offset.len() / 2) as f64)
539 * (turn_offset / reduction - TAU * (reduction - reduction.floor()) / reduction);
540
541 }
543
544 log::debug!(
545 "Residual (gearbox) {:?} (orbita) {:?}",
546 angle_diff(pos, (hall_zero * reduction) % TAU),
547 angle_diff(pos, (hall_zero * reduction) % TAU) / reduction
548 );
549 log::debug!("possible offset (orbita domain): {:?}", offset);
550 log::debug!("searching offset (gearbox domain): {:?}", offset_search);
551
552 log::debug!(
553 "current pos (gearbox): {:?} hardware_zero (gearbox): {:?} hall_idx: {:?} hall_zero: {:?} hall_offset: {:?} turn_offset: {:?}",
554 pos,
555 hardware_zero * reduction,
556 hall_index as f64,
557 hall_zero,
558 hall_offset,
559 turn_offset
560 );
561
562 let best = offset_search
563 .iter()
564 .map(|&p| {
565 let d = angle_diff(p, pos).abs();
566 log::debug!("Diff search: {:?}", d);
567 d
568 })
569 .enumerate()
570 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
571 .map(|(i, _)| offset[i])
572 .unwrap();
573
574 let best_idx = offset.iter().position(|&x| x == best).unwrap();
575 log::debug!(
576 "best offset (orbita domain): {} gearbox domain: {:?}",
577 best,
578 offset_search[best_idx]
579 );
580 log::debug!(
581 "It corresponds to {} turn (orbita domain)",
582 best_idx as i16 - (offset.len() / 2) as i16
583 );
584
585 (best, best_idx as i16 - (offset.len() / 2) as i16)
586}
587
588#[allow(dead_code)]
589pub fn angle_diff(angle_a: f64, angle_b: f64) -> f64 {
590 let mut angle = angle_a - angle_b;
591 angle = (angle + PI) % TAU - PI;
592 if angle < -PI {
593 angle + TAU
594 } else {
595 angle
596 }
597}
598
599#[allow(dead_code)]
600pub fn hall_diff(hall_a: u8, hall_b: u8) -> f64 {
601 let d: f64 = hall_a as f64 - hall_b as f64;
603 if d >= 0.0 {
604 if d >= 8.0 {
605 d - 16.0
606 } else {
607 d
608 }
609 } else if d >= -8.0 {
610 d
611 } else {
612 d + 16.0
613 }
614}
615
616fn wrap_to_pi(angle: f64) -> f64 {
619 (((angle + PI) % (2.0 * PI)) + (2.0 * PI)) % (2.0 * PI) - PI
620}
621
622#[cfg(test)]
623mod tests {
624 use rand::Rng;
625 use std::f64::consts::PI;
626
627 use crate::{
628 io::{poulpe_ethercat::find_closest_offset_to_zero, Orbita3dIOConfig},
629 Orbita3dConfig,
630 };
631
632 #[test]
633 fn parse_config_file() {
634 let f = std::fs::File::open("./config/ethercat_poulpe.yaml").unwrap();
635
636 let config: Result<Orbita3dConfig, _> = serde_yaml::from_reader(f);
637 assert!(config.is_ok());
638
639 let config = config.unwrap();
640
641 if let Orbita3dIOConfig::PoulpeEthercat(dxl_config) = config.io {
642 assert_eq!(config.kinematics_model.alpha, 54.0_f64.to_radians());
643 assert_eq!(config.kinematics_model.gamma_min, 40.0_f64.to_radians());
644 assert_eq!(config.kinematics_model.offset, 0.0);
645 assert_eq!(config.kinematics_model.beta, PI / 2.0);
646 assert_eq!(config.kinematics_model.gamma_max, PI);
647 assert!(config.kinematics_model.passiv_arms_direct);
648
649 if let crate::ZeroType::FirmwareZero(_) = config.disks.zeros {
650 } else {
651 panic!("Wrong config type");
652 }
653 assert_eq!(config.disks.reduction, 5.333_333_333_333_333);
654 assert_eq!(dxl_config.url, "http://127.0.0.1:50098");
657 assert_eq!(dxl_config.id, Some(0));
658 } else {
659 panic!("Wrong config type");
660 }
661 }
662
663 #[test]
664 fn test_approx() {
665 let mut rng = rand::thread_rng();
666
667 let pos = rng.gen_range(-PI..PI);
668 let reduction = rng.gen_range(0.5..2.0);
669 assert_eq!(find_closest_offset_to_zero(pos, pos, reduction), pos);
670
671 let pos = 0.0;
672 let hardware_zero = 0.25;
673 let reduction = 1.0;
674
675 assert_eq!(
676 find_closest_offset_to_zero(pos, hardware_zero, reduction),
677 0.25
678 );
679
680 let reduction = 100.0;
681 assert_eq!(
682 find_closest_offset_to_zero(pos, hardware_zero, reduction),
683 0.25 - 2.0 * PI / reduction
684 );
685 }
686}