openrr_gui/
joint_position_sender.rs

1use std::{collections::HashMap, f64, sync::Arc, time::Duration};
2
3use arci::{JointTrajectoryClient, Localization, MoveBase, Navigation};
4use eframe::egui;
5use openrr_client::RobotClient;
6use rand::Rng;
7use tracing::{debug, error, warn};
8use urdf_rs::JointType;
9
10use crate::Error;
11
12/// Launches GUI that send joint positions from GUI to the given `robot_client`.
13#[cfg(not(target_family = "wasm"))]
14pub fn joint_position_sender<L, M, N>(
15    robot_client: RobotClient<L, M, N>,
16    robot: urdf_rs::Robot,
17) -> Result<(), crate::Error>
18where
19    L: Localization + 'static,
20    M: MoveBase + 'static,
21    N: Navigation + 'static,
22{
23    let joints = joint_map(robot);
24    validate_joints(&joints, &robot_client)?;
25
26    let native_options = eframe::NativeOptions {
27        viewport: egui::ViewportBuilder::default()
28            .with_inner_size([400.0, 400.0])
29            .with_icon(
30                eframe::icon_data::from_png_bytes(include_bytes!("../assets/icon/openrr.png"))
31                    .unwrap(),
32            ),
33        ..eframe::NativeOptions::default()
34    };
35    eframe::run_native(
36        "Joint Position Sender",
37        native_options,
38        Box::new(|_cc| {
39            Ok(Box::new(
40                JointPositionSender::new(robot_client, joints).unwrap(),
41            ))
42        }),
43    )
44    .map_err(|e| crate::Error::Other(e.to_string()))?; // eframe::Error is not Send
45    Ok(())
46}
47
48struct JointPositionSender<L, M, N>
49where
50    L: Localization + 'static,
51    M: MoveBase + 'static,
52    N: Navigation + 'static,
53{
54    robot_client: RobotClient<L, M, N>,
55    joints: HashMap<String, urdf_rs::Joint>,
56
57    current_joint_trajectory_client: String,
58    joint_trajectory_client_names: Vec<String>,
59    show_joint_trajectory_client_list: bool,
60
61    joint_states: HashMap<String, Vec<JointState>>,
62
63    duration: Duration,
64    duration_input: String,
65}
66
67impl<L, M, N> JointPositionSender<L, M, N>
68where
69    L: Localization + 'static,
70    M: MoveBase + 'static,
71    N: Navigation + 'static,
72{
73    fn new(
74        robot_client: RobotClient<L, M, N>,
75        joints: HashMap<String, urdf_rs::Joint>,
76    ) -> Result<Self, crate::Error> {
77        let mut joint_trajectory_client_names = robot_client.joint_trajectory_clients_names();
78        joint_trajectory_client_names.sort_unstable();
79        debug!("{joint_trajectory_client_names:?}");
80
81        let joint_states = joint_trajectory_client_names
82            .iter()
83            .map(|client_name| {
84                (
85                    client_name.clone(),
86                    robot_client.joint_trajectory_clients()[client_name]
87                        .joint_names()
88                        .iter()
89                        .map(|joint_name| JointState {
90                            name: joint_name.clone(),
91                            ..Default::default()
92                        })
93                        .collect::<Vec<_>>(),
94                )
95            })
96            .collect();
97
98        let mut this = Self {
99            robot_client,
100            joints,
101            current_joint_trajectory_client: joint_trajectory_client_names[0].clone(),
102            joint_trajectory_client_names,
103            show_joint_trajectory_client_list: false,
104            joint_states,
105            duration: Duration::from_secs_f64(0.1),
106            duration_input: "0.1".to_owned(),
107        };
108
109        let joint_trajectory_client = this.current_joint_trajectory_client();
110        for (index, position) in joint_trajectory_client
111            .current_joint_positions()?
112            .into_iter()
113            .enumerate()
114        {
115            this.joint_states
116                .get_mut(&this.current_joint_trajectory_client)
117                .unwrap()[index]
118                .update_position(position);
119        }
120
121        Ok(this)
122    }
123
124    fn current_joint_trajectory_client(&self) -> Arc<dyn JointTrajectoryClient> {
125        self.robot_client.joint_trajectory_clients()[&self.current_joint_trajectory_client].clone()
126    }
127
128    fn current_joint_positions(&self) -> Vec<f64> {
129        self.joint_states[&self.current_joint_trajectory_client]
130            .iter()
131            .map(|joint| joint.position)
132            .collect()
133    }
134}
135
136impl<L, M, N> eframe::App for JointPositionSender<L, M, N>
137where
138    L: Localization + 'static,
139    M: MoveBase + 'static,
140    N: Navigation + 'static,
141{
142    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
143        egui::CentralPanel::default().show(ctx, |ui| {
144            egui::warn_if_debug_build(ui);
145
146            let mut send_joint_positions = false;
147
148            if ui.button(&self.current_joint_trajectory_client).clicked() {
149                self.show_joint_trajectory_client_list ^= true;
150            }
151            if self.show_joint_trajectory_client_list {
152                for client in &self.joint_trajectory_client_names {
153                    if ui
154                        .selectable_label(*client == self.current_joint_trajectory_client, client)
155                        .clicked()
156                    {
157                        match self.robot_client.joint_trajectory_clients()[client]
158                            .current_joint_positions()
159                        {
160                            Ok(joint_positions) => {
161                                for (index, position) in joint_positions.into_iter().enumerate() {
162                                    self.joint_states.get_mut(client).unwrap()[index]
163                                        .update_position(position);
164                                }
165                                self.current_joint_trajectory_client.clone_from(client);
166                            }
167                            Err(e) => {
168                                let msg = format!(
169                                    "failed to get current joint positions with '{client}' client: {e:#}"
170                                );
171                                error!(?msg);
172                                ui.colored_label(
173                                    ui.visuals().error_fg_color,
174                                    format!("Error: {msg:#}"),
175                                );
176                            }
177                        }
178                    }
179                }
180            }
181
182            if ui.button("Randomize").clicked() {
183                for joint_state in self
184                    .joint_states
185                    .get_mut(&self.current_joint_trajectory_client)
186                    .unwrap()
187                {
188                    let limit = &self.joints[&joint_state.name].limit;
189                    joint_state
190                        .update_position(rand::thread_rng().gen_range(limit.lower..=limit.upper));
191                }
192                send_joint_positions = true;
193            }
194
195            if ui.button("Zero").clicked() {
196                for joint_state in self
197                    .joint_states
198                    .get_mut(&self.current_joint_trajectory_client)
199                    .unwrap()
200                {
201                    joint_state.update_position(0.0);
202                }
203                send_joint_positions = true;
204            }
205
206            for joint_state in self
207                .joint_states
208                .get_mut(&self.current_joint_trajectory_client)
209                .unwrap()
210                .iter_mut()
211            {
212                let limit = &self.joints[&joint_state.name].limit;
213                if ui
214                    .add(
215                        egui::Slider::new(&mut joint_state.position, limit.lower..=limit.upper)
216                            .text(&self.joints[&joint_state.name].name),
217                    )
218                    .changed()
219                {
220                    send_joint_positions = true;
221                }
222            }
223
224            ui.add_space(20.0);
225            ui.horizontal(|ui| {
226                ui.label("Duration (sec)");
227                if ui.text_edit_singleline(&mut self.duration_input).changed() {
228                    match self.duration_input.parse::<f64>() {
229                        Ok(duration) => {
230                            // We don't round the input for now. If we do that, we also need to update
231                            // the text input that we show to the user.
232
233                            if !duration.is_normal() && !duration.is_sign_positive() {
234                                let msg = "Duration is not a normal number".to_string();
235                                warn!(?self.duration_input, ?msg);
236                                ui.colored_label(
237                                    ui.visuals().error_fg_color,
238                                    format!("Error: {msg:#}"),
239                                );
240                            }
241                            if !duration.is_sign_positive() {
242                                let msg = "Duration is not a positive number".to_string();
243                                warn!(?self.duration_input, ?msg);
244                                ui.colored_label(
245                                    ui.visuals().error_fg_color,
246                                    format!("Error: {msg:#}"),
247                                );
248                            }
249
250                            // self.errors.duration_input = None;
251                            self.duration = Duration::from_secs_f64(duration);
252                        }
253                        Err(e) => {
254                            let msg = "Duration is not a valid number".to_string();
255                            warn!(?self.duration_input, ?msg, "error=\"{e}\"");
256                            ui.colored_label(
257                                ui.visuals().error_fg_color,
258                                format!("Error: {msg:#}"),
259                            );
260                        }
261                    }
262                }
263            });
264
265            if send_joint_positions {
266                let joint_positions = self.current_joint_positions();
267                let joint_trajectory_client = self.current_joint_trajectory_client();
268                let duration = self.duration;
269                debug!(?joint_positions, ?duration, "send_joint_positions");
270                match joint_trajectory_client.send_joint_positions(joint_positions, duration) {
271                    Err(e) => {
272                        error!("{e}");
273                        ui.colored_label(ui.visuals().error_fg_color, format!("Error: {e:#}"));
274                    }
275                    // do not wait
276                    Ok(wait) => drop(wait),
277                }
278            }
279        });
280    }
281}
282
283fn joint_map(mut robot: urdf_rs::Robot) -> HashMap<String, urdf_rs::Joint> {
284    for joint in &mut robot.joints {
285        // If limit is not specified, urdf-rs assigns f64::default.
286        if JointType::Continuous == joint.joint_type {
287            joint.limit.lower = -f64::consts::PI;
288            joint.limit.upper = f64::consts::PI;
289        }
290    }
291
292    robot
293        .joints
294        .into_iter()
295        .map(|joint| (joint.name.clone(), joint))
296        .collect()
297}
298
299/// Returns an error if the joint names returned by joint trajectory clients do not exist in `joints`.
300fn validate_joints<L, M, N>(
301    joints: &HashMap<String, urdf_rs::Joint>,
302    client: &RobotClient<L, M, N>,
303) -> Result<(), Error>
304where
305    L: Localization + 'static,
306    M: MoveBase + 'static,
307    N: Navigation + 'static,
308{
309    for client in client.joint_trajectory_clients().values() {
310        for joint_name in client.joint_names() {
311            if !joints.contains_key(&joint_name) {
312                return Err(Error::Other(format!(
313                    "Joint '{joint_name}' not found in URDF"
314                )));
315            }
316        }
317    }
318    Ok(())
319}
320
321#[derive(Default)]
322struct JointState {
323    name: String,
324    position: f64,
325    // position_input: String,
326}
327
328impl JointState {
329    fn update_position(&mut self, position: f64) {
330        self.position = position;
331        // self.position_input = format!("{position:.2}");
332    }
333}
334
335#[cfg(test)]
336mod test {
337    use arci::{
338        DummyJointTrajectoryClient, DummyLocalization, DummyMoveBase, DummyNavigation,
339        DummySpeaker, Speaker,
340    };
341    use assert_approx_eq::assert_approx_eq;
342    use openrr_client::OpenrrClientsConfig;
343    use urdf_rs::{Axis, Joint, JointLimit, LinkName, Pose, Robot};
344
345    use super::*;
346
347    #[test]
348    fn test_joint_state() {
349        let mut joint_state = JointState {
350            name: String::from("joint_state"),
351            position: 0.,
352        };
353        joint_state.update_position(1.);
354        assert_approx_eq!(joint_state.position, 1.);
355    }
356
357    #[test]
358    fn test_joint_map() {
359        let joint = dummy_joint();
360
361        assert_eq!(joint["dummy_joint1"].name, String::from("dummy_joint1"));
362        assert_approx_eq!(joint["dummy_joint1"].limit.lower, -f64::consts::PI);
363        assert_approx_eq!(joint["dummy_joint1"].limit.upper, f64::consts::PI);
364        assert_approx_eq!(joint["dummy_joint2"].limit.upper, 2.7);
365        assert_approx_eq!(joint["dummy_joint1"].limit.effort, 0.2);
366        assert_eq!(
367            joint["dummy_joint1"].child.link,
368            String::from("dummy_child1")
369        );
370        assert_eq!(
371            joint["dummy_joint2"].parent.link,
372            String::from("dummy_parent2")
373        );
374    }
375
376    #[test]
377    fn test_validate_joints() {
378        let joints = dummy_joint();
379
380        let mut raw_joint_trajectory_clients = HashMap::from([(
381            String::from("dummy"),
382            Arc::new(DummyJointTrajectoryClient::new(vec![String::from(
383                "dummy_joint1",
384            )])) as Arc<dyn JointTrajectoryClient>,
385        )]);
386        let speakers = HashMap::from([(
387            String::from("speaker"),
388            Arc::new(DummySpeaker::new()) as Arc<dyn Speaker>,
389        )]);
390
391        let ok_client = dummy_robot_client(raw_joint_trajectory_clients.clone(), speakers.clone());
392
393        raw_joint_trajectory_clients
394            .entry(String::from("err"))
395            .or_insert(Arc::new(DummyJointTrajectoryClient::new(vec![String::from(
396                "no_exist_joint",
397            )])) as Arc<dyn JointTrajectoryClient>);
398
399        let err_client = dummy_robot_client(raw_joint_trajectory_clients, speakers);
400
401        assert!(validate_joints(&joints, &ok_client).is_ok());
402        assert!(validate_joints(&joints, &err_client).is_err());
403    }
404
405    #[test]
406    fn test_joint_position_sender() {
407        let raw_joint_trajectory_clients = HashMap::from([(
408            String::from("dummy"),
409            Arc::new(DummyJointTrajectoryClient::new(vec![String::from(
410                "dummy_joint1",
411            )])) as Arc<dyn JointTrajectoryClient>,
412        )]);
413        let speakers = HashMap::from([(
414            String::from("speaker"),
415            Arc::new(DummySpeaker::new()) as Arc<dyn Speaker>,
416        )]);
417
418        let robot_client = dummy_robot_client(raw_joint_trajectory_clients, speakers);
419
420        let joint_position_sender = JointPositionSender::new(robot_client, dummy_joint()).unwrap();
421
422        assert_eq!(
423            joint_position_sender.current_joint_trajectory_client,
424            String::from("dummy")
425        );
426        assert_approx_eq!(joint_position_sender.duration.as_secs_f64(), 0.1);
427    }
428
429    fn dummy_robot_client(
430        raw_joint_trajectory_clients: HashMap<String, Arc<dyn JointTrajectoryClient>>,
431        speakers: HashMap<String, Arc<dyn Speaker>>,
432    ) -> RobotClient<DummyLocalization, DummyMoveBase, DummyNavigation> {
433        RobotClient::new(
434            OpenrrClientsConfig::default(),
435            raw_joint_trajectory_clients,
436            speakers,
437            Some(DummyLocalization::new()),
438            Some(DummyMoveBase::new()),
439            Some(DummyNavigation::new()),
440        )
441        .unwrap()
442    }
443
444    fn dummy_joint() -> HashMap<String, Joint> {
445        let robot = Robot {
446            name: String::from("dummy"),
447            links: vec![],
448            joints: vec![
449                Joint {
450                    name: String::from("dummy_joint1"),
451                    joint_type: JointType::Continuous,
452                    origin: Pose {
453                        xyz: urdf_rs::Vec3([0., 1., 2.]),
454                        rpy: urdf_rs::Vec3([3., 4., 5.]),
455                    },
456                    parent: LinkName {
457                        link: String::from("dummy_parent1"),
458                    },
459                    child: LinkName {
460                        link: String::from("dummy_child1"),
461                    },
462                    axis: Axis {
463                        xyz: urdf_rs::Vec3([6., 7., 8.]),
464                    },
465                    limit: JointLimit {
466                        lower: 0.1,
467                        upper: 2.7,
468                        effort: 0.2,
469                        velocity: 0.3,
470                    },
471                    dynamics: None,
472                    mimic: None,
473                    safety_controller: None,
474                    calibration: None,
475                },
476                Joint {
477                    name: String::from("dummy_joint2"),
478                    joint_type: JointType::Revolute,
479                    origin: Pose {
480                        xyz: urdf_rs::Vec3([1., 1., 2.]),
481                        rpy: urdf_rs::Vec3([3., 4., 5.]),
482                    },
483                    parent: LinkName {
484                        link: String::from("dummy_parent2"),
485                    },
486                    child: LinkName {
487                        link: String::from("dummy_child2"),
488                    },
489                    axis: Axis {
490                        xyz: urdf_rs::Vec3([6., 7., 8.]),
491                    },
492                    limit: JointLimit {
493                        lower: 0.1,
494                        upper: 2.7,
495                        effort: 0.2,
496                        velocity: 0.3,
497                    },
498                    dynamics: None,
499                    mimic: None,
500                    safety_controller: None,
501                    calibration: None,
502                },
503            ],
504            materials: vec![],
505        };
506        joint_map(robot)
507    }
508}