openrr_apps/
robot_config.rs

1use std::{
2    collections::HashMap,
3    env, fmt, fs,
4    ops::Not,
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use anyhow::format_err;
10use arci::{JointTrajectoryClient, Localization, MoveBase, Navigation, Speaker};
11#[cfg(feature = "ros")]
12use arci_ros::{
13    RosCmdVelMoveBase, RosCmdVelMoveBaseConfig, RosControlActionClientConfig,
14    RosControlClientConfig, RosEspeakClient, RosEspeakClientConfig, RosLocalizationClient,
15    RosLocalizationClientConfig, RosNavClient, RosNavClientConfig,
16};
17use arci_speak_audio::AudioSpeaker;
18use arci_speak_cmd::LocalCommand;
19use arci_urdf_viz::{UrdfVizWebClient, UrdfVizWebClientConfig};
20use openrr_client::{OpenrrClientsConfig, PrintSpeaker, RobotClient};
21use openrr_plugin::PluginProxy;
22use openrr_tracing::Tracing;
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25use tracing::{debug, error, info};
26
27use crate::Error;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "kebab-case")]
31pub enum BuiltinClient {
32    /// [ROS1](https://ros.org)
33    Ros,
34    /// [urdf-viz](https://github.com/openrr/urdf-viz)
35    UrdfViz,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39#[serde(untagged)]
40pub enum ClientKind {
41    // Use builtin client, ros or urdf-viz.
42    Builtin(BuiltinClient),
43    // Use plugin.
44    Plugin(String),
45    // true: auto-selection
46    // false: disable
47    Auto(bool),
48}
49
50impl ClientKind {
51    /// Returns true if the use of the ros client is explicitly specified.
52    ///
53    /// This is always false when ros feature is disabled. (ensured by validate_ros_config)
54    #[cfg(feature = "ros")]
55    fn is_builtin_ros(&self) -> bool {
56        matches!(self, Self::Builtin(BuiltinClient::Ros))
57    }
58
59    fn is_auto(&self) -> bool {
60        matches!(self, Self::Auto(true))
61    }
62}
63
64impl Default for ClientKind {
65    fn default() -> Self {
66        Self::Auto(true)
67    }
68}
69
70#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
71#[serde(tag = "type", content = "args")]
72#[serde(deny_unknown_fields)]
73#[non_exhaustive] // The variants will increase depending on the feature flag.
74pub enum SpeakConfig {
75    #[default]
76    Print,
77    Command,
78    #[cfg(feature = "ros")]
79    RosEspeak {
80        config: RosEspeakClientConfig,
81    },
82    // Not public API.
83    // A dummy variant to catch that there is a config that requires the ros feature.
84    #[doc(hidden)]
85    #[cfg(not(feature = "ros"))]
86    RosEspeak {
87        #[schemars(schema_with = "unimplemented_schema")]
88        config: toml::Value,
89    },
90    Audio {
91        map: HashMap<String, PathBuf>,
92    },
93}
94
95impl SpeakConfig {
96    pub fn build(&self) -> Result<Box<dyn Speaker>, Error> {
97        Ok(match self {
98            SpeakConfig::RosEspeak { config } => self.create_ros_espeak_client(config),
99            SpeakConfig::Audio { map } => self.create_audio_speaker(map.clone())?,
100            SpeakConfig::Command => self.create_local_command_speaker(),
101            SpeakConfig::Print => self.create_print_speaker(),
102        })
103    }
104
105    fn create_print_speaker(&self) -> Box<dyn Speaker> {
106        Box::new(arci::Lazy::new(move || {
107            debug!("create_print_speaker: creating PrintSpeaker");
108            Ok(PrintSpeaker::new())
109        }))
110    }
111
112    fn create_local_command_speaker(&self) -> Box<dyn Speaker> {
113        Box::new(arci::Lazy::new(move || {
114            debug!("create_local_command_speaker: creating LocalCommand");
115            Ok(LocalCommand::new())
116        }))
117    }
118
119    fn create_audio_speaker(
120        &self,
121        mut hash_map: HashMap<String, PathBuf>,
122    ) -> Result<Box<dyn Speaker>, Error> {
123        for path in hash_map.values_mut() {
124            *path = openrr_config::evaluate(path.to_str().unwrap(), None)
125                .map_err(arci::Error::Other)?
126                .into();
127        }
128        Ok(Box::new(arci::Lazy::new(move || {
129            debug!("create_audio_speaker: creating AudioSpeaker");
130            Ok(AudioSpeaker::new(hash_map))
131        })))
132    }
133
134    #[cfg(feature = "ros")]
135    fn create_ros_espeak_client(&self, config: &RosEspeakClientConfig) -> Box<dyn Speaker> {
136        let topic = config.topic.to_string();
137        Box::new(arci::Lazy::new(move || {
138            debug!("create_ros_espeak_client: creating RosEspeakClient");
139            Ok(RosEspeakClient::new(&topic))
140        }))
141    }
142
143    #[cfg(not(feature = "ros"))]
144    fn create_ros_espeak_client(&self, _config: &toml::Value) -> Box<dyn Speaker> {
145        unreachable!()
146    }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
150#[serde(deny_unknown_fields)]
151pub struct PluginConfig {
152    /// Path to the plugin. If no extension is specified, the default extension
153    /// for `cdylib` on the current OS will be selected.
154    /// (linux: `.so`, macos: `.dylib`, windows: `.dll`)
155    pub path: PathBuf,
156    pub instances: Vec<PluginInstance>,
157}
158
159impl PluginConfig {
160    fn find_instances_by_name<'a>(
161        map: &'a HashMap<String, Self>,
162        instance_name: &'a str,
163        instance_kind: PluginInstanceKind,
164    ) -> impl Iterator<Item = (&'a str, &'a PluginInstance)> {
165        map.iter().flat_map(move |(plugin_name, plugin_config)| {
166            plugin_config
167                .instances
168                .iter()
169                .filter(move |instance| {
170                    instance.name == instance_name && instance.type_ == instance_kind
171                })
172                .map(move |instance| (plugin_name.as_str(), instance))
173        })
174    }
175
176    fn find_instances_by_kind(
177        map: &HashMap<String, Self>,
178        instance_kind: PluginInstanceKind,
179    ) -> impl Iterator<Item = (&str, &PluginInstance)> {
180        map.iter().flat_map(move |(plugin_name, plugin_config)| {
181            plugin_config
182                .instances
183                .iter()
184                .filter(move |instance| instance.type_ == instance_kind)
185                .map(move |instance| (plugin_name.as_str(), instance))
186        })
187    }
188
189    fn resolve_instance<'a>(
190        map: &'a HashMap<String, Self>,
191        instance_name: Option<&'a str>,
192        instance_kind: PluginInstanceKind,
193    ) -> Result<(&'a str, &'a PluginInstance), Error> {
194        let instances: Vec<_> = if let Some(instance_name) = instance_name {
195            Self::find_instances_by_name(map, instance_name, instance_kind).collect()
196        } else {
197            Self::find_instances_by_kind(map, instance_kind).collect()
198        };
199
200        if instances.is_empty() {
201            return Err(Error::NoPluginInstance {
202                name: instance_name.unwrap_or_default().to_string(),
203                kind: instance_kind.to_string(),
204            });
205        }
206        if instances.len() == 1 {
207            return Ok(instances[0]);
208        }
209
210        if let Some(instance_name) = instance_name {
211            Err(Error::DuplicateInstance(format!(
212                "Multiple {instance_kind:?} plugin instances {instance_name:?} are found. Consider renaming one of the instances",
213            )))
214        } else {
215            Err(Error::DuplicateInstance(format!(
216                "Multiple plugin instances for {instance_kind:?} are found. Consider specifying the instance to use",
217            )))
218        }
219    }
220}
221
222#[allow(clippy::needless_borrows_for_generic_args)] // https://github.com/rust-lang/rust-clippy/issues/13170
223pub(crate) fn resolve_plugin_path(
224    plugin_path: &mut PathBuf,
225    base_path: impl AsRef<Path>,
226) -> Result<(), Error> {
227    *plugin_path = openrr_client::resolve_relative_path(base_path, &plugin_path)?;
228    if plugin_path.extension().is_none() {
229        plugin_path.set_extension(env::consts::DLL_EXTENSION);
230    }
231    Ok(())
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
235#[serde(deny_unknown_fields)]
236pub struct PluginInstance {
237    /// Name of this plugin instance.
238    pub name: String,
239    /// Trait kind of this instance.
240    #[serde(rename = "type")]
241    pub type_: PluginInstanceKind,
242    /// Arguments passed when creating this instance.
243    pub args: Option<String>,
244    /// Pass the contents of the specified file as an argument.
245    pub args_from_path: Option<PathBuf>,
246}
247
248impl PluginInstance {
249    pub fn load_args(&self) -> Result<String, Error> {
250        if let Some(path) = &self.args_from_path {
251            fs::read_to_string(path).map_err(|e| Error::NoFile(path.to_owned(), e))
252        } else {
253            Ok(self.args.clone().unwrap_or_default())
254        }
255    }
256
257    fn create_lazy_instance<T, F>(
258        &self,
259        plugins: &mut PluginMap,
260        plugin_name: &str,
261        f: F,
262    ) -> Result<arci::Lazy<'static, T>, Error>
263    where
264        T: fmt::Debug,
265        F: FnOnce(&PluginProxy, String) -> Result<Option<T>, arci::Error> + Send + Sync + 'static,
266    {
267        let plugin = if let Some(plugin) = plugins.load(plugin_name)? {
268            plugin
269        } else {
270            return Err(Error::NoPluginInstance {
271                name: plugin_name.to_string(),
272                kind: self.type_.to_string(),
273            });
274        };
275        let args = self.load_args()?;
276        let plugin_name = plugin_name.to_string();
277        let instance_name = self.name.clone();
278        let instance_kind = self.type_;
279        Ok(arci::Lazy::new(move || match f(&plugin, args) {
280            Ok(Some(instance)) => {
281                info!(
282                    "created `{instance_kind:?}` instance `{instance_name}` from plugin `{plugin_name}`",
283                );
284                Ok(instance)
285            }
286            res => instance_create_error(res, instance_kind, instance_name, plugin_name)?,
287        }))
288    }
289}
290
291/// Trait kind of the instance.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
293#[serde(deny_unknown_fields)]
294#[non_exhaustive]
295pub enum PluginInstanceKind {
296    JointTrajectoryClient,
297    Localization,
298    MoveBase,
299    Navigation,
300    Speaker,
301}
302
303impl fmt::Display for PluginInstanceKind {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        write!(f, "{self:?}")
306    }
307}
308
309#[derive(Debug, Default)]
310pub struct PluginMap {
311    path: HashMap<String, PathBuf>,
312    cache: HashMap<String, Arc<PluginProxy>>,
313}
314
315impl PluginMap {
316    pub fn load(&mut self, name: impl AsRef<str>) -> Result<Option<Arc<PluginProxy>>, arci::Error> {
317        let name = name.as_ref();
318        if let Some((name, path)) = self.path.remove_entry(name) {
319            let plugin = Arc::new(PluginProxy::from_path(path)?);
320            self.cache.insert(name, plugin.clone());
321            Ok(Some(plugin))
322        } else {
323            Ok(self.cache.get(name).cloned())
324        }
325    }
326}
327
328impl<S: Into<String>, P: Into<PathBuf>> FromIterator<(S, P)> for PluginMap {
329    fn from_iter<T: IntoIterator<Item = (S, P)>>(iter: T) -> Self {
330        let path: HashMap<_, _> = iter
331            .into_iter()
332            .map(|(name, path)| (name.into(), path.into()))
333            .collect();
334        Self {
335            cache: HashMap::with_capacity(path.len() / 2),
336            path,
337        }
338    }
339}
340
341#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
342#[serde(deny_unknown_fields)]
343#[non_exhaustive] // The fields will increase depending on the feature flag.
344pub struct RobotConfig {
345    // TOML format has a restriction that if a table itself contains tables,
346    // all keys with non-table values must be emitted first.
347    // Therefore, these fields must be located at the start of the struct.
348    /// Joint trajectory clients to be used.
349    pub joint_trajectory_clients: Option<Vec<String>>,
350    /// Speakers to be used.
351    pub speakers: Option<Vec<String>>,
352    /// Localization to be used. `"ros"`, `"urdf-viz"`, `false`, or plugin instance name.
353    #[serde(skip_serializing_if = "ClientKind::is_auto")]
354    #[serde(default)]
355    pub localization: ClientKind,
356    /// MoveBase to be used. `"ros"`, `"urdf-viz"`, `false`, or plugin instance name.
357    #[serde(skip_serializing_if = "ClientKind::is_auto")]
358    #[serde(default)]
359    pub move_base: ClientKind,
360    /// Navigation to be used. `"ros"`, `"urdf-viz"`, `false`, or plugin instance name.
361    #[serde(skip_serializing_if = "ClientKind::is_auto")]
362    #[serde(default)]
363    pub navigation: ClientKind,
364
365    #[cfg(feature = "ros")]
366    #[serde(default)]
367    // https://github.com/alexcrichton/toml-rs/issues/258
368    #[serde(skip_serializing_if = "Vec::is_empty")]
369    pub ros_clients_configs: Vec<RosControlClientConfig>,
370    // A dummy field to catch that there is a config that requires the ros feature.
371    #[cfg(not(feature = "ros"))]
372    #[serde(default)]
373    #[serde(skip_serializing_if = "Vec::is_empty")]
374    #[schemars(schema_with = "unimplemented_schema")]
375    ros_clients_configs: Vec<toml::Value>,
376
377    #[cfg(feature = "ros")]
378    #[serde(default)]
379    // https://github.com/alexcrichton/toml-rs/issues/258
380    #[serde(skip_serializing_if = "Vec::is_empty")]
381    pub ros_action_clients_configs: Vec<RosControlActionClientConfig>,
382    // A dummy field to catch that there is a config that requires the ros feature.
383    #[cfg(not(feature = "ros"))]
384    #[serde(default)]
385    #[serde(skip_serializing_if = "Vec::is_empty")]
386    #[schemars(schema_with = "unimplemented_schema")]
387    ros_action_clients_configs: Vec<toml::Value>,
388
389    #[serde(default)]
390    // https://github.com/alexcrichton/toml-rs/issues/258
391    #[serde(skip_serializing_if = "Vec::is_empty")]
392    pub urdf_viz_clients_configs: Vec<UrdfVizWebClientConfig>,
393
394    #[serde(default)]
395    pub speak_configs: HashMap<String, SpeakConfig>,
396
397    #[cfg(feature = "ros")]
398    pub ros_cmd_vel_move_base_client_config: Option<RosCmdVelMoveBaseConfig>,
399    // A dummy field to catch that there is a config that requires the ros feature.
400    #[cfg(not(feature = "ros"))]
401    #[schemars(schema_with = "unimplemented_schema")]
402    ros_cmd_vel_move_base_client_config: Option<toml::Value>,
403
404    #[cfg(feature = "ros")]
405    pub ros_navigation_client_config: Option<RosNavClientConfig>,
406    // A dummy field to catch that there is a config that requires the ros feature.
407    #[cfg(not(feature = "ros"))]
408    #[schemars(schema_with = "unimplemented_schema")]
409    ros_navigation_client_config: Option<toml::Value>,
410
411    #[cfg(feature = "ros")]
412    pub ros_localization_client_config: Option<RosLocalizationClientConfig>,
413    // A dummy field to catch that there is a config that requires the ros feature.
414    #[cfg(not(feature = "ros"))]
415    #[schemars(schema_with = "unimplemented_schema")]
416    ros_localization_client_config: Option<toml::Value>,
417
418    #[serde(default)]
419    pub openrr_clients_config: OpenrrClientsConfig,
420
421    #[serde(default)]
422    pub openrr_tracing_config: OpenrrTracingConfig,
423
424    #[serde(default)]
425    pub plugins: HashMap<String, PluginConfig>,
426}
427
428#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
429#[serde(deny_unknown_fields)]
430pub struct OpenrrTracingConfig {
431    /// Enable openrr-tracing for Localization. `true or `false` (default to `false`).
432    #[serde(skip_serializing_if = "Not::not")]
433    #[serde(default)]
434    pub localization: bool,
435    /// Enable openrr-tracing for MoveBase. `true or `false` (default to `false`).
436    #[serde(skip_serializing_if = "Not::not")]
437    #[serde(default)]
438    pub move_base: bool,
439    /// Enable openrr-tracing for Navigation. `true or `false` (default to `false`).
440    #[serde(skip_serializing_if = "Not::not")]
441    #[serde(default)]
442    pub navigation: bool,
443}
444
445// Creates dummy schema for dummy fields.
446#[cfg(not(feature = "ros"))]
447fn unimplemented_schema(
448    _generator: &mut schemars::r#gen::SchemaGenerator,
449) -> schemars::schema::Schema {
450    unimplemented!()
451}
452
453impl RobotConfig {
454    const DEFAULT_SPEAKER_NAME: &'static str = "Default";
455
456    /// Returns true if ros-related configs exist, or it is explicitly specified
457    /// that the ros client is to be used.
458    ///
459    /// This is always false when ros feature is disabled.
460    #[cfg(feature = "ros")]
461    pub fn has_ros_clients(&self) -> bool {
462        let mut has_ros_clients = false;
463        let speak_configs = self.speak_configs.clone();
464        for (_, speak_config) in speak_configs {
465            has_ros_clients |= matches!(speak_config, SpeakConfig::RosEspeak { .. });
466        }
467        has_ros_clients |= !self.ros_clients_configs.is_empty();
468        has_ros_clients |= !self.ros_action_clients_configs.is_empty();
469        has_ros_clients |= self.move_base.is_builtin_ros()
470            || (self.move_base.is_auto() && self.ros_cmd_vel_move_base_client_config.is_some());
471        has_ros_clients |= self.navigation.is_builtin_ros()
472            || (self.navigation.is_auto() && self.ros_navigation_client_config.is_some());
473        has_ros_clients |= self.localization.is_builtin_ros()
474            || (self.localization.is_auto() && self.ros_localization_client_config.is_some());
475        has_ros_clients
476    }
477
478    #[cfg(not(feature = "ros"))]
479    pub fn has_ros_clients(&self) -> bool {
480        false
481    }
482
483    pub fn create_robot_client<L, M, N>(&self) -> Result<RobotClient<L, M, N>, Error>
484    where
485        L: Localization + From<Box<dyn Localization>>,
486        M: MoveBase + From<Box<dyn MoveBase>>,
487        N: Navigation + From<Box<dyn Navigation>>,
488    {
489        let mut plugins: PluginMap = self
490            .plugins
491            .iter()
492            .map(|(plugin_name, config)| (plugin_name, &config.path))
493            .collect();
494
495        let joint_trajectory_clients = self.create_raw_joint_trajectory_clients(&mut plugins)?;
496        let speakers = self.create_speakers(&mut plugins)?;
497        let localization = self.create_localization(&mut plugins)?;
498        let move_base = self.create_move_base(&mut plugins)?;
499        let navigation = self.create_navigation(&mut plugins)?;
500
501        Ok(RobotClient::new(
502            self.openrr_clients_config.clone(),
503            joint_trajectory_clients,
504            speakers,
505            localization.map(L::from),
506            move_base.map(M::from),
507            navigation.map(N::from),
508        )?)
509    }
510
511    fn create_localization_urdf_viz(&self) -> Box<dyn Localization> {
512        let loc = arci::Lazy::new(move || {
513            debug!("create_localization_urdf_viz: creating UrdfVizWebClient");
514            Ok(UrdfVizWebClient::default())
515        });
516        if self.openrr_tracing_config.localization {
517            Box::new(Tracing::new(loc))
518        } else {
519            Box::new(loc)
520        }
521    }
522
523    #[cfg(feature = "ros")]
524    fn create_localization_ros(&self) -> Option<Box<dyn Localization>> {
525        let config = self.ros_localization_client_config.clone()?;
526        let loc = arci::Lazy::new(move || {
527            debug!("create_localization_ros: creating RosLocalizationClient");
528            Ok(RosLocalizationClient::new_from_config(config))
529        });
530        if self.openrr_tracing_config.localization {
531            Some(Box::new(Tracing::new(loc)))
532        } else {
533            Some(Box::new(loc))
534        }
535    }
536
537    #[cfg(not(feature = "ros"))]
538    fn create_localization_ros(&self) -> Option<Box<dyn Localization>> {
539        unreachable!()
540    }
541
542    fn create_localization(
543        &self,
544        plugins: &mut PluginMap,
545    ) -> Result<Option<Box<dyn Localization>>, Error> {
546        let (plugin_name, instance) = match &self.localization {
547            ClientKind::Auto(false) => return Ok(None),
548            ClientKind::Auto(true) => {
549                if self.ros_localization_client_config.is_some() {
550                    return Ok(self.create_localization_ros());
551                }
552                match PluginConfig::resolve_instance(
553                    &self.plugins,
554                    None,
555                    PluginInstanceKind::Localization,
556                ) {
557                    Err(Error::NoPluginInstance { .. }) => {
558                        // If ros is already used, it would *not* usually be
559                        // assumed that urdf-viz would also be used.
560                        // Users who want to use both at the same time need to
561                        // specify it explicitly by `localization = "urdf-viz"`.
562                        if self.has_ros_clients() {
563                            return Ok(None);
564                        }
565                        return Ok(Some(self.create_localization_urdf_viz()));
566                    }
567                    res => res?,
568                }
569            }
570            ClientKind::Builtin(BuiltinClient::Ros) => {
571                return Ok(self.create_localization_ros());
572            }
573            ClientKind::Builtin(BuiltinClient::UrdfViz) => {
574                return Ok(Some(self.create_localization_urdf_viz()));
575            }
576            ClientKind::Plugin(instance_name) => PluginConfig::resolve_instance(
577                &self.plugins,
578                Some(instance_name),
579                PluginInstanceKind::Localization,
580            )?,
581        };
582
583        Ok(Some(Box::new(instance.create_lazy_instance(
584            plugins,
585            plugin_name,
586            PluginProxy::new_localization,
587        )?)))
588    }
589
590    fn create_navigation_urdf_viz(&self) -> Box<dyn Navigation> {
591        let nav = arci::Lazy::new(move || {
592            debug!("create_navigation_urdf_viz: creating UrdfVizWebClient");
593            Ok(UrdfVizWebClient::default())
594        });
595        if self.openrr_tracing_config.navigation {
596            Box::new(Tracing::new(nav))
597        } else {
598            Box::new(nav)
599        }
600    }
601
602    #[cfg(feature = "ros")]
603    fn create_navigation_ros(&self) -> Option<Box<dyn Navigation>> {
604        let config = self.ros_navigation_client_config.clone()?;
605        let nav = arci::Lazy::new(move || {
606            debug!("create_navigation_ros: creating RosNavClient");
607            Ok(RosNavClient::new_from_config(config))
608        });
609        if self.openrr_tracing_config.navigation {
610            Some(Box::new(Tracing::new(nav)))
611        } else {
612            Some(Box::new(nav))
613        }
614    }
615
616    #[cfg(not(feature = "ros"))]
617    fn create_navigation_ros(&self) -> Option<Box<dyn Navigation>> {
618        unreachable!()
619    }
620
621    fn create_navigation(
622        &self,
623        plugins: &mut PluginMap,
624    ) -> Result<Option<Box<dyn Navigation>>, Error> {
625        let (plugin_name, instance) = match &self.navigation {
626            ClientKind::Auto(false) => return Ok(None),
627            ClientKind::Auto(true) => {
628                if self.ros_navigation_client_config.is_some() {
629                    return Ok(self.create_navigation_ros());
630                }
631                match PluginConfig::resolve_instance(
632                    &self.plugins,
633                    None,
634                    PluginInstanceKind::Navigation,
635                ) {
636                    Err(Error::NoPluginInstance { .. }) => {
637                        // If ros is already used, it would *not* usually be
638                        // assumed that urdf-viz would also be used.
639                        // Users who want to use both at the same time need to
640                        // specify it explicitly by `navigation = "urdf-viz"`.
641                        if self.has_ros_clients() {
642                            return Ok(None);
643                        }
644                        return Ok(Some(self.create_navigation_urdf_viz()));
645                    }
646                    res => res?,
647                }
648            }
649            ClientKind::Builtin(BuiltinClient::Ros) => {
650                return Ok(self.create_navigation_ros());
651            }
652            ClientKind::Builtin(BuiltinClient::UrdfViz) => {
653                return Ok(Some(self.create_navigation_urdf_viz()));
654            }
655            ClientKind::Plugin(instance_name) => PluginConfig::resolve_instance(
656                &self.plugins,
657                Some(instance_name),
658                PluginInstanceKind::Navigation,
659            )?,
660        };
661
662        Ok(Some(Box::new(instance.create_lazy_instance(
663            plugins,
664            plugin_name,
665            PluginProxy::new_navigation,
666        )?)))
667    }
668
669    fn create_move_base_urdf_viz(&self) -> Box<dyn MoveBase> {
670        let base = arci::Lazy::new(move || {
671            debug!("create_move_base_urdf_viz: creating UrdfVizWebClient");
672            let urdf_viz_client = UrdfVizWebClient::default();
673            urdf_viz_client.run_send_velocity_thread();
674            Ok(urdf_viz_client)
675        });
676        if self.openrr_tracing_config.move_base {
677            Box::new(Tracing::new(base))
678        } else {
679            Box::new(base)
680        }
681    }
682
683    #[cfg(feature = "ros")]
684    fn create_move_base_ros(&self) -> Option<Box<dyn MoveBase>> {
685        let topic = self
686            .ros_cmd_vel_move_base_client_config
687            .as_ref()?
688            .topic
689            .to_string();
690        let base = arci::Lazy::new(move || {
691            debug!("create_move_base_ros: creating RosCmdVelMoveBase");
692            Ok(RosCmdVelMoveBase::new(&topic))
693        });
694        if self.openrr_tracing_config.move_base {
695            Some(Box::new(Tracing::new(base)))
696        } else {
697            Some(Box::new(base))
698        }
699    }
700
701    #[cfg(not(feature = "ros"))]
702    fn create_move_base_ros(&self) -> Option<Box<dyn MoveBase>> {
703        unreachable!()
704    }
705
706    fn create_move_base(
707        &self,
708        plugins: &mut PluginMap,
709    ) -> Result<Option<Box<dyn MoveBase>>, Error> {
710        let (plugin_name, instance) = match &self.move_base {
711            ClientKind::Auto(false) => return Ok(None),
712            ClientKind::Auto(true) => {
713                if self.ros_cmd_vel_move_base_client_config.is_some() {
714                    return Ok(self.create_move_base_ros());
715                }
716                match PluginConfig::resolve_instance(
717                    &self.plugins,
718                    None,
719                    PluginInstanceKind::MoveBase,
720                ) {
721                    Err(Error::NoPluginInstance { .. }) => {
722                        // If ros is already used, it would *not* usually be
723                        // assumed that urdf-viz would also be used.
724                        // Users who want to use both at the same time need to
725                        // specify it explicitly by `move_base = "urdf-viz"`.
726                        if self.has_ros_clients() {
727                            return Ok(None);
728                        }
729                        return Ok(Some(self.create_move_base_urdf_viz()));
730                    }
731                    res => res?,
732                }
733            }
734            ClientKind::Builtin(BuiltinClient::Ros) => {
735                return Ok(self.create_move_base_ros());
736            }
737            ClientKind::Builtin(BuiltinClient::UrdfViz) => {
738                return Ok(Some(self.create_move_base_urdf_viz()));
739            }
740            ClientKind::Plugin(instance_name) => PluginConfig::resolve_instance(
741                &self.plugins,
742                Some(instance_name),
743                PluginInstanceKind::MoveBase,
744            )?,
745        };
746
747        Ok(Some(Box::new(instance.create_lazy_instance(
748            plugins,
749            plugin_name,
750            PluginProxy::new_move_base,
751        )?)))
752    }
753
754    fn create_speakers(
755        &self,
756        plugins: &mut PluginMap,
757    ) -> Result<HashMap<String, Arc<dyn Speaker>>, Error> {
758        let mut speakers: HashMap<_, Arc<dyn Speaker>> = HashMap::new();
759        for (name, speak_config) in self
760            .speak_configs
761            .iter()
762            .filter(|(name, _)| self.speakers.as_ref().is_none_or(|v| v.contains(name)))
763        {
764            speakers.insert(name.to_owned(), speak_config.build()?.into());
765        }
766
767        for (plugin_name, config) in &self.plugins {
768            for instance in config.instances.iter().filter(|instance| {
769                instance.type_ == PluginInstanceKind::Speaker
770                    && self
771                        .speakers
772                        .as_ref()
773                        .is_none_or(|v| v.contains(&instance.name))
774            }) {
775                if speakers.contains_key(&instance.name) {
776                    return Err(Error::DuplicateInstance(format!(
777                        "Multiple {:?} instances {:?} are found. Consider renaming one of the instances",
778                        instance.type_, instance.name,
779                    )));
780                }
781
782                speakers.insert(
783                    instance.name.clone(),
784                    Arc::new(instance.create_lazy_instance(
785                        plugins,
786                        plugin_name,
787                        PluginProxy::new_speaker,
788                    )?),
789                );
790            }
791        }
792
793        if self.speakers.is_none() && speakers.is_empty() {
794            speakers.insert(
795                Self::DEFAULT_SPEAKER_NAME.to_owned(),
796                SpeakConfig::default().build()?.into(),
797            );
798        }
799        Ok(speakers)
800    }
801
802    fn create_raw_joint_trajectory_clients(
803        &self,
804        plugins: &mut PluginMap,
805    ) -> Result<HashMap<String, Arc<dyn JointTrajectoryClient>>, Error> {
806        // True if the name is included in the list of clients to use
807        // or if no list is specified.
808        let is_used = |client_name| {
809            self.joint_trajectory_clients
810                .as_ref()
811                .is_none_or(|v| v.contains(client_name))
812        };
813        let urdf_viz_clients_configs: Vec<_> = self
814            .urdf_viz_clients_configs
815            .iter()
816            .filter(|c| is_used(&c.name))
817            .cloned()
818            .collect();
819        #[cfg(feature = "ros")]
820        let ros_clients_configs: Vec<_> = self
821            .ros_clients_configs
822            .iter()
823            .filter(|c| is_used(&c.name))
824            .collect();
825        #[cfg(feature = "ros")]
826        let ros_action_clients_configs: Vec<_> = self
827            .ros_action_clients_configs
828            .iter()
829            .filter(|c| is_used(&c.name))
830            .collect();
831
832        let mut urdf_robot = None;
833        #[cfg(not(feature = "ros"))]
834        let use_urdf = !urdf_viz_clients_configs.is_empty();
835        #[cfg(feature = "ros")]
836        let use_urdf = !urdf_viz_clients_configs.is_empty()
837            || !ros_clients_configs.is_empty()
838            || !ros_action_clients_configs.is_empty();
839        if use_urdf && let Some(urdf_path) = self.openrr_clients_config.urdf_full_path() {
840            urdf_robot = Some(urdf_rs::utils::read_urdf_or_xacro(urdf_path)?);
841        }
842
843        let mut clients = arci_urdf_viz::create_joint_trajectory_clients_lazy(
844            urdf_viz_clients_configs,
845            urdf_robot.as_ref(),
846        )?;
847        #[cfg(feature = "ros")]
848        {
849            clients.extend(arci_ros::create_joint_trajectory_clients_lazy(
850                ros_clients_configs,
851                urdf_robot.as_ref(),
852            )?);
853            clients.extend(arci_ros::create_joint_trajectory_clients_lazy(
854                ros_action_clients_configs,
855                urdf_robot.as_ref(),
856            )?);
857        }
858
859        for (plugin_name, config) in &self.plugins {
860            for instance in config.instances.iter().filter(|instance| {
861                instance.type_ == PluginInstanceKind::JointTrajectoryClient
862                    && self
863                        .joint_trajectory_clients
864                        .as_ref()
865                        .is_none_or(|v| v.contains(&instance.name))
866            }) {
867                if clients.contains_key(&instance.name) {
868                    return Err(Error::DuplicateInstance(format!(
869                        "Multiple {:?} instances {:?} are found. Consider renaming one of the instances",
870                        instance.type_, instance.name,
871                    )));
872                }
873
874                let client = instance.create_lazy_instance(
875                    plugins,
876                    plugin_name,
877                    PluginProxy::new_joint_trajectory_client,
878                )?;
879                // If the `PluginProxy::new_joint_trajectory_client` returns
880                // `Err` or `None`, `JointTrajectoryClient::joint_names` will
881                // panic. Therefore, initialize it here to allow the user to
882                // handle error.
883                // `JointTrajectoryClientsContainer::new`, which is called inside
884                // `RobotClient::new`, calls `JointTrajectoryClient::joint_names`,
885                // so it makes no sense to make `JointTrajectoryClient` lazy here.
886                client.get_ref()?;
887                clients.insert(instance.name.clone(), Arc::new(client));
888            }
889        }
890
891        Ok(clients)
892    }
893}
894
895/// Convert relative path into absolute one
896#[allow(clippy::needless_borrows_for_generic_args)] // https://github.com/rust-lang/rust-clippy/issues/13170
897fn resolve_audio_file_path<P: AsRef<Path>>(
898    base_path: P,
899    relative_hash_map: &mut HashMap<String, PathBuf>,
900) -> Result<(), Error> {
901    for v in relative_hash_map.values_mut() {
902        let full_path = openrr_client::resolve_relative_path(base_path.as_ref(), &v)?;
903        *v = full_path;
904    }
905    Ok(())
906}
907
908fn instance_create_error<T: fmt::Debug, U>(
909    res: Result<T, arci::Error>,
910    instance_kind: PluginInstanceKind,
911    instance_name: String,
912    plugin_name: String,
913) -> Result<U, arci::Error> {
914    error!(
915        "failed to create `{instance_kind:?}` instance `{instance_name}` from plugin `{plugin_name}`: {res:?}",
916    );
917    res.and_then(|_| {
918        // TODO: error msg
919        Err(format_err!(
920            "failed to create `{instance_kind:?}` instance `{instance_name}` from plugin `{plugin_name}`: None",
921        )
922        .into())
923    })
924}
925
926impl RobotConfig {
927    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
928        Self::from_str(
929            &std::fs::read_to_string(&path)
930                .map_err(|e| Error::NoFile(path.as_ref().to_owned(), e))?,
931            &path,
932        )
933    }
934
935    pub fn from_str<P: AsRef<Path>>(s: &str, path: P) -> Result<Self, Error> {
936        let path = path.as_ref();
937
938        let mut config: RobotConfig =
939            toml::from_str(s).map_err(|e| Error::TomlParseFailure(path.to_owned(), e))?;
940
941        config.validate_ros_config()?;
942
943        if config.openrr_clients_config.urdf_path.is_some() {
944            config.openrr_clients_config.resolve_path(path)?;
945        }
946        for speak_config in config.speak_configs.values_mut() {
947            if let SpeakConfig::Audio { map } = speak_config {
948                resolve_audio_file_path(path, map)?;
949            }
950        }
951        for plugin_config in config.plugins.values_mut() {
952            resolve_plugin_path(&mut plugin_config.path, path)?;
953            for instance in &mut plugin_config.instances {
954                if let Some(args_path) = instance.args_from_path.take() {
955                    instance.args_from_path =
956                        Some(openrr_client::resolve_relative_path(path, args_path)?);
957                }
958            }
959        }
960        debug!("{config:?}");
961        Ok(config)
962    }
963
964    /// Returns an error if a config requires ros feature but ros feature is disabled.
965    fn validate_ros_config(&self) -> Result<(), Error> {
966        if cfg!(feature = "ros") {
967            return Ok(());
968        }
969
970        for (name, speak_config) in &self.speak_configs {
971            if matches!(speak_config, SpeakConfig::RosEspeak { .. }) {
972                return Err(Error::ConfigRequireRos(format!("speak_configs.{name}")));
973            }
974        }
975        if !self.ros_clients_configs.is_empty() {
976            return Err(Error::ConfigRequireRos("ros_clients_configs".into()));
977        }
978        if !self.ros_action_clients_configs.is_empty() {
979            return Err(Error::ConfigRequireRos("ros_action_clients_configs".into()));
980        }
981        match self.move_base {
982            ClientKind::Builtin(BuiltinClient::Ros) => {
983                return Err(Error::ConfigRequireRos("move_base".into()));
984            }
985            ClientKind::Auto(true) if self.ros_cmd_vel_move_base_client_config.is_some() => {
986                return Err(Error::ConfigRequireRos(
987                    "ros_cmd_vel_move_base_client_config".into(),
988                ));
989            }
990            _ => {}
991        }
992        match self.navigation {
993            ClientKind::Builtin(BuiltinClient::Ros) => {
994                return Err(Error::ConfigRequireRos("navigation".into()));
995            }
996            ClientKind::Auto(true) if self.ros_navigation_client_config.is_some() => {
997                return Err(Error::ConfigRequireRos(
998                    "ros_navigation_client_config".into(),
999                ));
1000            }
1001            _ => {}
1002        }
1003        match self.localization {
1004            ClientKind::Builtin(BuiltinClient::Ros) => {
1005                return Err(Error::ConfigRequireRos("localization".into()));
1006            }
1007            ClientKind::Auto(true) if self.ros_localization_client_config.is_some() => {
1008                return Err(Error::ConfigRequireRos(
1009                    "ros_localization_client_config".into(),
1010                ));
1011            }
1012            _ => {}
1013        }
1014        Ok(())
1015    }
1016}
1017
1018#[cfg(test)]
1019mod test {
1020    use super::*;
1021    #[test]
1022    fn test_resolve_audio_file_path() {
1023        let mut hash = HashMap::new();
1024        hash.insert("a".to_owned(), PathBuf::from("dir1/file.mp3"));
1025        hash.insert("b".to_owned(), PathBuf::from("../dir2/file.mp3"));
1026        resolve_audio_file_path("/config/some_file.toml", &mut hash).unwrap();
1027        assert_eq!(hash.len(), 2);
1028        assert_eq!(hash["a"], PathBuf::from("/config/dir1/file.mp3"));
1029        assert_eq!(hash["b"], PathBuf::from("/config/../dir2/file.mp3"));
1030    }
1031}