openrr_command/
robot_command.rs

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    /// Send joint positions.
39    SendJoints {
40        name: String,
41        #[clap(short, long, default_value = "3.0")]
42        duration: f64,
43        /// Interpolate target in cartesian space.
44        /// If you use this flag, joint values are not used as references but used in forward kinematics.
45        #[clap(name = "interpolate", short, long)]
46        use_interpolation: bool,
47        /// Specify joint parameters.
48        /// Like `--joint 0=1.2`.
49        /// In accordance with the sequence in which the "joint names" are defined in the configuration, they are numbered starting at 0.
50        #[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    /// Send predefined joint positions.
58    SendJointsPose {
59        name: String,
60        pose_name: String,
61        #[clap(short, long, default_value = "3.0")]
62        duration: f64,
63    },
64    /// Move with ik
65    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        /// Interpolate target in cartesian space.
82        #[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    /// Get joint positions and end pose if applicable.
92    GetState { name: String },
93    /// Load commands from file and execute them.
94    LoadCommands {
95        #[clap(value_parser)]
96        command_file_path: PathBuf,
97    },
98    /// List available clients.
99    List,
100    /// Speak text message.
101    Speak { name: String, message: Vec<String> },
102    /// Execute an external command.
103    ExecuteCommand { command: Vec<String> },
104    /// Get navigation current pose.
105    GetNavigationCurrentPose,
106    /// Send navigation goal pose.
107    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    /// Cancel navigation goal.
117    CancelNavigationGoal,
118    /// Send base velocity.
119    SendBaseVelocity {
120        x: f64,
121        y: f64,
122        theta: f64,
123        #[clap(short, long, default_value = "1.0")]
124        duration_secs: f64,
125    },
126    /// Shell completion
127    #[clap(subcommand)]
128    ShellCompletion(ShellType),
129}
130
131/// Enum type to handle clap_complete::Shell
132#[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                    // Parse the command
329                    let read_opt = RobotCommand::parse_from(command_parsed_iter);
330                    // Execute the parsed command
331                    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                // TODO: Parse quotations and comments
359                // Currently '"Foo bar" # buzz' is parsed as message in below command.
360                // 'openrr_apps_robot_command speak "Foo bar" # buzz'
361                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    /// Run interactive shell with the client
426    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        // no problem if there are no log file.
440        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                    // add dummy to make it the same as load command
446                    let line_with_arg0 = format!("dummy {line}");
447                    let command_parsed_iter = line_with_arg0.split_whitespace();
448                    // Parse the command
449                    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            // Ignore empty lines and comment lines
488            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        // This situation assumes that `command foo/bar "baz qux"` has been entered.
533        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        // This situation assumes that `command -f "{ type: Message, value: 1.0 }"` has been entered.
550        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}