Skip to main content

ros2_client/
context.rs

1use std::{
2  collections::HashMap,
3  sync::{Arc, Mutex},
4};
5//use futures::{pin_mut, StreamExt};
6#[cfg(feature = "security")]
7use std::path::{Path, PathBuf};
8
9#[allow(unused_imports)]
10use log::{debug, error, info, trace, warn};
11//use mio::Evented;
12use serde::Serialize;
13use rustdds::{
14  dds::CreateResult,
15  no_key::{DeserializerAdapter, SerializerAdapter},
16  policy::*,
17  *,
18};
19
20use crate::{
21  builtin_topics,
22  entities_info::{NodeEntitiesInfo, ParticipantEntitiesInfo},
23  gid::Gid,
24  names::*,
25  node::{Node, NodeOptions},
26  pubsub::{Publisher, Subscription},
27  NodeCreateError,
28};
29
30lazy_static! {
31/// Basic BestEffort QoS for subscribers
32///
33/// Note: If you want Reliable communication, both publisher and Subscriber
34/// must specify QoS Reliability = Reliable.
35  pub static ref DEFAULT_SUBSCRIPTION_QOS: QosPolicies = QosPolicyBuilder::new()
36    .durability(Durability::Volatile) // default per table in DDS Spec v1.4 Section 2.2.3 Supported QoS
37    .deadline(Deadline(Duration::INFINITE)) // default per table in DDS Spec v1.4 Section 2.2.3 Supported QoS
38    .ownership(Ownership::Shared) // default per table in DDS Spec v1.4 Section 2.2.3 Supported QoS
39    .reliability(Reliability::BestEffort) // default for DataReaders and Topics
40    .history(History::KeepLast { depth: 1 }) // default per table in DDS Spec v1.4 Section 2.2.3 Supported QoS
41    .lifespan(Lifespan {
42      // default per table in DDS Spec v1.4 Section 2.2.3 Supported QoS
43      duration: Duration::INFINITE
44    })
45    .build();
46}
47
48lazy_static! {
49/// Basic Reliable QoS for publishing.
50  pub static ref DEFAULT_PUBLISHER_QOS: QosPolicies = QosPolicyBuilder::new()
51    .durability(Durability::Volatile)
52    .deadline(Deadline(Duration::INFINITE))
53    .ownership(Ownership::Shared)
54    .reliability(Reliability::Reliable{max_blocking_time: Duration::from_millis(100)})
55      // Reliability = Reliable is the default for DataWriters, different from above.
56    .history(History::KeepLast { depth: 1 })
57    .lifespan(Lifespan {
58      duration: Duration::INFINITE
59    })
60    .build();
61}
62
63#[cfg(feature = "security")]
64struct SecurityConfig {
65  /// Path to a directory of configuration files.
66  security_config_dir: PathBuf,
67  /// Password used for decryption the private key file.
68  private_key_password: String,
69}
70
71/// Builder for configuring a `Context`
72pub struct ContextOptions {
73  domain_id: u16,
74  #[cfg(feature = "security")]
75  security_config: Option<SecurityConfig>,
76}
77
78impl ContextOptions {
79  pub fn new() -> Self {
80    Self {
81      domain_id: 0,
82      #[cfg(feature = "security")]
83      security_config: None,
84    }
85  }
86
87  /// Set the DDS Domain Id.
88  ///
89  /// Please refer to the
90  /// [ROS_DOMAIN_ID](https://docs.ros.org/en/iron/Concepts/Intermediate/About-Domain-ID.html)
91  /// or DDS documentation.
92  pub fn domain_id(mut self, domain_id: u16) -> Self {
93    self.domain_id = domain_id;
94    self
95  }
96
97  /// Enable DDS security features.
98  ///
99  /// Using security requires providing appropriate configuration files.
100  ///
101  /// [Security in RustDDS](https://github.com/jhelovuo/RustDDS/blob/master/SECURITY.md)
102  #[cfg(feature = "security")]
103  pub fn enable_security(
104    mut self,
105    security_config_dir: impl AsRef<Path>,
106    private_key_password: String,
107  ) -> Self {
108    self.security_config = Some(SecurityConfig {
109      security_config_dir: security_config_dir.as_ref().to_path_buf(),
110      private_key_password,
111    });
112    self
113  }
114}
115
116impl Default for ContextOptions {
117  fn default() -> Self {
118    Self::new()
119  }
120}
121
122/// [Context] communicates with other
123/// participants information in ROS2 network. It keeps track of
124/// [`NodeEntitiesInfo`]s. Also acts as a wrapper for a RustDDS instance.
125///
126/// Context is shut down by dropping it, and all of its RosNodes.
127/// There should be no need for `ok()` or `shutdown()` methods.
128#[derive(Clone)]
129pub struct Context {
130  inner: Arc<Mutex<ContextInner>>,
131}
132
133impl Context {
134  /// Create a new Context with default settings.
135  pub fn new() -> CreateResult<Context> {
136    Self::from_domain_participant(DomainParticipant::new(0)?)
137  }
138
139  /// Create a new Context.
140  pub fn with_options(opt: ContextOptions) -> CreateResult<Context> {
141    #[allow(unused_mut)] // only mutated with security
142    let mut dpb = DomainParticipantBuilder::new(opt.domain_id);
143
144    #[cfg(feature = "security")]
145    {
146      if let Some(sc) = opt.security_config {
147        dpb = dpb.builtin_security(
148          DomainParticipantSecurityConfigFiles::with_ros_default_names(
149            sc.security_config_dir,
150            sc.private_key_password,
151          ),
152        );
153      }
154    }
155
156    Self::from_domain_participant(dpb.build()?)
157  }
158
159  /// Create a new Context from an existing [`DomainParticipant`].
160  pub fn from_domain_participant(domain_participant: DomainParticipant) -> CreateResult<Context> {
161    // Check the runtime ROS_DISTRO against the distribution we were compiled
162    // for. Guarded so it logs only once per process even with many Contexts.
163    static ROS_DISTRO_CHECK: std::sync::Once = std::sync::Once::new();
164    ROS_DISTRO_CHECK.call_once(crate::distributions::verify_ros_distro_env);
165
166    let i = ContextInner::from_domain_participant(domain_participant)?;
167    Ok(Context {
168      inner: Arc::new(Mutex::new(i)),
169    })
170  }
171
172  /// Create a new ROS2 [`Node`]
173  pub fn new_node(
174    &self,
175    node_name: NodeName,
176    options: NodeOptions,
177  ) -> Result<Node, NodeCreateError> {
178    Node::new(node_name, options, self.clone())
179  }
180
181  /// Query which DDS Domain Id we are using.
182  pub fn domain_id(&self) -> u16 {
183    self.inner.lock().unwrap().domain_participant.domain_id()
184  }
185
186  /// Which topics have been discovered?
187  pub fn discovered_topics(&self) -> Vec<rustdds::discovery::DiscoveredTopicData> {
188    self.domain_participant().discovered_topics()
189  }
190
191  /// Gets the ParticipantEntitiesInfo describing the current state of
192  /// this Context. This is what we send to ROS Discovery.
193  pub fn participant_entities_info(&self) -> ParticipantEntitiesInfo {
194    self.inner.lock().unwrap().participant_entities_info()
195  }
196
197  /// Get a (handle to) the ROSOut logging Topic.
198  pub fn get_parameter_events_topic(&self) -> Topic {
199    self
200      .inner
201      .lock()
202      .unwrap()
203      .ros_parameter_events_topic
204      .clone()
205  }
206
207  /// Get a (handle to) the ROSOut logging Topic.
208  ///
209  /// Note: The recommended way to write log messages to ROSOut is via the
210  /// [`crate::rosout!`] macro.
211  pub fn get_rosout_topic(&self) -> Topic {
212    self.inner.lock().unwrap().ros_rosout_topic.clone()
213  }
214
215  /// Get the contained DDS [`DomainParticipant`].
216  ///
217  /// The return value is owned, but it is just a cloned smart pointer.
218  pub fn domain_participant(&self) -> DomainParticipant {
219    self.inner.lock().unwrap().domain_participant.clone()
220  }
221
222  // pub fn ros_discovery_stream(&self) -> impl Stream<Item =
223  // ReadResult<(ParticipantEntitiesInfo, MessageInfo)>> + FusedStream + '_ {
224  //   self.inner.lock().unwrap().node_reader.async_stream()
225  // }
226
227  // -----------------------------------------------------------------------
228
229  pub fn create_topic(
230    &self,
231    topic_dds_name: String,
232    type_name: MessageTypeName,
233    qos: &QosPolicies,
234  ) -> CreateResult<Topic> {
235    info!("Creating topic, DDS name: {topic_dds_name}");
236    let topic = self.domain_participant().create_topic(
237      topic_dds_name,
238      type_name.dds_msg_type(),
239      qos,
240      TopicKind::NoKey,
241    )?;
242    // ROS2 does not use WithKey topics, so always NoKey
243    info!("Created topic");
244    Ok(topic)
245  }
246
247  pub(crate) fn create_publisher<M>(
248    &self,
249    topic: &Topic,
250    qos: Option<QosPolicies>,
251  ) -> dds::CreateResult<Publisher<M>>
252  where
253    M: Serialize,
254  {
255    let datawriter = self
256      .get_ros_default_publisher()
257      .create_datawriter_no_key(topic, qos)?;
258
259    Ok(Publisher::new(datawriter))
260  }
261
262  pub(crate) fn create_subscription<M>(
263    &self,
264    topic: &Topic,
265    qos: Option<QosPolicies>,
266  ) -> dds::CreateResult<Subscription<M>>
267  where
268    M: 'static,
269  {
270    let datareader = self
271      .get_ros_default_subscriber()
272      .create_simple_datareader_no_key(topic, qos)?;
273    Ok(Subscription::new(datareader))
274  }
275
276  pub(crate) fn create_datawriter<M, SA>(
277    &self,
278    topic: &Topic,
279    qos: Option<QosPolicies>,
280  ) -> dds::CreateResult<no_key::DataWriter<M, SA>>
281  where
282    SA: SerializerAdapter<M>,
283  {
284    self
285      .get_ros_default_publisher()
286      .create_datawriter_no_key(topic, qos)
287  }
288
289  pub(crate) fn create_simpledatareader<M, DA>(
290    &self,
291    topic: &Topic,
292    qos: Option<QosPolicies>,
293  ) -> dds::CreateResult<no_key::SimpleDataReader<M, DA>>
294  where
295    M: 'static,
296    DA: 'static + DeserializerAdapter<M>,
297  {
298    self
299      .get_ros_default_subscriber()
300      .create_simple_datareader_no_key(topic, qos)
301  }
302
303  pub(crate) fn update_node(&mut self, node_info: NodeEntitiesInfo) {
304    self.inner.lock().unwrap().update_node(node_info);
305  }
306
307  pub(crate) fn remove_node(&mut self, node_name: &str) {
308    self.inner.lock().unwrap().remove_node(node_name);
309  }
310
311  fn get_ros_default_publisher(&self) -> rustdds::Publisher {
312    self.inner.lock().unwrap().ros_default_publisher.clone()
313  }
314
315  fn get_ros_default_subscriber(&self) -> rustdds::Subscriber {
316    self.inner.lock().unwrap().ros_default_subscriber.clone()
317  }
318
319  pub(crate) fn ros_discovery_topic(&self) -> Topic {
320    self.inner.lock().unwrap().ros_discovery_topic.clone()
321  }
322}
323
324struct ContextInner {
325  local_nodes: HashMap<String, NodeEntitiesInfo>,
326
327  // ROS Discovery: topic, reader and writer
328  ros_discovery_topic: Topic,
329  node_writer: Publisher<ParticipantEntitiesInfo>,
330  // Corresponding ParticipantEntitiesInfo Subscriber is
331  // (optionally) in Node --> Spinner, if it is
332  // activated. Context does not have its own thread of control, so
333  // it cannot do reading.
334  domain_participant: DomainParticipant,
335  // DDS Requires Publisher and Subscriber to create (and group)
336  // DataWriters and DataReaders, so we create one of each.
337  ros_default_publisher: rustdds::Publisher,
338  ros_default_subscriber: rustdds::Subscriber,
339
340  ros_parameter_events_topic: Topic,
341  ros_rosout_topic: Topic,
342}
343
344impl ContextInner {
345  // "new"
346  pub fn from_domain_participant(
347    domain_participant: DomainParticipant,
348  ) -> CreateResult<ContextInner> {
349    let ros_default_publisher = domain_participant.create_publisher(&DEFAULT_PUBLISHER_QOS)?;
350    let ros_default_subscriber = domain_participant.create_subscriber(&DEFAULT_SUBSCRIPTION_QOS)?;
351
352    // This is for tracking (ROS) Node to (DDS) Participant mapping
353    let ros_discovery_topic = domain_participant.create_topic(
354      builtin_topics::ros_discovery::TOPIC_NAME.to_string(),
355      builtin_topics::ros_discovery::TYPE_NAME.to_string(),
356      &builtin_topics::ros_discovery::QOS_PUB,
357      TopicKind::NoKey,
358    )?;
359
360    let ros_parameter_events_topic = domain_participant.create_topic(
361      builtin_topics::parameter_events::TOPIC_NAME.to_string(),
362      builtin_topics::parameter_events::TYPE_NAME.to_string(),
363      &builtin_topics::parameter_events::QOS,
364      TopicKind::NoKey,
365    )?;
366
367    let ros_rosout_topic = domain_participant.create_topic(
368      builtin_topics::rosout::TOPIC_NAME.to_string(),
369      builtin_topics::rosout::TYPE_NAME.to_string(),
370      &builtin_topics::rosout::QOS,
371      TopicKind::NoKey,
372    )?;
373
374    let node_writer =
375      Publisher::new(ros_default_publisher.create_datawriter_no_key(&ros_discovery_topic, None)?);
376
377    Ok(ContextInner {
378      local_nodes: HashMap::new(),
379      node_writer,
380
381      domain_participant,
382      ros_discovery_topic,
383      ros_default_publisher,
384      ros_default_subscriber,
385      ros_parameter_events_topic,
386      ros_rosout_topic,
387    })
388  }
389
390  /// Gets our current participant info we have sent to ROS2 network
391  pub fn participant_entities_info(&self) -> ParticipantEntitiesInfo {
392    ParticipantEntitiesInfo::new(
393      Gid::from(self.domain_participant.guid()),
394      self.local_nodes.values().cloned().collect(),
395    )
396  }
397
398  // Adds new NodeEntitiesInfo and updates our ContextInfo to ROS2 network
399  fn update_node(&mut self, mut node_info: NodeEntitiesInfo) {
400    // Each node connects also to the ROS discovery topic
401    node_info.add_writer(Gid::from(self.node_writer.guid()));
402
403    self
404      .local_nodes
405      .insert(node_info.fully_qualified_name(), node_info);
406    self.broadcast_node_infos();
407  }
408
409  /// Removes NodeEntitiesInfo and updates our ContextInfo to ROS2 network
410  fn remove_node(&mut self, node_fqn: &str) {
411    self.local_nodes.remove(node_fqn);
412    self.broadcast_node_infos();
413  }
414
415  fn broadcast_node_infos(&self) {
416    let pei = self.participant_entities_info();
417    debug!("ROS discovery publish: {pei:?}");
418    self
419      .node_writer
420      .publish(pei)
421      .unwrap_or_else(|e| error!("Failed to write into node_writer {e:?}"));
422  }
423} // impl ContextInner
424
425impl Drop for ContextInner {
426  fn drop(&mut self) {
427    // Clears all nodes and updates our ContextInfo to ROS2 network
428    self.local_nodes.clear();
429    self.broadcast_node_infos();
430  }
431}
432
433// -------------------------------------------------------------------------------------
434// -------------------------------------------------------------------------------------
435
436#[test]
437fn test_node_create() {
438  use crate::ParameterValue;
439
440  let context = Context::new().unwrap();
441  let _node = context
442    .new_node(
443      NodeName::new("/rustdds", "test_node").unwrap(),
444      NodeOptions::new().declare_parameter("foo", ParameterValue::Boolean(true)),
445    )
446    .is_ok();
447}