Skip to main content

nros_node/
node.rs

1//! Node implementation
2
3use heapless::Vec;
4use nros_core::RosMessage;
5use nros_rmw::{QoSProfile, TopicInfo};
6
7use crate::{publisher::PublisherHandle, subscriber::SubscriptionHandle};
8
9/// Node configuration
10#[derive(Debug, Clone)]
11pub struct NodeConfig<'a> {
12    /// Node name
13    pub name: &'a str,
14    /// Node namespace
15    pub namespace: &'a str,
16    /// Domain ID (default: 0)
17    pub domain_id: u32,
18}
19
20impl<'a> NodeConfig<'a> {
21    /// Create a new node configuration
22    pub const fn new(name: &'a str, namespace: &'a str) -> Self {
23        Self {
24            name,
25            namespace,
26            domain_id: 0,
27        }
28    }
29
30    /// Set the domain ID
31    pub const fn with_domain(mut self, domain_id: u32) -> Self {
32        self.domain_id = domain_id;
33        self
34    }
35}
36
37impl Default for NodeConfig<'_> {
38    fn default() -> Self {
39        Self::new("nros_node", "/")
40    }
41}
42
43/// Node error types
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum NodeError {
46    /// Maximum publishers reached
47    MaxPublishersReached,
48    /// Maximum subscribers reached
49    MaxSubscribersReached,
50    /// Invalid publisher handle
51    InvalidPublisherHandle,
52    /// Invalid subscriber handle
53    InvalidSubscriberHandle,
54    /// Serialization failed
55    SerializationFailed,
56    /// Deserialization failed
57    DeserializationFailed,
58    /// Buffer too small
59    BufferTooSmall,
60    /// Transport error
61    TransportError,
62    /// Not connected
63    NotConnected,
64    /// A topic/endpoint name exceeded the bounded string capacity (Phase 192.1 —
65    /// surfaced instead of silently truncating, which would corrupt the wire key).
66    TopicNameTooLong,
67    /// A node name exceeded the bounded string capacity.
68    NameTooLong,
69    /// A node namespace exceeded the bounded string capacity.
70    NamespaceTooLong,
71}
72
73/// Default serialization (tx) / reception (rx) buffer length for a `Node`.
74/// Phase 192.5 — names the inline `1024` (was duplicated at the field decl + the
75/// `Node::new` initializer).
76pub const NODE_TX_BUF_LEN: usize = 1024;
77/// Default reception buffer length for a `Node`.
78pub const NODE_RX_BUF_LEN: usize = 1024;
79
80/// Publisher registration info
81#[derive(Debug, Clone)]
82#[allow(dead_code)] // Fields used when transport is connected
83struct PublisherInfo {
84    /// Topic name
85    topic_name: heapless::String<64>,
86    /// Type name
87    type_name: &'static str,
88    /// Type hash
89    type_hash: &'static str,
90    /// QoS settings
91    qos: QoSProfile,
92    /// Active flag
93    active: bool,
94}
95
96/// Subscriber registration info
97#[derive(Debug, Clone)]
98#[allow(dead_code)] // Fields used when transport is connected
99struct SubscriberInfo {
100    /// Topic name
101    topic_name: heapless::String<64>,
102    /// Type name
103    type_name: &'static str,
104    /// Type hash
105    type_hash: &'static str,
106    /// QoS settings
107    qos: QoSProfile,
108    /// Active flag
109    active: bool,
110}
111
112/// ROS 2 Node for embedded systems
113///
114/// The node manages publishers and subscribers with static allocation.
115/// `MAX_PUBS` and `MAX_SUBS` define the maximum number of publishers
116/// and subscribers that can be created.
117///
118/// # Type Parameters
119///
120/// - `MAX_PUBS`: Maximum number of publishers
121/// - `MAX_SUBS`: Maximum number of subscribers
122pub struct Node<const MAX_PUBS: usize = 8, const MAX_SUBS: usize = 8> {
123    /// Node name
124    name: heapless::String<64>,
125    /// Node namespace
126    namespace: heapless::String<64>,
127    /// Domain ID
128    domain_id: u32,
129    /// Registered publishers
130    publishers: Vec<PublisherInfo, MAX_PUBS>,
131    /// Registered subscribers
132    subscribers: Vec<SubscriberInfo, MAX_SUBS>,
133    /// Serialization buffer for publishing
134    tx_buffer: [u8; NODE_TX_BUF_LEN],
135    /// Reception buffer for subscribing
136    #[allow(dead_code)] // Used when transport is connected
137    rx_buffer: [u8; NODE_RX_BUF_LEN],
138}
139
140/// Options for creating a publisher
141#[derive(Debug, Clone)]
142pub struct PublisherOptions<'a> {
143    /// Topic name
144    pub topic: &'a str,
145    /// QoS settings
146    pub qos: QoSProfile,
147}
148
149impl<'a> PublisherOptions<'a> {
150    /// Create new publisher options with the given topic and default QoS
151    pub fn new(topic: &'a str) -> Self {
152        Self {
153            topic,
154            qos: QoSProfile::default(),
155        }
156    }
157
158    /// Set the QoS settings
159    pub fn qos(mut self, qos: QoSProfile) -> Self {
160        self.qos = qos;
161        self
162    }
163}
164
165/// Options for creating a subscriber
166#[derive(Debug, Clone)]
167pub struct SubscriptionOptions<'a> {
168    /// Topic name
169    pub topic: &'a str,
170    /// QoS settings
171    pub qos: QoSProfile,
172}
173
174impl<'a> SubscriptionOptions<'a> {
175    /// Create new subscriber options with the given topic and default QoS
176    pub fn new(topic: &'a str) -> Self {
177        Self {
178            topic,
179            qos: QoSProfile::default(),
180        }
181    }
182
183    /// Set the QoS settings
184    pub fn qos(mut self, qos: QoSProfile) -> Self {
185        self.qos = qos;
186        self
187    }
188}
189
190impl<const MAX_PUBS: usize, const MAX_SUBS: usize> Node<MAX_PUBS, MAX_SUBS> {
191    /// Create a new node with the given configuration
192    pub fn new(config: NodeConfig) -> Result<Self, NodeError> {
193        // Phase 192.1 — validate the name/namespace fit the bounded buffers
194        // instead of silently truncating (a truncated node identity mis-routes).
195        let mut name = heapless::String::new();
196        name.push_str(config.name)
197            .map_err(|_| NodeError::NameTooLong)?;
198
199        let mut namespace = heapless::String::new();
200        namespace
201            .push_str(config.namespace)
202            .map_err(|_| NodeError::NamespaceTooLong)?;
203
204        Ok(Self {
205            name,
206            namespace,
207            domain_id: config.domain_id,
208            publishers: Vec::new(),
209            subscribers: Vec::new(),
210            tx_buffer: [0u8; NODE_TX_BUF_LEN],
211            rx_buffer: [0u8; NODE_RX_BUF_LEN],
212        })
213    }
214
215    /// Get the node name
216    pub fn name(&self) -> &str {
217        &self.name
218    }
219
220    /// Get the node namespace
221    pub fn namespace(&self) -> &str {
222        &self.namespace
223    }
224
225    /// Get the domain ID
226    pub fn domain_id(&self) -> u32 {
227        self.domain_id
228    }
229
230    /// Get the fully qualified node name.
231    ///
232    /// Phase 192.1 — fallible: namespace (≤64) + `/` + name (≤64) can reach 129
233    /// bytes, one over the `String<128>` capacity, so a maxed name+namespace
234    /// would otherwise silently truncate the FQN.
235    pub fn fully_qualified_name(&self) -> Result<heapless::String<128>, NodeError> {
236        // Delegates rather than joining again: `crate::names::fully_qualified_name`
237        // is the one implementation, and this used to be the second of three.
238        //
239        // The old body agreed with it on the cases anyone tested — `/my_ns` +
240        // `my_node`, and a root `/` collapsed correctly — and differed on a
241        // namespace with NO leading slash: it returned `my_ns/my_node`, which is
242        // not fully qualified. ROS 2 namespaces are absolute, so the shared
243        // helper adds the slash.
244        crate::names::fully_qualified_name(&self.name, &self.namespace)
245            .map_err(|()| NodeError::NameTooLong)
246    }
247
248    /// Create a publisher with the given options
249    pub fn create_publisher<M: RosMessage>(
250        &mut self,
251        options: PublisherOptions,
252    ) -> Result<PublisherHandle<M>, NodeError> {
253        if self.publishers.len() >= MAX_PUBS {
254            return Err(NodeError::MaxPublishersReached);
255        }
256
257        let mut topic_name = heapless::String::new();
258        // Phase 192.1 — error on overflow instead of silently truncating the
259        // topic name (a truncated key expression mis-routes on the wire).
260        topic_name
261            .push_str(options.topic)
262            .map_err(|_| NodeError::TopicNameTooLong)?;
263
264        let info = PublisherInfo {
265            topic_name,
266            type_name: M::TYPE_NAME,
267            type_hash: M::TYPE_HASH,
268            qos: options.qos,
269            active: true,
270        };
271
272        let index = self.publishers.len();
273        self.publishers
274            .push(info)
275            .map_err(|_| NodeError::MaxPublishersReached)?;
276
277        Ok(PublisherHandle::new(index))
278    }
279
280    /// Create a subscriber with the given options
281    pub fn create_subscription<M: RosMessage>(
282        &mut self,
283        options: SubscriptionOptions,
284    ) -> Result<SubscriptionHandle<M>, NodeError> {
285        if self.subscribers.len() >= MAX_SUBS {
286            return Err(NodeError::MaxSubscribersReached);
287        }
288
289        let mut topic_name = heapless::String::new();
290        // Phase 192.1 — error on overflow instead of silently truncating.
291        topic_name
292            .push_str(options.topic)
293            .map_err(|_| NodeError::TopicNameTooLong)?;
294
295        let info = SubscriberInfo {
296            topic_name,
297            type_name: M::TYPE_NAME,
298            type_hash: M::TYPE_HASH,
299            qos: options.qos,
300            active: true,
301        };
302
303        let index = self.subscribers.len();
304        self.subscribers
305            .push(info)
306            .map_err(|_| NodeError::MaxSubscribersReached)?;
307
308        Ok(SubscriptionHandle::new(index))
309    }
310
311    /// Get topic info for a publisher
312    pub fn publisher_topic_info(&self, handle: PublisherHandle<()>) -> Option<TopicInfo<'_>> {
313        self.publishers.get(handle.index()).map(|info| {
314            TopicInfo::new(&info.topic_name, info.type_name, info.type_hash)
315                .with_domain(self.domain_id)
316        })
317    }
318
319    /// Get topic info for a subscriber
320    pub fn subscription_topic_info(&self, handle: SubscriptionHandle<()>) -> Option<TopicInfo<'_>> {
321        self.subscribers.get(handle.index()).map(|info| {
322            TopicInfo::new(&info.topic_name, info.type_name, info.type_hash)
323                .with_domain(self.domain_id)
324        })
325    }
326
327    /// Serialize a message for publishing
328    ///
329    /// Returns the serialized bytes. The caller is responsible for
330    /// sending the bytes via the transport layer.
331    pub fn serialize_message<M: RosMessage>(
332        &mut self,
333        _handle: &PublisherHandle<M>,
334        msg: &M,
335    ) -> Result<&[u8], NodeError> {
336        let mut writer =
337            crate::tx_writer(&mut self.tx_buffer).map_err(|_| NodeError::BufferTooSmall)?;
338        msg.serialize(&mut writer)
339            .map_err(|_| NodeError::SerializationFailed)?;
340        let len = writer.position();
341
342        Ok(&self.tx_buffer[..len])
343    }
344
345    /// Deserialize a received message
346    ///
347    /// The caller provides the raw bytes received from the transport layer.
348    pub fn deserialize_message<M: RosMessage>(
349        &self,
350        _handle: &SubscriptionHandle<M>,
351        data: &[u8],
352    ) -> Result<M, NodeError> {
353        use nros_core::CdrReader;
354
355        let mut reader =
356            CdrReader::new_with_header(data).map_err(|_| NodeError::DeserializationFailed)?;
357        M::deserialize(&mut reader).map_err(|_| NodeError::DeserializationFailed)
358    }
359
360    /// Get the number of active publishers
361    pub fn publisher_count(&self) -> usize {
362        self.publishers.iter().filter(|p| p.active).count()
363    }
364
365    /// Get the number of active subscribers
366    pub fn subscription_count(&self) -> usize {
367        self.subscribers.iter().filter(|s| s.active).count()
368    }
369}
370
371impl<const MAX_PUBS: usize, const MAX_SUBS: usize> Default for Node<MAX_PUBS, MAX_SUBS> {
372    fn default() -> Self {
373        // The default config name ("nros_node") + namespace ("/") are short
374        // literals that always fit the bounded buffers, so `new` can't fail here.
375        Self::new(NodeConfig::default())
376            .expect("default NodeConfig fits the bounded name/namespace")
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    // Mock message type for testing
385    #[derive(Debug, Clone, Default)]
386    struct TestMessage {
387        data: i32,
388    }
389
390    impl RosMessage for TestMessage {
391        const TYPE_NAME: &'static str = "test_msgs::msg::TestMessage";
392        const TYPE_HASH: &'static str = "abc123";
393    }
394
395    impl nros_core::Serialize for TestMessage {
396        fn serialize(&self, writer: &mut nros_core::CdrWriter) -> Result<(), nros_core::SerError> {
397            self.data.serialize(writer)
398        }
399    }
400
401    impl nros_core::Deserialize for TestMessage {
402        fn deserialize(reader: &mut nros_core::CdrReader) -> Result<Self, nros_core::DeserError> {
403            Ok(Self {
404                data: i32::deserialize(reader)?,
405            })
406        }
407    }
408
409    #[test]
410    fn test_node_creation() {
411        let config = NodeConfig::new("test_node", "/test");
412        let node = Node::<4, 4>::new(config).unwrap();
413
414        assert_eq!(node.name(), "test_node");
415        assert_eq!(node.namespace(), "/test");
416        assert_eq!(node.domain_id(), 0);
417    }
418
419    #[test]
420    fn test_fully_qualified_name() {
421        let config = NodeConfig::new("my_node", "/my_ns");
422        let node = Node::<4, 4>::new(config).unwrap();
423
424        assert_eq!(
425            node.fully_qualified_name().unwrap().as_str(),
426            "/my_ns/my_node"
427        );
428    }
429
430    /// The two cases that separate a correct join from a plausible one, and
431    /// the reason this delegates to `names::fully_qualified_name` instead of
432    /// keeping its own three lines.
433    #[test]
434    fn fully_qualified_name_collapses_root_and_absolutises() {
435        for (ns, want) in [
436            // Root must not double the slash.
437            ("/", "/my_node"),
438            ("/my_ns", "/my_ns/my_node"),
439            // A namespace without a leading slash is still absolute. The
440            // hand-rolled body this replaced returned `my_ns/my_node`.
441            ("my_ns", "/my_ns/my_node"),
442        ] {
443            let node = Node::<4, 4>::new(NodeConfig::new("my_node", ns)).unwrap();
444            assert_eq!(
445                node.fully_qualified_name().unwrap().as_str(),
446                want,
447                "namespace {ns:?}"
448            );
449        }
450    }
451
452    #[test]
453    fn test_create_publisher() {
454        let mut node = Node::<4, 4>::default();
455        let handle = node.create_publisher::<TestMessage>(PublisherOptions::new("/test_topic"));
456
457        assert!(handle.is_ok());
458        assert_eq!(node.publisher_count(), 1);
459    }
460
461    #[test]
462    fn test_create_subscriber() {
463        let mut node = Node::<4, 4>::default();
464        let handle =
465            node.create_subscription::<TestMessage>(SubscriptionOptions::new("/test_topic"));
466
467        assert!(handle.is_ok());
468        assert_eq!(node.subscription_count(), 1);
469    }
470
471    #[test]
472    fn test_max_publishers() {
473        let mut node = Node::<2, 2>::default();
474
475        let _ = node.create_publisher::<TestMessage>(PublisherOptions::new("/topic1"));
476        let _ = node.create_publisher::<TestMessage>(PublisherOptions::new("/topic2"));
477        let result = node.create_publisher::<TestMessage>(PublisherOptions::new("/topic3"));
478
479        assert_eq!(result, Err(NodeError::MaxPublishersReached));
480    }
481
482    #[test]
483    fn test_serialize_deserialize() {
484        let mut node = Node::<4, 4>::default();
485        let pub_handle = node
486            .create_publisher::<TestMessage>(PublisherOptions::new("/test"))
487            .unwrap();
488        let sub_handle = node
489            .create_subscription::<TestMessage>(SubscriptionOptions::new("/test"))
490            .unwrap();
491
492        let msg = TestMessage { data: 42 };
493
494        // Copy bytes since serialize_message borrows the internal buffer
495        let mut buf = [0u8; 128];
496        let bytes = node.serialize_message(&pub_handle, &msg).unwrap();
497        let len = bytes.len();
498        buf[..len].copy_from_slice(bytes);
499
500        let received: TestMessage = node.deserialize_message(&sub_handle, &buf[..len]).unwrap();
501        assert_eq!(received.data, 42);
502    }
503}