1use std::{
2 error::Error,
3 fs::File,
4 io::{BufRead, BufReader},
5 path::PathBuf,
6 process::Command,
7 thread::sleep,
8 time::{Duration, Instant},
9};
10
11use arci::{BaseVelocity, Localization, MoveBase, Navigation};
12use async_recursion::async_recursion;
13use clap::Parser;
14use clap_complete::Shell;
15use k::nalgebra::{Isometry2, Vector2};
16use openrr_client::{isometry, RobotClient};
17use rustyline::{error::ReadlineError, DefaultEditor};
18use tracing::{error, info};
19
20use crate::Error as OpenrrCommandError;
21
22fn parse_joints<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync>>
23where
24 T: std::str::FromStr,
25 T::Err: Error + Send + Sync + 'static,
26 U: std::str::FromStr,
27 U::Err: Error + Send + Sync + 'static,
28{
29 let pos = s
30 .find('=')
31 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
32 Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
33}
34
35#[derive(Parser, Debug)]
36#[clap(rename_all = "snake_case")]
37pub enum RobotCommand {
38 SendJoints {
40 name: String,
41 #[clap(short, long, default_value = "3.0")]
42 duration: f64,
43 #[clap(name = "interpolate", short, long)]
46 use_interpolation: bool,
47 #[clap(short, value_parser = parse_joints::<usize, f64>, long)]
51 joint: Vec<(usize, f64)>,
52 #[clap(long, default_value = "0.05")]
53 max_resolution_for_interpolation: f64,
54 #[clap(long, default_value = "10")]
55 min_number_of_points_for_interpolation: i32,
56 },
57 SendJointsPose {
59 name: String,
60 pose_name: String,
61 #[clap(short, long, default_value = "3.0")]
62 duration: f64,
63 },
64 MoveIk {
66 name: String,
67 #[clap(short, long)]
68 x: Option<f64>,
69 #[clap(short, long)]
70 y: Option<f64>,
71 #[clap(short, long)]
72 z: Option<f64>,
73 #[clap(long)]
74 yaw: Option<f64>,
75 #[clap(short, long)]
76 pitch: Option<f64>,
77 #[clap(short, long)]
78 roll: Option<f64>,
79 #[clap(short, long, default_value = "3.0")]
80 duration: f64,
81 #[clap(name = "interpolate", short, long)]
83 use_interpolation: bool,
84 #[clap(name = "local", short, long)]
85 is_local: bool,
86 #[clap(long, default_value = "0.05")]
87 max_resolution_for_interpolation: f64,
88 #[clap(long, default_value = "10")]
89 min_number_of_points_for_interpolation: i32,
90 },
91 GetState { name: String },
93 LoadCommands {
95 #[clap(value_parser)]
96 command_file_path: PathBuf,
97 },
98 List,
100 Speak { name: String, message: Vec<String> },
102 ExecuteCommand { command: Vec<String> },
104 GetNavigationCurrentPose,
106 SendNavigationGoal {
108 x: f64,
109 y: f64,
110 yaw: f64,
111 #[clap(short, long, default_value = "map")]
112 frame_id: String,
113 #[clap(short, long, default_value = "100.0")]
114 timeout_secs: f64,
115 },
116 CancelNavigationGoal,
118 SendBaseVelocity {
120 x: f64,
121 y: f64,
122 theta: f64,
123 #[clap(short, long, default_value = "1.0")]
124 duration_secs: f64,
125 },
126 #[clap(subcommand)]
128 ShellCompletion(ShellType),
129}
130
131#[derive(Debug, Parser, Clone, Copy)]
133#[clap(rename_all = "snake_case")]
134#[non_exhaustive]
135pub enum ShellType {
136 Zsh,
137 Bash,
138 Fish,
139 PowerShell,
140}
141
142impl From<ShellType> for Shell {
143 fn from(shell: ShellType) -> Shell {
144 match shell {
145 ShellType::Bash => Shell::Bash,
146 ShellType::Zsh => Shell::Zsh,
147 ShellType::Fish => Shell::Fish,
148 ShellType::PowerShell => Shell::PowerShell,
149 }
150 }
151}
152
153pub struct RobotCommandExecutor {}
154
155impl RobotCommandExecutor {
156 #[async_recursion]
157 #[allow(clippy::only_used_in_recursion)]
158 pub async fn execute<L, M, N>(
159 &self,
160 client: &RobotClient<L, M, N>,
161 command: &RobotCommand,
162 ) -> Result<(), OpenrrCommandError>
163 where
164 L: Localization,
165 M: MoveBase,
166 N: Navigation,
167 {
168 match &command {
169 RobotCommand::SendJoints {
170 name,
171 duration,
172 use_interpolation,
173 joint,
174 max_resolution_for_interpolation,
175 min_number_of_points_for_interpolation,
176 } => {
177 let mut positions = client.current_joint_positions(name)?;
178
179 let mut should_send = false;
180 for (index, position) in joint {
181 if *index < positions.len() {
182 should_send = true;
183 positions[*index] = *position;
184 }
185 }
186 if !should_send {
187 return Ok(());
188 }
189 if *use_interpolation {
190 client
191 .send_joint_positions_with_pose_interpolation(
192 name,
193 &positions,
194 *duration,
195 *max_resolution_for_interpolation,
196 *min_number_of_points_for_interpolation,
197 )?
198 .await?;
199 } else {
200 client
201 .send_joint_positions(name, &positions, *duration)?
202 .await?;
203 }
204 }
205 RobotCommand::SendJointsPose {
206 name,
207 pose_name,
208 duration,
209 } => {
210 client.send_joints_pose(name, pose_name, *duration)?.await?;
211 }
212 RobotCommand::MoveIk {
213 name,
214 x,
215 y,
216 z,
217 yaw,
218 pitch,
219 roll,
220 duration,
221 use_interpolation,
222 is_local,
223 max_resolution_for_interpolation,
224 min_number_of_points_for_interpolation,
225 } => {
226 if !client.is_ik_client(name) {
227 return Err(OpenrrCommandError::NoIkClient(name.clone()));
228 }
229 let mut should_send = false;
230 let current_pose = client.current_end_transform(name)?;
231 let target_pose = [
232 if let Some(x) = x {
233 should_send = true;
234 *x
235 } else if *is_local {
236 0.0
237 } else {
238 current_pose.translation.x
239 },
240 if let Some(y) = y {
241 should_send = true;
242 *y
243 } else if *is_local {
244 0.0
245 } else {
246 current_pose.translation.y
247 },
248 if let Some(z) = z {
249 should_send = true;
250 *z
251 } else if *is_local {
252 0.0
253 } else {
254 current_pose.translation.z
255 },
256 if let Some(roll) = roll {
257 should_send = true;
258 *roll
259 } else if *is_local {
260 0.0
261 } else {
262 current_pose.rotation.euler_angles().0
263 },
264 if let Some(pitch) = pitch {
265 should_send = true;
266 *pitch
267 } else if *is_local {
268 0.0
269 } else {
270 current_pose.rotation.euler_angles().1
271 },
272 if let Some(yaw) = yaw {
273 should_send = true;
274 *yaw
275 } else if *is_local {
276 0.0
277 } else {
278 current_pose.rotation.euler_angles().2
279 },
280 ];
281 if !should_send {
282 return Ok(());
283 }
284 let target_pose = isometry(
285 target_pose[0],
286 target_pose[1],
287 target_pose[2],
288 target_pose[3],
289 target_pose[4],
290 target_pose[5],
291 );
292
293 let target_pose = if *is_local {
294 client.transform(name, &target_pose)?
295 } else {
296 target_pose
297 };
298 if *use_interpolation {
299 client
300 .move_ik_with_interpolation(
301 name,
302 &target_pose,
303 *duration,
304 *max_resolution_for_interpolation,
305 *min_number_of_points_for_interpolation,
306 )?
307 .await?
308 } else {
309 client.move_ik(name, &target_pose, *duration)?.await?
310 }
311 }
312 RobotCommand::GetState { name } => {
313 println!("Joint names : {:?}", client.joint_names(name)?);
314 println!(
315 "Joint positions : {:?}",
316 client.current_joint_positions(name)?
317 );
318 if client.is_ik_client(name) {
319 let pose = client.current_end_transform(name)?;
320 println!("End pose");
321 println!(" translation = {:?}", pose.translation.vector.data);
322 println!(" rotation = {:?}", pose.rotation.euler_angles());
323 }
324 }
325 RobotCommand::LoadCommands { command_file_path } => {
326 for command in load_command_file_and_filter(command_file_path.clone())? {
327 let command_parsed_iter = command.split_whitespace();
328 let read_opt = RobotCommand::parse_from(command_parsed_iter);
330 info!("Executing {command}");
332 self.execute(client, &read_opt).await?;
333 }
334 }
335 RobotCommand::List => {
336 println!("Raw joint trajectory clients");
337 for name in client.raw_joint_trajectory_clients_names() {
338 println!(" {name}");
339 }
340 println!("Joint trajectory clients");
341 for name in client.joint_trajectory_clients_names() {
342 println!(" {name}");
343 }
344 println!("Collision avoidance clients");
345 for name in client.collision_avoidance_clients_names() {
346 println!(" {name}");
347 }
348 println!("Collision check clients");
349 for name in client.collision_check_clients_names() {
350 println!(" {name}");
351 }
352 println!("Ik clients");
353 for name in client.ik_clients_names() {
354 println!(" {name}");
355 }
356 }
357 RobotCommand::Speak { name, message } => {
358 client.speak(name, &message.join(" "))?.await?;
362 }
363 RobotCommand::ExecuteCommand { command } => {
364 let command = filter_escape_double_quote(command);
365 let mut iter = command.iter();
366 let cmd_str = iter
367 .next()
368 .ok_or_else(|| OpenrrCommandError::NoCommand(command.to_owned()))?;
369 let output = Command::new(cmd_str).args(iter).output().map_err(|e| {
370 OpenrrCommandError::CommandExecutionFailure(command.to_owned(), e)
371 })?;
372 if output.status.success() {
373 info!("{}", String::from_utf8_lossy(&output.stdout));
374 } else {
375 error!("{}", String::from_utf8_lossy(&output.stdout));
376 error!("{}", String::from_utf8_lossy(&output.stderr));
377 return Err(OpenrrCommandError::CommandFailure(
378 command.to_owned(),
379 String::from_utf8_lossy(&output.stderr).to_string(),
380 ));
381 }
382 }
383 RobotCommand::GetNavigationCurrentPose => {
384 println!("Base Pose {}", client.current_pose("")?);
385 }
386 RobotCommand::SendNavigationGoal {
387 x,
388 y,
389 yaw,
390 frame_id,
391 timeout_secs,
392 } => {
393 client
394 .send_goal_pose(
395 Isometry2::new(Vector2::new(*x, *y), *yaw),
396 frame_id,
397 Duration::from_secs_f64(*timeout_secs),
398 )?
399 .await?;
400 }
401 RobotCommand::CancelNavigationGoal => {
402 client.cancel()?;
403 }
404 RobotCommand::SendBaseVelocity {
405 x,
406 y,
407 theta,
408 duration_secs,
409 } => {
410 let start = Instant::now();
411 let duration = Duration::from_secs_f64(*duration_secs);
412 let sleep_duration = Duration::from_secs_f64(0.01);
413 while start.elapsed() < duration {
414 client.send_velocity(&BaseVelocity::new(*x, *y, *theta))?;
415 sleep(sleep_duration);
416 }
417 }
418 _ => {
419 panic!("not supported {command:?}");
420 }
421 }
422 Ok(())
423 }
424
425 pub async fn run_interactive_shell<L, M, N>(
427 &self,
428 client: &RobotClient<L, M, N>,
429 ) -> Result<(), OpenrrCommandError>
430 where
431 L: Localization,
432 M: MoveBase,
433 N: Navigation,
434 {
435 let mut rl = DefaultEditor::with_config(
436 rustyline::Config::builder().auto_add_history(true).build(),
437 )?;
438 const HISTORY_FILE_NAME: &str = "openrr_apps_robot_command_log.txt";
439 let _ = rl.load_history(HISTORY_FILE_NAME);
441 loop {
442 let readline = rl.readline("\x1b[1;32m>> \x1b[0m");
443 match readline {
444 Ok(line) => {
445 let line_with_arg0 = format!("dummy {line}");
447 let command_parsed_iter = line_with_arg0.split_whitespace();
448 if let Ok(command) = RobotCommand::try_parse_from(command_parsed_iter) {
450 if let Err(e) = self.execute(client, &command).await {
451 println!("failed to execute: {e:?}");
452 }
453 } else if !line.is_empty() {
454 println!("failed to parse: {line:?}");
455 }
456 }
457 Err(ReadlineError::Interrupted) => {
458 println!("CTRL-C");
459 break;
460 }
461 Err(ReadlineError::Eof) => {
462 println!("CTRL-D");
463 break;
464 }
465 Err(err) => {
466 println!("Error: {err:?}");
467 break;
468 }
469 }
470 }
471 if let Err(err) = rl.save_history(HISTORY_FILE_NAME) {
472 println!("failed to save history {HISTORY_FILE_NAME}: {err:?}");
473 }
474 Ok(())
475 }
476}
477
478pub fn load_command_file_and_filter(file_path: PathBuf) -> Result<Vec<String>, OpenrrCommandError> {
479 let file = File::open(&file_path)
480 .map_err(|e| OpenrrCommandError::CommandFileOpenFailure(file_path, e.to_string()))?;
481 let buf = BufReader::new(file);
482 Ok(buf
483 .lines()
484 .map(|line| line.expect("Could not parse line"))
485 .filter(|command| {
486 let command_parsed_iter = command.split_whitespace();
487 command_parsed_iter.clone().count() > 0
489 && command_parsed_iter
490 .clone()
491 .next()
492 .unwrap()
493 .find('#')
494 .is_none()
495 })
496 .collect())
497}
498
499fn filter_escape_double_quote(robot_command: &[String]) -> Vec<String> {
500 let mut is_concatenation = false;
501 let mut command_vector: Vec<String> = Vec::new();
502
503 for element in robot_command {
504 let command_element = element.replace('\"', "");
505 if is_concatenation {
506 let new_command = command_vector
507 .pop()
508 .expect("Parse double quote logic error. Vector command_vector is empty.");
509 command_vector.push(format!("{new_command} {command_element}"));
510 } else {
511 command_vector.push(command_element.to_string());
512 }
513 if element.match_indices('\"').count() == 1 {
514 is_concatenation = !is_concatenation;
515 }
516 }
517 command_vector
518}
519
520#[cfg(test)]
521mod test {
522 use super::*;
523 #[test]
524 fn test_parse_joints() {
525 let val: (usize, usize) = parse_joints("0=2").unwrap();
526 assert_eq!(val.0, 0);
527 assert_eq!(val.1, 2);
528 }
529
530 #[test]
531 fn test_filter_escape_double_quote() {
532 let expected_command: Vec<String> = vec![
534 "command".to_string(),
535 "foo/bar".to_string(),
536 "baz qux".to_string(),
537 ];
538
539 let robot_command: Vec<String> = vec![
540 "command".to_string(),
541 "foo/bar".to_string(),
542 "\"baz".to_string(),
543 "qux\"".to_string(),
544 ];
545 let result_command = filter_escape_double_quote(&robot_command);
546
547 assert_eq!(expected_command, result_command);
548
549 let expected_dict_command: Vec<String> = vec![
551 "command".to_string(),
552 "-f".to_string(),
553 "{ type: Message, value: 1.0 }".to_string(),
554 ];
555 let robot_dict_command: Vec<String> = vec![
556 "command".to_string(),
557 "-f".to_string(),
558 "\"{".to_string(),
559 "type:".to_string(),
560 "Message,".to_string(),
561 "value:".to_string(),
562 "1.0".to_string(),
563 "}\"".to_string(),
564 ];
565 let result_dict_command = filter_escape_double_quote(&robot_dict_command);
566
567 assert_eq!(expected_dict_command, result_dict_command);
568 }
569}