Skip to content

Commit 513e7d6

Browse files
committed
Add subcommands and args to CLI
1 parent 9616fed commit 513e7d6

File tree

7 files changed

+382
-121
lines changed

7 files changed

+382
-121
lines changed

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ roslibrust = { version = "0.16.0", features = ["ros1", "codegen"] }
99
[dependencies]
1010
webrtc = "0.10"
1111
tokio = { version = "1", features = ["full"] }
12-
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
12+
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
1313
tungstenite = "0.20"
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
@@ -25,3 +25,9 @@ roslibrust = { version = "0.16.0", features = ["ros1", "rosbridge", "codegen"] }
2525
async-trait = "0.1.89"
2626
futures = "0.3.31"
2727
base64 = "0.22.1"
28+
clap = { version = "4.5.52", features = ["derive"] }
29+
dirs = "6.0.0"
30+
rustls = "*"
31+
32+
[dev-dependencies]
33+
tempfile = "3.23.0"

src/commands/config.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::Result;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Debug, Serialize, Deserialize, PartialEq)]
7+
pub struct AgentConfig {
8+
pub robot_id: String,
9+
pub signaling_url: String,
10+
}
11+
12+
pub fn get_default_path() -> Option<PathBuf> {
13+
dirs::config_dir().map(|mut path| {
14+
path.push("modulr_agent");
15+
path.push("config.json");
16+
path
17+
})
18+
}
19+
20+
pub fn read_config(override_path: Option<PathBuf>) -> Result<AgentConfig> {
21+
let config_path = override_path.or(get_default_path()).ok_or(anyhow::anyhow!(
22+
"No configuration file provided and default file cannot be found!"
23+
))?;
24+
let config = std::fs::read_to_string(&config_path).map_err(|_| {
25+
anyhow::anyhow!(
26+
"Unable to read config from file path: {}",
27+
&config_path.display()
28+
)
29+
})?;
30+
serde_json::from_str::<AgentConfig>(&config)
31+
.map_err(|_| anyhow::anyhow!("Unable to deserialize configuration from file!"))
32+
}
33+
34+
pub fn write_config(config: &AgentConfig, override_path: Option<PathBuf>) -> Result<()> {
35+
let config_path = override_path.or(get_default_path()).ok_or(anyhow::anyhow!(
36+
"No configuration file provided and default file path cannot be built!"
37+
))?;
38+
std::fs::write(
39+
&config_path,
40+
serde_json::to_string_pretty(config)
41+
.map_err(|_| anyhow::anyhow!("Unable to serialize configuration for writing!"))?,
42+
)
43+
.map_err(|_| {
44+
anyhow::anyhow!(
45+
"Unable to write config to file path: {}",
46+
&config_path.display()
47+
)
48+
})
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
use tempfile::NamedTempFile;
55+
56+
#[test]
57+
fn test_path_produced() {
58+
let path = get_default_path().unwrap();
59+
println!("Default path: {}", path.display());
60+
assert!(path.ends_with("modulr_agent/config.json"));
61+
}
62+
63+
#[test]
64+
fn test_read_config_roundtrip() {
65+
let config = AgentConfig {
66+
robot_id: "my_robot_id".to_string(),
67+
signaling_url: "my_signaling_url".to_string(),
68+
};
69+
let serialized = serde_json::to_string_pretty(&config).unwrap();
70+
println!("Serialized config: {}", serialized);
71+
let temp_file = NamedTempFile::new().unwrap();
72+
std::fs::write(temp_file.path(), &serialized).unwrap();
73+
74+
let deserialized = read_config(Some(temp_file.into_temp_path().to_path_buf())).unwrap();
75+
76+
assert_eq!(deserialized, config);
77+
}
78+
79+
#[test]
80+
fn test_write_config_roundtrip() {
81+
let config = AgentConfig {
82+
robot_id: "my_robot_id".to_string(),
83+
signaling_url: "my_signaling_url".to_string(),
84+
};
85+
let temp_file = NamedTempFile::new().unwrap();
86+
write_config(&config, Some(temp_file.path().to_path_buf())).unwrap();
87+
let deserialized = read_config(Some(temp_file.path().to_path_buf())).unwrap();
88+
assert_eq!(deserialized, config);
89+
}
90+
}

src/commands/initial_setup.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use anyhow::Result;
2+
use clap::Parser;
3+
use std::path::PathBuf;
4+
5+
use crate::commands::config::{AgentConfig, write_config};
6+
7+
#[derive(Parser, Debug)]
8+
pub struct InitialSetupArgs {
9+
/// Robot ID to use for connection with Modulr services
10+
#[arg(short, long)]
11+
robot_id: String,
12+
/// Signaling URL for establishing WebRTC link
13+
#[arg(short, long)]
14+
signaling_url: String,
15+
/// Override default config path
16+
#[arg(short, long, value_name = "FILE")]
17+
config_override: Option<PathBuf>,
18+
}
19+
20+
pub async fn initial_setup(args: InitialSetupArgs) -> Result<()> {
21+
let config = AgentConfig {
22+
robot_id: args.robot_id,
23+
signaling_url: args.signaling_url,
24+
};
25+
write_config(&config, args.config_override)
26+
}

src/commands/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod config;
2+
mod initial_setup;
3+
mod start;
4+
5+
pub use initial_setup::*;
6+
pub use start::*;

