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#[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()))?; 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 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.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 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 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
299fn 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 }
327
328impl JointState {
329 fn update_position(&mut self, position: f64) {
330 self.position = position;
331 }
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}