Skip to main content

nros_core/
message_info.rs

1//! Message metadata for received messages
2//!
3//! This module provides the `MessageInfo` type which contains metadata
4//! about received messages, matching the rclrs pattern.
5//!
6//! # Example
7//!
8//! ```text
9//! node.create_subscription("/topic", |msg: &Int32, info: &MessageInfo| {
10//!     println!("Received at {:?} from {:?}", info.source_timestamp(), info.publisher_gid());
11//! });
12//! ```
13
14use crate::Time;
15
16/// Size of the publisher Global Identifier (GID), in bytes.
17///
18/// **24, which is upstream's `RMW_GID_STORAGE_SIZE`** — measured against Humble,
19/// `/opt/ros/humble/include/rmw/rmw/types.h:42` reads `24u` — and the width of
20/// the ABI's own `rmw_gid_t::data` (`RMW_GID_STORAGE_SIZE`,
21/// `packages/core/nros-rmw-abi/include/nros/rmw_entity.h`). So a gid read out of
22/// a [`MessageInfo`] and a gid written by the `get_gid_for_publisher` vtable
23/// slot are the same TYPE, comparable without a conversion.
24///
25/// It was 16 until the phase-467 RMW gap-closure design study's Q1(a). The 16
26/// is *zenoh's* wire width, not a property of the identifier — see
27/// [`MessageInfo::publisher_gid`] for what that leaves a reader owing.
28pub const PUBLISHER_GID_SIZE: usize = 24;
29
30/// Zero-extend a backend's narrower identity into a full-width publisher GID.
31///
32/// A backend whose identity is shorter than [`PUBLISHER_GID_SIZE`] must pad the
33/// TAIL with zeros rather than leave it undefined, or two gids naming the same
34/// publisher compare unequal on stack garbage. This is the one spelling of that
35/// padding on the Rust side; the Cyclone backend does the same thing in C++
36/// (`cyclone_get_gid_for_publisher` in
37/// `packages/rmw/cyclonedds/nros-rmw-cyclonedds/src/vtable.cpp` — a `memset`,
38/// then a `memcpy` of the 16-byte DDS writer GUID).
39///
40/// An `N` wider than [`PUBLISHER_GID_SIZE`] is a compile error, which is the
41/// Rust spelling of Cyclone's
42/// `static_assert(sizeof(guid.v) <= RMW_GID_STORAGE_SIZE)`.
43pub const fn pad_publisher_gid<const N: usize>(narrow: &[u8; N]) -> [u8; PUBLISHER_GID_SIZE] {
44    const {
45        assert!(
46            N <= PUBLISHER_GID_SIZE,
47            "a publisher identity wider than PUBLISHER_GID_SIZE cannot be zero-extended into it"
48        );
49    }
50    let mut wide = [0u8; PUBLISHER_GID_SIZE];
51    let mut i = 0;
52    while i < N {
53        wide[i] = narrow[i];
54        i += 1;
55    }
56    wide
57}
58
59/// Metadata about a received message
60///
61/// Contains information about the source and timing of a message.
62///
63/// ADOPT-BOUNDED against rclrs's `MessageInfo`, which exposes the whole
64/// `rmw_message_info_t`. This is a flat struct of what an embedded backend can
65/// fill: both timestamps, both sequence numbers and a 24-byte publisher gid.
66/// Whether a message carries it at all is a BACKEND property — zenoh fills it
67/// from the attachment, Cyclone from `take_with_info`, and a backend that reports
68/// no metadata hands the callback `None` rather than a zeroed value. How many gid
69/// bytes mean anything is backend-specific too: see [`publisher_gid`](Self::publisher_gid).
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub struct MessageInfo {
72    /// Timestamp when the message was published (from the publisher's clock)
73    source_timestamp: Time,
74    /// Timestamp when the message was received (from the subscriber's clock)
75    received_timestamp: Time,
76    /// Sequence number of the message from the publisher
77    publication_sequence_number: i64,
78    /// Sequence number of the message at the subscriber
79    reception_sequence_number: i64,
80    /// Global identifier of the publisher
81    publisher_gid: [u8; PUBLISHER_GID_SIZE],
82}
83
84impl MessageInfo {
85    /// Create a new MessageInfo with all fields set to defaults
86    pub const fn new() -> Self {
87        Self {
88            source_timestamp: Time::new(0, 0),
89            received_timestamp: Time::new(0, 0),
90            publication_sequence_number: 0,
91            reception_sequence_number: 0,
92            publisher_gid: [0u8; PUBLISHER_GID_SIZE],
93        }
94    }
95
96    /// Create a MessageInfo with the given timestamps
97    pub const fn with_timestamps(source: Time, received: Time) -> Self {
98        Self {
99            source_timestamp: source,
100            received_timestamp: received,
101            publication_sequence_number: 0,
102            reception_sequence_number: 0,
103            publisher_gid: [0u8; PUBLISHER_GID_SIZE],
104        }
105    }
106
107    /// Get the timestamp when the message was published
108    pub const fn source_timestamp(&self) -> Time {
109        self.source_timestamp
110    }
111
112    /// Get the timestamp when the message was received
113    pub const fn received_timestamp(&self) -> Time {
114        self.received_timestamp
115    }
116
117    /// Get the publication sequence number
118    pub const fn publication_sequence_number(&self) -> i64 {
119        self.publication_sequence_number
120    }
121
122    /// Get the reception sequence number
123    pub const fn reception_sequence_number(&self) -> i64 {
124        self.reception_sequence_number
125    }
126
127    /// Get the publisher's Global Identifier (GID).
128    ///
129    /// **The array is 24 bytes. How many of them MEAN anything is a backend
130    /// property.**
131    ///
132    /// The width is upstream's ([`PUBLISHER_GID_SIZE`]), so this value and the
133    /// `rmw_gid_t` the `get_gid_for_publisher` vtable slot fills are one type
134    /// — and, since issue 1495, one VALUE on the backends that produce either:
135    ///
136    /// * **zenoh fills 16 and zero-pads 8.** The 16 are XXH3-128 of the
137    ///   sending entity's liveliness keyexpr — `rmw_zenoh_cpp`'s own
138    ///   derivation, so it is the gid the sender's `get_gid` reports and the
139    ///   gid `ros2 topic info --verbose` prints for it, ours or a stock peer's.
140    ///   `RMW_ATTACHMENT_SIZE` carries 16 because that is `rmw_zenoh_cpp`'s
141    ///   wire layout (its reader REJECTS any other length); the padding is
142    ///   written by [`pad_publisher_gid`]. Compare whole arrays anyway.
143    /// * **Cyclone fills 16 and zero-pads 8**: the sending WRITER's GUID,
144    ///   which is what its `get_gid` reports and a DDS graph prints. A stock
145    ///   Humble `rmw_cyclonedds_cpp` subscriber reports something else here
146    ///   (the 8-byte publication handle), so do not compare this field against
147    ///   one read from such a peer.
148    /// * **XRCE and uORB never fill it.** A backend that reports no metadata
149    ///   leaves the `message_info()` callback a `None`, not a zeroed gid.
150    ///
151    /// An all-zero gid therefore means "this backend did not say" (or, on
152    /// Cyclone, that the writer unmatched before the take), never "the
153    /// publisher's id is zero".
154    pub const fn publisher_gid(&self) -> &[u8; PUBLISHER_GID_SIZE] {
155        &self.publisher_gid
156    }
157
158    /// Set the source timestamp
159    pub fn set_source_timestamp(&mut self, ts: Time) {
160        self.source_timestamp = ts;
161    }
162
163    /// Set the received timestamp
164    pub fn set_received_timestamp(&mut self, ts: Time) {
165        self.received_timestamp = ts;
166    }
167
168    /// Set the publication sequence number
169    pub fn set_publication_sequence_number(&mut self, seq: i64) {
170        self.publication_sequence_number = seq;
171    }
172
173    /// Set the reception sequence number
174    pub fn set_reception_sequence_number(&mut self, seq: i64) {
175        self.reception_sequence_number = seq;
176    }
177
178    /// Set the publisher GID.
179    ///
180    /// Takes the full [`PUBLISHER_GID_SIZE`] width. A backend whose identity is
181    /// narrower zero-extends it through [`pad_publisher_gid`] rather than
182    /// building the array itself — see [`publisher_gid`](Self::publisher_gid)
183    /// for what a reader may and may not conclude from the result.
184    pub fn set_publisher_gid(&mut self, gid: [u8; PUBLISHER_GID_SIZE]) {
185        self.publisher_gid = gid;
186    }
187}
188
189/// Raw-subscription message info: [`MessageInfo`] metadata plus the
190/// sample's wire-level **attachment** bytes, borrowed for the callback
191/// scope.
192///
193/// Surfaced on the generic (type-erased) subscription path
194/// (`node.subscription(t).generic(..).message_info().build(cb)` — the
195/// `FnMut(&[u8], &RawMessageInfo)` callback). The attachment carries
196/// out-of-band tags such as the cross-RMW bridge's `bridge_origin`
197/// (read via [`attachment`](Self::attachment) for echo suppression).
198///
199/// The `'a` lifetime ties the borrowed attachment to the dispatch call;
200/// copy out what you need before the callback returns. Metadata
201/// accessors delegate to the inner [`MessageInfo`]; on backends without
202/// a combined raw+info+attachment take they read their defaults (the
203/// attachment is always populated).
204#[derive(Debug, Clone, Copy)]
205pub struct RawMessageInfo<'a> {
206    info: MessageInfo,
207    attachment: &'a [u8],
208}
209
210impl<'a> RawMessageInfo<'a> {
211    /// Build from an attachment slice (metadata defaulted).
212    pub const fn new(attachment: &'a [u8]) -> Self {
213        Self {
214            info: MessageInfo::new(),
215            attachment,
216        }
217    }
218
219    /// Build from explicit metadata + attachment.
220    pub const fn with_info(info: MessageInfo, attachment: &'a [u8]) -> Self {
221        Self { info, attachment }
222    }
223
224    /// The sample's wire-level attachment bytes (empty if none).
225    pub const fn attachment(&self) -> &'a [u8] {
226        self.attachment
227    }
228
229    /// The underlying metadata.
230    pub const fn info(&self) -> &MessageInfo {
231        &self.info
232    }
233
234    /// Timestamp when the message was published.
235    pub const fn source_timestamp(&self) -> Time {
236        self.info.source_timestamp()
237    }
238
239    /// Timestamp when the message was received.
240    pub const fn received_timestamp(&self) -> Time {
241        self.info.received_timestamp()
242    }
243
244    /// Publisher's Global Identifier (GID).
245    ///
246    /// Same bound as [`MessageInfo::publisher_gid`]: 24 bytes wide, with the
247    /// meaningful prefix decided by the backend and today never the whole
248    /// array.
249    pub const fn publisher_gid(&self) -> &[u8; PUBLISHER_GID_SIZE] {
250        self.info.publisher_gid()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_message_info_default() {
260        let info = MessageInfo::new();
261        assert_eq!(info.source_timestamp(), Time::new(0, 0));
262        assert_eq!(info.publication_sequence_number(), 0);
263        assert_eq!(info.publisher_gid(), &[0u8; PUBLISHER_GID_SIZE]);
264    }
265
266    /// The width is upstream's, and that is the whole point of it: measured in
267    /// the `ros2` box, `/opt/ros/humble/include/rmw/rmw/types.h:42` reads
268    /// `#define RMW_GID_STORAGE_SIZE 24u`, and our own ABI header takes the
269    /// same number. A gid taken from a sample and a gid written into an
270    /// `rmw_gid_t` are one type only while this holds.
271    #[test]
272    fn publisher_gid_is_upstreams_storage_size() {
273        assert_eq!(PUBLISHER_GID_SIZE, 24);
274    }
275
276    /// zenoh's identity is 16 bytes and cannot move — that is
277    /// `rmw_zenoh_cpp`'s `RMW_ATTACHMENT_SIZE` wire layout, whose reader
278    /// rejects any other length. Padding is what lets it be stored at the
279    /// upstream width without either side moving.
280    #[test]
281    fn a_narrow_backend_identity_zero_extends_into_the_tail() {
282        let wire = [0xABu8; 16];
283        let wide = pad_publisher_gid(&wire);
284        assert_eq!(&wide[..16], &wire[..]);
285        assert_eq!(&wide[16..], &[0u8; PUBLISHER_GID_SIZE - 16]);
286
287        // Deterministic, so two samples from one publisher still compare equal
288        // over the WHOLE array — the property `test_gid_consistency` reads.
289        assert_eq!(pad_publisher_gid(&wire), wide);
290    }
291
292    #[test]
293    fn test_message_info_with_timestamps() {
294        let source = Time::new(1, 500_000_000);
295        let received = Time::new(1, 600_000_000);
296        let info = MessageInfo::with_timestamps(source, received);
297        assert_eq!(info.source_timestamp(), source);
298        assert_eq!(info.received_timestamp(), received);
299    }
300}