src/commands/start.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::path::PathBuf;
2+
use std::sync::Arc;
3+
4+
use anyhow::Result;
5+
use bytes::Bytes;
6+
use clap::Parser;
7+
use log::{debug, info, warn};
8+
use tokio::sync::Mutex;
9+
10+
use crate::commands::config::read_config;
11+
use crate::ros_bridge::Ros1Bridge;
12+
use crate::ros_bridge::Ros2Bridge;
13+
use crate::ros_bridge::RosBridge;
14+
use crate::video_pipeline::VideoPipeline;
15+
use crate::video_pipeline::VideoPipelineError;
16+
use crate::webrtc_link::WebRtcLink;
17+
use crate::webrtc_link::WebRtcLinkError;
18+
use crate::webrtc_message::WebRtcMessage;
19+
20+
const ROS1: bool = false;
21+
22+
#[derive(Parser, Debug)]
23+
pub struct StartArgs {
24+
/// Override default config path
25+
#[arg(short, long, value_name = "FILE")]
26+
pub config_override: Option<PathBuf>,
27+
/// Allow not checking certificates on WebRTC link
28+
#[arg(long, default_value_t = false)]
29+
allow_skip_cert_check: bool,
30+
}
31+
32+
pub async fn start(args: StartArgs) -> Result<()> {
33+
let config = read_config(args.config_override)?;
34+
35+
let robot_id = config.robot_id;
36+
let signaling_url = config.signaling_url;
37+
let webrtc_link = Arc::new(Mutex::new(WebRtcLink::new(
38+
&robot_id,
39+
&signaling_url,
40+
args.allow_skip_cert_check,
41+
)));
42+
let pipeline = Arc::new(Mutex::new(VideoPipeline::new()));
43+
44+
let bridge: Arc<Mutex<dyn RosBridge>> = if ROS1 {
45+
Arc::new(Mutex::new(Ros1Bridge::new()))
46+
} else {
47+
Arc::new(Mutex::new(Ros2Bridge::new()))
48+
};
49+
50+
info!("Creating system components and callbacks");
51+
52+
// Browser -> WebRTC -> ROS
53+
let bridge_clone = Arc::clone(&bridge);
54+
webrtc_link
55+
.lock()
56+
.await
57+
.on_webrtc_message(Box::new(move |msg: &WebRtcMessage| {
58+
let bridge_clone = Arc::clone(&bridge_clone);
59+
let msg_clone = msg.clone();
60+
Box::pin(async move {
61+
match msg_clone {
62+
WebRtcMessage::MovementCommand(cmd) => {
63+
bridge_clone
64+
.lock()
65+
.await
66+
.post_movement_command(&cmd)
67+
.await
68+
.expect("Failed to post movement command!");
69+
}
70+
}
71+
})
72+
}))
73+
.await;
74+
75+
// TODO: only queue frames in pipeline when WebRTC session is established
76+
77+
// Pipeline -> WebRTC -> Browser
78+
let webrtc_link_clone = Arc::clone(&webrtc_link);
79+
pipeline
80+
.lock()
81+
.await
82+
.on_frame_ready(Box::new(move |frame: &Bytes| {
83+
let webrtc_link_clone = Arc::clone(&webrtc_link_clone);
84+
let frame_clone = Bytes::copy_from_slice(frame);
85+
Box::pin(async move {
86+
let err = webrtc_link_clone
87+
.lock()
88+
.await
89+
.write_frame(frame_clone)
90+
.await;
91+
match err {
92+
Err(WebRtcLinkError::IncorrectStateForOperation) => {
93+
warn!("Still waiting for WebRTC pipeline to connect.")
94+
}
95+
Err(_) => {
96+
panic!("Failed to write frame!")
97+
}
98+
_ => (),
99+
}
100+
})
101+
}))
102+
.await;
103+
104+
// Robot frame -> Pipeline
105+
let pipeline_clone = Arc::clone(&pipeline);
106+
bridge
107+
.lock()
108+
.await
109+
.on_image_frame_received(Box::new(move |data: &Bytes| {
110+
let data_clone = Bytes::copy_from_slice(data);
111+
let pipeline_clone = Arc::clone(&pipeline_clone);
112+
Box::pin(async move {
113+
pipeline_clone
114+
.lock()
115+
.await
116+
.queue_frame(&data_clone)
117+
.await
118+
.expect("Failed to write camera frame to pipeline!");
119+
})
120+
}))
121+
.await;
122+
123+
info!("Starting all tasks running");
124+
125+
tokio::spawn(async move {
126+
let mut guard = webrtc_link.lock().await;
127+
guard.try_connect().await?;
128+
guard.try_register().await?;
129+
Ok::<(), WebRtcLinkError>(())
130+
});
131+
let pipeline_clone = Arc::clone(&pipeline);
132+
tokio::spawn(async move {
133+
pipeline_clone.lock().await.launch().await?;
134+
debug!("Finished launching video pipeline");
135+
Ok::<(), VideoPipelineError>(())
136+
});
137+
bridge.lock().await.launch().await?;
138+
139+
let _ = tokio::signal::ctrl_c().await;
140+
info!("Exit requested, cleaning up...");
141+
pipeline.lock().await.stop_pipeline().await?;
142+
Ok(())
143+
}

0 commit comments

Comments
 (0)