openrr_apps/
utils.rs

1use std::{
2    ffi::OsStr,
3    fs, io,
4    path::{Path, PathBuf},
5};
6
7use anyhow::Result;
8use rand::prelude::*;
9use serde::Deserialize;
10use tracing::{debug, warn, Event, Level, Subscriber};
11use tracing_appender::{
12    non_blocking::WorkerGuard,
13    rolling::{RollingFileAppender, Rotation},
14};
15use tracing_subscriber::{
16    fmt::{
17        format::{Format, Writer},
18        FmtContext, FormatEvent, FormatFields, Layer,
19    },
20    layer::SubscriberExt,
21    registry::LookupSpan,
22    EnvFilter,
23};
24
25use crate::{RobotConfig, RobotTeleopConfig};
26
27const OPENRR_APPS_CONFIG_ENV_NAME: &str = "OPENRR_APPS_ROBOT_CONFIG_PATH";
28const DEFAULT_JOINT_CLIENT_NAME: &str = "all";
29
30/// Get robot config from input or env OPENRR_APPS_ROBOT_CONFIG_PATH
31pub fn get_apps_robot_config(config: Option<PathBuf>) -> Option<PathBuf> {
32    if config.is_some() {
33        config
34    } else {
35        std::env::var(OPENRR_APPS_CONFIG_ENV_NAME)
36            .map(|s| {
37                warn!("### ENV VAR {s} is used ###");
38                PathBuf::from(s)
39            })
40            .ok()
41    }
42}
43
44fn evaluate_overwrite_str(doc: &str, scripts: &str, path: Option<&Path>) -> Result<String> {
45    if path.and_then(Path::extension).and_then(OsStr::to_str) == Some("toml") {
46        if let Err(e) = toml::from_str::<toml::Value>(doc) {
47            warn!(
48                "config {} is not valid toml: {}",
49                path.unwrap().display(),
50                e
51            );
52        }
53    }
54    openrr_config::overwrite_str(
55        &openrr_config::evaluate(doc, None)?,
56        &openrr_config::evaluate(scripts, None)?,
57    )
58}
59
60fn evaluate(doc: &str, path: Option<&Path>) -> Result<String> {
61    if path.and_then(Path::extension).and_then(OsStr::to_str) == Some("toml") {
62        if let Err(e) = toml::from_str::<toml::Value>(doc) {
63            warn!(
64                "config {} is not valid toml: {}",
65                path.unwrap().display(),
66                e
67            );
68        }
69    }
70    openrr_config::evaluate(doc, None)
71}
72
73pub fn resolve_robot_config(
74    config_path: Option<&Path>,
75    overwrite: Option<&str>,
76) -> Result<RobotConfig> {
77    match (config_path, overwrite) {
78        (Some(config_path), Some(overwrite)) => {
79            let s = &fs::read_to_string(config_path)?;
80            let s = &evaluate_overwrite_str(s, overwrite, Some(config_path))?;
81            Ok(RobotConfig::from_str(s, config_path)?)
82        }
83        (Some(config_path), None) => {
84            let s = &evaluate(&fs::read_to_string(config_path)?, Some(config_path))?;
85            Ok(RobotConfig::from_str(s, config_path)?)
86        }
87        (None, overwrite) => {
88            let mut config = RobotConfig::default();
89            config
90                .urdf_viz_clients_configs
91                .push(arci_urdf_viz::UrdfVizWebClientConfig {
92                    name: DEFAULT_JOINT_CLIENT_NAME.into(),
93                    joint_names: None,
94                    wrap_with_joint_position_limiter: false,
95                    wrap_with_joint_velocity_limiter: false,
96                    joint_velocity_limits: None,
97                    joint_position_limits: None,
98                });
99            if let Some(overwrite) = overwrite {
100                let s = &toml::to_string(&config)?;
101                let s = &evaluate_overwrite_str(s, overwrite, None)?;
102                config = toml::from_str(s)?;
103            }
104            Ok(config)
105        }
106    }
107}
108
109pub fn resolve_teleop_config(
110    config_path: Option<&Path>,
111    overwrite: Option<&str>,
112) -> Result<RobotTeleopConfig> {
113    match (config_path, overwrite) {
114        (Some(teleop_config_path), Some(overwrite)) => {
115            let s = &fs::read_to_string(teleop_config_path)?;
116            let s = &evaluate_overwrite_str(s, overwrite, Some(teleop_config_path))?;
117            Ok(RobotTeleopConfig::from_str(s, teleop_config_path)?)
118        }
119        (Some(teleop_config_path), None) => {
120            let s = &evaluate(
121                &fs::read_to_string(teleop_config_path)?,
122                Some(teleop_config_path),
123            )?;
124            Ok(RobotTeleopConfig::from_str(s, teleop_config_path)?)
125        }
126        (None, overwrite) => {
127            let mut config = RobotTeleopConfig::default();
128            config.control_modes_config.move_base_mode = Some("base".into());
129            if let Some(overwrite) = overwrite {
130                let s = &toml::to_string(&config)?;
131                let s = &evaluate_overwrite_str(s, overwrite, None)?;
132                config = toml::from_str(s)?;
133            }
134            Ok(config)
135        }
136    }
137}
138
139#[cfg(test)]
140mod test {
141    use super::*;
142    #[test]
143    fn test_get_apps_robot_config() {
144        let path = get_apps_robot_config(Some(PathBuf::from("a.toml")));
145        assert!(path.is_some());
146        assert_eq!(path.unwrap(), PathBuf::from("a.toml"));
147        //
148        std::env::set_var(OPENRR_APPS_CONFIG_ENV_NAME, "b.yaml");
149        let path = get_apps_robot_config(Some(PathBuf::from("a.toml")));
150        assert!(path.is_some());
151        assert_eq!(path.unwrap(), PathBuf::from("a.toml"));
152        std::env::remove_var(OPENRR_APPS_CONFIG_ENV_NAME);
153
154        let path = get_apps_robot_config(None);
155        assert!(path.is_none());
156
157        std::env::set_var(OPENRR_APPS_CONFIG_ENV_NAME, "b.yaml");
158        let path = get_apps_robot_config(None);
159        assert!(path.is_some());
160        assert_eq!(path.unwrap(), PathBuf::from("b.yaml"));
161        std::env::remove_var(OPENRR_APPS_CONFIG_ENV_NAME);
162    }
163    #[test]
164    fn test_log_config_default() {
165        let default = LogConfig::default();
166        assert_eq!(default.level, LogLevel::default());
167        assert_eq!(default.rotation, LogRotation::default());
168        assert_eq!(default.prefix, default_log_prefix());
169    }
170    #[test]
171    fn test_openrr_formatter() {
172        let config = LogConfig {
173            directory: tempfile::tempdir().unwrap().path().to_path_buf(),
174            ..Default::default()
175        };
176        {
177            let _guard = init_tracing_with_file_appender(config.clone(), "prefix".to_string());
178            tracing::info!("log");
179        }
180        assert!(config.directory.is_dir());
181        for entry in std::fs::read_dir(config.directory).unwrap() {
182            let log = std::fs::read_to_string(entry.unwrap().path()).unwrap();
183            assert!(log.starts_with("prefix "));
184        }
185    }
186}
187
188/// Do something needed to start the program
189pub fn init(name: &str, config: &RobotConfig) {
190    #[cfg(feature = "ros")]
191    if config.has_ros_clients() {
192        arci_ros::init(name);
193    }
194    debug!("init {name} with {config:?}");
195}
196
197/// Do something needed to start the program for multiple
198pub fn init_with_anonymize(name: &str, config: &RobotConfig) {
199    let suffix: u64 = rand::thread_rng().gen();
200    let anon_name = format!("{name}_{suffix}");
201    init(&anon_name, config);
202}
203
204pub fn init_tracing() {
205    tracing_subscriber::fmt()
206        .with_env_filter(EnvFilter::from_default_env())
207        .with_writer(io::stderr)
208        .init();
209}
210
211#[derive(Clone)]
212pub struct OpenrrFormatter {
213    formatter: Format,
214    name: String,
215}
216
217impl OpenrrFormatter {
218    fn new(name: String) -> Self {
219        Self {
220            formatter: tracing_subscriber::fmt::format(),
221            name,
222        }
223    }
224}
225
226impl<S, N> FormatEvent<S, N> for OpenrrFormatter
227where
228    S: Subscriber + for<'a> LookupSpan<'a>,
229    N: for<'a> FormatFields<'a> + 'static,
230{
231    fn format_event(
232        &self,
233        ctx: &FmtContext<'_, S, N>,
234        mut writer: Writer<'_>,
235        event: &Event<'_>,
236    ) -> std::fmt::Result {
237        write!(writer, "{} ", self.name)?;
238        self.formatter.format_event(ctx, writer, event)
239    }
240}
241
242pub fn init_tracing_with_file_appender(config: LogConfig, name: String) -> WorkerGuard {
243    let default_level = match config.level {
244        LogLevel::TRACE => Level::TRACE,
245        LogLevel::DEBUG => Level::DEBUG,
246        LogLevel::INFO => Level::INFO,
247        LogLevel::WARN => Level::WARN,
248        LogLevel::ERROR => Level::ERROR,
249    };
250    let rotation = match config.rotation {
251        LogRotation::MINUTELY => Rotation::MINUTELY,
252        LogRotation::HOURLY => Rotation::HOURLY,
253        LogRotation::DAILY => Rotation::DAILY,
254        LogRotation::NEVER => Rotation::NEVER,
255    };
256    let formatter = OpenrrFormatter::new(name);
257    let file_appender = RollingFileAppender::new(rotation, config.directory, config.prefix);
258    let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
259    let subscriber = tracing_subscriber::registry()
260        .with(EnvFilter::from_default_env().add_directive(default_level.into()))
261        .with(
262            Layer::new()
263                .event_format(formatter.clone())
264                .with_writer(io::stderr),
265        )
266        .with(
267            Layer::new()
268                .event_format(formatter)
269                .with_writer(file_writer),
270        );
271    tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector");
272    guard
273}
274
275#[derive(Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
276pub enum LogLevel {
277    TRACE,
278    DEBUG,
279    #[default]
280    INFO,
281    WARN,
282    ERROR,
283}
284
285#[derive(Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
286pub enum LogRotation {
287    MINUTELY,
288    #[default]
289    HOURLY,
290    DAILY,
291    NEVER,
292}
293
294#[derive(Deserialize, PartialEq, Clone)]
295pub struct LogConfig {
296    #[serde(default)]
297    pub level: LogLevel,
298    #[serde(default)]
299    pub rotation: LogRotation,
300    #[serde(default = "default_log_directory")]
301    pub directory: PathBuf,
302    #[serde(default = "default_log_prefix")]
303    pub prefix: PathBuf,
304}
305
306impl Default for LogConfig {
307    fn default() -> Self {
308        Self {
309            directory: default_log_directory(),
310            prefix: default_log_prefix(),
311            level: LogLevel::default(),
312            rotation: LogRotation::default(),
313        }
314    }
315}
316
317fn default_log_prefix() -> PathBuf {
318    PathBuf::from("log")
319}
320
321fn default_log_directory() -> PathBuf {
322    std::env::temp_dir()
323}