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}