openrr_apps_robot_teleop/
robot_teleop.rs
1#[cfg(feature = "ros")]
2use std::thread;
3use std::{fs, path::PathBuf, sync::Arc};
4
5use anyhow::{format_err, Result};
6use arci_gamepad_gilrs::GilGamepad;
7use clap::Parser;
8use openrr_apps::{
9 utils::{init_tracing, init_tracing_with_file_appender, LogConfig},
10 BuiltinGamepad, Error, GamepadKind, RobotTeleopConfig,
11};
12use openrr_client::ArcRobotClient;
13use openrr_plugin::PluginProxy;
14use openrr_teleop::ControlModeSwitcher;
15use tracing::info;
16
17#[derive(Parser, Debug)]
19#[clap(name = env!("CARGO_BIN_NAME"))]
20pub struct RobotTeleopArgs {
21 #[clap(short, long, value_parser)]
23 config_path: Option<PathBuf>,
24 #[clap(long)]
27 teleop_config: Option<String>,
28 #[clap(long)]
31 robot_config: Option<String>,
32 #[clap(long)]
34 show_default_config: bool,
35 #[clap(long, value_parser)]
37 log_directory: Option<PathBuf>,
38}
39
40#[tokio::main]
41async fn main() -> Result<()> {
42 let args = RobotTeleopArgs::parse();
43
44 if args.show_default_config {
45 print!(
46 "{}",
47 toml::to_string(&RobotTeleopConfig::default()).unwrap()
48 );
49 return Ok(());
50 }
51
52 if args.log_directory.is_none() {
53 init_tracing();
54 }
55 #[cfg(not(feature = "ros"))]
56 let _guard = args.log_directory.map(|log_directory| {
57 init_tracing_with_file_appender(
58 LogConfig {
59 directory: log_directory,
60 ..Default::default()
61 },
62 env!("CARGO_BIN_NAME").to_string(),
63 )
64 });
65
66 let teleop_config = openrr_apps::utils::resolve_teleop_config(
67 args.config_path.as_deref(),
68 args.teleop_config.as_deref(),
69 )?;
70 let robot_config_path = teleop_config.robot_config_full_path();
71 let robot_config = openrr_apps::utils::resolve_robot_config(
72 robot_config_path.as_deref(),
73 args.robot_config.as_deref(),
74 )?;
75
76 openrr_apps::utils::init(env!("CARGO_BIN_NAME"), &robot_config);
77 #[cfg(feature = "ros")]
78 let use_ros = robot_config.has_ros_clients();
79 #[cfg(feature = "ros")]
80 let _guard = args.log_directory.map(|log_directory| {
81 init_tracing_with_file_appender(
82 LogConfig {
83 directory: log_directory,
84 ..Default::default()
85 },
86 if use_ros {
87 arci_ros::name()
88 } else {
89 env!("CARGO_BIN_NAME").to_string()
90 },
91 )
92 });
93 let client: Arc<ArcRobotClient> = Arc::new(robot_config.create_robot_client()?);
94
95 let speaker = client.speakers().values().next().unwrap();
96
97 let modes = teleop_config
98 .control_modes_config
99 .create_control_modes(
100 args.config_path,
101 client.clone(),
102 speaker.clone(),
103 client.joint_trajectory_clients(),
104 client.ik_solvers(),
105 Some(client.clone()),
106 robot_config.openrr_clients_config.joints_poses,
107 )
108 .unwrap();
109 if modes.is_empty() {
110 panic!("No valid modes");
111 }
112
113 let initial_mode_index = if teleop_config.initial_mode.is_empty() {
114 info!("Use first mode as initial mode");
115 0
116 } else if let Some(initial_mode_index) = modes
117 .iter()
118 .position(|mode| mode.mode() == teleop_config.initial_mode)
119 {
120 initial_mode_index
121 } else {
122 return Err(Error::NoSpecifiedMode(teleop_config.initial_mode).into());
123 };
124
125 let switcher = Arc::new(ControlModeSwitcher::new(
126 modes,
127 speaker.clone(),
128 initial_mode_index,
129 ));
130 #[cfg(feature = "ros")]
131 if use_ros {
132 let switcher_cloned = switcher.clone();
133 thread::spawn(move || {
134 let rate = arci_ros::rate(1.0);
135 while arci_ros::is_ok() {
136 rate.sleep();
137 }
138 switcher_cloned.stop();
139 });
140 }
141
142 match teleop_config.gamepad {
143 GamepadKind::Builtin(BuiltinGamepad::Gilrs) => {
144 switcher
145 .main(GilGamepad::new_from_config(
146 teleop_config.gil_gamepad_config,
147 ))
148 .await;
149 }
150 #[cfg(unix)]
151 GamepadKind::Builtin(BuiltinGamepad::Keyboard) => {
152 switcher
153 .main(arci_gamepad_keyboard::KeyboardGamepad::new())
154 .await;
155 }
156 #[cfg(windows)]
157 GamepadKind::Builtin(BuiltinGamepad::Keyboard) => {
158 anyhow::bail!("`gamepad = \"keyboard\"` is not supported on windows");
159 }
160 #[cfg(feature = "ros")]
161 GamepadKind::Builtin(BuiltinGamepad::RosJoyGamepad) => {
162 if !use_ros {
163 arci_ros::init(env!("CARGO_BIN_NAME"));
164 }
165 switcher
166 .main(arci_ros::RosJoyGamepad::new_from_config(
167 &teleop_config.ros_joy_gamepad_config,
168 ))
169 .await;
170 }
171 #[cfg(not(feature = "ros"))]
172 GamepadKind::Builtin(BuiltinGamepad::RosJoyGamepad) => {
173 anyhow::bail!("`gamepad = \"ros-joy-gamepad\"` requires \"ros\" feature");
174 }
175 GamepadKind::Plugin(name) => {
176 let mut gamepad = None;
177 for (plugin_name, config) in teleop_config.plugins {
178 if name == plugin_name {
179 let args = if let Some(path) = &config.args_from_path {
180 fs::read_to_string(path).map_err(|e| Error::NoFile(path.to_owned(), e))?
181 } else {
182 config.args.unwrap_or_default()
183 };
184 let plugin = PluginProxy::from_path(&config.path)?;
185 gamepad = Some(plugin.new_gamepad(args)?.ok_or_else(|| {
186 format_err!("failed to create `Gamepad` instance `{name}`: None")
187 })?);
188 break;
189 }
190 }
191 match gamepad {
192 Some(gamepad) => {
193 switcher.main(gamepad).await;
194 }
195 None => {
196 return Err(Error::NoPluginInstance {
197 name,
198 kind: "Gamepad".to_string(),
199 }
200 .into());
201 }
202 }
203 }
204 }
205
206 Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211 use clap::CommandFactory;
212
213 use super::*;
214
215 #[test]
216 fn parse_args() {
217 let bin = env!("CARGO_BIN_NAME");
218 assert!(RobotTeleopArgs::try_parse_from([bin]).is_ok());
219 assert!(RobotTeleopArgs::try_parse_from([bin, "--show-default-config"]).is_ok());
220 assert!(RobotTeleopArgs::try_parse_from([bin, "--config-path", "path"]).is_ok());
221 assert!(RobotTeleopArgs::try_parse_from([
222 bin,
223 "--show-default-config",
224 "--config-path",
225 "path"
226 ])
227 .is_ok());
228 }
229
230 #[test]
231 fn assert_app() {
232 RobotTeleopArgs::command().debug_assert();
233 }
234}