nros_rmw/traits.rs
1//! Transport abstraction traits.
2//!
3//! Defines the backend-agnostic interface that transport implementations
4//! (zenoh-pico, XRCE-DDS) must satisfy. The core trait hierarchy is:
5//!
6//! - [`Session`] — connection lifecycle and handle creation
7//! - [`Publisher`] / [`Subscription`] — pub/sub data transport
8//! - [`ServiceTrait`] / [`ClientTrait`] — request/reply
9//! - [`Rmw`] — top-level factory that creates sessions
10
11use crate::service_inbox::ServiceInboxSpec;
12use nros_core::{Deserialize, RosMessage, RosService, Serialize};
13
14/// Topic information for pub/sub
15#[derive(Debug, Clone)]
16pub struct TopicInfo<'a> {
17 /// Topic name (e.g., "/chatter")
18 pub name: &'a str,
19 /// ROS type name (e.g., "std_msgs::msg::dds_::String_")
20 pub type_name: &'a str,
21 /// Type hash for compatibility checking
22 pub type_hash: &'a str,
23 /// Domain ID (default: 0)
24 pub domain_id: u32,
25 /// Node name for liveliness token generation.
26 /// `None` means no node association — no liveliness token will be declared.
27 pub node_name: Option<&'a str>,
28 /// Node namespace for liveliness token generation (default: "/").
29 /// In ROS 2, "/" is the root namespace and the standard default.
30 pub namespace: &'a str,
31 /// Phase 231 (RFC-0038) — receive-buffer size hint, bytes. The executor sets
32 /// this from the subscription's `RX_BUF`; a backend may use it to route the
33 /// subscription to a size-class receive buffer (zenoh-pico: small vs large).
34 /// `0` = unset (backend picks its default). Ignored by backends that don't
35 /// size-class their receive storage.
36 pub rx_buffer_hint: usize,
37 /// phase-519 W3 (RFC-0074) — the rate this subscription DECLARED it can
38 /// take (`arrival_curve.rate_hz`), in millihertz. `0` = undeclared. Carried to
39 /// the backend in `rmw_subscription_options_t::arrival_rate_mhz`; the XRCE
40 /// backend turns it into the agent's `max_bytes_per_second`, zenoh ignores
41 /// it (its enforcement point 1 is the router rule). Ignored by publishers.
42 pub arrival_rate_mhz: u32,
43 /// phase-279 (#145) — publisher-side "express" hint: this topic's samples
44 /// bypass transport tx batching (zenoh: the wire EXPRESS flag; a batching
45 /// zenoh-pico session sends them immediately instead of queueing them for
46 /// the next flush). For control-tier / latency-sensitive topics. Ignored by
47 /// subscriptions and by backends without a batching concept.
48 pub tx_express: bool,
49}
50
51impl<'a> TopicInfo<'a> {
52 /// Create new topic info
53 pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
54 Self {
55 name,
56 type_name,
57 type_hash,
58 domain_id: 0,
59 node_name: None,
60 namespace: "/",
61 rx_buffer_hint: 0,
62 arrival_rate_mhz: 0,
63 tx_express: false,
64 }
65 }
66
67 /// Create topic info with custom domain ID
68 pub const fn with_domain(mut self, domain_id: u32) -> Self {
69 self.domain_id = domain_id;
70 self
71 }
72
73 /// Phase 231 (RFC-0038) — set the receive-buffer size hint (bytes) used by
74 /// size-classing backends (zenoh-pico) to route the subscription's receive
75 /// buffer to the small or large class.
76 pub const fn with_rx_buffer_hint(mut self, hint: usize) -> Self {
77 self.rx_buffer_hint = hint;
78 self
79 }
80
81 /// phase-519 W3 (RFC-0074) — set the declared arrival rate, millihertz
82 /// (`rate_hz x 1000`). `0` leaves it undeclared.
83 pub const fn with_arrival_rate_mhz(mut self, rate_mhz: u32) -> Self {
84 self.arrival_rate_mhz = rate_mhz;
85 self
86 }
87
88 /// phase-279 (#145) — mark this publisher's samples express: they bypass
89 /// transport tx batching (sent immediately even when `ZPICO_TX_BATCH` is
90 /// on). For control-tier / latency-sensitive topics.
91 pub const fn with_tx_express(mut self, express: bool) -> Self {
92 self.tx_express = express;
93 self
94 }
95
96 /// Set the node name for liveliness token generation
97 pub const fn with_node_name(mut self, node_name: &'a str) -> Self {
98 self.node_name = Some(node_name);
99 self
100 }
101
102 /// Set the node namespace for liveliness token generation
103 pub const fn with_namespace(mut self, namespace: &'a str) -> Self {
104 self.namespace = namespace;
105 self
106 }
107}
108
109/// Service information for service client/server
110#[derive(Debug, Clone)]
111pub struct ServiceInfo<'a> {
112 /// Service name (e.g., "/add_two_ints")
113 pub name: &'a str,
114 /// ROS service type name (e.g., "example_interfaces::srv::dds_::AddTwoInts_")
115 pub type_name: &'a str,
116 /// Type hash for compatibility checking
117 pub type_hash: &'a str,
118 /// Domain ID (default: 0)
119 pub domain_id: u32,
120 /// Node name for liveliness token generation.
121 /// `None` means no node association — no liveliness token will be declared.
122 pub node_name: Option<&'a str>,
123 /// Node namespace for liveliness token generation (default: "/").
124 /// In ROS 2, "/" is the root namespace and the standard default.
125 pub namespace: &'a str,
126}
127
128/// Issue 1913 — the REP-2011 hash each of an action's five endpoints is keyed
129/// on. An Iron+ `rmw_zenoh_cpp` peer keys every endpoint on its OWN type: the
130/// `<Action>_SendGoal` / `<Action>_GetResult` services, the standard
131/// `action_msgs/srv/CancelGoal`, `<Action>_FeedbackMessage`, and the standard
132/// `action_msgs/msg/GoalStatusArray`. One hash for all five matched none of
133/// them, which is what every registration path carried until 1913.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct ActionEndpointHashes<'a> {
136 pub send_goal: &'a str,
137 pub cancel_goal: &'a str,
138 pub get_result: &'a str,
139 pub feedback: &'a str,
140 pub status: &'a str,
141}
142
143impl<'a> ActionEndpointHashes<'a> {
144 /// Every endpoint on one value — Humble's `TypeHashNotSupported`, or a
145 /// caller that has nothing better (it will not match an Iron+ peer).
146 pub const fn uniform(hash: &'a str) -> Self {
147 Self {
148 send_goal: hash,
149 cancel_goal: hash,
150 get_result: hash,
151 feedback: hash,
152 status: hash,
153 }
154 }
155}
156
157impl ActionEndpointHashes<'static> {
158 /// The five hashes a typed action carries.
159 pub const fn of<A: nros_core::RosAction>() -> Self {
160 Self {
161 send_goal: A::SEND_GOAL_SERVICE_HASH,
162 cancel_goal: A::CANCEL_GOAL_SERVICE_HASH,
163 get_result: A::GET_RESULT_SERVICE_HASH,
164 feedback: <A::FeedbackMessage as nros_core::RosMessage>::TYPE_HASH,
165 status: A::STATUS_MESSAGE_HASH,
166 }
167 }
168}
169
170/// Action information for action client/server
171///
172/// Actions in ROS 2 use 5 communication channels:
173/// - `send_goal` service: `<action_name>/_action/send_goal`
174/// - `cancel_goal` service: `<action_name>/_action/cancel_goal`
175/// - `get_result` service: `<action_name>/_action/get_result`
176/// - `feedback` topic: `<action_name>/_action/feedback`
177/// - `status` topic: `<action_name>/_action/status`
178#[derive(Debug, Clone)]
179pub struct ActionInfo<'a> {
180 /// Action name (e.g., "/fibonacci")
181 pub name: &'a str,
182 /// ROS action type name (e.g., "example_interfaces::action::dds_::Fibonacci_")
183 pub type_name: &'a str,
184 /// Type hash for compatibility checking
185 pub type_hash: &'a str,
186 /// Domain ID (default: 0)
187 pub domain_id: u32,
188}
189
190impl<'a> ActionInfo<'a> {
191 /// Create new action info
192 pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
193 Self {
194 name,
195 type_name,
196 type_hash,
197 domain_id: 0,
198 }
199 }
200
201 /// Create action info with custom domain ID
202 pub const fn with_domain(mut self, domain_id: u32) -> Self {
203 self.domain_id = domain_id;
204 self
205 }
206
207 /// Generate the send_goal service name
208 /// Returns: `<action>/_action/send_goal`
209 pub fn send_goal_key<const N: usize>(&self) -> heapless::String<N> {
210 self.sub_name::<N>("send_goal")
211 }
212
213 /// Generate the cancel_goal service name
214 /// Returns: `<action>/_action/cancel_goal`
215 pub fn cancel_goal_key<const N: usize>(&self) -> heapless::String<N> {
216 self.sub_name::<N>("cancel_goal")
217 }
218
219 /// Generate the get_result service name
220 /// Returns: `<action>/_action/get_result`
221 pub fn get_result_key<const N: usize>(&self) -> heapless::String<N> {
222 self.sub_name::<N>("get_result")
223 }
224
225 /// Generate the feedback topic name
226 /// Returns: `<action>/_action/feedback`
227 pub fn feedback_key<const N: usize>(&self) -> heapless::String<N> {
228 self.sub_name::<N>("feedback")
229 }
230
231 /// Generate the status topic name
232 /// Returns: `<action>/_action/status`
233 pub fn status_key<const N: usize>(&self) -> heapless::String<N> {
234 self.sub_name::<N>("status")
235 }
236
237 /// Generate a sub-entity name for an action component
238 /// Returns: `<action>/_action/<suffix>` (e.g., `fibonacci/_action/send_goal`)
239 ///
240 /// The caller is responsible for constructing the full key expression
241 /// by wrapping this name in a `ServiceInfo` or `TopicInfo` with the
242 /// correct sub-service/sub-topic type name.
243 fn sub_name<const N: usize>(&self, suffix: &str) -> heapless::String<N> {
244 let mut name = heapless::String::new();
245 let action_stripped = self.name.trim_matches('/');
246 let _ = core::fmt::write(
247 &mut name,
248 format_args!("/{}/_action/{}", action_stripped, suffix),
249 );
250 name
251 }
252}
253
254impl<'a> ServiceInfo<'a> {
255 /// Create new service info
256 pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
257 Self {
258 name,
259 type_name,
260 type_hash,
261 domain_id: 0,
262 node_name: None,
263 namespace: "/",
264 }
265 }
266
267 /// Create service info with custom domain ID
268 pub const fn with_domain(mut self, domain_id: u32) -> Self {
269 self.domain_id = domain_id;
270 self
271 }
272
273 /// Set the node name for liveliness token generation
274 pub const fn with_node_name(mut self, node_name: &'a str) -> Self {
275 self.node_name = Some(node_name);
276 self
277 }
278
279 /// Set the node namespace for liveliness token generation
280 pub const fn with_namespace(mut self, namespace: &'a str) -> Self {
281 self.namespace = namespace;
282 self
283 }
284}
285
286/// Transport error types.
287///
288/// No longer `Copy` — the `Backend` / `BackendDynamic` variants carry a
289/// string diagnostic, which can't be `Copy`. Rust callers that used to
290/// copy a `TransportError` value repeatedly now need `.clone()` or
291/// `ref` in match arms. C/C++ callers are unaffected — both map
292/// `TransportError` to integer codes (`rcl_ret_t` / `ErrorCode`)
293/// before crossing the FFI boundary.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub enum TransportError {
296 /// Failed to connect to transport
297 ConnectionFailed,
298 /// Connection was closed
299 Disconnected,
300 /// Failed to create publisher
301 PublisherCreationFailed,
302 /// Failed to create subscriber
303 SubscriberCreationFailed,
304 /// Failed to create service server
305 ServiceServerCreationFailed,
306 /// Failed to create service client
307 ServiceClientCreationFailed,
308 /// Failed to publish message
309 PublishFailed,
310 /// Failed to send service request
311 ServiceRequestFailed,
312 /// Failed to send service reply
313 ServiceReplyFailed,
314 /// Serialization error
315 SerializationError,
316 /// Deserialization error
317 DeserializationError,
318 /// Buffer too small
319 BufferTooSmall,
320 /// Incoming message exceeded the static buffer capacity
321 MessageTooLarge,
322 /// Timeout waiting for message
323 Timeout,
324 /// Invalid configuration
325 InvalidConfig,
326 /// Resource (slot, buffer, queue) momentarily unavailable. Retry.
327 /// Phase 99: returned by `try_loan` when arena slots are full and
328 /// by `try_borrow` when no message is ready (alternative to
329 /// `Ok(None)` for backends that prefer the error variant).
330 WouldBlock,
331 /// Requested allocation exceeds backend capacity. Phase 99:
332 /// `try_loan(len)` returns this when `len` > arena slot size.
333 TooLarge,
334 /// Failed to start background tasks
335 TaskStartFailed,
336 /// Failed to poll for incoming messages
337 PollFailed,
338 /// Failed to send keepalive
339 KeepaliveFailed,
340 /// Failed to send join message
341 JoinFailed,
342 /// Caller supplied a NULL pointer, an out-of-range value, or an
343 /// inconsistent argument combination. Phase 102.1.
344 InvalidArgument,
345 /// The backend does not implement this operation. Optional
346 /// callbacks return this; the runtime then falls back to a
347 /// default path. Phase 102.1.
348 Unsupported,
349 /// Memory allocation failed. Returned by backends on `std` /
350 /// `alloc`-equipped platforms when heap allocation fails.
351 /// Bare-metal backends generally do not produce this — they
352 /// preallocate at session-open time. Phase 102.1.
353 BadAlloc,
354 /// Publisher and subscriber QoS profiles do not match in a way
355 /// the backend cannot reconcile. Phase 102.1.
356 ///
357 /// **It NAMES the policy** — phase-417 G7. The payload is the single
358 /// [`QoSPolicyMask`] bit that was refused, so
359 /// `err.qos_policy_name()` (or `mask.policy_name()`) turns the refusal
360 /// into `"RELIABILITY"` without an allocator. Until this carried a
361 /// payload, [`QoSProfile::validate_against`] computed the offending bit
362 /// and threw it away, while `rmw_vtable.h`'s `supported_qos_policies`
363 /// block told the reader those creates fail "naming a policy". They did
364 /// not. Issue 1329's NONE-over-union choice rests on that sentence, so the
365 /// choice was right and its evidence was not yet true.
366 ///
367 /// [`QoSPolicyMask::NONE`] is the honest answer where the refusal crossed
368 /// a boundary that cannot carry one — a C backend returns
369 /// `NROS_RMW_RET_INCOMPATIBLE_QOS`, an `rmw_ret_t` with no room for a
370 /// mask — and `policy_name()` then answers `None` rather than naming a
371 /// policy nobody identified.
372 IncompatibleQos(QoSPolicyMask),
373 /// Topic, service, or action name failed validation. Phase 102.1.
374 TopicNameInvalid,
375 /// A request referenced a node that does not exist in this
376 /// session. Phase 102.1.
377 NodeNameNonExistent,
378 /// The backend does not support loaned messages on this entity,
379 /// or the loan slot is currently in use. Phase 102.1.
380 LoanNotSupported,
381 /// No data was available on a non-blocking receive. Distinct
382 /// from `Timeout`: fires immediately, not after a bounded wait.
383 /// Phase 102.1.
384 NoData,
385 /// Phase 115.A.2 — caller passed a versioned vtable struct
386 /// (e.g. `NrosTransportOps`) with an `abi_version` the runtime
387 /// doesn't know. Maps to `NROS_RMW_RET_INCOMPATIBLE_ABI` at
388 /// the C boundary.
389 IncompatibleAbi,
390 /// Backend-specific error with a `'static` diagnostic string.
391 ///
392 /// Useful for zenoh-pico / XRCE-DDS return codes that map to a
393 /// fixed set of known messages. `no_std`-compatible.
394 Backend(&'static str),
395 /// Backend-specific error with an owned diagnostic string.
396 ///
397 /// Available only with the `alloc` feature. Use this when the
398 /// diagnostic is formatted at runtime (e.g. from a C error code
399 /// plus a socket address).
400 #[cfg(feature = "alloc")]
401 BackendDynamic(alloc::string::String),
402}
403
404/// One line per variant, in the defining crate, so every consumer renders the
405/// same words (issue 0783: `NodeError` used to fall back to `{:?}` because
406/// this impl did not exist, and writing the prose there would have put a
407/// backend's wording in the wrong crate). `no_std`: no allocator, only
408/// `write_str` and the payloads the variants already carry.
409impl core::fmt::Display for TransportError {
410 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
411 let s = match self {
412 Self::ConnectionFailed => "failed to connect to the transport",
413 Self::Disconnected => "the transport connection was closed",
414 Self::PublisherCreationFailed => "failed to create a publisher",
415 Self::SubscriberCreationFailed => "failed to create a subscriber",
416 Self::ServiceServerCreationFailed => "failed to create a service server",
417 Self::ServiceClientCreationFailed => "failed to create a service client",
418 Self::PublishFailed => "failed to publish a message",
419 Self::ServiceRequestFailed => "failed to send a service request",
420 Self::ServiceReplyFailed => "failed to send a service reply",
421 Self::SerializationError => "serialization failed",
422 Self::DeserializationError => "deserialization failed",
423 Self::BufferTooSmall => "buffer too small",
424 Self::MessageTooLarge => "incoming message exceeds the static buffer",
425 Self::Timeout => "timed out",
426 Self::InvalidConfig => "invalid configuration",
427 Self::WouldBlock => "resource momentarily unavailable; retry",
428 Self::TooLarge => "requested size exceeds the backend's capacity",
429 Self::TaskStartFailed => "failed to start a background task",
430 Self::PollFailed => "failed to poll for incoming messages",
431 Self::KeepaliveFailed => "failed to send a keepalive",
432 Self::JoinFailed => "failed to send a join message",
433 Self::InvalidArgument => "invalid argument",
434 Self::Unsupported => "operation not supported by this backend",
435 Self::BadAlloc => "memory allocation failed",
436 Self::IncompatibleQos(policy) => {
437 return match policy.policy_name() {
438 Some(name) => write!(f, "incompatible QoS: {name}"),
439 None => f.write_str("incompatible QoS"),
440 };
441 }
442 Self::TopicNameInvalid => "invalid topic, service or action name",
443 Self::NodeNameNonExistent => "no such node in this session",
444 Self::LoanNotSupported => "message loans unavailable on this entity",
445 Self::NoData => "no data available",
446 Self::IncompatibleAbi => "incompatible ABI version",
447 Self::Backend(msg) => return write!(f, "backend error: {msg}"),
448 #[cfg(feature = "alloc")]
449 Self::BackendDynamic(msg) => return write!(f, "backend error: {msg}"),
450 };
451 f.write_str(s)
452 }
453}
454
455impl TransportError {
456 /// The QoS policy this error names, or `None`.
457 ///
458 /// phase-417 G7 — the one accessor the three surfaces share. `Some` only
459 /// for [`Self::IncompatibleQos`] carrying exactly one
460 /// [`QoSPolicyMask`] bit; `None` for every other error, and for a
461 /// refusal that crossed a boundary with no room for the mask.
462 ///
463 /// The string is borrowed from [`QoSPolicyMask::NAMED`], so this costs no
464 /// allocator and no formatter and is available on a bare-metal target.
465 pub fn qos_policy_name(&self) -> Option<&'static str> {
466 match self {
467 Self::IncompatibleQos(policy) => policy.policy_name(),
468 _ => None,
469 }
470 }
471}
472
473/// QoS history policy
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
475pub enum QoSHistoryPolicy {
476 /// **The caller did not state a history policy** — the backend picks.
477 /// Upstream's `RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT`; lowers to
478 /// `NROS_RMW_HISTORY_SYSTEM_DEFAULT` (0) across the C ABI.
479 ///
480 /// See [`QoSSystemDefaults`] for how a backend resolves it. Listed
481 /// first to match rclrs's enumerator order.
482 SystemDefault,
483 /// Keep last N messages (where N is defined in QoSProfile)
484 #[default]
485 KeepLast,
486 /// Keep all messages (up to resource limits)
487 KeepAll,
488 /// **The backend could not determine this policy.** Upstream's
489 /// `RMW_QOS_POLICY_HISTORY_UNKNOWN`; lowers to `NROS_RMW_HISTORY_UNKNOWN`
490 /// (3) across the C ABI.
491 ///
492 /// An ABSENCE, not a value: it is what a `*_get_actual_qos` read-back
493 /// writes for a policy it looked at and cannot report, and it is never a
494 /// request an application makes. Distinct from `SystemDefault`, which is
495 /// "nobody asked, so the middleware chooses" — see
496 /// [`QoSProfile::QOS_PROFILE_UNKNOWN`].
497 Unknown,
498}
499
500/// QoS reliability policy
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
502pub enum QoSReliabilityPolicy {
503 /// **The caller did not state a reliability policy** — the backend picks.
504 /// Upstream's `RMW_QOS_POLICY_RELIABILITY_SYSTEM_DEFAULT`; lowers to
505 /// `NROS_RMW_RELIABILITY_SYSTEM_DEFAULT` (0) across the C ABI.
506 ///
507 /// See [`QoSSystemDefaults`].
508 SystemDefault,
509 /// Reliable delivery (retransmit if needed).
510 ///
511 /// Default — matches ROS 2 `rmw_qos_profile_default` and the
512 /// `QoSProfile::default()` / `QOS_PROFILE_DEFAULT` aggregates.
513 #[default]
514 Reliable,
515 /// Best-effort delivery (no retransmits)
516 BestEffort,
517 /// **The backend could not determine this policy.** Upstream's
518 /// `RMW_QOS_POLICY_RELIABILITY_UNKNOWN`; lowers to `NROS_RMW_RELIABILITY_UNKNOWN`
519 /// (3) across the C ABI.
520 ///
521 /// An ABSENCE, not a value: it is what a `*_get_actual_qos` read-back
522 /// writes for a policy it looked at and cannot report, and it is never a
523 /// request an application makes. Distinct from `SystemDefault`, which is
524 /// "nobody asked, so the middleware chooses" — see
525 /// [`QoSProfile::QOS_PROFILE_UNKNOWN`].
526 Unknown,
527 /// **Resolve at creation against the discovered graph** (issue 1973).
528 /// Upstream's `RMW_QOS_POLICY_RELIABILITY_BEST_AVAILABLE` (Iron+); lowers
529 /// to `NROS_RMW_RELIABILITY_BEST_AVAILABLE` (4) across the C ABI.
530 ///
531 /// A REQUEST, never a grant: the runtime replaces it before the backend's
532 /// create slot runs ([`QoSProfile::resolve_best_available`]), so a
533 /// `*_get_actual_qos` read-back reports the concrete policy chosen.
534 BestAvailable,
535}
536
537/// QoS durability policy
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
539pub enum QoSDurabilityPolicy {
540 /// **The caller did not state a durability policy** — the backend picks.
541 /// Upstream's `RMW_QOS_POLICY_DURABILITY_SYSTEM_DEFAULT`; lowers to
542 /// `NROS_RMW_DURABILITY_SYSTEM_DEFAULT` (0) across the C ABI.
543 ///
544 /// See [`QoSSystemDefaults`].
545 SystemDefault,
546 /// Messages are discarded when subscriber disconnects
547 #[default]
548 Volatile,
549 /// Messages are persisted for late-joining subscribers
550 TransientLocal,
551 /// **The backend could not determine this policy.** Upstream's
552 /// `RMW_QOS_POLICY_DURABILITY_UNKNOWN`; lowers to `NROS_RMW_DURABILITY_UNKNOWN`
553 /// (3) across the C ABI.
554 ///
555 /// An ABSENCE, not a value: it is what a `*_get_actual_qos` read-back
556 /// writes for a policy it looked at and cannot report, and it is never a
557 /// request an application makes. Distinct from `SystemDefault`, which is
558 /// "nobody asked, so the middleware chooses" — see
559 /// [`QoSProfile::QOS_PROFILE_UNKNOWN`].
560 Unknown,
561 /// **Resolve at creation against the discovered graph** (issue 1973).
562 /// Upstream's `RMW_QOS_POLICY_DURABILITY_BEST_AVAILABLE` (Iron+); lowers
563 /// to `NROS_RMW_DURABILITY_BEST_AVAILABLE` (4) across the C ABI. A
564 /// request only — see [`QoSReliabilityPolicy::BestAvailable`].
565 BestAvailable,
566}
567
568/// QoS liveliness policy. Matches DDS `LIVELINESS` semantics.
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
570#[repr(u8)]
571pub enum QoSLivelinessPolicy {
572 /// Discriminant 0 — the same value the C ABI spells
573 /// `NROS_RMW_LIVELINESS_SYSTEM_DEFAULT` and upstream spells
574 /// `RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT`.
575 ///
576 /// phase-428 W10 — the NAME diverges from both and the VALUE does not, so
577 /// this is what an upstream-mirroring profile carries. Read it as "the
578 /// middleware's choice", not as "liveliness is off": a backend that sees 0
579 /// omits the DDS call entirely and inherits the DDS default, which is
580 /// AUTOMATIC. `None` is our spelling, kept because it is the C ABI
581 /// discriminant three enums already agree on; renaming it is a separate
582 /// question from getting the value right, and only the value crosses.
583 ///
584 /// No liveliness assertion or tracking of our own.
585 #[default]
586 None = 0,
587 /// Backend's keepalive task asserts liveliness automatically.
588 Automatic = 1,
589 /// Application calls `assert_liveliness()` at the node level.
590 ///
591 /// Phase 376 W5/B2 — this was 3 and `ManualByTopic` was 2, the opposite of
592 /// upstream. The discriminant IS the C ABI value (`liveliness_kind` is
593 /// written with `as u8`), and the cyclonedds backend turns it into a real
594 /// DDS liveliness kind that a ROS peer matches on, so the swap was visible
595 /// on the wire and nowhere else.
596 ManualByNode = 2,
597 /// Application calls `assert_liveliness()` per topic explicitly.
598 ManualByTopic = 3,
599 /// **The backend could not determine this policy.** Discriminant 4, the
600 /// value the C ABI spells `NROS_RMW_LIVELINESS_UNKNOWN`.
601 ///
602 /// An ABSENCE, not a value — see [`QoSProfile::QOS_PROFILE_UNKNOWN`]. A
603 /// `*_get_actual_qos` read-back writes it for a policy it cannot report;
604 /// an application never requests it.
605 Unknown = 4,
606 /// **Resolve at creation against the discovered graph** (issue 1973).
607 /// Discriminant 5, upstream's `RMW_QOS_POLICY_LIVELINESS_BEST_AVAILABLE`
608 /// (Iron+) and the C ABI's `NROS_RMW_LIVELINESS_BEST_AVAILABLE`. A
609 /// request only — see [`QoSReliabilityPolicy::BestAvailable`]. Not manual:
610 /// it resolves to `Automatic` or `ManualByTopic` before anything asks.
611 BestAvailable = 5,
612}
613
614impl QoSLivelinessPolicy {
615 /// `true` for the two kinds under which the APPLICATION owns the
616 /// assertion — [`ManualByNode`](Self::ManualByNode) and
617 /// [`ManualByTopic`](Self::ManualByTopic).
618 ///
619 /// The ONE spelling of that test (the phase-467 RMW gap-closure design
620 /// study's Row 7). It decides what [`Publisher::assert_liveliness`] owes:
621 /// under a manual kind the caller asked for something a backend either
622 /// performs or cannot, so a backend that cannot must answer
623 /// `Unsupported`; under `Automatic` / `None` there is nothing to assert
624 /// and `Ok(())` is the whole truth. `Unknown` is an absence, never a
625 /// request, so it is not manual.
626 #[must_use]
627 pub const fn is_manual(self) -> bool {
628 matches!(self, Self::ManualByNode | Self::ManualByTopic)
629 }
630}
631
632/// Phase 211.H — which side of a topic a [`QoSOverride`] targets.
633/// Mirrors the `<role>` segment of a ROS 2
634/// `qos_overrides.<topic>.<role>.<policy>` launch parameter.
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636pub enum QoSOverrideRole {
637 /// `qos_overrides.<topic>.publisher.*`
638 Publisher,
639 /// `qos_overrides.<topic>.subscription.*`
640 Subscription,
641}
642
643/// Phase 211.H — a single policy value a [`QoSOverride`] sets. A typed enum
644/// (not a string) so the codegen that bakes these from the plan catches an
645/// unknown policy / mistyped value at generation time rather than silently
646/// no-op-ing at runtime.
647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
648pub enum QoSOverrideValue {
649 /// `.reliability` → Reliable / BestEffort.
650 Reliability(QoSReliabilityPolicy),
651 /// `.durability` → Volatile / TransientLocal.
652 Durability(QoSDurabilityPolicy),
653 /// `.history` → KeepLast / KeepAll.
654 History(QoSHistoryPolicy),
655 /// `.depth` → KeepLast depth.
656 Depth(u32),
657 /// Issue 0303 — `.deadline` → [`QoSProfile::deadline_ms`]. `0` /
658 /// [`DURATION_INFINITE_MS`] = no deadline check.
659 Deadline(u32),
660 /// Issue 0303 — `.lifespan` → [`QoSProfile::lifespan_ms`].
661 Lifespan(u32),
662 /// Issue 0303 — `.liveliness` → [`QoSProfile::liveliness_kind`].
663 Liveliness(QoSLivelinessPolicy),
664 /// Issue 0303 — `.liveliness_lease_duration` →
665 /// [`QoSProfile::liveliness_lease_ms`].
666 LivelinessLease(u32),
667}
668
669/// Issue 0303 — the wire form a baked QoS override travels in:
670/// `(topic, role, policy, value)`, all primitives.
671///
672/// One numbering for every language: `rclc_qos_override_t` (C),
673/// `nros_cpp_qos_override_t` (C++), `RuntimeCtx::qos_overrides` (the Rust
674/// entry bake) and `NodeRecord::qos_overrides` are all THIS tuple. Codes rather
675/// than [`QoSOverride`] because the table crosses the C ABI and rides
676/// `nros-platform`, which sits below this crate in the layer graph.
677///
678/// `topic` is `&'static` because every producer bakes a literal.
679pub type QoSOverrideCode = (&'static str, u8, u8, u32);
680
681/// `role` codes for [`QoSOverrideCode`].
682pub mod qos_override_role {
683 /// The override targets publishers on the topic.
684 pub const PUBLISHER: u8 = 0;
685 /// The override targets subscriptions on the topic.
686 pub const SUBSCRIPTION: u8 = 1;
687}
688
689/// `policy` codes for [`QoSOverrideCode`].
690///
691/// Append-only: these numbers are baked into shipped images and mirrored in
692/// two C headers. Never renumber — add.
693pub mod qos_override_policy {
694 /// value `0` = best_effort, `1` = reliable.
695 pub const RELIABILITY: u8 = 0;
696 /// value `0` = volatile, `1` = transient_local.
697 pub const DURABILITY: u8 = 1;
698 /// value `0` = keep_last, `1` = keep_all.
699 pub const HISTORY: u8 = 2;
700 /// value = the KeepLast depth.
701 pub const DEPTH: u8 = 3;
702 /// value = milliseconds (issue 0303).
703 pub const DEADLINE: u8 = 4;
704 /// value = milliseconds (issue 0303).
705 pub const LIFESPAN: u8 = 5;
706 /// value = [`super::QoSLivelinessPolicy`] discriminant (issue 0303).
707 pub const LIVELINESS: u8 = 6;
708 /// value = milliseconds (issue 0303).
709 pub const LIVELINESS_LEASE: u8 = 7;
710}
711
712/// Decode one [`QoSOverrideCode`] into a typed [`QoSOverride`].
713///
714/// `None` for an unrecognised role or policy code. THE one decoder: before
715/// issue 0303 this match existed four times (nros-node, nros-c, nros-cpp, and
716/// the executor's node record), each with a silent catch-all, so adding a
717/// policy meant finding all four — and the two FFI copies had already been
718/// forgotten once.
719pub fn decode_qos_override(code: &QoSOverrideCode) -> Option<QoSOverride> {
720 let (topic, role, policy, value) = *code;
721 decode_qos_override_parts(topic, role, policy, value)
722}
723
724/// [`decode_qos_override`] over loose parts — for the FFI paths, which read the
725/// fields out of a `#[repr(C)]` struct rather than a tuple.
726pub fn decode_qos_override_parts(
727 topic: &'static str,
728 role: u8,
729 policy: u8,
730 value: u32,
731) -> Option<QoSOverride> {
732 Some(QoSOverride {
733 topic,
734 role: decode_qos_override_role(role)?,
735 value: decode_qos_override_value(policy, value)?,
736 })
737}
738
739/// Decode a `role` code. `None` for an unrecognised one.
740pub fn decode_qos_override_role(role: u8) -> Option<QoSOverrideRole> {
741 match role {
742 qos_override_role::PUBLISHER => Some(QoSOverrideRole::Publisher),
743 qos_override_role::SUBSCRIPTION => Some(QoSOverrideRole::Subscription),
744 _ => None,
745 }
746}
747
748/// Decode a `(policy, value)` code pair. `None` for an unrecognised policy or
749/// an out-of-range enum value.
750///
751/// Split out from [`decode_qos_override`] for the FFI paths: they have already
752/// matched the topic against a `*const c_char`, so they need the VALUE without
753/// a `&'static str` to build a whole [`QoSOverride`] around.
754pub fn decode_qos_override_value(policy: u8, value: u32) -> Option<QoSOverrideValue> {
755 let out = match policy {
756 qos_override_policy::RELIABILITY => QoSOverrideValue::Reliability(if value == 0 {
757 QoSReliabilityPolicy::BestEffort
758 } else {
759 QoSReliabilityPolicy::Reliable
760 }),
761 qos_override_policy::DURABILITY => QoSOverrideValue::Durability(if value == 1 {
762 QoSDurabilityPolicy::TransientLocal
763 } else {
764 QoSDurabilityPolicy::Volatile
765 }),
766 qos_override_policy::HISTORY => QoSOverrideValue::History(if value == 1 {
767 QoSHistoryPolicy::KeepAll
768 } else {
769 QoSHistoryPolicy::KeepLast
770 }),
771 qos_override_policy::DEPTH => QoSOverrideValue::Depth(value),
772 qos_override_policy::DEADLINE => QoSOverrideValue::Deadline(value),
773 qos_override_policy::LIFESPAN => QoSOverrideValue::Lifespan(value),
774 // The encoder is `nros_orchestration_ir::qos_override`; both ends name
775 // the variant rather than its number, so W5/B2's renumbering moved them
776 // together instead of silently swapping two policies.
777 qos_override_policy::LIVELINESS => QoSOverrideValue::Liveliness(match value {
778 v if v == QoSLivelinessPolicy::None as u32 => QoSLivelinessPolicy::None,
779 v if v == QoSLivelinessPolicy::Automatic as u32 => QoSLivelinessPolicy::Automatic,
780 v if v == QoSLivelinessPolicy::ManualByNode as u32 => QoSLivelinessPolicy::ManualByNode,
781 v if v == QoSLivelinessPolicy::ManualByTopic as u32 => {
782 QoSLivelinessPolicy::ManualByTopic
783 }
784 _ => return None,
785 }),
786 qos_override_policy::LIVELINESS_LEASE => QoSOverrideValue::LivelinessLease(value),
787 _ => return None,
788 };
789 Some(out)
790}
791
792/// Phase 211.H — one per-topic QoS override, lowered from a ROS 2
793/// `qos_overrides.<topic>.<role>.<policy>` launch parameter by the planner and
794/// baked into a `&'static [QoSOverride]` table by the entry codegen. The node
795/// folds the matching entries into the entity's [`QoSProfile`] at
796/// `create_publisher` / `create_subscription` time (setup-time, single
797/// linear scan, no alloc), *before* the backend-compat `validate_against` —
798/// so an override the active RMW can't honour still errors loudly, never a
799/// silent downgrade.
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
801pub struct QoSOverride {
802 /// The resolved (remapped) topic name the override targets, e.g.
803 /// `"/chatter"`. Matched exactly against the entity's topic.
804 pub topic: &'static str,
805 /// Publisher or subscription side.
806 pub role: QoSOverrideRole,
807 /// The policy + value to set.
808 pub value: QoSOverrideValue,
809}
810
811/// Phase-301 (issue 0241) — explicit "infinite" spelling for the u32
812/// millisecond QoS duration fields (`deadline_ms`, `lifespan_ms`,
813/// `liveliness_lease_ms`). Semantically identical to `0` (unset /
814/// no-check) at every check site; exists so a caller can distinguish
815/// "default" from "deliberately infinite". Mirrors the C header's
816/// `NROS_RMW_DURATION_INFINITE_MS`.
817pub const DURATION_INFINITE_MS: u32 = u32::MAX;
818
819/// Issue 1973 — the "best available" spelling for `deadline_ms` and
820/// `liveliness_lease_ms`: resolve at creation against the discovered graph.
821///
822/// Upstream spells it `RMW_QOS_DEADLINE_BEST_AVAILABLE` /
823/// `RMW_QOS_LIVELINESS_LEASE_DURATION_BEST_AVAILABLE`, both
824/// `RMW_DURATION_INFINITE - 1`; this is the same idea in our u32-millisecond
825/// fields: [`DURATION_INFINITE_MS`] minus one. A REQUEST only — the runtime
826/// replaces it before a backend sees the profile
827/// ([`QoSProfile::resolve_best_available`]). Mirrors the C header's
828/// `NROS_RMW_DURATION_BEST_AVAILABLE_MS`. Not meaningful for `lifespan_ms`,
829/// as upstream defines no best-available lifespan.
830pub const DURATION_BEST_AVAILABLE_MS: u32 = DURATION_INFINITE_MS - 1;
831
832/// Phase-301 (issue 0241) — lower a [`core::time::Duration`] into a u32
833/// millisecond QoS field. Boundary contract:
834///
835/// - zero stays `0` (unset / no-check);
836/// - sub-millisecond remainders CEIL to the next ms (rounding down
837/// would silently turn a real deadline into "no deadline");
838/// - values at or past [`DURATION_INFINITE_MS`] ms are a create-time
839/// error, never a clamp (infinite is requested via the sentinel or
840/// `0`, not by a huge finite duration). The bound starts one below, at
841/// [`DURATION_BEST_AVAILABLE_MS`] (issue 1973), so no finite duration can
842/// collide with that sentinel either.
843pub fn duration_to_qos_ms(d: core::time::Duration) -> Result<u32, TransportError> {
844 if d.is_zero() {
845 return Ok(0);
846 }
847 let mut ms = d.as_millis();
848 if !d.subsec_nanos().is_multiple_of(1_000_000) {
849 ms += 1;
850 }
851 if ms >= DURATION_BEST_AVAILABLE_MS as u128 {
852 return Err(TransportError::InvalidArgument);
853 }
854 Ok(ms as u32)
855}
856
857/// Full DDS-shaped QoS profile. Matches the field set of upstream
858/// `rmw_qos_profile_t`.
859///
860/// Backends advertise per-policy support via
861/// [`Session::supported_qos_policies`]; entities created with a
862/// profile the active backend can't honour return
863/// [`TransportError::IncompatibleQos`] synchronously at create time
864/// — no silent downgrade.
865///
866/// Zero-valued time-window fields ("off") mean infinite — the policy
867/// is effectively disabled for the entity.
868#[derive(Debug, Clone, Copy, PartialEq, Eq)]
869pub struct QoSProfile {
870 /// History policy
871 pub history: QoSHistoryPolicy,
872 /// Reliability policy
873 pub reliability: QoSReliabilityPolicy,
874 /// Durability policy
875 pub durability: QoSDurabilityPolicy,
876 /// Liveliness policy
877 pub liveliness_kind: QoSLivelinessPolicy,
878 /// History depth (only used if history is KeepLast)
879 pub depth: u32,
880 /// Subscription max-inter-arrival / publisher offered-rate, ms.
881 /// `0` = infinite (no deadline check).
882 pub deadline_ms: u32,
883 /// Sample expiry, ms. Subscribers filter samples older than this.
884 /// `0` = infinite (no expiry).
885 pub lifespan_ms: u32,
886 /// Liveliness lease, ms. `0` = infinite.
887 pub liveliness_lease_ms: u32,
888 /// If `true`, topic-name encoding skips the `/rt/` ROS prefix.
889 pub avoid_ros_namespace_conventions: bool,
890 /// Phase 282 (#145) — publisher-side "express" hint: this publisher's
891 /// samples bypass transport tx batching (zenoh: the wire EXPRESS flag; a
892 /// batching zenoh-pico session sends them immediately instead of queueing
893 /// them for the next flush). A transport hint, not a DDS policy — no RxO
894 /// matching, no backend-compat validation; ignored by subscriptions and by
895 /// backends without a batching concept.
896 pub tx_express: bool,
897}
898
899impl Default for QoSProfile {
900 fn default() -> Self {
901 Self::QOS_PROFILE_DEFAULT
902 }
903}
904
905/// The depth sentinel — "the caller did not state a queue depth".
906///
907/// issue 0829. Upstream spells it `RMW_QOS_POLICY_DEPTH_SYSTEM_DEFAULT = 0`
908/// (`rmw/include/rmw/types.h`); depth is `size_t` there and `uint16_t` in our
909/// C ABI, but 0 is 0 in both. The value was already free on every backend:
910/// Cyclone REJECTS `KEEP_LAST` with depth 0 outright
911/// (`validate_history_qospolicy`, `ddsi_plist.c:2603-2604`), the XRCE client
912/// already reads it as "unstated" and drops the field from the wire
913/// (`create_entities_bin.c:148`), and both `read_entity_qos`
914/// (`nros-rmw-cyclonedds/src/qos.cpp:138`) and `report_qos_downgrade`
915/// (`nros-rmw-cffi/src/lib.rs:1903`) already treat a 0 read-back as "no
916/// answer" rather than as an answer.
917pub const DEPTH_SYSTEM_DEFAULT: u32 = 0;
918
919/// What ONE backend resolves the `SYSTEM_DEFAULT` sentinel to.
920///
921/// issue 0829. `rmw_qos_profile_system_default` is an absence, and the RMW
922/// fills it — which means the answer is per backend and there is no constant
923/// this crate could bake. Each backend declares its own `QoSSystemDefaults`
924/// and applies it with [`QoSProfile::resolve_system_default`] at its create
925/// entry, **before anything is derived from the QoS**.
926///
927/// That ordering is load-bearing on the zenoh path: the profile is serialised
928/// into the liveliness-token keyexpr that a ROS `rmw_zenoh_cpp` peer parses
929/// out of the graph (`nros-rmw-zenoh/src/keyexpr.rs`). Upstream resolves in
930/// best_available_qos before the entity and its token exist, so its tokens
931/// never carry a sentinel; ours must not either, or we advertise `0:0:0,0` to
932/// peers as though it were a policy.
933///
934/// The values a backend picks should mirror **the corresponding upstream
935/// RMW**, not the raw middleware default — interop with a ROS peer is the
936/// requirement, and the two differ. Leaving `dds_qset_reliability` unset gives
937/// Cyclone's own reader default of BEST_EFFORT (`ddsi_plist.c:3470`), where
938/// `rmw_cyclonedds_cpp` deliberately picks RELIABLE.
939#[derive(Debug, Clone, Copy, PartialEq, Eq)]
940pub struct QoSSystemDefaults {
941 /// Concrete reliability. Must not itself be `SystemDefault`.
942 pub reliability: QoSReliabilityPolicy,
943 /// Concrete durability. Must not itself be `SystemDefault`.
944 pub durability: QoSDurabilityPolicy,
945 /// Concrete history. Must not itself be `SystemDefault`.
946 pub history: QoSHistoryPolicy,
947 /// Concrete queue depth. `0` means the backend genuinely has no answer and
948 /// defers further down the stack — the XRCE case, where the client encodes
949 /// exactly that (`optional_history_depth = false`) and the Agent's DDS
950 /// layer resolves it.
951 pub depth: u32,
952}
953
954impl QoSProfile {
955 /// Phase 211.H — fold the plan's `qos_overrides` matching `topic` + `role`
956 /// into this profile, returning the overridden profile. Setup-time only
957 /// (called from `create_publisher`/`create_subscription`): a single linear
958 /// scan over the baked `&'static` table, no alloc, RT-safe. Later entries
959 /// win on a duplicate `(topic, role, policy)` (last-write), matching the
960 /// planner's sorted, de-conflicted emit. Non-matching entries are ignored,
961 /// so passing the whole node table to every entity is cheap + correct.
962 #[must_use]
963 pub fn apply_overrides(
964 mut self,
965 topic: &str,
966 role: QoSOverrideRole,
967 overrides: &[QoSOverride],
968 ) -> Self {
969 for ovr in overrides {
970 if ovr.topic == topic && ovr.role == role {
971 self.apply_override_value(ovr.value);
972 }
973 }
974 self
975 }
976
977 /// Issue 0303 — apply ONE decoded override value. The single place a
978 /// policy maps to the field it sets; `apply_overrides` and the FFI folds
979 /// both go through it, so a new policy cannot reach some paths only.
980 pub fn apply_override_value(&mut self, value: QoSOverrideValue) {
981 match value {
982 QoSOverrideValue::Reliability(r) => self.reliability = r,
983 QoSOverrideValue::Durability(d) => self.durability = d,
984 QoSOverrideValue::History(h) => self.history = h,
985 QoSOverrideValue::Depth(d) => self.depth = d,
986 QoSOverrideValue::Deadline(ms) => self.deadline_ms = ms,
987 QoSOverrideValue::Lifespan(ms) => self.lifespan_ms = ms,
988 QoSOverrideValue::Liveliness(k) => self.liveliness_kind = k,
989 QoSOverrideValue::LivelinessLease(ms) => self.liveliness_lease_ms = ms,
990 }
991 }
992
993 /// Issue 0303 — fold baked [`QoSOverrideCode`]s for one `(topic, role)`.
994 /// Unrecognised codes are skipped; the producers reject them at BAKE time
995 /// (`nros_orchestration_ir::qos_override`), so a code reaching here that
996 /// this build does not know is an older image running newer data, not a
997 /// user error to diagnose at runtime.
998 pub fn apply_override_codes(
999 self,
1000 topic: &str,
1001 role: QoSOverrideRole,
1002 codes: &[QoSOverrideCode],
1003 ) -> Self {
1004 let mut qos = self;
1005 for code in codes {
1006 if let Some(ovr) = decode_qos_override(code) {
1007 qos = qos.apply_overrides(topic, role, core::slice::from_ref(&ovr));
1008 }
1009 }
1010 qos
1011 }
1012}
1013
1014/// The ONE authoring site for every named [`QoSProfile`] preset — phase-428 W10.
1015///
1016/// Upstream's profiles were hand-transcribed here and in three other places
1017/// (`nros::qos`, `rclcpp/qos.hpp`, `rmw_entity.h`) with nothing binding them, and
1018/// they drifted exactly as every other hand-mirror in this tree has: issue 0793
1019/// caught `PARAMETERS` claiming TRANSIENT_LOCAL, fixed that one profile, and
1020/// left `PARAMETER_EVENTS` sitting at `KeepAll, depth 0` under a comment saying
1021/// *"matches rmw_qos_profile_parameter_events"* — where upstream is
1022/// `KEEP_LAST, 1000`. A RELIABLE writer with KEEP_ALL **blocks** when history
1023/// fills where KEEP_LAST(1000) overwrites, so that was an inverting difference,
1024/// not a weaker one, and the test beneath it asserted the wrong value.
1025///
1026/// Three properties are what make this a table rather than ten `const`s:
1027///
1028/// 1. **Every field is stated.** The `Self::build(...)` helper this replaces
1029/// supplied `liveliness_kind`, the three duration windows and both flags
1030/// silently, so five of ten fields per profile were invisible at the
1031/// authoring site and no reader could compare a row against upstream.
1032/// 2. **Every row declares its PROVENANCE** — the upstream symbol it mirrors,
1033/// or `nros: <why>` for a preset we invented. A profile cannot be added
1034/// without answering "whose constant is this?".
1035/// 3. **The field ORDER is upstream's** (`rmw_qos_profile_t` in
1036/// `rmw/include/rmw/types.h`: history, depth, reliability, durability,
1037/// deadline, lifespan, liveliness, lease, avoid-ns), so a row reads against
1038/// the header positionally.
1039///
1040/// Expansion is exactly the `pub const` items that were here before — no table
1041/// is materialised, nothing is `static`, and the crate stays `no_std` and
1042/// zero-cost on every target it reaches.
1043///
1044/// `scripts/check-qos-profile-ssot.py` parses this invocation and asserts every
1045/// upstream-mirroring row field-by-field against
1046/// `docs/reference/rmw-qos-profiles.txt`, which is itself extracted from a real
1047/// ROS install. The other three sites bind to this block.
1048macro_rules! qos_profiles {
1049 ($(
1050 $(#[$meta:meta])*
1051 $name:ident : $provenance:literal = {
1052 history: $history:ident,
1053 depth: $depth:expr,
1054 reliability: $reliability:ident,
1055 durability: $durability:ident,
1056 deadline_ms: $deadline_ms:expr,
1057 lifespan_ms: $lifespan_ms:expr,
1058 liveliness_kind: $liveliness_kind:ident,
1059 liveliness_lease_ms: $liveliness_lease_ms:expr,
1060 avoid_ros_namespace_conventions: $avoid:expr,
1061 tx_express: $tx_express:expr,
1062 };
1063 )+) => {
1064 impl QoSProfile {
1065 $(
1066 $(#[$meta])*
1067 ///
1068 #[doc = concat!("Provenance: `", $provenance, "`.")]
1069 pub const $name: Self = Self {
1070 history: QoSHistoryPolicy::$history,
1071 depth: $depth,
1072 reliability: QoSReliabilityPolicy::$reliability,
1073 durability: QoSDurabilityPolicy::$durability,
1074 deadline_ms: $deadline_ms,
1075 lifespan_ms: $lifespan_ms,
1076 liveliness_kind: QoSLivelinessPolicy::$liveliness_kind,
1077 liveliness_lease_ms: $liveliness_lease_ms,
1078 avoid_ros_namespace_conventions: $avoid,
1079 tx_express: $tx_express,
1080 };
1081 )+
1082 }
1083 };
1084}
1085
1086// nros-qos-table: BEGIN
1087// The SSoT. Everything between this marker and `nros-qos-table: END` is parsed
1088// by `scripts/check-qos-profile-ssot.py`; do not add a `QoSProfile` preset
1089// anywhere else in this crate — the gate refuses one.
1090qos_profiles! {
1091 /// Best-effort QoS (for real-time).
1092 BEST_EFFORT: "nros: best-effort keep-last(1); a shorthand, not an upstream constant" = {
1093 history: KeepLast,
1094 depth: 1,
1095 reliability: BestEffort,
1096 durability: Volatile,
1097 deadline_ms: 0,
1098 lifespan_ms: 0,
1099 liveliness_kind: Automatic,
1100 liveliness_lease_ms: 0,
1101 avoid_ros_namespace_conventions: false,
1102 tx_express: false,
1103 };
1104
1105 /// Reliable QoS.
1106 RELIABLE: "nros: reliable keep-last(10); a shorthand, not an upstream constant" = {
1107 history: KeepLast,
1108 depth: 10,
1109 reliability: Reliable,
1110 durability: Volatile,
1111 deadline_ms: 0,
1112 lifespan_ms: 0,
1113 liveliness_kind: Automatic,
1114 liveliness_lease_ms: 0,
1115 avoid_ros_namespace_conventions: false,
1116 tx_express: false,
1117 };
1118
1119 /// `rmw_qos_profile_system_default` — **an absence, not a profile.**
1120 ///
1121 /// issue 0829. Every field is the sentinel: upstream's constant names no
1122 /// concrete policy at all, and the two reference RMWs fill the absence with
1123 /// different numbers — `rmw_cyclonedds_cpp`'s `create_readwrite_qos` folds
1124 /// `RMW_QOS_POLICY_DEPTH_SYSTEM_DEFAULT` to `KEEP_LAST(1)`, while
1125 /// `rmw_zenoh_cpp`'s `QoS::QoS()` fills it from
1126 /// `RMW_ZENOH_DEFAULT_HISTORY_DEPTH`, which is 42, over a comment stating
1127 /// the contract outright: *"If the depth field in the qos profile is set to
1128 /// 0, the RMW implementation has the liberty to assign a default depth."*
1129 ///
1130 /// So no baked number can be right. This carried a concrete
1131 /// `Reliable / Volatile / KeepLast(1)` until 2026-09-03, and
1132 /// `nros::qos::SYSTEM_DEFAULT` carried a concrete depth **10**, which is how
1133 /// one name shipped two queue depths. Both are gone; the backend resolves
1134 /// this at its create entry via [`QoSProfile::resolve_system_default`].
1135 ///
1136 /// `liveliness_kind` is [`QoSLivelinessPolicy::None`], which IS the
1137 /// sentinel on that policy — it lowers to
1138 /// `NROS_RMW_LIVELINESS_SYSTEM_DEFAULT` (0), the two having collapsed onto
1139 /// one value in phase-376 W5/B2. This is therefore the ONE preset that
1140 /// matches upstream on all nine fields.
1141 QOS_PROFILE_SYSTEM_DEFAULT: "rmw_qos_profile_system_default" = {
1142 history: SystemDefault,
1143 depth: DEPTH_SYSTEM_DEFAULT,
1144 reliability: SystemDefault,
1145 durability: SystemDefault,
1146 deadline_ms: 0,
1147 lifespan_ms: 0,
1148 liveliness_kind: None,
1149 liveliness_lease_ms: 0,
1150 avoid_ros_namespace_conventions: false,
1151 tx_express: false,
1152 };
1153
1154 /// `rmw_qos_profile_unknown` — **"the backend could not determine this"**,
1155 /// and distinct from [`QOS_PROFILE_SYSTEM_DEFAULT`](Self::QOS_PROFILE_SYSTEM_DEFAULT).
1156 ///
1157 /// `SYSTEM_DEFAULT` means "nobody asked, so the middleware chooses";
1158 /// this means "we looked and cannot say", which is what a
1159 /// `*_get_actual_qos` read-back owes its caller for a policy it cannot
1160 /// report. It is the starting value the read-back helper pre-loads, so a
1161 /// field the backend does not overwrite reads as an ABSENCE rather than as
1162 /// a confident grant of whatever was asked.
1163 ///
1164 /// **Never a request.** No `create_*` accepts it — a backend's
1165 /// `qos_admit` refuses `UNKNOWN` on every policy, because an application
1166 /// asking for "I cannot say" is a bug, not a preference.
1167 ///
1168 /// The C ABI twin is `NROS_RMW_QOS_PROFILE_UNKNOWN` (`rmw_entity.h`),
1169 /// which existed from phase-428 W10 with no Rust counterpart: three of the
1170 /// four policy enums had no `Unknown` variant to build it from. They do
1171 /// now.
1172 QOS_PROFILE_UNKNOWN: "rmw_qos_profile_unknown" = {
1173 history: Unknown,
1174 depth: 0,
1175 reliability: Unknown,
1176 durability: Unknown,
1177 deadline_ms: 0,
1178 lifespan_ms: 0,
1179 liveliness_kind: Unknown,
1180 liveliness_lease_ms: 0,
1181 avoid_ros_namespace_conventions: false,
1182 tx_express: false,
1183 };
1184
1185 /// Default QoS profile — reliable, volatile, keep-last(10).
1186 QOS_PROFILE_DEFAULT: "rmw_qos_profile_default" = {
1187 history: KeepLast,
1188 depth: 10,
1189 reliability: Reliable,
1190 durability: Volatile,
1191 deadline_ms: 0,
1192 lifespan_ms: 0,
1193 liveliness_kind: None,
1194 liveliness_lease_ms: 0,
1195 avoid_ros_namespace_conventions: false,
1196 tx_express: false,
1197 };
1198
1199 /// Sensor data QoS profile — best-effort, volatile, keep-last(5).
1200 QOS_PROFILE_SENSOR_DATA: "rmw_qos_profile_sensor_data" = {
1201 history: KeepLast,
1202 depth: 5,
1203 reliability: BestEffort,
1204 durability: Volatile,
1205 deadline_ms: 0,
1206 lifespan_ms: 0,
1207 liveliness_kind: None,
1208 liveliness_lease_ms: 0,
1209 avoid_ros_namespace_conventions: false,
1210 tx_express: false,
1211 };
1212
1213 /// Services default QoS profile — reliable, volatile, keep-last(10).
1214 QOS_PROFILE_SERVICES_DEFAULT: "rmw_qos_profile_services_default" = {
1215 history: KeepLast,
1216 depth: 10,
1217 reliability: Reliable,
1218 durability: Volatile,
1219 deadline_ms: 0,
1220 lifespan_ms: 0,
1221 liveliness_kind: None,
1222 liveliness_lease_ms: 0,
1223 avoid_ros_namespace_conventions: false,
1224 tx_express: false,
1225 };
1226
1227 /// Parameters QoS profile — KEEP_LAST(1000), RELIABLE, **VOLATILE**.
1228 ///
1229 /// issue 0793 — this said `TransientLocal` until 2026-08-25, disagreeing
1230 /// both with upstream (`/opt/ros/<distro>/include/rmw/rmw/qos_profiles.h`)
1231 /// and with our own second copy of the same profile, `nros::qos::PARAMETERS`,
1232 /// which was already correct. Two copies of one profile that disagree is the
1233 /// defect; the wrong one being the one named after the upstream constant is
1234 /// what made it hard to see.
1235 QOS_PROFILE_PARAMETERS: "rmw_qos_profile_parameters" = {
1236 history: KeepLast,
1237 depth: 1000,
1238 reliability: Reliable,
1239 durability: Volatile,
1240 deadline_ms: 0,
1241 lifespan_ms: 0,
1242 liveliness_kind: None,
1243 liveliness_lease_ms: 0,
1244 avoid_ros_namespace_conventions: false,
1245 tx_express: false,
1246 };
1247
1248 /// Parameter events QoS profile — KEEP_LAST(1000), RELIABLE, VOLATILE.
1249 ///
1250 /// phase-428 W10 — this was `KeepAll, depth 0` until 2026-09-05, under a
1251 /// comment claiming it matched `rmw_qos_profile_parameter_events`. Upstream
1252 /// is `KEEP_LAST, 1000` (identical to `rmw_qos_profile_parameters`), and
1253 /// `rclcpp::ParameterEventsQoS` — which our own C++ surface mirrors — was
1254 /// already correct, so the two languages shipped different profiles under
1255 /// one name.
1256 ///
1257 /// The difference INVERTS behaviour rather than weakening it: both DDS
1258 /// backends map KEEP_ALL faithfully, and a RELIABLE writer with KEEP_ALL
1259 /// **blocks** once history fills, where KEEP_LAST(1000) overwrites. A
1260 /// parameter server whose events nobody is reading would stall the node
1261 /// instead of dropping the oldest event.
1262 QOS_PROFILE_PARAMETER_EVENTS: "rmw_qos_profile_parameter_events" = {
1263 history: KeepLast,
1264 depth: 1000,
1265 reliability: Reliable,
1266 durability: Volatile,
1267 deadline_ms: 0,
1268 lifespan_ms: 0,
1269 liveliness_kind: None,
1270 liveliness_lease_ms: 0,
1271 avoid_ros_namespace_conventions: false,
1272 tx_express: false,
1273 };
1274
1275 /// Action status default QoS profile — reliable, transient-local,
1276 /// keep-last(1). From `rcl_action`, not `rmw`.
1277 QOS_PROFILE_ACTION_STATUS_DEFAULT: "rcl_action_qos_profile_status_default" = {
1278 history: KeepLast,
1279 depth: 1,
1280 reliability: Reliable,
1281 durability: TransientLocal,
1282 deadline_ms: 0,
1283 lifespan_ms: 0,
1284 liveliness_kind: None,
1285 liveliness_lease_ms: 0,
1286 avoid_ros_namespace_conventions: false,
1287 tx_express: false,
1288 };
1289
1290 /// `rmw_qos_profile_best_available` (Iron+) — KEEP_LAST(10) with every
1291 /// policy that CAN be best-available set so (issue 1973).
1292 ///
1293 /// Reliability, durability, liveliness, deadline and lease are resolved at
1294 /// creation against the endpoints already discovered on the topic, by
1295 /// [`QoSProfile::resolve_best_available`]; history, depth and lifespan
1296 /// are stated, exactly as upstream states them. A request only: no
1297 /// backend is handed this profile unresolved.
1298 QOS_PROFILE_BEST_AVAILABLE: "rmw_qos_profile_best_available" = {
1299 history: KeepLast,
1300 depth: 10,
1301 reliability: BestAvailable,
1302 durability: BestAvailable,
1303 deadline_ms: DURATION_BEST_AVAILABLE_MS,
1304 lifespan_ms: 0,
1305 liveliness_kind: BestAvailable,
1306 liveliness_lease_ms: DURATION_BEST_AVAILABLE_MS,
1307 avoid_ros_namespace_conventions: false,
1308 tx_express: false,
1309 };
1310
1311 /// Clock QoS profile — sensor-data shaped with depth 1.
1312 // phase-428 W4 — the provenance string said "/clock has no rmw constant",
1313 // which is true and was read as "no upstream at all". `rclrs::QOS_PROFILE_CLOCK`
1314 // EXISTS under this exact name (`rclrs/src/qos.rs:598`, it is rclcpp's
1315 // `ClockQoS`), and Rust follows rclrs. So this row mirrors something after
1316 // all, and the gate — which only compares upstream-MIRRORING rows — could
1317 // not see it.
1318 //
1319 // That is why it kept `Automatic` when W10 moved the other five to the
1320 // sentinel: F2's fourth instance, sitting inside the fix for F2's third.
1321 // The one row the instrument does not reach is the one that kept the
1322 // defect.
1323 QOS_PROFILE_CLOCK: "nros: mirrors rclrs::QOS_PROFILE_CLOCK; no rmw constant exists, so the gate cannot bind it — phase-428 W4" = {
1324 history: KeepLast,
1325 depth: 1,
1326 reliability: BestEffort,
1327 durability: Volatile,
1328 deadline_ms: 0,
1329 lifespan_ms: 0,
1330 liveliness_kind: None,
1331 liveliness_lease_ms: 0,
1332 avoid_ros_namespace_conventions: false,
1333 tx_express: false,
1334 };
1335
1336 /// PX4 companion QoS profile (Phase 233 / RFC-0039 Track B). Matches the
1337 /// QoS PX4's `uxrce_dds_client` uses on `/fmu/out/*` and `/fmu/in/*` —
1338 /// `BEST_EFFORT` + `VOLATILE` + `KEEP_LAST(1)`. A nano-ros node talking to
1339 /// the same `MicroXRCEAgent` must use this (a reliable or
1340 /// `TRANSIENT_LOCAL` reader will not match PX4's volatile best-effort
1341 /// writers). Verified against real PX4 SITL (`nros-px4-sitl-test`):
1342 /// `TRANSIENT_LOCAL` durability silently fails to match `/fmu/out/*`.
1343 /// Adjust depth via `.keep_last(n)` for higher-rate streams.
1344 ///
1345 /// Liveliness is the `None` sentinel, not `Automatic` (issue 2002): this
1346 /// profile exists for the XRCE backend, and `uxrQoS_t` carries no
1347 /// liveliness, so XRCE refuses a stated one (issue 1329). Stating it made
1348 /// every companion fail to advertise its first topic.
1349 QOS_PROFILE_PX4: "nros: PX4 uxrce_dds_client companion QoS; no upstream constant" = {
1350 history: KeepLast,
1351 depth: 1,
1352 reliability: BestEffort,
1353 durability: Volatile,
1354 deadline_ms: 0,
1355 lifespan_ms: 0,
1356 liveliness_kind: None,
1357 liveliness_lease_ms: 0,
1358 avoid_ros_namespace_conventions: false,
1359 tx_express: false,
1360 };
1361}
1362// nros-qos-table: END
1363
1364// Where we knowingly differ from upstream, and where we knowingly have no
1365// counterpart. phase-428 W10.
1366//
1367// These lines are DATA, not prose: `check-qos-profile-ssot.py` requires a
1368// deviation to name the field, BOTH values and a tracking reference, and it
1369// re-measures `ours=` and `upstream=` against the table and the extracted
1370// record on every run. So a deviation cannot survive either side changing —
1371// which is the failure mode the phase-428 sweep found in `ARG_DEVIATIONS`,
1372// where 42 values were reason strings against a membership test and nothing
1373// checked that the reason was still true.
1374//
1375// The liveliness deviation is CLOSED (issue 1329, 2026-09-12) and is
1376// recorded here because the reason it survived two phases is worth keeping.
1377//
1378// Every application-facing preset used to state AUTOMATIC where upstream
1379// states the SYSTEM_DEFAULT sentinel. W10 moved this table and
1380// `rmw_entity.h` onto the sentinel and could not move the remaining three —
1381// `nros-c`'s `NROS_QOS_*`, ~~`rclc_c_qos_default()`~~ and `nros::QoS`'s
1382// constructor — because `liveliness_kind` crosses the C ABI as its
1383// discriminant, so flipping one surface alone makes a C caller's default and
1384// a Rust caller's default differ in a field cyclonedds puts on the wire. All
1385// three moved together instead.
1386//
1387// MEASURED consequences of the move, 2026-09-05 and re-read 2026-09-12:
1388// - cyclonedds: wire-identical. `nros_rmw_cyclonedds::make_dds_qos`
1389// (`qos.cpp`; the name here read ~~`nros_rmw_cyclonedds_qos_apply`~~ until
1390// phase-428 W9 went looking for it — no such symbol has ever existed)
1391// calls `dds_qset_liveliness` only when the kind is
1392// NOT the sentinel, and Cyclone's own reader/writer default IS
1393// `DDS_LIVELINESS_AUTOMATIC` — so both spellings put AUTOMATIC on the
1394// wire.
1395// - zenoh: invisible. `QosKeyExpr::to_qos_string` (`keyexpr.rs`)
1396// emits `"{rel}:{dur}:{hist},{depth}:,:,:,,"` (defaults elided, issue
1397// 1495) — the liveliness and lease
1398// positions are EMPTY for every profile, so the value never reaches a
1399// peer's graph parse.
1400// - xrce: invisible. `uxrQoS_t` has no liveliness field at all.
1401// The one live consequence was `required_policies()`, which set
1402// `LIVELINESS_AUTOMATIC` for presets upstream leaves unset — an over-demand,
1403// so it could only turn an acceptance into a rejection. It flipped no
1404// verdict while every `supported_qos_policies()` in the tree advertised the
1405// bit, and issue 1329 is exactly the change that stops them doing so: XRCE
1406// honours no liveliness and now says so, which would have refused every C
1407// application on that backend over a policy nobody asked for. The fix is to
1408// stop asking, not to let a backend claim what it does not implement.
1409//
1410// The `BEST_EFFORT` / `RELIABLE` shorthands still state AUTOMATIC. They
1411// mirror no upstream constant — their doc strings say so — so they are
1412// choices rather than deviations, and a caller picking one is asking for
1413// automatic liveliness on purpose. `QOS_PROFILE_PX4` stated it too, and was
1414// the counterexample to that reading (issue 2002): it is the profile FOR the
1415// XRCE backend, which honours no liveliness, so nobody picking it could have
1416// meant it. It carries the sentinel now.
1417//
1418// `rmw_qos_profile_unknown` HAS a counterpart now — `QOS_PROFILE_UNKNOWN` in the
1419// table below, added by this phase. The note that used to stand here declared it
1420// absent because three of our four policy enums had no `Unknown` variant to
1421// build it from; they have one now, which is what made the read-back able to
1422// say "I cannot report this policy" instead of echoing the request back as a
1423// grant.
1424
1425/// phase-515 R8 helper -- a QoS duration as the record's whole milliseconds:
1426/// saturating at `u32::MAX`, and a non-zero duration under 1 ms rounded UP to
1427/// 1, because `0` is "infinite" in the record.
1428const fn qos_duration_ms(d: core::time::Duration) -> u32 {
1429 let ms = d.as_millis();
1430 if ms == 0 && !d.is_zero() {
1431 1
1432 } else if ms > u32::MAX as u128 {
1433 u32::MAX
1434 } else {
1435 ms as u32
1436 }
1437}
1438
1439impl QoSProfile {
1440 /// Create new QoS settings with defaults (matches `QOS_PROFILE_DEFAULT`:
1441 /// Reliable, Volatile, KeepLast(10)).
1442 pub const fn new() -> Self {
1443 Self::QOS_PROFILE_DEFAULT
1444 }
1445
1446 // --- Static constructor methods (matching rclrs API) ---
1447
1448 /// Get the default QoS profile for ordinary topics
1449 pub const fn topics_default() -> Self {
1450 Self::QOS_PROFILE_DEFAULT
1451 }
1452
1453 /// The PX4 companion QoS profile ([`QOS_PROFILE_PX4`](Self::QOS_PROFILE_PX4))
1454 /// — use for `/fmu/out/*` subscriptions and `/fmu/in/*` publications against
1455 /// a `MicroXRCEAgent`.
1456 pub const fn px4() -> Self {
1457 Self::QOS_PROFILE_PX4
1458 }
1459
1460 /// Get the default QoS profile for sensor data topics
1461 pub const fn sensor_data_default() -> Self {
1462 Self::QOS_PROFILE_SENSOR_DATA
1463 }
1464
1465 /// Get the default QoS profile for services
1466 pub const fn services_default() -> Self {
1467 Self::QOS_PROFILE_SERVICES_DEFAULT
1468 }
1469
1470 /// The parameter services' profile — rclrs's
1471 /// `QoSProfile::parameter_services_default()`.
1472 ///
1473 /// `rmw_qos_profile_parameters`: KEEP_LAST(**1000**), RELIABLE, VOLATILE.
1474 /// The depth is the point — 1000 against the generic services profile's
1475 /// 10 — and it is what a tool setting many parameters at once depends on.
1476 ///
1477 /// **RENAMED in phase-417 G6, and the old spelling is GONE.** It was
1478 /// ~~`parameters_default`~~, an ours-only word for a name rclrs has; the
1479 /// ledger carried the pair as `rust:QoSProfile::parameters_default`
1480 /// (`rename` — ours is the one that should change) and
1481 /// `rust:QoSProfile::parameter_services_default` (`gap`). Adding the
1482 /// upstream name beside ours would have been a FOURTH spelling of one
1483 /// preset (`QOS_PROFILE_PARAMETERS` and `QosPresets::PARAMETERS` are the
1484 /// other two) and would have left the `rename` row open for good. No
1485 /// deprecated forwarder: a stale call should fail on the IDENTIFIER rather
1486 /// than on a deprecation, which is stage 6 step B's rule and what the
1487 /// sibling `parameter_events_default` — already at rclrs's spelling —
1488 /// makes the pair consistent with.
1489 pub const fn parameter_services_default() -> Self {
1490 Self::QOS_PROFILE_PARAMETERS
1491 }
1492
1493 /// Get the default QoS profile for parameter events
1494 pub const fn parameter_events_default() -> Self {
1495 Self::QOS_PROFILE_PARAMETER_EVENTS
1496 }
1497
1498 /// Get the system default QoS profile
1499 pub const fn system_default() -> Self {
1500 Self::QOS_PROFILE_SYSTEM_DEFAULT
1501 }
1502
1503 /// Get the default QoS profile for action status topics
1504 pub const fn action_status_default() -> Self {
1505 Self::QOS_PROFILE_ACTION_STATUS_DEFAULT
1506 }
1507
1508 /// Get the default QoS profile for clock topics
1509 pub const fn clock_default() -> Self {
1510 Self::QOS_PROFILE_CLOCK
1511 }
1512
1513 /// rclrs's `QoSProfile::best_available()` — `rmw_qos_profile_best_available`
1514 /// ([`QOS_PROFILE_BEST_AVAILABLE`](Self::QOS_PROFILE_BEST_AVAILABLE)),
1515 /// issue 1973.
1516 ///
1517 /// Reliability, durability, liveliness, deadline and liveliness lease are
1518 /// chosen when the entity is created, from the endpoints the backend has
1519 /// already discovered on the topic ([`QoSProfile::resolve_best_available`]
1520 /// states the rule); read the outcome back with the entity's
1521 /// `actual_qos()`. Upstream's caveat holds: the choice is not revisited
1522 /// when endpoints appear later.
1523 pub const fn best_available() -> Self {
1524 Self::QOS_PROFILE_BEST_AVAILABLE
1525 }
1526
1527 // --- Builder methods ---
1528
1529 /// Set history to keep last N messages
1530 pub const fn keep_last(mut self, depth: u32) -> Self {
1531 self.history = QoSHistoryPolicy::KeepLast;
1532 self.depth = depth;
1533 self
1534 }
1535
1536 /// Set history to keep all messages
1537 pub const fn keep_all(mut self) -> Self {
1538 self.history = QoSHistoryPolicy::KeepAll;
1539 self
1540 }
1541
1542 /// Set reliability to reliable
1543 pub const fn reliable(mut self) -> Self {
1544 self.reliability = QoSReliabilityPolicy::Reliable;
1545 self
1546 }
1547
1548 /// Set reliability to best-effort
1549 pub const fn best_effort(mut self) -> Self {
1550 self.reliability = QoSReliabilityPolicy::BestEffort;
1551 self
1552 }
1553
1554 /// Set durability to volatile
1555 pub const fn volatile(mut self) -> Self {
1556 self.durability = QoSDurabilityPolicy::Volatile;
1557 self
1558 }
1559
1560 /// Set durability to transient local
1561 pub const fn transient_local(mut self) -> Self {
1562 self.durability = QoSDurabilityPolicy::TransientLocal;
1563 self
1564 }
1565
1566 /// Set reliability policy explicitly
1567 pub const fn reliability(mut self, policy: QoSReliabilityPolicy) -> Self {
1568 self.reliability = policy;
1569 self
1570 }
1571
1572 /// Set durability policy explicitly
1573 pub const fn durability(mut self, policy: QoSDurabilityPolicy) -> Self {
1574 self.durability = policy;
1575 self
1576 }
1577
1578 /// Set history policy explicitly
1579 pub const fn history(mut self, policy: QoSHistoryPolicy) -> Self {
1580 self.history = policy;
1581 self
1582 }
1583
1584 /// Set history depth explicitly
1585 pub const fn depth(mut self, depth: u32) -> Self {
1586 self.depth = depth;
1587 self
1588 }
1589
1590 /// rclrs's `QoSProfile::deadline_duration(deadline)` (phase-515 R8).
1591 ///
1592 /// ADOPT-BOUNDED: stored as whole MILLISECONDS in [`Self::deadline_ms`]
1593 /// (the repr(C) QoS record the C and C++ surfaces share), saturating at
1594 /// `u32::MAX`; a non-zero duration under 1 ms rounds UP to 1 ms, because
1595 /// `0` here means "infinite" and must not be reached by accident.
1596 pub const fn deadline_duration(mut self, deadline: core::time::Duration) -> Self {
1597 self.deadline_ms = qos_duration_ms(deadline);
1598 self
1599 }
1600
1601 /// rclrs's `QoSProfile::lifespan_duration(lifespan)` (phase-515 R8),
1602 /// stored in [`Self::lifespan_ms`] as `deadline_duration` stores its value.
1603 pub const fn lifespan_duration(mut self, lifespan: core::time::Duration) -> Self {
1604 self.lifespan_ms = qos_duration_ms(lifespan);
1605 self
1606 }
1607
1608 /// rclrs's `QoSProfile::liveliness_lease_duration(lease_duration)`
1609 /// (phase-515 R8), stored in [`Self::liveliness_lease_ms`] as
1610 /// `deadline_duration` stores its value.
1611 pub const fn liveliness_lease_duration(mut self, lease_duration: core::time::Duration) -> Self {
1612 self.liveliness_lease_ms = qos_duration_ms(lease_duration);
1613 self
1614 }
1615
1616 /// Phase 282 (#145) — mark this publisher's samples "express": they
1617 /// bypass transport tx batching (sent immediately even when the batching
1618 /// knob is on). A transport hint for control-tier / latency-sensitive
1619 /// topics; ignored on subscriptions and by backends without batching.
1620 pub const fn tx_express(mut self, express: bool) -> Self {
1621 self.tx_express = express;
1622 self
1623 }
1624
1625 /// Get history depth (for backwards compatibility)
1626 pub const fn history_depth(&self) -> u8 {
1627 if self.depth > 255 {
1628 255
1629 } else {
1630 self.depth as u8
1631 }
1632 }
1633}
1634
1635/// Transport session configuration
1636#[derive(Debug, Clone)]
1637pub struct TransportConfig<'a> {
1638 /// Peer locator (e.g., "tcp/192.168.1.1:7447" or "serial//dev/ttyUSB0#baudrate=115200")
1639 pub locator: Option<&'a str>,
1640 /// Session mode: client, peer, or router
1641 pub mode: SessionMode,
1642 /// Additional transport properties (key-value pairs)
1643 ///
1644 /// These are passed through to the underlying transport backend.
1645 /// For zenoh-pico, recognized keys include:
1646 /// - `"multicast_scouting"` - Enable/disable multicast scouting (`"true"` or `"false"`)
1647 /// - `"scouting_timeout_ms"` - Scouting timeout in milliseconds
1648 /// - `"multicast_locator"` - Multicast group address
1649 /// - `"listen"` - Listen endpoint (e.g., `"tcp/0.0.0.0:0"`)
1650 /// - `"add_timestamp"` - Add timestamps to messages (`"true"` or `"false"`)
1651 pub properties: &'a [(&'a str, &'a str)],
1652 /// Node name for ROS 2 graph discovery liveliness token.
1653 ///
1654 /// Empty string (`""`) means no node-liveliness token is declared (preserves
1655 /// the pre-#104 behaviour). Non-empty causes the session to declare a
1656 /// `@ros2_lv/<domain>/<zid>/0/0/NN/%/<ns>/<node>` token on open.
1657 pub node_name: &'a str,
1658 /// Node namespace for the liveliness token (e.g., `""` or `"/ns1"`).
1659 ///
1660 /// Empty string is treated as root `"/"` by the keyexpr builder.
1661 pub namespace: &'a str,
1662 /// ROS 2 domain ID used in the liveliness token key expression.
1663 pub domain_id: u32,
1664}
1665
1666impl Default for TransportConfig<'_> {
1667 fn default() -> Self {
1668 Self {
1669 locator: None,
1670 mode: SessionMode::Client,
1671 properties: &[],
1672 node_name: "",
1673 namespace: "",
1674 domain_id: 0,
1675 }
1676 }
1677}
1678
1679/// Middleware-agnostic session configuration.
1680///
1681/// `RmwConfig` provides a uniform interface that any RMW backend can
1682/// interpret. Backends map the universal fields to their own connection
1683/// parameters and interpret `properties` for anything backend-specific.
1684///
1685/// # Examples
1686///
1687/// ```
1688/// use nros_rmw::{RmwConfig, SessionMode};
1689///
1690/// let config = RmwConfig {
1691/// locator: "tcp/192.168.1.1:7447",
1692/// mode: SessionMode::Client,
1693/// domain_id: 0,
1694/// node_name: "talker",
1695/// namespace: "",
1696/// properties: &[],
1697/// };
1698/// ```
1699#[derive(Debug, Clone, Copy)]
1700pub struct RmwConfig<'a> {
1701 /// Middleware-specific connection string.
1702 ///
1703 /// - zenoh: `"tcp/192.168.1.1:7447"` or `"udp/224.0.0.224:7447"`
1704 /// - XRCE-DDS: `"udp/192.168.1.1:2019"`
1705 pub locator: &'a str,
1706 /// Session mode (zenoh: client/peer; XRCE-DDS: always client)
1707 pub mode: SessionMode,
1708 /// ROS 2 domain ID (maps to DDS domain or zenoh key prefix)
1709 pub domain_id: u32,
1710 /// Node name (e.g., `"talker"`)
1711 pub node_name: &'a str,
1712 /// Node namespace (e.g., `""` or `"/ns1"`)
1713 pub namespace: &'a str,
1714 /// Backend-specific key/value properties.
1715 ///
1716 /// Uniform escape hatch for backend-specific tuning that doesn't fit
1717 /// the universal fields above. Each backend documents the keys it
1718 /// understands; unknown keys are ignored. Passing `&[]` is always
1719 /// valid.
1720 ///
1721 /// Examples:
1722 /// - zenoh: `"tls.root_ca"`, `"scouting.multicast.enabled"`
1723 /// - XRCE-DDS: `"agent_port"`, `"client_key"`
1724 pub properties: &'a [(&'a str, &'a str)],
1725}
1726
1727impl Default for RmwConfig<'_> {
1728 fn default() -> Self {
1729 Self {
1730 locator: "tcp/127.0.0.1:7447",
1731 mode: SessionMode::Client,
1732 domain_id: 0,
1733 node_name: "node",
1734 namespace: "",
1735 properties: &[],
1736 }
1737 }
1738}
1739
1740/// Locator transport protocol
1741#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1742pub enum LocatorProtocol {
1743 /// TCP transport (e.g., "tcp/127.0.0.1:7447")
1744 Tcp,
1745 /// UDP transport (e.g., "udp/192.168.1.50:2019" — common for XRCE-DDS)
1746 Udp,
1747 /// Serial/UART transport (e.g., "serial//dev/ttyUSB0#baudrate=115200")
1748 Serial,
1749 /// Unknown protocol
1750 Unknown,
1751}
1752
1753/// Parse the protocol from a locator string
1754pub fn locator_protocol(locator: &str) -> LocatorProtocol {
1755 if locator.starts_with("tcp/") {
1756 LocatorProtocol::Tcp
1757 } else if locator.starts_with("udp/") {
1758 LocatorProtocol::Udp
1759 } else if locator.starts_with("serial/") {
1760 LocatorProtocol::Serial
1761 } else {
1762 LocatorProtocol::Unknown
1763 }
1764}
1765
1766/// Validate a locator string format.
1767///
1768/// Returns `Ok(())` if the locator is well-formed, or an error message describing
1769/// the problem. This provides early feedback before zenoh-pico or XRCE-DDS rejects
1770/// a bad locator.
1771///
1772/// Supported formats:
1773/// - TCP: `tcp/<host>:<port>` (e.g., `tcp/127.0.0.1:7447`)
1774/// - UDP: `udp/<host>:<port>` (e.g., `udp/192.168.1.50:2019`)
1775/// - Serial: `serial/<device>#baudrate=<rate>` (e.g., `serial//dev/ttyUSB0#baudrate=115200`)
1776pub fn validate_locator(locator: &str) -> Result<(), &'static str> {
1777 match locator_protocol(locator) {
1778 LocatorProtocol::Tcp => {
1779 let rest = &locator[4..]; // skip "tcp/"
1780 if !rest.contains(':') {
1781 return Err("TCP locator must contain host:port (e.g., tcp/127.0.0.1:7447)");
1782 }
1783 Ok(())
1784 }
1785 LocatorProtocol::Udp => {
1786 let rest = &locator[4..]; // skip "udp/"
1787 if !rest.contains(':') {
1788 return Err("UDP locator must contain host:port (e.g., udp/192.168.1.50:2019)");
1789 }
1790 Ok(())
1791 }
1792 LocatorProtocol::Serial => {
1793 let rest = &locator[7..]; // skip "serial/"
1794 if rest.is_empty() {
1795 return Err(
1796 "serial locator must specify device (e.g., serial//dev/ttyUSB0#baudrate=115200)",
1797 );
1798 }
1799 if !rest.contains("#baudrate=") {
1800 return Err(
1801 "serial locator must include #baudrate=RATE (e.g., serial//dev/ttyUSB0#baudrate=115200)",
1802 );
1803 }
1804 // Validate baudrate is numeric
1805 if let Some(baud_str) = rest.split("#baudrate=").nth(1) {
1806 let baud_str = baud_str.split('#').next().unwrap_or(baud_str);
1807 if baud_str.parse::<u32>().is_err() {
1808 return Err("serial baudrate must be a number");
1809 }
1810 }
1811 Ok(())
1812 }
1813 LocatorProtocol::Unknown => {
1814 Err("unknown locator protocol (expected tcp/, udp/, or serial/)")
1815 }
1816 }
1817}
1818
1819/// phase-521 W3 (RFC-0113 D4) — how a session reports that data arrived.
1820///
1821/// The executor's park ends for exactly two reasons: the tier's next deadline,
1822/// or readiness. This is the readiness half, in one of two forms, and the
1823/// executor picks its wait from it rather than blocking inside the transport.
1824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1825pub enum ReadinessSource {
1826 /// Neither form: the executor can only learn of an arrival by blocking in
1827 /// [`Session::drive_io`]. RFC-0113 D5 refuses this once phase-521 W6
1828 /// lands.
1829 None,
1830 /// The backend calls the wake callback [`Session::set_wake_callback`]
1831 /// installed when data arrives (a transport thread, a listener, an RX
1832 /// interrupt).
1833 Notify,
1834 /// An OS handle that becomes readable when data arrives — a file
1835 /// descriptor on POSIX and NuttX. The executor multiplexes it with its own
1836 /// wake object and drives the session non-blockingly after the park. It
1837 /// must stay valid until the session closes.
1838 Waitable(i32),
1839}
1840
1841/// Session mode
1842#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1843pub enum SessionMode {
1844 /// Connect as client to a router
1845 #[default]
1846 Client,
1847 /// Connect as peer for peer-to-peer communication
1848 Peer,
1849}
1850
1851/// Transport session trait — the per-process anchor an RMW backend
1852/// gives to the executor.
1853///
1854/// # Threading
1855///
1856/// `&mut self` on every method means the executor serialises all
1857/// session calls onto a single thread. A backend may rely on this
1858/// — no internal locking is required for `create_*` / `close` /
1859/// `drive_io`. **Publisher / subscriber / service handles created
1860/// from the session, however, are typically used from worker
1861/// threads** and must carry their own synchronisation (see the
1862/// [`Publisher`] / [`Subscription`] trait docs).
1863///
1864/// # Calling pattern
1865///
1866/// 1. Open the session (backend-specific factory; not on this trait).
1867/// 2. `create_*` for every entity at startup. Creating entities mid-
1868/// flight after `drive_io` has run is allowed but not common.
1869/// 3. The executor calls `drive_io` periodically. Worker threads
1870/// publish / receive in parallel.
1871/// 4. `close` once at shutdown. Entities must be dropped first.
1872pub trait Session {
1873 /// RFC-0088 — the serialization format this backend speaks, as ROS 2's
1874 /// `rmw_get_serialization_format()` reports it ("One middleware can only
1875 /// have one encoding").
1876 ///
1877 /// Defaulted to CDR because every backend in tree except uORB speaks it,
1878 /// and a backend that speaks something else says so by overriding these
1879 /// two. They travel together: `SERIALIZATION_FORMAT` is the identity that
1880 /// crosses images (bridge config, tooling, the vtable slot) and
1881 /// `SERIALIZATION_FORMAT_ID` is the image-local discriminant used for the
1882 /// one-byte comparison a bridge makes at construction.
1883 const SERIALIZATION_FORMAT: &'static str =
1884 nros_serdes::format::SerializationFormatId::Cdr.as_str();
1885
1886 /// Image-local discriminant for [`Self::SERIALIZATION_FORMAT`]. Never
1887 /// persisted, never compared across images — see `nros_serdes::format`.
1888 const SERIALIZATION_FORMAT_ID: nros_serdes::format::SerializationFormatId =
1889 nros_serdes::format::SerializationFormatId::Cdr;
1890
1891 /// RFC-0088 — this session's serialization format, as ROS 2's
1892 /// `rmw_get_serialization_format()` reports it.
1893 ///
1894 /// **Per session, not per process.** ROS 2's function takes no handle
1895 /// because one process links one middleware; an `Executor::open_multi`
1896 /// image links two, so the answer must be asked of the session.
1897 ///
1898 /// **`None` means the backend has NOT SAID**, and that is not a synonym
1899 /// for `"cdr"`. The seam's contract is stated once, for the
1900 /// `get_serialization_format` slot in
1901 /// `packages/core/nros-rmw-abi/include/nros/rmw_vtable.h`: "NULL slot: the
1902 /// runtime answers NULL — it does NOT guess `cdr`." This method is the
1903 /// Rust face of that slot and carries the same absence. A `&'static str`
1904 /// could not: the only value it had to hand back was the literal `"cdr"`,
1905 /// indistinguishable at every call site from a backend that declared CDR.
1906 ///
1907 /// The default answers `Some(Self::SERIALIZATION_FORMAT)`, which is right
1908 /// for any backend whose format is a compile-time fact — it HAS said, in
1909 /// the const. A session that dispatches to a backend chosen at run time —
1910 /// the C-ABI adapter, whose vtable carries the answer — overrides this to
1911 /// ask the backend, and hands on the `None` a NULL slot gives it.
1912 fn serialization_format(&self) -> Option<&'static str> {
1913 Some(Self::SERIALIZATION_FORMAT)
1914 }
1915
1916 /// The backend's name, as ROS 2's `rmw_get_implementation_identifier()`
1917 /// reports it — the nano-ros name (`"zenoh"`, `"cyclonedds"`, `"xrce"`,
1918 /// `"uorb"`), never a stock `rmw_*_cpp` name, for the reason the
1919 /// `get_implementation_identifier` slot in
1920 /// `packages/core/nros-rmw-abi/include/nros/rmw_vtable.h` states.
1921 ///
1922 /// `None` = the backend has not said. A backend whose name is a
1923 /// compile-time fact states it here; it must be the same string it stamps
1924 /// into a gid, since gids are comparable only under one identifier.
1925 const IMPLEMENTATION_IDENTIFIER: Option<&'static str> = None;
1926
1927 /// This session's backend name — the per-session form of
1928 /// `rmw_get_implementation_identifier()`, for the same reason
1929 /// [`serialization_format`](Self::serialization_format) is per session: an
1930 /// `Executor::open_multi` image links two backends.
1931 ///
1932 /// The default answers [`Self::IMPLEMENTATION_IDENTIFIER`]. A session that
1933 /// dispatches to a backend chosen at run time (the C-ABI adapter) overrides
1934 /// this to ask the backend's vtable.
1935 fn implementation_identifier(&self) -> Option<&'static str> {
1936 Self::IMPLEMENTATION_IDENTIFIER
1937 }
1938
1939 /// Error type for this session
1940 type Error;
1941 /// Publisher handle type
1942 type PublisherHandle;
1943 /// Subscription handle type
1944 type SubscriptionHandle;
1945 /// Service server handle type
1946 type ServiceHandle;
1947 /// Service client handle type
1948 type ClientHandle;
1949
1950 /// Create a publisher bound to this session.
1951 ///
1952 /// May allocate transport resources (zenoh declarations, DDS
1953 /// writers). Returns a handle that outlives the call but not the
1954 /// session — drop the handle before `close()`.
1955 fn create_publisher(
1956 &mut self,
1957 topic: &TopicInfo,
1958 qos: QoSProfile,
1959 ) -> Result<Self::PublisherHandle, Self::Error>;
1960
1961 /// Create a subscriber bound to this session.
1962 ///
1963 /// Subscribers may start receiving immediately after creation if
1964 /// the transport supports late-joining publishers. Late messages
1965 /// are buffered up to the QoS depth.
1966 fn create_subscription(
1967 &mut self,
1968 topic: &TopicInfo,
1969 qos: QoSProfile,
1970 ) -> Result<Self::SubscriptionHandle, Self::Error>;
1971
1972 /// Create a service server bound to this session. Replies are
1973 /// matched to requests by the sequence number returned from
1974 /// [`ServiceTrait::take_request`].
1975 ///
1976 /// `qos` is applied to both the request and reply endpoints (a
1977 /// service is two DDS topics; rmw uses one profile for both). The
1978 /// default is [`QoSProfile::services_default`]
1979 /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
1980 ///
1981 /// The server receives through the backend's own inbox. A builtin family
1982 /// that sizes its own ring calls [`create_service_with_inbox`] instead.
1983 ///
1984 /// [`create_service_with_inbox`]: Session::create_service_with_inbox
1985 fn create_service(
1986 &mut self,
1987 service: &ServiceInfo,
1988 qos: QoSProfile,
1989 ) -> Result<Self::ServiceHandle, Self::Error>;
1990
1991 /// phase-461 W2 -- does this backend accept a caller-owned request ring
1992 /// ([`ServiceInboxSpec::Caller`])?
1993 ///
1994 /// A `const`, so the family that would bring one picks at COMPILE time
1995 /// and a `Caller` spec never degrades silently into the backend's own
1996 /// geometry (RFC-0052: a knob that is dropped is a knob that lies). The
1997 /// caller reads this and passes [`ServiceInboxSpec::Backend`] where it is
1998 /// `false`, which is how a family refuses at BUILD time rather than
1999 /// discovering at boot that its ring went nowhere -- `nros-node`'s
2000 /// `PARAM_INBOX` is the worked example.
2001 ///
2002 /// `false` is the right default and every backend in tree states it
2003 /// explicitly with its own reason, because "this backend has no caller
2004 /// ring" and "nobody thought about it" are different facts.
2005 ///
2006 /// [`create_service_with_inbox`]: Session::create_service_with_inbox
2007 const SUPPORTS_CALLER_INBOX: bool = false;
2008
2009 /// Create a service server that receives through `inbox`.
2010 ///
2011 /// phase-461 W2 / issue 1352 -- the ROS parameter services are six
2012 /// queryables per node and the REP-2002 lifecycle services five, and on a
2013 /// board they are most of the image's queryables. Their requests are
2014 /// bounded by the contract's declared parameters and their clients are
2015 /// sequential, so they want one small slot where a user service wants four
2016 /// large ones. The family that knows that bound brings the ring; the
2017 /// backend keeps the per-queryable header.
2018 ///
2019 /// The default body is [`Self::SUPPORTS_CALLER_INBOX`] `= false` spelled
2020 /// out: it serves [`ServiceInboxSpec::Backend`] and nothing else. It can
2021 /// only be reached with a `Caller` spec by a caller that ignored the
2022 /// const, so the `debug_assert` names that -- the REFUSAL itself is the
2023 /// caller's, at compile time, because a runtime error here would be a
2024 /// boot failure where a build failure was available (`nros-node`'s
2025 /// `PARAM_INBOX` is the worked example).
2026 ///
2027 /// A backend that overrides this must also state the const `true`.
2028 fn create_service_with_inbox(
2029 &mut self,
2030 service: &ServiceInfo,
2031 qos: QoSProfile,
2032 inbox: ServiceInboxSpec,
2033 ) -> Result<Self::ServiceHandle, Self::Error> {
2034 debug_assert!(
2035 inbox.caller().is_none(),
2036 "a caller-owned service inbox reached a backend whose \
2037 SUPPORTS_CALLER_INBOX is false; the caller must read the const"
2038 );
2039 self.create_service(service, qos)
2040 }
2041
2042 /// Create a service client bound to this session.
2043 ///
2044 /// `qos` is applied to both the request and reply endpoints (a
2045 /// service is two DDS topics; rmw uses one profile for both). The
2046 /// default is [`QoSProfile::services_default`]
2047 /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
2048 fn create_client(
2049 &mut self,
2050 service: &ServiceInfo,
2051 qos: QoSProfile,
2052 ) -> Result<Self::ClientHandle, Self::Error>;
2053
2054 /// Close the session, releasing transport resources. All entity
2055 /// handles created from this session must already be dropped.
2056 fn close(&mut self) -> Result<(), Self::Error>;
2057
2058 /// Drive transport I/O (poll network, dispatch callbacks).
2059 ///
2060 /// Both zenoh-pico and XRCE-DDS are pull-based: they require the
2061 /// application to periodically call this method to read from the
2062 /// network socket and dispatch incoming messages to subscriber
2063 /// buffers.
2064 ///
2065 /// `timeout_ms` is the maximum time to wait for data (0 = non-blocking;
2066 /// negative values mean "block indefinitely" — see Phase 84.D7 for the
2067 /// planned migration to `core::time::Duration`).
2068 ///
2069 /// **Required**. There is no default body — both shipped backends
2070 /// (zenoh and XRCE) must drive I/O, and a silent no-op default was a
2071 /// trap for third-party implementers. If your backend genuinely
2072 /// receives data via OS callbacks (push-based) and has nothing to do
2073 /// here, return `Ok(())` explicitly.
2074 fn drive_io(&mut self, timeout_ms: i32) -> Result<(), Self::Error>;
2075
2076 /// Phase 109 — report which QoS policies the active backend
2077 /// honours. The runtime validates requested QoS against this mask
2078 /// at entity-create time and returns
2079 /// [`TransportError::IncompatibleQos`] if the requested profile
2080 /// includes a policy the backend can't enforce. **No silent
2081 /// downgrade.**
2082 ///
2083 /// # The mask is DERIVED, not authored — phase-428 W9
2084 ///
2085 /// Every bit an implementation sets must be backed by a
2086 /// `nros-qos-honours:` claim sited on the code that honours the policy,
2087 /// and `check-qos-mask-derivation` re-measures those claims on every
2088 /// push. Deleting the code deletes the claim, which is the property this
2089 /// item exists to buy: before W9 this was a hand-written constant, so a
2090 /// backend could advertise a policy it had never implemented and nothing
2091 /// noticed. Three did — `LIVELINESS_MANUAL_BY_NODE` was advertised by
2092 /// every backend in the tree and implemented by none.
2093 ///
2094 /// "Honours" means the backend reads the field and either APPLIES it or
2095 /// REFUSES a value it cannot serve. Passing the field on to someone else
2096 /// (a discovery keyexpr, a lowered C struct) is not honouring it, and the
2097 /// gate does not count it: see `nros-qos-discovery-only`.
2098 ///
2099 /// # The default is EMPTY, deliberately
2100 ///
2101 /// It returned [`QoSPolicyMask::CORE`] until phase-428 W9 — four policies
2102 /// granted to any implementation that had written no code at all, which
2103 /// is precisely the lie above with the trait as its author. An
2104 /// implementation now states what it honours or is told it honours
2105 /// nothing; the failure is loud (`IncompatibleQos` at create) rather than
2106 /// a silent downgrade at runtime, which is the direction this trait has
2107 /// chosen everywhere else.
2108 ///
2109 /// # A C backend answers through the vtable — issue 1329
2110 ///
2111 /// `CffiSession` is the one implementation that cannot answer from
2112 /// compile-time knowledge: which backend registered is a run-time fact. It
2113 /// returned the UNION of what any C backend it routes to honours, which
2114 /// over-claims for each of them; it now asks the backend through
2115 /// `nros_rmw_vtable_t::supported_qos_policies` and returns what it said. A
2116 /// NULL slot there carries the same meaning as this default — the backend
2117 /// has not said, so it honours nothing — deliberately, so the ABI and the
2118 /// trait give one answer to one question.
2119 fn supported_qos_policies(&self) -> QoSPolicyMask {
2120 QoSPolicyMask::NONE
2121 }
2122
2123 /// Phase 110.0 — backend's next internal-event deadline in
2124 /// milliseconds from now (lease keepalive, heartbeat, reader
2125 /// ACK-NACK timeout, etc.).
2126 ///
2127 /// The executor caps its `drive_io` timeout against
2128 /// `min(user_timeout, timer_deadline, this)` so quiet links don't
2129 /// wake early, see no user-visible work, and round-trip back into
2130 /// `drive_io`. Returns `None` when the backend has no internal
2131 /// deadlines or chooses not to expose them.
2132 ///
2133 /// Default `None` keeps existing backends working unchanged; opt-in
2134 /// per backend.
2135 fn next_deadline_ms(&self) -> Option<u32> {
2136 None
2137 }
2138
2139 /// Phase 124.B.1 — install (or clear, when `cb.is_none()`) the
2140 /// executor wake callback. The runtime calls this once per
2141 /// session after `open` with `cb` pointing at a runtime-owned
2142 /// function and `ctx` pointing at the executor's wake state.
2143 /// The backend stores `(cb, ctx)` in its per-session state and
2144 /// calls `cb(ctx)` whenever its transport notification path
2145 /// fires (datagram arrival, condvar wake, etc.) — the runtime
2146 /// cb does flag-write + condvar-signal atomically, so a
2147 /// `spin_once` blocked on the wake condvar resumes immediately
2148 /// instead of waiting for the next poll iteration.
2149 ///
2150 /// # Safety
2151 ///
2152 /// When `cb` is `Some`, `ctx` must remain valid until the callback is
2153 /// cleared or the session is closed. The backend may invoke `cb(ctx)` from
2154 /// its transport notification path.
2155 ///
2156 /// Default body: ignore the call. Poll-only backends (XRCE,
2157 /// bare-metal) leave the default in place; the executor still
2158 /// drains them on its deadline-bound cv-wait boundary.
2159 unsafe fn set_wake_callback(
2160 &mut self,
2161 cb: Option<unsafe extern "C" fn(ctx: *mut core::ffi::c_void)>,
2162 ctx: *mut core::ffi::c_void,
2163 ) {
2164 let _ = (cb, ctx);
2165 }
2166
2167 /// Phase 130.4 — does this backend actually honour
2168 /// [`set_wake_callback`]?
2169 ///
2170 /// `true` means the backend installs the callback and will
2171 /// fire it from its async notify path (worker thread, ISR,
2172 /// signalfd, …). `false` (the default) means
2173 /// `set_wake_callback` was a no-op — the executor must drive
2174 /// I/O for the caller's full timeout because no async wake
2175 /// will pre-empt it.
2176 ///
2177 /// The executor uses this to choose between the wake-primitive
2178 /// wait (`NodeWake::wait_ms` / `std::Condvar::wait_timeout_while`)
2179 /// and a direct `drive_io(timeout_ms)`. Poll-only backends
2180 /// (XRCE-DDS-Client, bare-metal smoltcp) return `false`;
2181 /// event-driven backends (zenoh-pico with an RX task that
2182 /// invokes the callback on packet arrival) return `true`.
2183 ///
2184 /// [`set_wake_callback`]: Self::set_wake_callback
2185 fn supports_wake_callback(&self) -> bool {
2186 false
2187 }
2188
2189 /// phase-521 W3 (RFC-0113 D4) — which form of readiness this session has.
2190 ///
2191 /// Default: [`ReadinessSource::Notify`] when the backend honours
2192 /// [`set_wake_callback`](Self::set_wake_callback), else
2193 /// [`ReadinessSource::None`] — what the executor inferred before this
2194 /// method existed, so a backend that already delivers a wake callback
2195 /// needs no change. A backend with a waitable OS handle overrides it.
2196 fn readiness_source(&self) -> ReadinessSource {
2197 if self.supports_wake_callback() {
2198 ReadinessSource::Notify
2199 } else {
2200 ReadinessSource::None
2201 }
2202 }
2203
2204 /// Phase 124.F.1 — session-level connectivity probe.
2205 ///
2206 /// Sends a wire-level round-trip probe and waits up to
2207 /// `timeout_ms`. `Ok(())` on reply, `Err(TransportError::Timeout)`
2208 /// on no reply, `Err(TransportError::Unsupported)` when the
2209 /// backend can't probe (DDS without participant introspection).
2210 /// Lesson from micro-ROS's `rmw_uros_ping_agent`.
2211 ///
2212 /// Default body: `Err(Unsupported)`. Backends with a native
2213 /// ping API (XRCE: `uxr_ping_agent_session`; zenoh, which has no
2214 /// round-trip ping, sends one `zpico_send_keep_alive` frame) opt in by
2215 /// overriding.
2216 fn ping_session(&mut self, timeout_ms: i32) -> Result<(), Self::Error>
2217 where
2218 Self::Error: From<TransportError>,
2219 {
2220 let _ = timeout_ms;
2221 Err(TransportError::Unsupported.into())
2222 }
2223
2224 /// phase-381 W3 — enumerate the nodes this session can see.
2225 ///
2226 /// A VISITOR, not a returned collection, because upstream's
2227 /// `rcutils_string_array_t` allocates two levels deep and there is no
2228 /// allocator at this seam. A caller-provided buffer is worse than it looks:
2229 /// the graph has no bound the CALLER can know. So the backend streams from
2230 /// state it already holds, peak extra RAM is one entry, and a caller with a
2231 /// bound stops early by returning `false`.
2232 ///
2233 /// `namespace` and `name` are ROS names. `enclave` is `None` where the
2234 /// backend does not track one — which is what lets this one method answer
2235 /// both `rmw_get_node_names` and `rmw_get_node_names_with_enclaves`.
2236 ///
2237 /// Every string is BORROWED for the duration of the call.
2238 ///
2239 /// **Must not block on the wire, and takes no timeout.** It reports what has
2240 /// ALREADY arrived, so the first call after startup legitimately returns a
2241 /// partial graph — a backend feeds its view from `drive_io`. Letting this
2242 /// block was considered and rejected: it would stall the executor's only
2243 /// thread inside an introspection call, on a runtime whose premise is that
2244 /// there is no other thread to do the work.
2245 ///
2246 /// Default body: `Err(Unsupported)` — a backend with no graph (XRCE) says
2247 /// so, and the runtime can tell that from an empty graph.
2248 fn get_node_names(
2249 &mut self,
2250 visit: &mut dyn FnMut(&str, &str, Option<&str>) -> bool,
2251 ) -> Result<(), Self::Error>
2252 where
2253 Self::Error: From<TransportError>,
2254 {
2255 let _ = visit;
2256 Err(TransportError::Unsupported.into())
2257 }
2258
2259 /// phase-381 W3 — how many publishers this session can see on `topic_name`.
2260 ///
2261 /// `topic_name` is a ROS name (`/chatter`); the backend mangles as needed.
2262 /// Same warm-up caveat as [`Self::get_node_names`]: a count reflects what
2263 /// has already been discovered, so it can be low right after startup and is
2264 /// never a proof of absence.
2265 ///
2266 /// Default body: `Err(Unsupported)` — distinct from `Ok(0)`, which claims
2267 /// there are none.
2268 fn count_publishers(&mut self, topic_name: &str) -> Result<usize, Self::Error>
2269 where
2270 Self::Error: From<TransportError>,
2271 {
2272 let _ = topic_name;
2273 Err(TransportError::Unsupported.into())
2274 }
2275
2276 /// phase-381 W3 — how many subscribers this session can see on `topic_name`.
2277 /// See [`Self::count_publishers`] for the caveats.
2278 fn count_subscribers(&mut self, topic_name: &str) -> Result<usize, Self::Error>
2279 where
2280 Self::Error: From<TransportError>,
2281 {
2282 let _ = topic_name;
2283 Err(TransportError::Unsupported.into())
2284 }
2285
2286 /// Issue 1978 — how many service CLIENTS this session can see on
2287 /// `service_name`, upstream (Jazzy) `rmw_count_clients`. The service twin
2288 /// of [`Self::count_publishers`], with the same caveats: `service_name` is
2289 /// a ROS name the backend mangles, and a count reflects what has already
2290 /// been discovered.
2291 ///
2292 /// Default body: `Err(Unsupported)` — distinct from `Ok(0)`.
2293 fn count_clients(&mut self, service_name: &str) -> Result<usize, Self::Error>
2294 where
2295 Self::Error: From<TransportError>,
2296 {
2297 let _ = service_name;
2298 Err(TransportError::Unsupported.into())
2299 }
2300
2301 /// Issue 1978 — how many service SERVERS this session can see on
2302 /// `service_name`, upstream (Jazzy) `rmw_count_services`. See
2303 /// [`Self::count_clients`].
2304 fn count_services(&mut self, service_name: &str) -> Result<usize, Self::Error>
2305 where
2306 Self::Error: From<TransportError>,
2307 {
2308 let _ = service_name;
2309 Err(TransportError::Unsupported.into())
2310 }
2311
2312 /// Issue 1978 — upstream (Jazzy) `rmw_event_type_is_supported`: whether
2313 /// ANY entity of this backend can carry status event `kind`.
2314 ///
2315 /// STATIC, like [`Self::SERIALIZATION_FORMAT`]: upstream's signature
2316 /// takes no entity and no session, so the C-ABI adapter answers it from
2317 /// the backend TYPE. Per-entity registration
2318 /// ([`Publisher::supports_event`], [`Subscription::supports_event`]) stays
2319 /// the authority for one entity; a backend's per-entity answer must be a
2320 /// subset of this one, and the way to keep it so is to derive both from
2321 /// one helper in the backend (zenoh's `shim::events`).
2322 ///
2323 /// Default: `false` — the backend states no event support, the answer a
2324 /// NULL vtable slot carries.
2325 fn event_type_is_supported(kind: crate::event::EventKind) -> bool
2326 where
2327 Self: Sized,
2328 {
2329 let _ = kind;
2330 false
2331 }
2332
2333 /// phase-381 W3 — every topic, with the types published or subscribed on it.
2334 ///
2335 /// A visitor for the same reason as [`Self::get_node_names`], and one call
2336 /// per distinct NAME: the contract hands over a name and the types on it,
2337 /// so a topic carrying two types is one visit with two entries, not two
2338 /// visits.
2339 ///
2340 /// `types_count` may legitimately be 0 on a partially discovered graph —
2341 /// reporting the name without a type beats dropping it.
2342 ///
2343 /// Default body: `Err(Unsupported)`.
2344 fn get_topic_names_and_types(
2345 &mut self,
2346 visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2347 ) -> Result<(), Self::Error>
2348 where
2349 Self::Error: From<TransportError>,
2350 {
2351 let _ = visit;
2352 Err(TransportError::Unsupported.into())
2353 }
2354
2355 /// phase-381 W3 — every service, with its types. As
2356 /// [`Self::get_topic_names_and_types`], over servers and clients.
2357 fn get_service_names_and_types(
2358 &mut self,
2359 visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2360 ) -> Result<(), Self::Error>
2361 where
2362 Self::Error: From<TransportError>,
2363 {
2364 let _ = visit;
2365 Err(TransportError::Unsupported.into())
2366 }
2367
2368 /// phase-381 W3 — what ONE named node publishes / subscribes / serves /
2369 /// calls.
2370 ///
2371 /// `node_name` and `node_namespace` are ROS names; a node the graph has not
2372 /// discovered yields no visits, which is NOT an error — see
2373 /// [`Self::get_node_names`] for why an empty answer is "not seen yet".
2374 ///
2375 /// `kind` selects which of the four upstream `*_by_node` questions this
2376 /// answers. One method rather than four because the four differ ONLY by
2377 /// which entity kind they keep, and four trait methods would be four copies
2378 /// of one filter.
2379 ///
2380 /// The trait keeps the ABI's `subscriber` vocabulary because
2381 /// `rmw_get_subscriber_names_and_types_by_node` is what upstream rmw calls
2382 /// it and RFC-0054 makes the C headers the SSoT. The USER-facing spelling
2383 /// is per language and settled at that layer: rcl says `subscriber`, rclcpp
2384 /// and rclrs say `subscription`.
2385 ///
2386 /// Default body: `Err(Unsupported)`.
2387 fn get_names_and_types_by_node(
2388 &mut self,
2389 kind: GraphEntityKind,
2390 node_name: &str,
2391 node_namespace: &str,
2392 visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2393 ) -> Result<(), Self::Error>
2394 where
2395 Self::Error: From<TransportError>,
2396 {
2397 let _ = (kind, node_name, node_namespace, visit);
2398 Err(TransportError::Unsupported.into())
2399 }
2400
2401 /// phase-381 W3 — the endpoints on one topic, with the QoS each GRANTED.
2402 ///
2403 /// `publishers` selects `rmw_get_publishers_info_by_topic` (`true`) or
2404 /// `rmw_get_subscriptions_info_by_topic` (`false`).
2405 ///
2406 /// The granted profile is the whole reason a consumer asks: "why is nothing
2407 /// arriving" is usually a QoS incompatibility, and the REQUESTED profile
2408 /// cannot answer it. A backend that cannot read a remote's granted QoS says
2409 /// so per field rather than echoing the request back — see
2410 /// `rmw_topic_endpoint_info_t`.
2411 ///
2412 /// Default body: `Err(Unsupported)`.
2413 fn get_endpoint_info_by_topic(
2414 &mut self,
2415 publishers: bool,
2416 topic_name: &str,
2417 visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool,
2418 ) -> Result<(), Self::Error>
2419 where
2420 Self::Error: From<TransportError>,
2421 {
2422 let _ = (publishers, topic_name, visit);
2423 Err(TransportError::Unsupported.into())
2424 }
2425
2426 /// phase-467 Row 8 — install (or clear, when `callback` is `None`) the
2427 /// callback the backend fires when the ROS GRAPH changes.
2428 ///
2429 /// The Rust twin of `nros_rmw_vtable_t::node_get_graph_guard_condition`, which
2430 /// is upstream's `rmw_node_get_graph_guard_condition` under the
2431 /// `set_wake_callback` shape: upstream RETURNS a guard condition the caller
2432 /// adds to a wait set, we have no wait set, and a guard condition here is
2433 /// an executor concept the backend never hands out. So the edge is PUSHED,
2434 /// not polled — and the ABI slot has said so since phase-376 W4, while
2435 /// this is the trait twin it never had.
2436 ///
2437 /// The callback is an EDGE and carries no payload. `count` is how many
2438 /// changes the backend coalesced, never a description of WHAT changed:
2439 /// delivering that would mean buffering the graph, which is the cache a
2440 /// small target cannot afford.
2441 ///
2442 /// **A backend must not fire it for a no-op.** The contract is "the graph
2443 /// this session can SEE differs from what it saw", which both real
2444 /// backends already computed and threw away — zenoh in
2445 /// `zpico_graph_set_apply`'s return value, Cyclone in
2446 /// `on_data_available`'s first parameter.
2447 ///
2448 /// # Safety
2449 ///
2450 /// When `callback` is `Some`, `user_data` must remain valid until the
2451 /// callback is cleared or the session is closed; the backend may invoke it
2452 /// from a transport or worker thread. **The installer owes the clear** —
2453 /// see `Executor::clear_graph_change_signal`, which is issue 1385's lesson
2454 /// applied before the hazard rather than after it.
2455 ///
2456 /// Default body: `Err(Unsupported)` — a backend with no graph (XRCE, uORB)
2457 /// says "cannot tell you" rather than accepting a callback it will never
2458 /// fire. That answer is also the capability PROBE the runtime uses: a
2459 /// `None` clear on a graphless backend returns `Unsupported`, so nothing
2460 /// else has to be asked and no handle is consumed before the answer.
2461 unsafe fn set_graph_change_callback(
2462 &mut self,
2463 callback: Option<unsafe extern "C" fn(user_data: *const core::ffi::c_void, count: usize)>,
2464 user_data: *const core::ffi::c_void,
2465 ) -> Result<(), Self::Error>
2466 where
2467 Self::Error: From<TransportError>,
2468 {
2469 let _ = (callback, user_data);
2470 Err(TransportError::Unsupported.into())
2471 }
2472}
2473
2474/// Which entity kind a `*_by_node` graph query keeps — phase-381 W3.
2475///
2476/// Named for upstream rmw's four `*_by_node` slots. `Subscriber` carries rmw's
2477/// word, not rclcpp's `subscription`; the user-facing spelling is chosen per
2478/// language one layer up.
2479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2480pub enum GraphEntityKind {
2481 Publisher,
2482 Subscriber,
2483 Service,
2484 Client,
2485}
2486
2487/// One discovered endpoint on a topic — phase-381 W3, the Rust view of
2488/// `rmw_topic_endpoint_info_t`.
2489///
2490/// Every string BORROWS from the backend's own state for the duration of the
2491/// visit. A caller that needs one afterwards copies it; that is what lets the
2492/// graph stream without an allocator.
2493#[derive(Debug, Clone)]
2494pub struct GraphEndpointInfo<'a> {
2495 /// Node that owns the endpoint.
2496 pub node_name: &'a str,
2497 /// That node's namespace.
2498 pub node_namespace: &'a str,
2499 /// Fully-qualified type on the wire, e.g. `"std_msgs/msg/Int32"`.
2500 pub topic_type: &'a str,
2501 /// `true` for a publisher, `false` for a subscription.
2502 pub is_publisher: bool,
2503 /// The endpoint's 24-byte identity; all-zero when the backend has none.
2504 pub endpoint_gid: [u8; 24],
2505 /// The profile the endpoint is known by — what the backend MATCHED on.
2506 ///
2507 /// [`QoSProfile::QOS_PROFILE_UNKNOWN`] (every policy `Unknown`, windows 0)
2508 /// when the backend cannot read it, which is the C seam's contract on
2509 /// `rmw_topic_endpoint_info_t::qos_profile` too.
2510 pub qos: QoSProfile,
2511}
2512
2513// The `qos` field arrived with issue 1973, because resolving a best-available
2514// profile needs every peer's profile and this is the seam that walks them.
2515//
2516// It was left out before on the reading that zenoh's liveliness token carries
2517// the DECLARING side's profile, not a negotiated grant. Measured against what
2518// the token actually holds, that distinction does not exist on zenoh: there is
2519// no negotiation, our token is written from `shim/qos.rs`'s GRANT
2520// (`keyexpr.rs`), and a stock `rmw_zenoh_cpp` peer writes its own resolved
2521// profile and reports the same string from `rmw_get_publishers_info_by_topic`.
2522// So the token is what the endpoint does, which is what the C seam's
2523// `qos_profile` promises ("reports what it MATCHED on"). Cyclone fills it from
2524// the discovered endpoint's DDS QoS (`graph_query.cpp`). A backend that cannot
2525// read one writes `QOS_PROFILE_UNKNOWN`, never the local request.
2526
2527/// Bitmask of QoS policies a backend can honour. See
2528/// [`Session::supported_qos_policies`].
2529///
2530/// One bit per value `QoSProfile::required_policies` can request, and that
2531/// function is the definition of what each bit MEANS — `check-qos-mask-
2532/// derivation` reads the bit->field mapping out of its body rather than
2533/// restating it, so a policy that gains a bit here and no arm there is a gate
2534/// failure instead of a bit nothing can ever ask for.
2535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2536pub struct QoSPolicyMask(pub u32);
2537
2538impl QoSPolicyMask {
2539 pub const RELIABILITY: Self = Self(1 << 0);
2540 pub const DURABILITY_VOLATILE: Self = Self(1 << 1);
2541 pub const DURABILITY_TRANSIENT_LOCAL: Self = Self(1 << 2);
2542 pub const HISTORY: Self = Self(1 << 3);
2543 pub const DEPTH: Self = Self(1 << 4);
2544 pub const DEADLINE: Self = Self(1 << 5);
2545 pub const LIFESPAN: Self = Self(1 << 6);
2546 pub const LIVELINESS_AUTOMATIC: Self = Self(1 << 7);
2547 pub const LIVELINESS_MANUAL_BY_TOPIC: Self = Self(1 << 8);
2548 pub const LIVELINESS_MANUAL_BY_NODE: Self = Self(1 << 9);
2549 pub const LIVELINESS_LEASE: Self = Self(1 << 10);
2550 pub const AVOID_ROS_NAMESPACE_CONVENTIONS: Self = Self(1 << 11);
2551
2552 /// Honours nothing. The default answer for an implementation that has not
2553 /// said otherwise — phase-428 W9, see [`Session::supported_qos_policies`].
2554 pub const NONE: Self = Self(0);
2555
2556 /// The four policies every `QoSProfile` states a value for.
2557 ///
2558 /// NOT "policies every nano-ros backend implements", which is what this
2559 /// said until phase-428 W9 measured it: the zenoh shim honoured exactly
2560 /// none of the four, serialising all four into a discovery keyexpr and
2561 /// enforcing a build-time ring depth. The name describes the SHAPE of a
2562 /// profile — a caller who states nothing else still states these — and a
2563 /// backend still has to earn each bit.
2564 pub const CORE: Self =
2565 Self(Self::RELIABILITY.0 | Self::DURABILITY_VOLATILE.0 | Self::HISTORY.0 | Self::DEPTH.0);
2566
2567 /// Every policy bit paired with the name it PRINTS — the vocabulary the
2568 /// three languages name a refused policy in (phase-417 G7, ledger row
2569 /// `cpp:qos_policy_kind_to_cstr`).
2570 ///
2571 /// The name of a bit is the CONSTANT'S OWN IDENTIFIER, never an
2572 /// independently chosen word. That is what keeps this from becoming the
2573 /// parallel table this campaign has paid for twice — the RMW parity map's
2574 /// 28 stale slots and the layout gate's three authored type names — and it
2575 /// is what `check-qos-mask-derivation`'s R8 measures: the entries here must
2576 /// be exactly the `pub const … = Self(1 << n)` bits above, each paired with
2577 /// its own spelling. A bit added above and not here is a gate failure, not
2578 /// a policy that silently prints as nothing.
2579 ///
2580 /// Uppercase because that is upstream's convention for the same strings:
2581 /// `rclcpp::qos_policy_kind_to_cstr` returns `"DURABILITY"`, `"DEADLINE"`,
2582 /// `"LIVELINESS"` and the rest.
2583 ///
2584 /// `NONE` and `CORE` are deliberately absent: they are not policies, they
2585 /// are the empty mask and a named union, and neither can be the answer to
2586 /// "which policy was refused".
2587 ///
2588 /// The name is stored as a [`core::ffi::CStr`] and not as a `&str`,
2589 /// because the C entry point (`rclc_qos_policy_kind_to_cstr`) needs the
2590 /// trailing NUL and a `&str` does not carry one. Storing the `&str` form
2591 /// and NUL-terminating it in `nros-c` was the first shape here, and it put
2592 /// the twelve identifiers in a second file — which is the drift this row
2593 /// exists to avoid. One table, two readings: `policy_name` slices the NUL
2594 /// off for Rust, the C entry point hands the pointer straight over.
2595 pub const NAMED: &'static [(Self, &'static core::ffi::CStr)] = &[
2596 (Self::RELIABILITY, c"RELIABILITY"),
2597 (Self::DURABILITY_VOLATILE, c"DURABILITY_VOLATILE"),
2598 (
2599 Self::DURABILITY_TRANSIENT_LOCAL,
2600 c"DURABILITY_TRANSIENT_LOCAL",
2601 ),
2602 (Self::HISTORY, c"HISTORY"),
2603 (Self::DEPTH, c"DEPTH"),
2604 (Self::DEADLINE, c"DEADLINE"),
2605 (Self::LIFESPAN, c"LIFESPAN"),
2606 (Self::LIVELINESS_AUTOMATIC, c"LIVELINESS_AUTOMATIC"),
2607 (
2608 Self::LIVELINESS_MANUAL_BY_TOPIC,
2609 c"LIVELINESS_MANUAL_BY_TOPIC",
2610 ),
2611 (
2612 Self::LIVELINESS_MANUAL_BY_NODE,
2613 c"LIVELINESS_MANUAL_BY_NODE",
2614 ),
2615 (Self::LIVELINESS_LEASE, c"LIVELINESS_LEASE"),
2616 (
2617 Self::AVOID_ROS_NAMESPACE_CONVENTIONS,
2618 c"AVOID_ROS_NAMESPACE_CONVENTIONS",
2619 ),
2620 ];
2621
2622 /// The name of the ONE policy this mask is, as a C string.
2623 ///
2624 /// `None` for the empty mask and for a mask carrying more than one bit:
2625 /// both are real answers a diagnostic must be able to give. "The backend
2626 /// did not say which" is not a policy name, and printing a neighbouring
2627 /// one would be the plausible wrong answer this whole family exists to
2628 /// refuse.
2629 ///
2630 /// Borrowed from a table in flash: no allocator, no formatting, reaches a
2631 /// bare-metal target, and these are the exact bytes the C and C++ entry
2632 /// points hand back.
2633 pub const fn policy_name_cstr(self) -> Option<&'static core::ffi::CStr> {
2634 let mut i = 0;
2635 while i < Self::NAMED.len() {
2636 if Self::NAMED[i].0.0 == self.0 {
2637 return Some(Self::NAMED[i].1);
2638 }
2639 i += 1;
2640 }
2641 None
2642 }
2643
2644 /// [`Self::policy_name_cstr`] as a Rust string — the same bytes without
2645 /// the terminator.
2646 pub fn policy_name(self) -> Option<&'static str> {
2647 // Every entry is an ASCII literal, so this cannot fail; `ok()` rather
2648 // than `expect()` because a panic in a diagnostic path is worse than
2649 // the diagnostic being absent.
2650 self.policy_name_cstr().and_then(|c| c.to_str().ok())
2651 }
2652
2653 /// The policies in `self` that `supported` does not carry.
2654 pub const fn missing_from(self, supported: Self) -> Self {
2655 Self(self.0 & !supported.0)
2656 }
2657
2658 /// The lowest-numbered policy bit set here, or [`Self::NONE`].
2659 ///
2660 /// Which single policy a multi-policy refusal REPORTS. Lowest bit rather
2661 /// than "any": a stable choice means the same profile against the same
2662 /// backend names the same policy on every run and in every language, so a
2663 /// test can assert it and two surfaces cannot disagree about which one to
2664 /// mention first.
2665 pub const fn lowest_policy(self) -> Self {
2666 Self(self.0 & self.0.wrapping_neg())
2667 }
2668
2669 /// `true` if `self` contains every policy in `other`.
2670 pub const fn contains(self, other: Self) -> bool {
2671 self.0 & other.0 == other.0
2672 }
2673
2674 /// Bitwise OR of two masks.
2675 pub const fn union(self, other: Self) -> Self {
2676 Self(self.0 | other.0)
2677 }
2678}
2679
2680impl core::ops::BitOr for QoSPolicyMask {
2681 type Output = Self;
2682 fn bitor(self, rhs: Self) -> Self {
2683 self.union(rhs)
2684 }
2685}
2686
2687impl core::ops::BitOrAssign for QoSPolicyMask {
2688 fn bitor_assign(&mut self, rhs: Self) {
2689 self.0 |= rhs.0;
2690 }
2691}
2692
2693impl QoSProfile {
2694 /// Does any policy in this profile carry its `Unknown` sentinel?
2695 ///
2696 /// True only for a profile that came out of a `*_get_actual_qos`
2697 /// read-back on a backend that could not report every policy — never for
2698 /// one an application built. [`validate_against`](Self::validate_against)
2699 /// refuses such a profile at create time, so "I cannot say" cannot be
2700 /// laundered into a request.
2701 pub const fn has_unknown(&self) -> bool {
2702 matches!(self.reliability, QoSReliabilityPolicy::Unknown)
2703 || matches!(self.durability, QoSDurabilityPolicy::Unknown)
2704 || matches!(self.history, QoSHistoryPolicy::Unknown)
2705 || matches!(self.liveliness_kind, QoSLivelinessPolicy::Unknown)
2706 }
2707
2708 /// Compute the set of QoS policies actually requested by this profile.
2709 ///
2710 /// Zero-valued time fields and `QoSLivelinessPolicy::None` count as "not
2711 /// requesting" the corresponding policy — the cheap default.
2712 ///
2713 /// issue 0829 — the four `CORE` bits (reliability, durability, history,
2714 /// depth) used to be added UNCONDITIONALLY, on the reasoning that every
2715 /// nano-ros backend honours them. That is true and still not the right
2716 /// test: this function answers *what did the caller ASK FOR*, and a
2717 /// `SYSTEM_DEFAULT` policy asks for nothing. Starting from `CORE` made
2718 /// `QOS_PROFILE_SYSTEM_DEFAULT` demand all four, so a backend that could
2719 /// not honour one would reject the profile with `IncompatibleQos` — for
2720 /// requesting nothing. The pattern was already here for the extended
2721 /// policies three lines down (a zero `deadline_ms` declines its bit); it
2722 /// just never reached the four CORE ones.
2723 ///
2724 /// This RELAXES the mask, so it can only turn a rejection into an
2725 /// acceptance, never the reverse.
2726 ///
2727 /// It used to say it flipped no verdict "because every
2728 /// `supported_qos_policies` impl in the tree returns at least `CORE`".
2729 /// That stopped being true in phase-428 W9: the trait default is
2730 /// [`QoSPolicyMask::NONE`] now, so an implementation that claims nothing
2731 /// is refused on the first CORE bit a caller states — which is the whole
2732 /// point, and which makes THIS function's sentinel handling the thing
2733 /// standing between `QOS_PROFILE_SYSTEM_DEFAULT` and a hard refusal.
2734 pub fn required_policies(&self) -> QoSPolicyMask {
2735 let mut mask = QoSPolicyMask(0);
2736 if self.reliability != QoSReliabilityPolicy::SystemDefault
2737 && self.reliability != QoSReliabilityPolicy::BestAvailable
2738 {
2739 mask |= QoSPolicyMask::RELIABILITY;
2740 }
2741 if self.history != QoSHistoryPolicy::SystemDefault {
2742 mask |= QoSPolicyMask::HISTORY;
2743 }
2744 // Depth's sentinel is a VALUE, not a variant: `DEPTH_SYSTEM_DEFAULT`
2745 // is 0, the same 0 that a `KEEP_ALL` profile carries to mean "depth is
2746 // not used here" (`QoSProfile::keep_all()` leaves whatever depth the
2747 // profile had, and a caller building one from scratch leaves it 0).
2748 // Both mean the caller did not ask for a depth, so both decline the
2749 // bit. No NAMED preset is `KEEP_ALL` any more — phase-428 W10;
2750 // `QOS_PROFILE_PARAMETER_EVENTS` used to be the exemplar here and was
2751 // the defect, not the example.
2752 if self.depth != DEPTH_SYSTEM_DEFAULT {
2753 mask |= QoSPolicyMask::DEPTH;
2754 }
2755 match self.durability {
2756 QoSDurabilityPolicy::SystemDefault => {}
2757 QoSDurabilityPolicy::Volatile => mask |= QoSPolicyMask::DURABILITY_VOLATILE,
2758 QoSDurabilityPolicy::TransientLocal => {
2759 mask |= QoSPolicyMask::DURABILITY_TRANSIENT_LOCAL
2760 }
2761 // `Unknown` is a READ-BACK sentinel, never a request, so there is
2762 // no capability bit to demand for it — there is nothing a backend
2763 // could support. Refusing it belongs one level up, in
2764 // `validate_against`, where it is refused for every policy at once
2765 // rather than smuggled in as an unsatisfiable mask bit.
2766 QoSDurabilityPolicy::Unknown => {}
2767 // issue 1973 — a best-available field demands NOTHING: the
2768 // resolver clamps its choice to the backend's own mask
2769 // (`resolve_best_available`), so whatever it picks is honoured.
2770 QoSDurabilityPolicy::BestAvailable => {}
2771 }
2772 // Phase-301 (issue 0241): DURATION_INFINITE_MS reads the same as 0
2773 // (infinite = no check) at every duration check site. The
2774 // best-available sentinel demands nothing either (see above).
2775 if self.deadline_ms != 0
2776 && self.deadline_ms != DURATION_INFINITE_MS
2777 && self.deadline_ms != DURATION_BEST_AVAILABLE_MS
2778 {
2779 mask |= QoSPolicyMask::DEADLINE;
2780 }
2781 if self.lifespan_ms != 0 && self.lifespan_ms != DURATION_INFINITE_MS {
2782 mask |= QoSPolicyMask::LIFESPAN;
2783 }
2784 match self.liveliness_kind {
2785 QoSLivelinessPolicy::None => {}
2786 QoSLivelinessPolicy::Automatic => mask |= QoSPolicyMask::LIVELINESS_AUTOMATIC,
2787 QoSLivelinessPolicy::ManualByTopic => mask |= QoSPolicyMask::LIVELINESS_MANUAL_BY_TOPIC,
2788 QoSLivelinessPolicy::ManualByNode => mask |= QoSPolicyMask::LIVELINESS_MANUAL_BY_NODE,
2789 // See the durability arm.
2790 QoSLivelinessPolicy::Unknown => {}
2791 QoSLivelinessPolicy::BestAvailable => {}
2792 }
2793 if self.liveliness_lease_ms != 0
2794 && self.liveliness_lease_ms != DURATION_INFINITE_MS
2795 && self.liveliness_lease_ms != DURATION_BEST_AVAILABLE_MS
2796 {
2797 mask |= QoSPolicyMask::LIVELINESS_LEASE;
2798 }
2799 if self.avoid_ros_namespace_conventions {
2800 mask |= QoSPolicyMask::AVOID_ROS_NAMESPACE_CONVENTIONS;
2801 }
2802 mask
2803 }
2804
2805 /// Returns `Err(TransportError::IncompatibleQos(policy))` if any policy
2806 /// this profile requires is missing from the backend's `supported` mask.
2807 /// Used at entity-create time to enforce the **no silent
2808 /// degradation** contract.
2809 ///
2810 /// **The error NAMES the policy** (phase-417 G7). This computed the
2811 /// offending bit and discarded it until then, returning a bare
2812 /// `IncompatibleQos` — so every one of the six `create_*` paths in
2813 /// `nros-node` refused with the one fact the caller needed missing, while
2814 /// `rmw_vtable.h` told the reader the opposite. Which bit is reported when
2815 /// several are missing is [`QoSPolicyMask::lowest_policy`]'s choice, and it
2816 /// is stable so a test can assert it.
2817 pub fn validate_against(&self, supported: QoSPolicyMask) -> Result<(), TransportError> {
2818 // issue 1437 — `UNKNOWN` is what a `*_get_actual_qos` read-back writes
2819 // for a policy it cannot report. It is an ABSENCE, so it is meaningless
2820 // as a REQUEST, and the way it reaches a create call is a caller
2821 // feeding a read-back profile straight back in — the exact round trip
2822 // that would otherwise turn "I cannot say" into a silently different
2823 // entity. Refused here, once, for all four policies.
2824 //
2825 // The refusal names NO policy, and that is not laziness: the mask's
2826 // bits are CAPABILITIES a backend declares, and no backend could ever
2827 // declare one that makes "I cannot say" servable — `required_policies`
2828 // has no bit to set, so there is none to report. `policy_name` already
2829 // answers `None` for the empty mask, which the diagnostic renders as
2830 // the honest "the profile is not a request", rather than naming
2831 // `DURABILITY_VOLATILE` for a durability that is not volatile.
2832 if self.has_unknown() {
2833 return Err(TransportError::IncompatibleQos(QoSPolicyMask::NONE));
2834 }
2835 let missing = self.required_policies().missing_from(supported);
2836 if missing.0 == 0 {
2837 Ok(())
2838 } else {
2839 Err(TransportError::IncompatibleQos(missing.lowest_policy()))
2840 }
2841 }
2842
2843 /// Replace every `SYSTEM_DEFAULT` field with the backend's own answer.
2844 ///
2845 /// issue 0829 — a backend calls this at its create entry, **before**
2846 /// anything is derived from the profile (see [`QoSSystemDefaults`] for why
2847 /// the ordering matters on the zenoh path). Fields the caller DID state
2848 /// are left exactly as they are: this resolves an absence, it never
2849 /// overrides a request.
2850 ///
2851 /// Idempotent, and safe to call on a fully concrete profile — a profile
2852 /// with no sentinel in it is returned unchanged.
2853 #[must_use]
2854 pub const fn resolve_system_default(mut self, defaults: &QoSSystemDefaults) -> Self {
2855 if matches!(self.reliability, QoSReliabilityPolicy::SystemDefault) {
2856 self.reliability = defaults.reliability;
2857 }
2858 if matches!(self.durability, QoSDurabilityPolicy::SystemDefault) {
2859 self.durability = defaults.durability;
2860 }
2861 if matches!(self.history, QoSHistoryPolicy::SystemDefault) {
2862 self.history = defaults.history;
2863 }
2864 if self.depth == DEPTH_SYSTEM_DEFAULT {
2865 self.depth = defaults.depth;
2866 }
2867 self
2868 }
2869
2870 /// `true` if any field is still the `SYSTEM_DEFAULT` sentinel.
2871 ///
2872 /// Depth is deliberately NOT part of this test: a `KEEP_ALL` profile
2873 /// legitimately carries depth 0 forever (any profile a caller builds with
2874 /// [`QoSProfile::keep_all`] and never gave a depth to — no NAMED preset is
2875 /// `KEEP_ALL` since phase-428 W10), and a backend that resolves the
2876 /// sentinel depth to 0 — XRCE does, on purpose — leaves a resolved profile
2877 /// reading 0 here. This asks about the
2878 /// three POLICY fields, where the sentinel is a distinct variant and so
2879 /// cannot be confused with a stated value.
2880 #[must_use]
2881 pub const fn has_unresolved_system_default(&self) -> bool {
2882 matches!(self.reliability, QoSReliabilityPolicy::SystemDefault)
2883 || matches!(self.durability, QoSDurabilityPolicy::SystemDefault)
2884 || matches!(self.history, QoSHistoryPolicy::SystemDefault)
2885 }
2886
2887 /// `true` if any field still asks to be resolved best-available (issue
2888 /// 1973): one of the three policy variants, or the
2889 /// [`DURATION_BEST_AVAILABLE_MS`] sentinel in `deadline_ms` /
2890 /// `liveliness_lease_ms`.
2891 #[must_use]
2892 pub const fn has_best_available(&self) -> bool {
2893 matches!(self.reliability, QoSReliabilityPolicy::BestAvailable)
2894 || matches!(self.durability, QoSDurabilityPolicy::BestAvailable)
2895 || matches!(self.liveliness_kind, QoSLivelinessPolicy::BestAvailable)
2896 || self.deadline_ms == DURATION_BEST_AVAILABLE_MS
2897 || self.liveliness_lease_ms == DURATION_BEST_AVAILABLE_MS
2898 }
2899
2900 /// Replace every best-available field with a concrete value — issue 1973,
2901 /// the ONE resolution rule every backend reaches.
2902 ///
2903 /// The rule is upstream's, transcribed from `rmw_dds_common`'s
2904 /// `qos_profile_get_best_available_for_{subscription,publisher}` and
2905 /// `qos_profile_update_best_available_for_services` (jazzy):
2906 ///
2907 /// | policy | subscription (peers = publishers) | publisher (peers = subscriptions) |
2908 /// |-------------|----------------------------------------------------|------------------------------------------------|
2909 /// | reliability | RELIABLE iff ≥1 peer and every peer is RELIABLE, else BEST_EFFORT | RELIABLE |
2910 /// | durability | TRANSIENT_LOCAL iff ≥1 peer and all are, else VOLATILE | TRANSIENT_LOCAL |
2911 /// | liveliness | MANUAL_BY_TOPIC iff ≥1 peer and all are, else AUTOMATIC | MANUAL_BY_TOPIC iff any peer is, else AUTOMATIC |
2912 /// | deadline | largest finite peer deadline, else default (0) | smallest finite peer deadline, else default (0) |
2913 /// | lease | largest finite peer lease, else default (0) | smallest finite peer lease, else default (0) |
2914 ///
2915 /// A service or client ([`BestAvailableRole::Service`]) takes each field
2916 /// from `rmw_qos_profile_services_default`, as upstream does.
2917 ///
2918 /// **No peers is a real answer, not a failure.** It is what a backend with
2919 /// no graph (XRCE, uORB) always reports, and it yields upstream's own
2920 /// "nothing discovered yet" profile: BEST_EFFORT / VOLATILE / AUTOMATIC for
2921 /// a subscription, RELIABLE / TRANSIENT_LOCAL / AUTOMATIC for a publisher.
2922 ///
2923 /// **A peer whose profile the backend cannot read** (its fields are the
2924 /// `Unknown` sentinels, durations 0) contributes its LEAST demanding value:
2925 /// not reliable, not transient-local, not manual, no deadline. For a
2926 /// subscription that keeps the choice compatible with the peer whatever it
2927 /// really offers; a publisher cannot be protected the same way and offers
2928 /// what the readable peers ask for.
2929 ///
2930 /// **Last, the choice is clamped to what the backend honours**
2931 /// (`supported`, its [`Session::supported_qos_policies`]): a resolved value
2932 /// the backend could not grant steps down to the next one it can —
2933 /// TRANSIENT_LOCAL → VOLATILE, MANUAL_BY_TOPIC → AUTOMATIC, a finite
2934 /// deadline or lease → default — and to the `SYSTEM_DEFAULT` sentinel when
2935 /// the backend honours the policy not at all. "Best available" is then
2936 /// literally the best this backend has; the alternative, choosing a value
2937 /// the next line refuses with `IncompatibleQos`, would make the request
2938 /// fail for asking to be accommodated. Fields the caller stated are never
2939 /// touched.
2940 ///
2941 /// Idempotent: a profile with nothing best-available is returned as is.
2942 #[must_use]
2943 pub fn resolve_best_available(
2944 mut self,
2945 role: BestAvailableRole,
2946 peers: &BestAvailablePeers,
2947 supported: QoSPolicyMask,
2948 ) -> Self {
2949 let rel = matches!(self.reliability, QoSReliabilityPolicy::BestAvailable);
2950 let dur = matches!(self.durability, QoSDurabilityPolicy::BestAvailable);
2951 let liv = matches!(self.liveliness_kind, QoSLivelinessPolicy::BestAvailable);
2952 let dl = self.deadline_ms == DURATION_BEST_AVAILABLE_MS;
2953 let lease = self.liveliness_lease_ms == DURATION_BEST_AVAILABLE_MS;
2954 let any = peers.count > 0;
2955 match role {
2956 BestAvailableRole::Subscription => {
2957 if rel {
2958 self.reliability = if any && peers.reliable == peers.count {
2959 QoSReliabilityPolicy::Reliable
2960 } else {
2961 QoSReliabilityPolicy::BestEffort
2962 };
2963 }
2964 if dur {
2965 self.durability = if any && peers.transient_local == peers.count {
2966 QoSDurabilityPolicy::TransientLocal
2967 } else {
2968 QoSDurabilityPolicy::Volatile
2969 };
2970 }
2971 if liv {
2972 self.liveliness_kind = if any && peers.manual_by_topic == peers.count {
2973 QoSLivelinessPolicy::ManualByTopic
2974 } else {
2975 QoSLivelinessPolicy::Automatic
2976 };
2977 }
2978 if dl {
2979 self.deadline_ms = peers.deadline_max;
2980 }
2981 if lease {
2982 self.liveliness_lease_ms = peers.lease_max;
2983 }
2984 }
2985 BestAvailableRole::Publisher => {
2986 if rel {
2987 self.reliability = QoSReliabilityPolicy::Reliable;
2988 }
2989 if dur {
2990 self.durability = QoSDurabilityPolicy::TransientLocal;
2991 }
2992 if liv {
2993 self.liveliness_kind = if peers.manual_by_topic > 0 {
2994 QoSLivelinessPolicy::ManualByTopic
2995 } else {
2996 QoSLivelinessPolicy::Automatic
2997 };
2998 }
2999 if dl {
3000 self.deadline_ms = peers.deadline_min;
3001 }
3002 if lease {
3003 self.liveliness_lease_ms = peers.lease_min;
3004 }
3005 }
3006 BestAvailableRole::Service => {
3007 let svc = Self::QOS_PROFILE_SERVICES_DEFAULT;
3008 if rel {
3009 self.reliability = svc.reliability;
3010 }
3011 if dur {
3012 self.durability = svc.durability;
3013 }
3014 if liv {
3015 self.liveliness_kind = svc.liveliness_kind;
3016 }
3017 if dl {
3018 self.deadline_ms = svc.deadline_ms;
3019 }
3020 if lease {
3021 self.liveliness_lease_ms = svc.liveliness_lease_ms;
3022 }
3023 }
3024 }
3025
3026 // The clamp — only on fields that WERE best-available.
3027 if rel && !supported.contains(QoSPolicyMask::RELIABILITY) {
3028 self.reliability = QoSReliabilityPolicy::SystemDefault;
3029 }
3030 if dur {
3031 if matches!(self.durability, QoSDurabilityPolicy::TransientLocal)
3032 && !supported.contains(QoSPolicyMask::DURABILITY_TRANSIENT_LOCAL)
3033 {
3034 self.durability = QoSDurabilityPolicy::Volatile;
3035 }
3036 if matches!(self.durability, QoSDurabilityPolicy::Volatile)
3037 && !supported.contains(QoSPolicyMask::DURABILITY_VOLATILE)
3038 {
3039 self.durability = QoSDurabilityPolicy::SystemDefault;
3040 }
3041 }
3042 if liv {
3043 if matches!(self.liveliness_kind, QoSLivelinessPolicy::ManualByTopic)
3044 && !supported.contains(QoSPolicyMask::LIVELINESS_MANUAL_BY_TOPIC)
3045 {
3046 self.liveliness_kind = QoSLivelinessPolicy::Automatic;
3047 }
3048 if matches!(self.liveliness_kind, QoSLivelinessPolicy::Automatic)
3049 && !supported.contains(QoSPolicyMask::LIVELINESS_AUTOMATIC)
3050 {
3051 self.liveliness_kind = QoSLivelinessPolicy::None;
3052 }
3053 }
3054 if dl && self.deadline_ms != 0 && !supported.contains(QoSPolicyMask::DEADLINE) {
3055 self.deadline_ms = 0;
3056 }
3057 if lease
3058 && self.liveliness_lease_ms != 0
3059 && !supported.contains(QoSPolicyMask::LIVELINESS_LEASE)
3060 {
3061 self.liveliness_lease_ms = 0;
3062 }
3063 self
3064 }
3065}
3066
3067/// Which end of the graph a best-available profile is resolved for — issue
3068/// 1973. See [`QoSProfile::resolve_best_available`].
3069#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3070pub enum BestAvailableRole {
3071 /// A publisher; resolved against the topic's discovered SUBSCRIPTIONS.
3072 Publisher,
3073 /// A subscription; resolved against the topic's discovered PUBLISHERS.
3074 Subscription,
3075 /// A service server or client; resolved to
3076 /// `rmw_qos_profile_services_default`, never against the graph.
3077 Service,
3078}
3079
3080/// The discovered peers of one topic, folded down to what
3081/// [`QoSProfile::resolve_best_available`] reads — issue 1973.
3082///
3083/// A fold rather than a list so a graph walk can feed it one endpoint at a
3084/// time, with no allocator and no bound on how many endpoints there are.
3085#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3086pub struct BestAvailablePeers {
3087 /// Peers observed.
3088 pub count: usize,
3089 /// …of which RELIABLE.
3090 pub reliable: usize,
3091 /// …of which TRANSIENT_LOCAL.
3092 pub transient_local: usize,
3093 /// …of which MANUAL_BY_TOPIC.
3094 pub manual_by_topic: usize,
3095 /// Smallest finite deadline seen, ms; `0` when none was finite.
3096 pub deadline_min: u32,
3097 /// Largest finite deadline seen, ms; `0` when none was finite.
3098 pub deadline_max: u32,
3099 /// Smallest finite liveliness lease seen, ms; `0` when none was finite.
3100 pub lease_min: u32,
3101 /// Largest finite liveliness lease seen, ms; `0` when none was finite.
3102 pub lease_max: u32,
3103}
3104
3105impl BestAvailablePeers {
3106 /// No peers — what a backend with no graph reports.
3107 pub const NONE: Self = Self {
3108 count: 0,
3109 reliable: 0,
3110 transient_local: 0,
3111 manual_by_topic: 0,
3112 deadline_min: 0,
3113 deadline_max: 0,
3114 lease_min: 0,
3115 lease_max: 0,
3116 };
3117
3118 /// Fold one discovered endpoint's profile in.
3119 ///
3120 /// `0`, [`DURATION_INFINITE_MS`] and the best-available sentinel all read
3121 /// as "no finite window" — upstream's `RMW_QOS_DEADLINE_DEFAULT`
3122 /// comparison, widened to our two spellings of infinite.
3123 pub fn observe(&mut self, peer: &QoSProfile) {
3124 self.count += 1;
3125 if matches!(peer.reliability, QoSReliabilityPolicy::Reliable) {
3126 self.reliable += 1;
3127 }
3128 if matches!(peer.durability, QoSDurabilityPolicy::TransientLocal) {
3129 self.transient_local += 1;
3130 }
3131 if matches!(peer.liveliness_kind, QoSLivelinessPolicy::ManualByTopic) {
3132 self.manual_by_topic += 1;
3133 }
3134 fn finite(ms: u32) -> Option<u32> {
3135 (ms != 0 && ms < DURATION_BEST_AVAILABLE_MS).then_some(ms)
3136 }
3137 fn fold(min: &mut u32, max: &mut u32, v: Option<u32>) {
3138 if let Some(v) = v {
3139 *min = if *min == 0 { v } else { (*min).min(v) };
3140 *max = (*max).max(v);
3141 }
3142 }
3143 fold(
3144 &mut self.deadline_min,
3145 &mut self.deadline_max,
3146 finite(peer.deadline_ms),
3147 );
3148 fold(
3149 &mut self.lease_min,
3150 &mut self.lease_max,
3151 finite(peer.liveliness_lease_ms),
3152 );
3153 }
3154}
3155
3156/// Resolve `qos`'s best-available fields against what `session` has
3157/// discovered on `topic_name` — issue 1973. The graph half of
3158/// [`QoSProfile::resolve_best_available`], shared by every create path.
3159///
3160/// Queries [`Session::get_endpoint_info_by_topic`] for the OTHER side of the
3161/// topic (subscriptions for a publisher, publishers for a subscription) and
3162/// folds each endpoint's [`GraphEndpointInfo::qos`]. A backend that cannot
3163/// answer — `Unsupported`, or any error from the walk — is treated as having
3164/// discovered nothing, which resolves to upstream's documented
3165/// nothing-discovered profile rather than to a guess. A
3166/// [`BestAvailableRole::Service`] never walks the graph.
3167///
3168/// Returns `qos` unchanged when nothing in it is best-available, so it is
3169/// free to call on every create.
3170pub fn resolve_best_available_on<S: Session + ?Sized>(
3171 session: &mut S,
3172 role: BestAvailableRole,
3173 topic_name: &str,
3174 qos: QoSProfile,
3175) -> QoSProfile
3176where
3177 S::Error: From<TransportError>,
3178{
3179 if !qos.has_best_available() {
3180 return qos;
3181 }
3182 let mut peers = BestAvailablePeers::NONE;
3183 let publishers = match role {
3184 BestAvailableRole::Publisher => Some(false),
3185 BestAvailableRole::Subscription => Some(true),
3186 BestAvailableRole::Service => None,
3187 };
3188 if let Some(publishers) = publishers {
3189 let mut walked = BestAvailablePeers::NONE;
3190 let ok = session
3191 .get_endpoint_info_by_topic(publishers, topic_name, &mut |info| {
3192 walked.observe(&info.qos);
3193 true
3194 })
3195 .is_ok();
3196 if ok {
3197 peers = walked;
3198 }
3199 }
3200 let supported = session.supported_qos_policies();
3201 qos.resolve_best_available(role, &peers, supported)
3202}
3203
3204/// Publisher trait for sending messages.
3205///
3206/// The name is rclrs's, the role is the backend seam: rclrs's `Publisher<T>` is a
3207/// typed, shared handle (`Arc<PublisherState<T>>`); this trait is what an RMW
3208/// backend implements, raw bytes in. The typed handle a node hands an application
3209/// is one type with no `Arc` (`nros::EmbeddedPublisher<M>`). Loaning exists:
3210/// `EmbeddedRawPublisher::try_loan` / `loan`, zero-copy through `SlotLending`
3211/// when the `lending` feature is on and the backend supports it.
3212///
3213/// # Threading
3214///
3215/// `&self` on `publish_raw` — implementors must allow concurrent
3216/// publishes from multiple threads. Internal locking (or lock-free
3217/// queues) is the backend's responsibility.
3218///
3219/// # Buffer ownership
3220///
3221/// `data` in `publish_raw` is borrowed for the duration of the call.
3222/// The backend must either send it inline or copy into its own
3223/// buffer before returning — the slice is invalid after the call.
3224///
3225/// # Blocking
3226///
3227/// `publish_raw` is expected to be non-blocking on best-effort QoS
3228/// and bounded-blocking on reliable QoS (waiting for outbound queue
3229/// space). Backends should *not* block waiting for ack from a
3230/// matched subscriber.
3231pub trait Publisher {
3232 /// Error type for publish operations
3233 type Error;
3234
3235 /// Publish a CDR-serialised message.
3236 ///
3237 /// Returns once the message has been handed to the transport
3238 /// (queued or fired-and-forgotten depending on QoS). Does **not**
3239 /// wait for delivery.
3240 fn publish_raw(&self, data: &[u8]) -> Result<(), Self::Error>;
3241
3242 /// Phase 128.F.4 — publish with an opaque attachment block.
3243 ///
3244 /// `attachment` rides alongside the payload at the wire layer.
3245 /// Receivers can read it back via
3246 /// [`Subscription::take_serialized_with_attachment`].
3247 ///
3248 /// Primary use case: cross-RMW bridges stamp a bridge_origin
3249 /// tag (the source backend's RMW name) so a paired return
3250 /// bridge can deterministically drop echoed frames.
3251 ///
3252 /// Default body delegates to [`publish_raw`](Self::publish_raw)
3253 /// and discards the attachment — backends that do not natively
3254 /// carry attachments (XRCE today, DDS without a user-data hook)
3255 /// see no change. Backends with native attachment support
3256 /// (zenoh-pico's `z_publisher_put_options::attachment`,
3257 /// Cyclone DDS user-data) override to write the bytes onto the
3258 /// wire.
3259 fn publish_raw_with_attachment(
3260 &self,
3261 data: &[u8],
3262 _attachment: &[u8],
3263 ) -> Result<(), Self::Error> {
3264 self.publish_raw(data)
3265 }
3266
3267 /// Phase 124.E.1 — streamed publish.
3268 ///
3269 /// `size_cb` reports the total payload length once; `chunk_cb`
3270 /// fills the slot in chunks. What it saves is the CALLER-side
3271 /// buffer holding a whole serialised message — which is STACK,
3272 /// not `.bss`: `EmbeddedPublisher::publish` serialises into a
3273 /// per-CALL array of `DEFAULT_TX_BUF`, and this trait's own
3274 /// fallback below stages on the caller's stack too. There is no
3275 /// per-publisher staging buffer and none of it is in `.bss`;
3276 /// issue 0782 corrected that fiction on the C slot and left this
3277 /// copy of it standing (phase-428 W12's F2, one layer over).
3278 ///
3279 /// Default body — the **staging-buffer fallback** (124.E.2).
3280 /// Asks `size_cb` for the total length, fills a stack-allocated
3281 /// `[u8; STAGE_CAP]` via `chunk_cb`, then forwards to
3282 /// `publish_raw`. `STAGE_CAP` is the 4096 constant declared in
3283 /// the body, not a knob: this said `NROS_MAX_STREAM_CHUNK` until
3284 /// 2026-09-12 and no such setting has ever existed in any form,
3285 /// so a reader looking for it found nothing and had no way to
3286 /// learn the limit. Returns `Err(BufferTooSmall)` if the total
3287 /// exceeds that cap so the caller can drop back to a regular
3288 /// `publish_raw` with a heap-sized buffer.
3289 ///
3290 /// Concrete backends opt in by overriding to stream straight
3291 /// into the network buffer (zenoh: write into the zenoh-pico
3292 /// outbound buffer; XRCE: micro-CDR streaming APIs).
3293 ///
3294 /// # Safety
3295 ///
3296 /// The caller must ensure `user_ctx` is valid for every invocation of
3297 /// `size_cb` and `chunk_cb` during this call, and that both callbacks obey
3298 /// their out-pointer contracts.
3299 ///
3300 /// `size_cb` and `chunk_cb` may be called from the same thread
3301 /// that called `publish_streamed`. Backends MUST NOT defer the
3302 /// calls past the function return; the caller's `user_ctx`
3303 /// pointer is only guaranteed valid for the duration of the
3304 /// call.
3305 unsafe fn publish_streamed(
3306 &self,
3307 size_cb: unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut core::ffi::c_void),
3308 chunk_cb: unsafe extern "C" fn(
3309 out_buf: *mut u8,
3310 cap: usize,
3311 out_written: *mut usize,
3312 user_ctx: *mut core::ffi::c_void,
3313 ),
3314 user_ctx: *mut core::ffi::c_void,
3315 ) -> Result<(), Self::Error>
3316 where
3317 Self::Error: From<TransportError>,
3318 {
3319 /// Default staging-buffer cap. Stack-allocated, so embedded
3320 /// callers don't pay for it unless they actually invoke this
3321 /// fallback. 4 KiB matches typical RTPS frag-size +
3322 /// micro-XRCE message ceilings.
3323 const STAGE_CAP: usize = 4096;
3324
3325 let mut total = 0usize;
3326 // SAFETY: caller's contract on `size_cb` matches our trait
3327 // doc — fire once with a writable `*mut usize` slot.
3328 unsafe { size_cb(&mut total as *mut usize, user_ctx) };
3329 if total > STAGE_CAP {
3330 return Err(TransportError::BufferTooSmall.into());
3331 }
3332 let mut stage = [0u8; STAGE_CAP];
3333 let mut written_so_far = 0usize;
3334 while written_so_far < total {
3335 let mut chunk_written = 0usize;
3336 let remaining = total - written_so_far;
3337 // SAFETY: `chunk_cb` writes ≤ `cap` bytes to
3338 // `out_buf` and reports the count via `out_written`.
3339 unsafe {
3340 chunk_cb(
3341 stage.as_mut_ptr().add(written_so_far),
3342 remaining,
3343 &mut chunk_written as *mut usize,
3344 user_ctx,
3345 );
3346 }
3347 if chunk_written == 0 {
3348 // Caller signalled EOF early — treat the partial
3349 // write as a malformed sequence; reporting it as
3350 // BufferTooSmall keeps the surface tight without
3351 // adding a new variant.
3352 return Err(TransportError::BufferTooSmall.into());
3353 }
3354 written_so_far += chunk_written;
3355 }
3356 self.publish_raw(&stage[..total])
3357 }
3358
3359 /// Publish a typed message (serializes automatically)
3360 fn publish<M: RosMessage>(&self, msg: &M, buf: &mut [u8]) -> Result<(), Self::Error> {
3361 use nros_core::CdrWriter;
3362
3363 let mut writer = CdrWriter::new_with_header(buf).map_err(|_| self.buffer_error())?;
3364 msg.serialize(&mut writer)
3365 .map_err(|_| self.serialization_error())?;
3366 let len = writer.position();
3367 self.publish_raw(&buf[..len])
3368 }
3369
3370 /// Return a buffer-too-small error (implementation specific)
3371 fn buffer_error(&self) -> Self::Error;
3372
3373 /// Return a serialization error (implementation specific)
3374 fn serialization_error(&self) -> Self::Error;
3375
3376 /// Phase 108 — `true` if the backend can generate this event for
3377 /// this publisher. Default returns `false`; backends override per
3378 /// supported event kind.
3379 ///
3380 /// Only [`EventKind::LivelinessLost`](crate::event::EventKind::LivelinessLost) and
3381 /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) are publisher-side events;
3382 /// other kinds always return `false` here.
3383 fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
3384 false
3385 }
3386
3387 /// Phase 108 — register a callback fired when the named status
3388 /// event occurs. `deadline_ms` applies to
3389 /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) only; ignored otherwise.
3390 /// Default impl returns the backend's "unsupported"-shaped error.
3391 ///
3392 /// # Safety
3393 ///
3394 /// `cb` and `user_ctx` must remain valid for the entity's
3395 /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
3396 /// responsible for keeping the closure / context arena alive.
3397 unsafe fn register_event_callback(
3398 &mut self,
3399 _kind: crate::event::EventKind,
3400 _deadline_ms: u32,
3401 _cb: crate::event::EventCallback,
3402 _user_ctx: *mut core::ffi::c_void,
3403 ) -> Result<(), Self::Error> {
3404 Err(self.unsupported_event_error())
3405 }
3406
3407 /// Phase 108 — backend's error variant for "this event kind is
3408 /// not supported." Default impl reuses `serialization_error()`
3409 /// since most backends share an `Unsupported` variant; backends
3410 /// override if they have a distinct `Unsupported` mapping.
3411 fn unsupported_event_error(&self) -> Self::Error {
3412 self.serialization_error()
3413 }
3414
3415 /// Phase 109 — assert this publisher's liveliness manually.
3416 /// Required for publishers configured with
3417 /// [`QoSLivelinessPolicy::ManualByTopic`] / [`ManualByNode`](QoSLivelinessPolicy::ManualByNode);
3418 /// nothing to do for the other kinds.
3419 ///
3420 /// **`Ok(())` MEANS THE ASSERTION REACHED THE WIRE, or that there was
3421 /// nothing to assert** (the phase-467 RMW gap-closure design study's
3422 /// Row 7). Under a kind for which [`QoSLivelinessPolicy::is_manual`]
3423 /// holds, a backend that cannot renew a lease a PEER can observe must
3424 /// answer `Unsupported` — an `Ok(())` from a call that sent nothing is
3425 /// the silently-wrong shape RFC-0089 Part I refuses, and it is what the
3426 /// zenoh shim answered until this study. A backend is free to keep a
3427 /// LOCAL record of the call (zenoh drives its own `LivelinessLost`
3428 /// watchdog off one) and still report `Unsupported`; the return value is
3429 /// about what a peer can see, not about what we noted down.
3430 ///
3431 /// The default body is `Ok(())`, which is correct only for a publisher
3432 /// whose kind is not manual — a backend that can be created with a manual
3433 /// kind must override.
3434 fn assert_liveliness(&self) -> Result<(), Self::Error> {
3435 Ok(())
3436 }
3437
3438 /// This publisher's own global identifier — upstream
3439 /// `rmw_get_gid_for_publisher`, the vtable's `get_gid_for_publisher`.
3440 ///
3441 /// The phase-467 RMW gap-closure design study's Q1. **Not the same
3442 /// question as [`MessageInfo::publisher_gid`](nros_core::MessageInfo::publisher_gid)**,
3443 /// which is the identity of whoever sent a message we RECEIVED. Since
3444 /// that study's Q1(a) the two are one TYPE — both
3445 /// [`PUBLISHER_GID_SIZE`](nros_core::PUBLISHER_GID_SIZE) wide, nothing
3446 /// truncated, no mapping — and since issue 1495 they are one VALUE on
3447 /// zenoh and Cyclone: a take reports the gid this publisher's own
3448 /// `get_gid` reports, so the two compare. (XRCE answers `get_gid` — the
3449 /// DDS GID its graph derives, issue 2110 — but reports no take-side gid;
3450 /// uORB answers neither.)
3451 ///
3452 /// **How many of the 24 bytes MEAN anything is a backend property.** A
3453 /// backend whose identity is narrower zero-extends through
3454 /// [`pad_publisher_gid`](nros_core::pad_publisher_gid) rather than
3455 /// leaving the tail undefined, so two gids naming one entity compare
3456 /// equal. Gids from DIFFERENT backends are never comparable.
3457 ///
3458 /// The default is `Err(Unsupported)`, never a fabricated or all-zero id:
3459 /// an all-zero gid is what an unwritten buffer holds, so handing one back
3460 /// would make "this backend has no identity for this publisher"
3461 /// indistinguishable from an answer. That is the same NULL-slot
3462 /// discipline the ABI header states for `get_serialization_format`.
3463 fn get_gid(&self) -> Result<[u8; nros_core::PUBLISHER_GID_SIZE], Self::Error>
3464 where
3465 Self::Error: From<TransportError>,
3466 {
3467 Err(TransportError::Unsupported.into())
3468 }
3469
3470 /// How many subscriptions are MATCHED to this publisher — upstream
3471 /// `rmw_publisher_count_matched_subscriptions`, the vtable's
3472 /// `publisher_count_matched_subscriptions`. Issue 1643.
3473 ///
3474 /// Matched means QoS-compatible with THIS publisher, which is upstream's
3475 /// question and a stronger one than "how many subscriptions are on the
3476 /// topic" (the graph's `count_subscribers`): an incompatible peer is on
3477 /// the topic and not matched.
3478 ///
3479 /// The default is `Err(Unsupported)`, never `Ok(0)` — `0` means "nobody
3480 /// is listening", and a backend that cannot tell must not say that. A
3481 /// caller with a weaker fallback (the node API's topic-wide graph count)
3482 /// takes it on `Unsupported`.
3483 fn count_matched_subscriptions(&self) -> Result<usize, Self::Error>
3484 where
3485 Self::Error: From<TransportError>,
3486 {
3487 Err(TransportError::Unsupported.into())
3488 }
3489
3490 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
3491 /// not what the call site requested.
3492 ///
3493 /// Upstream's `rmw_publisher_get_actual_qos`. The two differ whenever a
3494 /// backend serves something other than what it was asked for, and the
3495 /// difference is exactly what answers "why is nothing arriving": a
3496 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
3497 /// history depth drops samples the caller believed were kept.
3498 ///
3499 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
3500 /// determines four policies and not the fifth returns the four and leaves
3501 /// the fifth at its `Unknown` variant. Never echo the request into a field
3502 /// you did not read back — that is the inverse of this method's meaning,
3503 /// and it is what the six C-ABI slots did until this phase.
3504 ///
3505 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
3506 /// absence — which is the honest answer for a backend with no read-back at
3507 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
3508 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
3509 fn actual_qos(&self) -> QoSProfile {
3510 QoSProfile::QOS_PROFILE_UNKNOWN
3511 }
3512}
3513
3514/// Subscription trait for receiving messages.
3515///
3516/// # Threading
3517///
3518/// `&mut self` on `take_serialized` — the executor takes exclusive
3519/// ownership of the subscriber for the duration of a receive. A
3520/// backend that wants to allow concurrent receives must split into
3521/// per-thread sub-handles internally.
3522///
3523/// # Buffer ownership
3524///
3525/// `buf` is caller-owned. The implementation copies the next ready
3526/// message into `buf` and returns the byte count. The caller may
3527/// re-use or drop `buf` immediately after the call.
3528///
3529/// # Blocking
3530///
3531/// `take_serialized` is **non-blocking**: returns `Ok(None)` (or
3532/// equivalent for backends that map empty into a zero-length read)
3533/// when no message is ready. Use [`Session::drive_io`] to wait for
3534/// data; never sleep inside `take_serialized`.
3535pub trait Subscription {
3536 /// Error type for receive operations
3537 type Error;
3538
3539 /// Check if data is available without consuming it.
3540 ///
3541 /// Non-destructive — does not advance the receive cursor.
3542 /// Conservative default returns `true` (always assume data may
3543 /// be available); backends should override with a real check
3544 /// to avoid spurious receive attempts.
3545 fn has_data(&self) -> bool {
3546 true
3547 }
3548
3549 /// Try to receive one message into `buf`.
3550 ///
3551 /// Non-blocking. On success returns `Ok(Some(len))` where `len`
3552 /// is the byte count written into `buf[..len]`. Returns
3553 /// `Ok(None)` if no message is ready. If `buf` is too small the
3554 /// backend may either truncate (and document it) or return an
3555 /// error (preferred).
3556 fn take_serialized(&mut self, buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
3557
3558 /// Issue 1612 — how many bytes the sample the most recent take REFUSED
3559 /// as too big for the caller's buffer needed.
3560 ///
3561 /// Read it right after a take returns `BufferTooSmall`; a later take may
3562 /// overwrite it. `None` means the backend does not know the size (the
3563 /// default, and the honest answer for one that never measured it), never
3564 /// "zero bytes" — a zero-byte sample fits every buffer, so no refusal can
3565 /// need zero.
3566 ///
3567 /// A query beside the error rather than a payload on
3568 /// `TransportError::BufferTooSmall`: that variant is the same word a dozen
3569 /// unrelated paths return (arena exhaustion, a short reply buffer), so
3570 /// giving it a field would make every one of them invent a size. Only a
3571 /// take ever has one, and only a take is asked.
3572 fn refused_sample_len(&self) -> Option<usize> {
3573 None
3574 }
3575
3576 /// Phase 128.F.4 — receive with attachment bytes alongside the
3577 /// payload.
3578 ///
3579 /// On success returns `Ok(Some((payload_len, attachment_len)))`
3580 /// with the payload written into `buf[..payload_len]` and the
3581 /// attachment (if any) written into
3582 /// `att_buf[..attachment_len]`. `attachment_len == 0` means the
3583 /// incoming sample carried no attachment.
3584 ///
3585 /// Default body falls back to [`take_serialized`](Self::take_serialized)
3586 /// and reports a 0-length attachment. Backends with native
3587 /// attachment support override to populate `att_buf`. Cross-RMW
3588 /// bridges use the attachment to read the bridge_origin tag
3589 /// stamped by the sending side.
3590 fn take_serialized_with_attachment(
3591 &mut self,
3592 buf: &mut [u8],
3593 _att_buf: &mut [u8],
3594 ) -> Result<Option<(usize, usize)>, Self::Error> {
3595 match self.take_serialized(buf)? {
3596 Some(len) => Ok(Some((len, 0))),
3597 None => Ok(None),
3598 }
3599 }
3600
3601 /// Phase 124.D.1 — burst-take.
3602 ///
3603 /// Drain up to `max_msgs` queued samples into the contiguous
3604 /// `buf` block in one call, with the i-th sample at
3605 /// `buf[i * per_msg_cap .. i * per_msg_cap + out_lens[i]]`.
3606 /// Returns the number of messages actually delivered. Partial
3607 /// drains MUST report the count, not error out.
3608 ///
3609 /// Default body loop-drives `take_serialized` so callers can
3610 /// commit to the batched API regardless of backend support.
3611 /// Concrete backends opt in by overriding with a native batch
3612 /// take (zenoh queue drain, `dds_take(max_samples)`).
3613 fn take_sequence(
3614 &mut self,
3615 buf: &mut [u8],
3616 per_msg_cap: usize,
3617 max_msgs: usize,
3618 out_lens: &mut [usize],
3619 ) -> Result<usize, Self::Error> {
3620 if per_msg_cap == 0 || max_msgs == 0 {
3621 return Ok(0);
3622 }
3623 let limit = max_msgs.min(out_lens.len());
3624 let mut count = 0;
3625 for i in 0..limit {
3626 let slot = &mut buf[i * per_msg_cap..(i + 1) * per_msg_cap];
3627 // Issue 0971 — NOT `take_serialized(slot)?`. The `?` propagates the
3628 // error and DISCARDS `count`, which is precisely what the doc
3629 // comment above forbids ("Partial drains MUST report the count, not
3630 // error out") and what `c3af8c1d1` removed from the two concrete
3631 // implementations. A backend that does not override this body got
3632 // the original defect back through the default.
3633 //
3634 // The shape is the one that fix established: a drain that has
3635 // already taken messages reports the COUNT, and the error is
3636 // delivered by the NEXT call — the caller sees every message it was
3637 // handed, then the reason the drain stopped. With nothing taken
3638 // there is no count to protect, so the error goes out immediately.
3639 //
3640 // Parking is per-implementation state, which a default body has no
3641 // place to keep. So it does the half it can do correctly: it
3642 // returns the partial count and lets the error surface on the
3643 // caller's next `take_serialized`, which is where an unconsumed
3644 // backend error still sits.
3645 match self.take_serialized(slot) {
3646 Ok(Some(len)) => {
3647 out_lens[i] = len;
3648 count += 1;
3649 }
3650 Ok(None) => break,
3651 Err(e) => {
3652 if count == 0 {
3653 return Err(e);
3654 }
3655 break;
3656 }
3657 }
3658 }
3659 Ok(count)
3660 }
3661
3662 /// Try to receive a typed message (non-blocking)
3663 fn take<M: RosMessage>(&mut self, buf: &mut [u8]) -> Result<Option<M>, Self::Error> {
3664 use nros_core::CdrReader;
3665
3666 match self.take_serialized(buf)? {
3667 Some(len) => {
3668 let mut reader = CdrReader::new_with_header(&buf[..len])
3669 .map_err(|_| self.deserialization_error())?;
3670 let msg = M::deserialize(&mut reader).map_err(|_| self.deserialization_error())?;
3671 Ok(Some(msg))
3672 }
3673 None => Ok(None),
3674 }
3675 }
3676
3677 /// Process the received message in-place without copying.
3678 ///
3679 /// Calls `f` with a reference to the raw CDR bytes in the subscriber's
3680 /// internal receive buffer, avoiding a copy into a caller-provided buffer.
3681 /// While `f` executes the buffer is exclusively borrowed — any messages
3682 /// arriving from the transport during that time are dropped to prevent
3683 /// data races.
3684 ///
3685 /// Returns `Ok(true)` if a message was available and `f` was called,
3686 /// `Ok(false)` if no message was available.
3687 ///
3688 /// **Default body**: returns `Err(MessageTooLarge)` — the old default
3689 /// silently truncated anything larger than 1 KB into a stack buffer,
3690 /// which broke large messages with no diagnostic. Backends must
3691 /// override this with a real zero-copy path if they advertise support
3692 /// for `process_raw_in_place`; callers that hit the default should
3693 /// use `take_serialized` with a caller-sized buffer instead.
3694 fn process_raw_in_place(&mut self, f: impl FnOnce(&[u8])) -> Result<bool, Self::Error>
3695 where
3696 Self::Error: From<TransportError>,
3697 {
3698 let _ = f;
3699 Err(TransportError::MessageTooLarge.into())
3700 }
3701
3702 /// Whether this backend implements the in-place dispatch methods
3703 /// ([`process_raw_in_place`](Subscription::process_raw_in_place) /
3704 /// [`process_raw_in_place_with_info`](Subscription::process_raw_in_place_with_info))
3705 /// with a real zero-copy borrow.
3706 ///
3707 /// The executor consults this at subscription registration to choose the
3708 /// **in-place** arena dispatch (borrow + deserialize from the backend slot, no
3709 /// arena buffer) over the **buffered** dispatch (copy into an arena buffer
3710 /// first). Backends that leave the in-place methods at their unsupported
3711 /// default return `false` (the default) and keep the buffered path. (RFC-0038,
3712 /// Phase 231 Wave 0.2.)
3713 fn supports_process_in_place(&self) -> bool {
3714 false
3715 }
3716
3717 /// In-place processing variant that also surfaces publisher metadata.
3718 ///
3719 /// Same borrow contract as
3720 /// [`process_raw_in_place`](Subscription::process_raw_in_place): `f` receives
3721 /// the raw CDR bytes plus the parsed [`MessageInfo`](nros_core::MessageInfo)
3722 /// — the co-located attachment (publisher GID / sequence / source timestamp),
3723 /// or `None` when no attachment was present — for the duration of the call;
3724 /// the slot is released after `f` returns. `Ok(true)` = a message was
3725 /// available and `f` was called; `Ok(false)` = none ready.
3726 ///
3727 /// **Default body**: returns the unsupported error (mirrors
3728 /// `process_raw_in_place`). Backends that advertise in-place support override
3729 /// this with a real zero-copy path; callers that hit the default should use
3730 /// the buffered [`take_serialized_with_info`](Subscription::take_serialized_with_info)
3731 /// path instead. (RFC-0038, Phase 231 Wave 0.1.)
3732 fn process_raw_in_place_with_info(
3733 &mut self,
3734 f: impl FnOnce(&[u8], Option<nros_core::MessageInfo>),
3735 ) -> Result<bool, Self::Error>
3736 where
3737 Self::Error: From<TransportError>,
3738 {
3739 let _ = f;
3740 Err(TransportError::MessageTooLarge.into())
3741 }
3742
3743 /// Try to receive raw data along with publisher metadata.
3744 ///
3745 /// When available, [`MessageInfo`](nros_core::MessageInfo) contains
3746 /// the publisher's GID (Global Identifier) and source timestamp,
3747 /// extracted from a transport-level attachment on the incoming message.
3748 ///
3749 /// Returns `Ok(Some((len, info)))` if data is available, where:
3750 /// - `len` is the number of bytes written to the buffer
3751 /// - `info` is the parsed publisher metadata (if attachment was present)
3752 ///
3753 /// Default: delegates to [`take_serialized`](Subscription::take_serialized) with no info.
3754 fn take_serialized_with_info(
3755 &mut self,
3756 buf: &mut [u8],
3757 ) -> Result<Option<(usize, Option<nros_core::MessageInfo>)>, Self::Error> {
3758 self.take_serialized(buf)
3759 .map(|opt| opt.map(|len| (len, None)))
3760 }
3761
3762 /// Try to receive raw data with E2E safety validation (CRC + sequence tracking).
3763 ///
3764 /// Returns `Ok(Some((len, status)))` if data is available, where:
3765 /// - `len` is the number of bytes written to the buffer
3766 /// - `status` is the integrity validation result
3767 ///
3768 /// Default: delegates to `take_serialized` with no CRC info.
3769 #[cfg(feature = "safety-e2e")]
3770 fn take_validated(
3771 &mut self,
3772 buf: &mut [u8],
3773 ) -> Result<Option<(usize, crate::IntegrityStatus)>, Self::Error> {
3774 self.take_serialized(buf).map(|opt| {
3775 opt.map(|len| {
3776 (
3777 len,
3778 crate::IntegrityStatus {
3779 gap: 0,
3780 duplicate: false,
3781 crc_valid: None,
3782 },
3783 )
3784 })
3785 })
3786 }
3787
3788 /// Register an async waker to be notified when data arrives.
3789 ///
3790 /// Called from `Future::poll()` implementations to store the waker.
3791 /// The transport backend calls `waker.wake()` from its receive callback
3792 /// when new data is available, enabling event-driven async without
3793 /// busy-polling.
3794 ///
3795 /// Returns `true` when the backend will wake `waker` on an arrival, and
3796 /// `false` when it cannot (issue 1940): the caller must then poll, or
3797 /// rely on the session-wide wake, and a C caller reports
3798 /// `RCL_RET_UNSUPPORTED` rather than `OK`.
3799 ///
3800 /// Default: `false` — a backend that writes no code cannot wake anyone.
3801 fn register_waker(&self, _waker: &core::task::Waker) -> bool {
3802 false
3803 }
3804
3805 /// Return a deserialization error (implementation specific)
3806 fn deserialization_error(&self) -> Self::Error;
3807
3808 /// Phase 108 — `true` if the backend can generate this event for
3809 /// this subscriber. Default returns `false`; backends override per
3810 /// supported event kind.
3811 ///
3812 /// Subscription-side event kinds:
3813 /// [`EventKind::LivelinessChanged`](crate::event::EventKind::LivelinessChanged),
3814 /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed),
3815 /// [`EventKind::MessageLost`](crate::event::EventKind::MessageLost).
3816 /// Publisher kinds always return `false` here.
3817 fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
3818 false
3819 }
3820
3821 /// Phase 108 — register a callback fired when the named status
3822 /// event occurs. `deadline_ms` applies to
3823 /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed) only; ignored otherwise.
3824 /// Default impl returns the backend's "unsupported"-shaped error.
3825 ///
3826 /// # Safety
3827 ///
3828 /// `cb` and `user_ctx` must remain valid for the entity's
3829 /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
3830 /// responsible for keeping the closure / context arena alive.
3831 unsafe fn register_event_callback(
3832 &mut self,
3833 _kind: crate::event::EventKind,
3834 _deadline_ms: u32,
3835 _cb: crate::event::EventCallback,
3836 _user_ctx: *mut core::ffi::c_void,
3837 ) -> Result<(), Self::Error> {
3838 Err(self.unsupported_event_error())
3839 }
3840
3841 /// Phase 108 — backend's error variant for "this event kind is
3842 /// not supported." Default reuses `deserialization_error()` for
3843 /// backends that don't have a distinct `Unsupported` mapping.
3844 fn unsupported_event_error(&self) -> Self::Error {
3845 self.deserialization_error()
3846 }
3847 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
3848 /// not what the call site requested.
3849 ///
3850 /// Upstream's `rmw_subscription_get_actual_qos`. The two differ whenever a
3851 /// backend serves something other than what it was asked for, and the
3852 /// difference is exactly what answers "why is nothing arriving": a
3853 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
3854 /// history depth drops samples the caller believed were kept.
3855 ///
3856 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
3857 /// determines four policies and not the fifth returns the four and leaves
3858 /// the fifth at its `Unknown` variant. Never echo the request into a field
3859 /// you did not read back — that is the inverse of this method's meaning,
3860 /// and it is what the six C-ABI slots did until this phase.
3861 ///
3862 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
3863 /// absence — which is the honest answer for a backend with no read-back at
3864 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
3865 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
3866 fn actual_qos(&self) -> QoSProfile {
3867 QoSProfile::QOS_PROFILE_UNKNOWN
3868 }
3869
3870 /// How many publishers are MATCHED to this subscription — upstream
3871 /// `rmw_subscription_count_matched_publishers`, the vtable's
3872 /// `subscription_count_matched_publishers`. Issue 1643.
3873 ///
3874 /// The subscription half of
3875 /// [`Publisher::count_matched_subscriptions`], which states the contract:
3876 /// matched, not merely discovered; `Err(Unsupported)` by default, never a
3877 /// fabricated `0`.
3878 fn count_matched_publishers(&self) -> Result<usize, Self::Error>
3879 where
3880 Self::Error: From<TransportError>,
3881 {
3882 Err(TransportError::Unsupported.into())
3883 }
3884}
3885
3886/// Service request from a client
3887pub struct ServiceRequest<'a> {
3888 /// Raw request data (CDR encoded)
3889 pub data: &'a [u8],
3890 /// Sequence number for request/response matching
3891 pub sequence_number: i64,
3892}
3893
3894// ============================================================================
3895// Phase 99 — zero-copy raw API: SlotLending / SlotBorrowing
3896// ============================================================================
3897//
3898// Backends that can lend a slot directly into their outbound buffer
3899// (zenoh-pico w/ unstable-zenoh-api, XRCE-DDS via uxr_prepare_output_stream,
3900// full DDS w/ SHM transport) implement these traits. Backends that cannot
3901// (uORB, default zenoh-pico) do NOT impl them — `EmbeddedRawPublisher` then
3902// falls back to its per-publisher arena and memcpys at commit time. Both
3903// paths land at `Publisher::publish_raw` for the actual wire write; only
3904// the user-side copy is eliminated when lending is available.
3905//
3906// Selection is **compile-time** via the `lending` Cargo feature. Each
3907// backend crate forwards its own `lending` feature to `nros-rmw/lending`
3908// when it can satisfy the trait. nros-node's `rmw-lending` aggregates.
3909// User opting `nros/rmw-lending` w/ a non-lending backend (e.g. uORB)
3910// gets a clear compile error from the unsatisfied trait bound on the
3911// concrete `RmwPublisher`.
3912
3913/// Backend can lend a writable slot into its outbound buffer.
3914///
3915/// The returned slot's lifetime is tied to `&self`; user fills it in
3916/// place, then calls [`commit_slot`](Self::commit_slot) to publish.
3917/// Dropping the slot without commit is a no-op (slot returned to free
3918/// pool); concurrent loan attempts that would exceed backend capacity
3919/// return [`TransportError::WouldBlock`] — never block.
3920#[cfg(feature = "lending")]
3921pub trait SlotLending: Publisher {
3922 /// Backend-owned writable slot. Holds a `&'a mut [u8]` and any
3923 /// state needed for commit_slot.
3924 type Slot<'a>: AsMut<[u8]> + 'a
3925 where
3926 Self: 'a;
3927
3928 /// Reserve a writable slot of `len` bytes from the backend's
3929 /// outbound buffer. Returns `Ok(None)` if the backend has no slot
3930 /// available (full); never blocks.
3931 fn try_lend_slot(&self, len: usize) -> Result<Option<Self::Slot<'_>>, Self::Error>;
3932
3933 /// Commit a previously-lent slot. Consumes the slot and triggers
3934 /// the actual wire write. Returns `Err` on backend send failure;
3935 /// the slot's bytes are lost in that case (caller must re-lend +
3936 /// re-fill to retry).
3937 fn commit_slot(&self, slot: Self::Slot<'_>) -> Result<(), Self::Error>;
3938}
3939
3940/// Backend can lend a read-only view into its receive buffer.
3941///
3942/// The returned view's lifetime is tied to `&mut self` (subscriber-
3943/// exclusive); dropping the view releases any backend lock and lets
3944/// the next message advance into the buffer.
3945#[cfg(feature = "lending")]
3946pub trait SlotBorrowing: Subscription {
3947 /// Backend-owned read-only view. Holds a `&'a [u8]` and any state
3948 /// needed to release the borrow on Drop.
3949 type View<'a>: AsRef<[u8]> + 'a
3950 where
3951 Self: 'a;
3952
3953 /// Try to borrow the next available message in place. Returns
3954 /// `Ok(None)` if no message is ready; never blocks.
3955 fn try_borrow(&mut self) -> Result<Option<Self::View<'_>>, Self::Error>;
3956}
3957
3958/// Issues 1612 / 1632 — the size a refused take needed, as a drop log prints
3959/// it: the number, or `?` when unknown. No allocator.
3960///
3961/// [`RefusedLen::for_error`] is the one rule for WHICH refusal may carry a
3962/// size: only `BufferTooSmall`, the caller's-buffer refusal a take measures.
3963/// `MessageTooLarge` is the backend's own staging buffer refusing before any
3964/// take saw a length, so a size recorded by an earlier `BufferTooSmall` must
3965/// not be printed beside it. The C++ subscription drop log and `nros-node`'s
3966/// arena drop log both read it from here, so the two cannot disagree.
3967#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3968pub struct RefusedLen(pub Option<usize>);
3969
3970impl RefusedLen {
3971 /// The size `recorded` (a `refused_*_len()` reading) for `err`, or
3972 /// unknown when `err` is not a refusal a take measured.
3973 pub fn for_error(err: &TransportError, recorded: Option<usize>) -> Self {
3974 Self(match err {
3975 TransportError::BufferTooSmall => recorded,
3976 _ => None,
3977 })
3978 }
3979}
3980
3981impl core::fmt::Display for RefusedLen {
3982 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3983 match self.0 {
3984 Some(n) => write!(f, "{n}"),
3985 None => f.write_str("?"),
3986 }
3987 }
3988}
3989
3990/// Service server trait for handling requests.
3991///
3992/// # Threading
3993///
3994/// `&mut self` on `take_request` and `send_response` — the executor
3995/// owns the server while a request is being handled. Handler bodies
3996/// run synchronously on the executor thread; long handlers should
3997/// dispatch work to a worker queue and reply later via the recorded
3998/// `sequence_number`.
3999///
4000/// # Calling pattern
4001///
4002/// 1. Executor calls `take_request(buf)`.
4003/// 2. If `Some(req)` returned, decode, run handler, encode reply.
4004/// 3. `send_response(req.sequence_number, &reply_buf)`.
4005///
4006/// `sequence_number` is the canonical request → reply correlation
4007/// token; backends derive it from the wire-level metadata (zenoh
4008/// query id, DDS sample identity).
4009pub trait ServiceTrait {
4010 /// Error type for service operations
4011 type Error;
4012
4013 /// Check if a request is available without consuming it.
4014 ///
4015 /// Non-destructive. Default returns `true` (always assume one
4016 /// may be available); backends should override with a real
4017 /// check.
4018 fn has_request(&self) -> bool {
4019 true
4020 }
4021
4022 /// Phase 122.3.c.6.e — register a `Waker` for event-driven
4023 /// service servers. Mirrors the matching method on
4024 /// `SubscriberTrait` / `ClientTrait`. Backends that
4025 /// surface incoming-request notifications wake `waker` when
4026 /// `has_request()` flips true. Returns `false` when the backend cannot
4027 /// (issue 1940); the caller then falls back to polling. Default: `false`.
4028 fn register_waker(&self, _waker: &core::task::Waker) -> bool {
4029 false
4030 }
4031
4032 /// Try to receive a service request into `buf` (non-blocking).
4033 ///
4034 /// On success returns a `ServiceRequest` that borrows from
4035 /// `buf`. The borrow is released when the returned struct is
4036 /// dropped — typically before `send_response` is called, since
4037 /// `send_response` takes `&mut self`.
4038 fn take_request<'a>(
4039 &mut self,
4040 buf: &'a mut [u8],
4041 ) -> Result<Option<ServiceRequest<'a>>, Self::Error>;
4042
4043 /// Issue 1632 — how many bytes the request the most recent
4044 /// [`take_request`](Self::take_request) REFUSED as too big for the
4045 /// caller's buffer needed.
4046 ///
4047 /// [`Subscription::refused_sample_len`]'s contract, for the service
4048 /// server: read it right after a take returns `BufferTooSmall`; `None`
4049 /// means the backend does not know (the default), never "zero bytes".
4050 fn refused_request_len(&self) -> Option<usize> {
4051 None
4052 }
4053
4054 /// Send a reply for the given sequence number. Non-blocking
4055 /// from the application's perspective; the backend may queue
4056 /// the reply for transport-level transmission.
4057 fn send_response(&mut self, sequence_number: i64, data: &[u8]) -> Result<(), Self::Error>;
4058
4059 /// Issue 1942 — the per-request metadata of the request the most recent
4060 /// successful [`take_request`](Self::take_request) returned: client GID +
4061 /// client sequence number, and the timestamps the backend can supply.
4062 ///
4063 /// rclrs's `ServiceInfo`. Read it after a take and before the next one.
4064 /// **A field the backend cannot report is `None`**, and the default — for
4065 /// a backend with no per-request metadata at all — is
4066 /// [`ServiceInfo::NOT_REPORTED`](nros_core::ServiceInfo::NOT_REPORTED),
4067 /// never a zeroed or invented value (the `MessageInfo` precedent).
4068 ///
4069 /// Not [`ServiceRequest::sequence_number`]: that is the token this backend
4070 /// replies against, which on zenoh is a local reply-slot index, while
4071 /// `request_id.sequence_number` is the number the CLIENT assigned.
4072 fn request_info(&self) -> nros_core::ServiceInfo {
4073 nros_core::ServiceInfo::NOT_REPORTED
4074 }
4075
4076 /// Handle a service request with typed messages
4077 fn handle_request<S: RosService>(
4078 &mut self,
4079 req_buf: &mut [u8],
4080 reply_buf: &mut [u8],
4081 handler: impl FnOnce(&S::Request) -> S::Reply,
4082 ) -> Result<bool, Self::Error>
4083 where
4084 Self::Error: From<TransportError>,
4085 {
4086 self.handle_request_with_info::<S>(req_buf, reply_buf, |req, _info| handler(req))
4087 }
4088
4089 /// Issue 1942 — [`handle_request`](Self::handle_request), with the
4090 /// request's [`ServiceInfo`](nros_core::ServiceInfo) (read through
4091 /// [`request_info`](Self::request_info) right after the take) handed to
4092 /// the handler beside it. rclrs's `(request, ServiceInfo)` callback.
4093 fn handle_request_with_info<S: RosService>(
4094 &mut self,
4095 req_buf: &mut [u8],
4096 reply_buf: &mut [u8],
4097 handler: impl FnOnce(&S::Request, &nros_core::ServiceInfo) -> S::Reply,
4098 ) -> Result<bool, Self::Error>
4099 where
4100 Self::Error: From<TransportError>,
4101 {
4102 use nros_core::{CdrReader, CdrWriter};
4103
4104 // First, try to receive a request and extract necessary data.
4105 // Capture the data slice's offset within `req_buf` so we can
4106 // re-borrow it after the `ServiceRequest` (which holds a
4107 // borrow into `req_buf`) is dropped. Some backends prepend a
4108 // header (DDS: 8-byte sequence number) and place the CDR
4109 // payload at a non-zero offset in the buffer; others (zenoh)
4110 // put it at offset 0. Reading from offset 0 unconditionally
4111 // would feed the prefix bytes to the CDR deserializer and
4112 // silently corrupt the request.
4113 let buf_start = req_buf.as_ptr() as usize;
4114 let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
4115 Some(request) => {
4116 let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
4117 (offset, request.data.len(), request.sequence_number)
4118 }
4119 None => return Ok(false),
4120 };
4121
4122 // Issue 1942 — the take that just succeeded is the one this describes.
4123 let info = self.request_info();
4124
4125 // Deserialize request from the captured offset.
4126 let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
4127 .map_err(|_| TransportError::DeserializationError)?;
4128 let req = S::Request::deserialize(&mut reader)
4129 .map_err(|_| TransportError::DeserializationError)?;
4130
4131 // Call handler
4132 let reply = handler(&req, &info);
4133
4134 // Serialize reply
4135 let mut writer =
4136 CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
4137 reply
4138 .serialize(&mut writer)
4139 .map_err(|_| TransportError::SerializationError)?;
4140 let len = writer.position();
4141
4142 // Send reply (now we can borrow self mutably again)
4143 self.send_response(sequence_number, &reply_buf[..len])?;
4144 Ok(true)
4145 }
4146
4147 /// Handle a service request where the handler returns `Box<S::Reply>`
4148 ///
4149 /// Identical to `handle_request` but the handler returns a heap-allocated reply.
4150 /// This is needed for services with large response types (e.g., parameter services
4151 /// where `Vec<ParameterValue, 64>` is ~1MB+) that would overflow the stack.
4152 #[cfg(feature = "alloc")]
4153 fn handle_request_boxed<S: RosService>(
4154 &mut self,
4155 req_buf: &mut [u8],
4156 reply_buf: &mut [u8],
4157 handler: impl FnOnce(&S::Request) -> alloc::boxed::Box<S::Reply>,
4158 ) -> Result<bool, Self::Error>
4159 where
4160 Self::Error: From<TransportError>,
4161 {
4162 use nros_core::{CdrReader, CdrWriter};
4163
4164 let buf_start = req_buf.as_ptr() as usize;
4165 let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
4166 Some(request) => {
4167 let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
4168 (offset, request.data.len(), request.sequence_number)
4169 }
4170 None => return Ok(false),
4171 };
4172
4173 let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
4174 .map_err(|_| TransportError::DeserializationError)?;
4175 let req = S::Request::deserialize(&mut reader)
4176 .map_err(|_| TransportError::DeserializationError)?;
4177
4178 let reply = handler(&req);
4179
4180 let mut writer =
4181 CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
4182 reply
4183 .serialize(&mut writer)
4184 .map_err(|_| TransportError::SerializationError)?;
4185 let len = writer.position();
4186
4187 self.send_response(sequence_number, &reply_buf[..len])?;
4188 Ok(true)
4189 }
4190
4191 /// Handle one request by STREAMING: the handler reads fields off the wire
4192 /// and writes the reply's fields straight back, so neither the request nor
4193 /// the reply is ever materialised as a value.
4194 ///
4195 /// phase-382 W1'. `handle_request_boxed` above boxes the REPLY because the
4196 /// parameter responses are enormous — `GetParametersResponse` measures
4197 /// 1,176,072 bytes. It does not box the REQUEST, which is deserialized by
4198 /// value into a stack local one line above the handler, and
4199 /// `SetParametersRequest` measures **1,192,968 bytes**. So every
4200 /// `ros2 param set` against a node put a 1.19 MB local on the calling
4201 /// task's stack — larger than the reply the boxing exists for, on every
4202 /// platform, with `param-services` live on Zephyr.
4203 ///
4204 /// Streaming removes both, and removes the `alloc` requirement with them:
4205 /// the whole value is never needed, because serialisation happens on the
4206 /// line after construction. Three things make it safe rather than clever:
4207 ///
4208 /// * **No `rcl_interfaces` message uses a DHEADER** — every `Serialize` impl
4209 /// is plain sequential CDR, so hand-written field writes are byte-identical
4210 /// to the generated ones. (If a future message gains XCDR2 extensibility
4211 /// this stops being true for THAT message; see RFC-0055.)
4212 /// * `req_buf` and `reply_buf` are disjoint, so a handler can hold the
4213 /// reader and the writer at once.
4214 /// * `CdrReader::read_string` borrows out of `req_buf` rather than copying,
4215 /// so a handler can look a name up without a buffer of its own.
4216 ///
4217 /// The cost is that the hand-written writes can drift from the generated
4218 /// `Serialize`. Guard it with a round-trip test that deserialises the
4219 /// streamed bytes back into the generated type — the by-value handler makes
4220 /// a good test-only oracle.
4221 ///
4222 /// No `alloc`, deliberately: this is the seam that lets `param-services` and
4223 /// `lifecycle-services` build without it.
4224 fn handle_request_raw(
4225 &mut self,
4226 req_buf: &mut [u8],
4227 reply_buf: &mut [u8],
4228 handler: impl FnOnce(
4229 &mut nros_core::CdrReader<'_>,
4230 &mut nros_core::CdrWriter<'_>,
4231 ) -> Result<(), TransportError>,
4232 ) -> Result<bool, Self::Error>
4233 where
4234 Self::Error: From<TransportError>,
4235 {
4236 use nros_core::{CdrReader, CdrWriter};
4237
4238 let buf_start = req_buf.as_ptr() as usize;
4239 let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
4240 Some(request) => {
4241 let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
4242 (offset, request.data.len(), request.sequence_number)
4243 }
4244 None => return Ok(false),
4245 };
4246
4247 // Split the borrow so the reader (over `req_buf`) and the writer (over
4248 // `reply_buf`) can be live at the same time. They are separate
4249 // parameters, so this is disjoint by construction.
4250 let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
4251 .map_err(|_| TransportError::DeserializationError)?;
4252 let mut writer =
4253 CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
4254
4255 handler(&mut reader, &mut writer)?;
4256 let len = writer.position();
4257
4258 self.send_response(sequence_number, &reply_buf[..len])?;
4259 Ok(true)
4260 }
4261 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
4262 /// not what the call site requested.
4263 ///
4264 /// Upstream's `rmw_service_request_subscription_get_actual_qos`. The two differ whenever a
4265 /// backend serves something other than what it was asked for, and the
4266 /// difference is exactly what answers "why is nothing arriving": a
4267 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
4268 /// history depth drops samples the caller believed were kept.
4269 ///
4270 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
4271 /// determines four policies and not the fifth returns the four and leaves
4272 /// the fifth at its `Unknown` variant. Never echo the request into a field
4273 /// you did not read back — that is the inverse of this method's meaning,
4274 /// and it is what the six C-ABI slots did until this phase.
4275 ///
4276 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
4277 /// absence — which is the honest answer for a backend with no read-back at
4278 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
4279 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
4280 fn request_subscription_actual_qos(&self) -> QoSProfile {
4281 QoSProfile::QOS_PROFILE_UNKNOWN
4282 }
4283
4284 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
4285 /// not what the call site requested.
4286 ///
4287 /// Upstream's `rmw_service_response_publisher_get_actual_qos`. The two differ whenever a
4288 /// backend serves something other than what it was asked for, and the
4289 /// difference is exactly what answers "why is nothing arriving": a
4290 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
4291 /// history depth drops samples the caller believed were kept.
4292 ///
4293 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
4294 /// determines four policies and not the fifth returns the four and leaves
4295 /// the fifth at its `Unknown` variant. Never echo the request into a field
4296 /// you did not read back — that is the inverse of this method's meaning,
4297 /// and it is what the six C-ABI slots did until this phase.
4298 ///
4299 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
4300 /// absence — which is the honest answer for a backend with no read-back at
4301 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
4302 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
4303 fn response_publisher_actual_qos(&self) -> QoSProfile {
4304 QoSProfile::QOS_PROFILE_UNKNOWN
4305 }
4306}
4307
4308/// Decode a CDR service reply (encapsulation header included) — the typed
4309/// half [`ClientTrait::take_response`] and [`ClientTrait::take_response_for`]
4310/// share.
4311fn decode_reply<S: RosService>(bytes: &[u8]) -> Result<S::Reply, TransportError> {
4312 use nros_core::CdrReader;
4313
4314 let mut reader =
4315 CdrReader::new_with_header(bytes).map_err(|_| TransportError::DeserializationError)?;
4316 S::Reply::deserialize(&mut reader).map_err(|_| TransportError::DeserializationError)
4317}
4318
4319/// Service client trait for sending requests.
4320///
4321/// # Threading
4322///
4323/// `&mut self` on every method — the client is single-owner. For
4324/// fan-out request patterns, create one client per worker thread.
4325///
4326/// # Calling pattern
4327///
4328/// All in-tree backends route blocking waits through the executor:
4329///
4330/// 1. `send_request_raw(buf)` — non-blocking; returns once the
4331/// request is queued for transmission.
4332/// 2. The executor's `drive_io` runs.
4333/// 3. `take_response_raw(buf)` — non-blocking; returns
4334/// `Ok(Some(len))` when the reply is back.
4335///
4336/// Phase-301 (issue 0240): the deprecated blocking `call_raw` path is
4337/// DELETED — `send_request_raw` + `take_response_raw` is the one
4338/// request/reply path, and both are required for service-capable
4339/// backends.
4340pub trait ClientTrait {
4341 /// Error type for service operations
4342 type Error;
4343
4344 /// Send a service request without waiting for a reply (non-blocking).
4345 ///
4346 /// Returns the SEQUENCE ID the backend assigned. The caller must
4347 /// subsequently poll [`take_response_raw`](Self::take_response_raw) and
4348 /// match that id against the one the reply carries.
4349 ///
4350 /// Issue 0778 — this returned `()` until 2026-08-25, and every backend
4351 /// computed an id and discarded it. With nothing to correlate by, a client
4352 /// with two calls outstanding could not tell the replies apart, so each
4353 /// backend picked a policy: cyclonedds abandoned the older request, zenoh
4354 /// took the first reply. Both are wrong for `send_goal` and
4355 /// `SetParameters`, which travel this path and are not idempotent.
4356 fn send_request_raw(&mut self, request: &[u8]) -> Result<i64, Self::Error>;
4357
4358 /// Poll for a reply (non-blocking).
4359 ///
4360 /// Returns `Ok(Some((len, sequence_id)))` when a reply has arrived,
4361 /// `Ok(None)` if not yet available, or `Err` on failure. The
4362 /// `sequence_id` is the one [`send_request_raw`](Self::send_request_raw)
4363 /// returned for the request this answers.
4364 ///
4365 /// It used to say "a reply to the MOST RECENTLY sent request", which was
4366 /// the single-outstanding-call assumption written into the contract.
4367 fn take_response_raw(
4368 &mut self,
4369 reply_buf: &mut [u8],
4370 ) -> Result<Option<(usize, i64)>, Self::Error>;
4371
4372 /// Issue 1632 — how many bytes the reply the most recent
4373 /// [`take_response_raw`](Self::take_response_raw) REFUSED as too big for
4374 /// the caller's buffer needed. [`Subscription::refused_sample_len`]'s
4375 /// contract, for the client: `None` is "the backend does not know".
4376 fn refused_response_len(&self) -> Option<usize> {
4377 None
4378 }
4379
4380 /// Issue 2136 — the metadata of the reply the most recent successful
4381 /// [`take_response_raw`](Self::take_response_raw) returned: upstream
4382 /// `rmw_take_response`'s `rmw_service_info_t`, the client twin of
4383 /// [`ServiceTrait::request_info`].
4384 ///
4385 /// - `request_id` — the request this reply answers: this client's own GID
4386 /// and the sequence number of its request;
4387 /// - `source_timestamp` — when the SERVER sent the reply, on its clock;
4388 /// - `received_timestamp` — when this client received it, on its clock.
4389 ///
4390 /// Read it after a take and before the next one. **A field the backend
4391 /// cannot report is `None`**, and the default — for a backend with no
4392 /// per-reply metadata at all — is
4393 /// [`ServiceInfo::NOT_REPORTED`](nros_core::ServiceInfo::NOT_REPORTED).
4394 fn response_info(&self) -> nros_core::ServiceInfo {
4395 nros_core::ServiceInfo::NOT_REPORTED
4396 }
4397
4398 /// Send a typed service request without waiting for a reply (non-blocking).
4399 ///
4400 /// Serializes the request into `req_buf` and calls
4401 /// [`send_request_raw`](Self::send_request_raw), returning the SEQUENCE ID
4402 /// the backend assigned — [`crate::SEQUENCE_ID_UNKNOWN`] on a backend that
4403 /// cannot assign one. Hand it to
4404 /// [`take_response_for`](Self::take_response_for) so this call takes only
4405 /// its own reply.
4406 ///
4407 /// Issue 2033 — this returned `()` after issue 2009, discarding the id the
4408 /// raw path had carried since issue 0778, so a typed caller had nothing to
4409 /// correlate by.
4410 fn send_request<S: RosService>(
4411 &mut self,
4412 request: &S::Request,
4413 req_buf: &mut [u8],
4414 ) -> Result<i64, Self::Error>
4415 where
4416 Self::Error: From<TransportError>,
4417 {
4418 use nros_core::CdrWriter;
4419
4420 let mut writer =
4421 CdrWriter::new_with_header(req_buf).map_err(|_| TransportError::BufferTooSmall)?;
4422 request
4423 .serialize(&mut writer)
4424 .map_err(|_| TransportError::SerializationError)?;
4425 let req_len = writer.position();
4426
4427 self.send_request_raw(&req_buf[..req_len])
4428 }
4429
4430 /// Poll for the next typed reply (non-blocking), WHICHEVER request it
4431 /// answers.
4432 ///
4433 /// Returns `Ok(Some((reply, sequence_id)))`: `sequence_id` is the id
4434 /// [`send_request`](Self::send_request) returned for the request this
4435 /// reply answers, and the caller decides whom it belongs to — upstream's
4436 /// `rcl_take_response` request header. A caller with ONE request in mind
4437 /// wants [`take_response_for`](Self::take_response_for), which drops the
4438 /// replies of requests it abandoned.
4439 ///
4440 /// Issue 2033 — this used to return the reply alone and was documented as
4441 /// "the reply to the most recently sent request", which it never checked:
4442 /// after a resend or a timeout, the abandoned request's late reply was
4443 /// handed to the new one.
4444 fn take_response<S: RosService>(
4445 &mut self,
4446 reply_buf: &mut [u8],
4447 ) -> Result<Option<(S::Reply, i64)>, Self::Error>
4448 where
4449 Self::Error: From<TransportError>,
4450 {
4451 match self.take_response_raw(reply_buf)? {
4452 Some((len, seq)) => Ok(Some((decode_reply::<S>(&reply_buf[..len])?, seq))),
4453 None => Ok(None),
4454 }
4455 }
4456
4457 /// Poll for the typed reply to the request `sequence_id` names
4458 /// (non-blocking), dropping any reply that answers a different one.
4459 ///
4460 /// `sequence_id` is what [`send_request`](Self::send_request) returned.
4461 /// The rule is [`crate::correlation`]'s, the one every other client
4462 /// surface applies (issue 2009): a reply is delivered only to the request
4463 /// whose id it carries, a stale reply is DROPPED (at most
4464 /// [`crate::MAX_STALE_DRAIN_PER_POLL`] per call), and an id-less reply or
4465 /// request ([`crate::SEQUENCE_ID_UNKNOWN`]) matches the sole request in
4466 /// mind. `Ok(None)` — nothing for this request yet.
4467 fn take_response_for<S: RosService>(
4468 &mut self,
4469 sequence_id: i64,
4470 reply_buf: &mut [u8],
4471 ) -> Result<Option<S::Reply>, Self::Error>
4472 where
4473 Self::Error: From<TransportError>,
4474 {
4475 let mut pending = crate::RequestCorrelator::<(), 1>::new();
4476 // A one-entry table cannot be full on its first insert.
4477 let _ = pending.insert(sequence_id, ());
4478 match crate::take_correlated(self, reply_buf, &mut pending)? {
4479 Some((len, _seq, ())) => Ok(Some(decode_reply::<S>(&reply_buf[..len])?)),
4480 None => Ok(None),
4481 }
4482 }
4483
4484 /// Register an async waker to be notified when a reply arrives.
4485 ///
4486 /// Called from `Future::poll()` implementations to store the waker.
4487 /// The transport backend calls `waker.wake()` from its reply callback
4488 /// when a response is available, enabling event-driven async without
4489 /// busy-polling.
4490 ///
4491 /// Returns `false` when the backend cannot wake `waker` (issue 1940);
4492 /// the caller then polls. Default: `false`.
4493 fn register_waker(&self, _waker: &core::task::Waker) -> bool {
4494 false
4495 }
4496
4497 // phase-428 W13 — the ~~`start_server_discovery`~~ / ~~`poll_server_discovery`~~
4498 // pair was DELETED here. It existed because the only zenoh backend could
4499 // answer "is a server up?" only by issuing a `z_liveliness_get`, which is
4500 // start-then-poll, so the synchronous `service_is_ready` below had nothing
4501 // to return but a latch (issue 1087). The pair had no vtable slot, so every
4502 // C-ABI backend inherited its default and `wait_for_service` answered
4503 // without asking. The zenoh backend now keeps a matched-server SET fed by
4504 // its liveliness subscriber — the state upstream's DDS discovery cache
4505 // keeps — and `service_is_ready` reads it synchronously; the wait loops
4506 // spin on that one query, and nothing needs the pair.
4507
4508 /// Whether a matching service server is currently discoverable.
4509 ///
4510 /// Mirrors `rclcpp::ClientBase::service_is_ready` — the NAME is upstream's.
4511 /// The SHAPE is `rcl`'s: `rcl_service_server_is_available(node, client,
4512 /// bool *is_available)` returns `RCL_RET_OK` "if the check was made
4513 /// successfully (regardless of the service readiness)", i.e. the return
4514 /// code says whether the CHECK worked and the out-param carries the ANSWER.
4515 /// rclcpp collapses that to a bare `bool` and moves the error to
4516 /// exceptions; RFC-0018 forbids exceptions, so `Result<bool, _>` is how the
4517 /// same contract is expressed here (phase-379 W6, RFC-0036).
4518 ///
4519 /// Returns `Ok(true)` if at least one matching server is CURRENTLY
4520 /// discoverable, `Ok(false)` if none is, or `Err(_)` if the backend
4521 /// cannot answer (e.g. XRCE — micro-XRCE-DDS-Client has no participant
4522 /// enumeration).
4523 ///
4524 /// CURRENT, not latched: upstream `rmw.h` says the outcome reflects a
4525 /// change in either direction, and `rclcpp::ClientBase::service_is_ready`
4526 /// calls `rcl_service_server_is_available` on every invocation. A backend
4527 /// answers from the discovery state it maintains — the DDS built-in
4528 /// topics, or the zenoh matched-server set fed by liveliness PUT/DELETE
4529 /// (phase-428 W13) — so a server that goes away reads unavailable again.
4530 /// Every wait loop in the tree (`wait_for_service`,
4531 /// `wait_for_action_server`, their C and C++ bindings) is a spin on this
4532 /// one method; there is no separate asynchronous probe (issue 1087).
4533 ///
4534 /// Those are three answers, not two, and keeping them apart is the whole
4535 /// point: `is_server_ready` used to sit beside this method collapsing
4536 /// "don't know" and "no server" into one `false`, its trait default was
4537 /// `true`, and only zenoh overrode it — so on every other image
4538 /// `wait_for_service` returned success without probing anything. Issue
4539 /// 1008 deleted it rather than fixing it, because a caller cannot recover
4540 /// from an answer that does not distinguish the two.
4541 ///
4542 /// User-facing surface: `Client<S>::server_available()` in Rust,
4543 /// `rclc_client_server_available()` in C/C++. Clients use this
4544 /// to gate the first request so a startup-ordering race
4545 /// (client opens before server's discovery announcement lands)
4546 /// doesn't surface as a request-side timeout.
4547 ///
4548 /// Default impl: `Err(TransportError::Unsupported)` — backends
4549 /// that support graph introspection (zenoh queryable interest,
4550 /// DDS built-in topic readers) opt in by overriding.
4551 fn service_is_ready(&self) -> Result<bool, Self::Error>
4552 where
4553 Self::Error: From<TransportError>,
4554 {
4555 Err(TransportError::Unsupported.into())
4556 }
4557
4558 /// This client's own global identifier — upstream (Jazzy)
4559 /// `rmw_get_gid_for_client`, the vtable's `get_gid_for_client`. Issue
4560 /// 1978.
4561 ///
4562 /// The client twin of [`Publisher::get_gid`]: the identity a server keys
4563 /// a request id on (zenoh: the gid stamped into every request
4564 /// attachment; Cyclone: the request writer's GUID), zero-extended to
4565 /// [`PUBLISHER_GID_SIZE`](nros_core::PUBLISHER_GID_SIZE) by
4566 /// [`pad_publisher_gid`](nros_core::pad_publisher_gid).
4567 ///
4568 /// The default is `Err(Unsupported)`, never an all-zero id — the same
4569 /// discipline, for the same reason, as `Publisher::get_gid`.
4570 fn get_gid(&self) -> Result<[u8; nros_core::PUBLISHER_GID_SIZE], Self::Error>
4571 where
4572 Self::Error: From<TransportError>,
4573 {
4574 Err(TransportError::Unsupported.into())
4575 }
4576
4577 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
4578 /// not what the call site requested.
4579 ///
4580 /// Upstream's `rmw_client_request_publisher_get_actual_qos`. The two differ whenever a
4581 /// backend serves something other than what it was asked for, and the
4582 /// difference is exactly what answers "why is nothing arriving": a
4583 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
4584 /// history depth drops samples the caller believed were kept.
4585 ///
4586 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
4587 /// determines four policies and not the fifth returns the four and leaves
4588 /// the fifth at its `Unknown` variant. Never echo the request into a field
4589 /// you did not read back — that is the inverse of this method's meaning,
4590 /// and it is what the six C-ABI slots did until this phase.
4591 ///
4592 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
4593 /// absence — which is the honest answer for a backend with no read-back at
4594 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
4595 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
4596 fn request_publisher_actual_qos(&self) -> QoSProfile {
4597 QoSProfile::QOS_PROFILE_UNKNOWN
4598 }
4599
4600 /// The profile this entity is ACTUALLY running — what the backend GRANTED,
4601 /// not what the call site requested.
4602 ///
4603 /// Upstream's `rmw_client_response_subscription_get_actual_qos`. The two differ whenever a
4604 /// backend serves something other than what it was asked for, and the
4605 /// difference is exactly what answers "why is nothing arriving": a
4606 /// RELIABLE reader does not match a BEST_EFFORT writer, and a clamped
4607 /// history depth drops samples the caller believed were kept.
4608 ///
4609 /// **Per-field, and an unreportable field is an ABSENCE.** A backend that
4610 /// determines four policies and not the fifth returns the four and leaves
4611 /// the fifth at its `Unknown` variant. Never echo the request into a field
4612 /// you did not read back — that is the inverse of this method's meaning,
4613 /// and it is what the six C-ABI slots did until this phase.
4614 ///
4615 /// The default is [`QoSProfile::QOS_PROFILE_UNKNOWN`] — every field an
4616 /// absence — which is the honest answer for a backend with no read-back at
4617 /// all (XRCE has none; zenoh-pico's QoS is per-message flags). It is NOT
4618 /// the request, and it is NOT `QOS_PROFILE_DEFAULT`.
4619 fn response_subscription_actual_qos(&self) -> QoSProfile {
4620 QoSProfile::QOS_PROFILE_UNKNOWN
4621 }
4622}
4623
4624/// Transport backend trait (legacy).
4625///
4626/// Use [`Rmw`] for new code. This trait is retained for backward compatibility
4627/// with existing code that uses [`TransportConfig`] directly.
4628pub trait Transport {
4629 /// Error type for this transport
4630 type Error;
4631 /// Session type for this transport
4632 type Session: Session;
4633
4634 /// Open a new session with the given configuration
4635 fn open(config: &TransportConfig) -> Result<Self::Session, Self::Error>;
4636}
4637
4638/// Factory trait for compile-time middleware selection.
4639///
4640/// Embedded crates select a backend via feature flag:
4641/// ```rust,ignore
4642/// #[cfg(feature = "rmw-cffi")]
4643/// type DefaultRmw = nros_rmw_cffi::CffiRmw;
4644/// ```
4645///
4646/// Each backend provides its own `Rmw` implementation that bridges
4647/// from the middleware-agnostic [`RmwConfig`] to backend-specific
4648/// initialization.
4649///
4650/// Phase 84.E2: `open` consumes `self`. Backends carry their own
4651/// configuration (agent addresses, serial ports, TLS CA slots)
4652/// inside the factory value and hand that over to the session at
4653/// `open` time. In-tree, every backend sits behind the C vtable, so the one
4654/// production impl is `CffiRmw`, spelled `CffiRmw::default().open(&config)`.
4655pub trait Rmw {
4656 /// Session type returned by [`open`](Rmw::open)
4657 type Session: Session;
4658 /// Error type for session creation
4659 type Error: core::fmt::Debug;
4660
4661 /// Open a new middleware session with the given configuration.
4662 ///
4663 /// The backend maps [`RmwConfig`] fields to its own connection
4664 /// parameters (e.g., zenoh locator and session mode, XRCE-DDS
4665 /// agent address). Any backend-specific pre-open state stored
4666 /// on `self` (e.g. configured agent IP / port) is moved into the
4667 /// returned `Session`.
4668 fn open(self, config: &RmwConfig) -> Result<Self::Session, Self::Error>;
4669}
4670
4671#[cfg(test)]
4672mod tests {
4673 use super::*;
4674
4675 /// phase-515 R8 -- rclrs's `*_duration` QoS builders, in the record's ms.
4676 #[test]
4677 fn qos_duration_builders_store_whole_milliseconds() {
4678 use core::time::Duration;
4679 let q = QoSProfile::new()
4680 .deadline_duration(Duration::from_millis(250))
4681 .lifespan_duration(Duration::from_secs(2))
4682 .liveliness_lease_duration(Duration::from_micros(1500));
4683 assert_eq!(q.deadline_ms, 250);
4684 assert_eq!(q.lifespan_ms, 2000);
4685 assert_eq!(q.liveliness_lease_ms, 1, "1.5 ms truncates to whole ms");
4686 // Sub-millisecond is never "infinite" (0) by accident.
4687 assert_eq!(
4688 QoSProfile::new()
4689 .deadline_duration(Duration::from_nanos(1))
4690 .deadline_ms,
4691 1
4692 );
4693 assert_eq!(
4694 QoSProfile::new()
4695 .deadline_duration(Duration::ZERO)
4696 .deadline_ms,
4697 0
4698 );
4699 assert_eq!(
4700 QoSProfile::new()
4701 .lifespan_duration(Duration::from_secs(u64::MAX))
4702 .lifespan_ms,
4703 u32::MAX,
4704 "saturates"
4705 );
4706 }
4707
4708 /// issue 0783 -- the variants with a payload render it; the rest render
4709 /// their fixed line. `heapless`, so the check runs without `alloc`.
4710 #[test]
4711 fn transport_error_display_renders_payloads() {
4712 use core::fmt::Write;
4713 fn show(e: &TransportError) -> heapless::String<64> {
4714 let mut s = heapless::String::new();
4715 write!(s, "{e}").unwrap();
4716 s
4717 }
4718 assert_eq!(show(&TransportError::Timeout).as_str(), "timed out");
4719 assert_eq!(
4720 show(&TransportError::IncompatibleQos(QoSPolicyMask::RELIABILITY)).as_str(),
4721 "incompatible QoS: RELIABILITY"
4722 );
4723 assert_eq!(
4724 show(&TransportError::IncompatibleQos(QoSPolicyMask::NONE)).as_str(),
4725 "incompatible QoS"
4726 );
4727 assert_eq!(
4728 show(&TransportError::Backend("z_open refused")).as_str(),
4729 "backend error: z_open refused"
4730 );
4731 }
4732
4733 /// Issue 0971 — the DEFAULT `take_sequence` body must report a partial
4734 /// count rather than discard it, which is what its own doc comment says and
4735 /// what `?` did not do.
4736 ///
4737 /// This is the default's negative control: a backend that takes two
4738 /// messages and then errors must hand the caller those two. Before the fix
4739 /// the `?` threw them away and the caller could not tell two messages had
4740 /// been consumed — they were gone from the queue and absent from the
4741 /// result.
4742 mod take_sequence_default {
4743 use super::*;
4744
4745 #[derive(Debug, PartialEq)]
4746 struct Boom;
4747
4748 /// Yields `n` one-byte messages, then errors forever.
4749 struct ThenErrors {
4750 left: usize,
4751 }
4752
4753 impl Subscription for ThenErrors {
4754 type Error = Boom;
4755
4756 fn take_serialized(&mut self, buf: &mut [u8]) -> Result<Option<usize>, Self::Error> {
4757 if self.left == 0 {
4758 return Err(Boom);
4759 }
4760 self.left -= 1;
4761 buf[0] = 0xAB;
4762 Ok(Some(1))
4763 }
4764
4765 fn deserialization_error(&self) -> Self::Error {
4766 Boom
4767 }
4768 }
4769
4770 #[test]
4771 fn a_partial_drain_reports_its_count_instead_of_the_error() {
4772 let mut sub = ThenErrors { left: 2 };
4773 let mut buf = [0u8; 4 * 8];
4774 let mut lens = [0usize; 4];
4775
4776 // Two messages are available, the third call errors.
4777 let got = sub.take_sequence(&mut buf, 8, 4, &mut lens);
4778
4779 assert_eq!(
4780 got,
4781 Ok(2),
4782 "a drain that took messages must report them; `?` discarded the \
4783 count and the caller lost two consumed messages"
4784 );
4785 assert_eq!(&lens[..2], &[1, 1]);
4786 }
4787
4788 #[test]
4789 fn an_error_with_nothing_taken_is_returned_immediately() {
4790 let mut sub = ThenErrors { left: 0 };
4791 let mut buf = [0u8; 4 * 8];
4792 let mut lens = [0usize; 4];
4793
4794 // No count to protect, so the caller should see the error itself
4795 // rather than an ambiguous `Ok(0)`.
4796 assert_eq!(sub.take_sequence(&mut buf, 8, 4, &mut lens), Err(Boom));
4797 }
4798 }
4799
4800 #[test]
4801 fn test_topic_info() {
4802 let topic = TopicInfo::new("/chatter", "std_msgs::msg::dds_::String_", "abc123");
4803 assert_eq!(topic.name, "/chatter");
4804 assert_eq!(topic.domain_id, 0);
4805 }
4806
4807 #[test]
4808 fn qos_apply_overrides_matches_topic_and_role() {
4809 // Default is Reliable / Volatile / KeepLast(10).
4810 static OVERRIDES: &[QoSOverride] = &[
4811 QoSOverride {
4812 topic: "/chatter",
4813 role: QoSOverrideRole::Publisher,
4814 value: QoSOverrideValue::Reliability(QoSReliabilityPolicy::BestEffort),
4815 },
4816 QoSOverride {
4817 topic: "/chatter",
4818 role: QoSOverrideRole::Publisher,
4819 value: QoSOverrideValue::Depth(5),
4820 },
4821 QoSOverride {
4822 topic: "/scan",
4823 role: QoSOverrideRole::Subscription,
4824 value: QoSOverrideValue::Durability(QoSDurabilityPolicy::TransientLocal),
4825 },
4826 ];
4827
4828 // Matching topic + publisher role → reliability + depth applied.
4829 let pub_qos = QoSProfile::default().apply_overrides(
4830 "/chatter",
4831 QoSOverrideRole::Publisher,
4832 OVERRIDES,
4833 );
4834 assert_eq!(pub_qos.reliability, QoSReliabilityPolicy::BestEffort);
4835 assert_eq!(pub_qos.depth, 5);
4836 assert_eq!(pub_qos.durability, QoSDurabilityPolicy::Volatile); // untouched
4837
4838 // Same topic but subscription role → publisher overrides DON'T apply;
4839 // the /scan override is for a different topic → also no change.
4840 let sub_qos = QoSProfile::default().apply_overrides(
4841 "/chatter",
4842 QoSOverrideRole::Subscription,
4843 OVERRIDES,
4844 );
4845 assert_eq!(sub_qos, QoSProfile::default());
4846
4847 // The /scan subscription override applies only to /scan+subscription.
4848 let scan_qos = QoSProfile::default().apply_overrides(
4849 "/scan",
4850 QoSOverrideRole::Subscription,
4851 OVERRIDES,
4852 );
4853 assert_eq!(scan_qos.durability, QoSDurabilityPolicy::TransientLocal);
4854
4855 // Empty table → identity (the zero-override fast path).
4856 assert_eq!(
4857 QoSProfile::default().apply_overrides("/x", QoSOverrideRole::Publisher, &[]),
4858 QoSProfile::default()
4859 );
4860 }
4861
4862 #[test]
4863 fn test_qos_defaults() {
4864 let qos = QoSProfile::default();
4865 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
4866 }
4867
4868 #[test]
4869 fn test_action_info() {
4870 let action = ActionInfo::new(
4871 "/fibonacci",
4872 "example_interfaces::action::dds_::Fibonacci_",
4873 "abc123",
4874 );
4875 assert_eq!(action.name, "/fibonacci");
4876 assert_eq!(action.domain_id, 0);
4877 }
4878
4879 #[test]
4880 fn test_action_info_with_domain() {
4881 let action = ActionInfo::new(
4882 "/fibonacci",
4883 "example_interfaces::action::dds_::Fibonacci_",
4884 "abc123",
4885 )
4886 .with_domain(42);
4887 assert_eq!(action.domain_id, 42);
4888 }
4889
4890 #[test]
4891 fn test_action_send_goal_key() {
4892 let action = ActionInfo::new(
4893 "/fibonacci",
4894 "example_interfaces::action::dds_::Fibonacci_",
4895 "abc123",
4896 )
4897 .with_domain(0);
4898
4899 let key: heapless::String<256> = action.send_goal_key();
4900 // ActionInfo returns the sub-entity name with leading slash for ROS 2 compatibility
4901 assert_eq!(key.as_str(), "/fibonacci/_action/send_goal");
4902 }
4903
4904 #[test]
4905 fn test_action_feedback_key() {
4906 let action = ActionInfo::new(
4907 "/fibonacci",
4908 "example_interfaces::action::dds_::Fibonacci_",
4909 "abc123",
4910 )
4911 .with_domain(0);
4912
4913 let key: heapless::String<256> = action.feedback_key();
4914 assert_eq!(key.as_str(), "/fibonacci/_action/feedback");
4915 }
4916
4917 #[test]
4918 fn test_action_all_sub_names() {
4919 let action = ActionInfo::new(
4920 "/fibonacci",
4921 "example_interfaces::action::dds_::Fibonacci_",
4922 "abc123",
4923 )
4924 .with_domain(0);
4925
4926 let cancel: heapless::String<256> = action.cancel_goal_key();
4927 assert_eq!(cancel.as_str(), "/fibonacci/_action/cancel_goal");
4928
4929 let result: heapless::String<256> = action.get_result_key();
4930 assert_eq!(result.as_str(), "/fibonacci/_action/get_result");
4931
4932 let status: heapless::String<256> = action.status_key();
4933 assert_eq!(status.as_str(), "/fibonacci/_action/status");
4934 }
4935
4936 // --- QoS Profile Tests ---
4937
4938 #[test]
4939 fn test_qos_profile_sensor_data() {
4940 let qos = QoSProfile::QOS_PROFILE_SENSOR_DATA;
4941 assert_eq!(qos.reliability, QoSReliabilityPolicy::BestEffort);
4942 assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
4943 assert_eq!(qos.history, QoSHistoryPolicy::KeepLast);
4944 assert_eq!(qos.depth, 5);
4945 }
4946
4947 #[test]
4948 fn test_qos_profile_default() {
4949 let qos = QoSProfile::QOS_PROFILE_DEFAULT;
4950 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
4951 assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
4952 assert_eq!(qos.depth, 10);
4953 }
4954
4955 #[test]
4956 fn test_qos_profile_services_default() {
4957 let qos = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
4958 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
4959 assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
4960 }
4961
4962 /// issue 0829 — `SYSTEM_DEFAULT` asks for NOTHING, so it demands no policy.
4963 ///
4964 /// `required_policies` started from `QoSPolicyMask::CORE` unconditionally,
4965 /// which made the sentinel profile demand reliability, durability, history
4966 /// AND depth — so a backend that could not honour one would answer
4967 /// `IncompatibleQos` to a profile that requested none of them.
4968 #[test]
4969 fn system_default_profile_requires_no_policy() {
4970 let required = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.required_policies();
4971 assert_eq!(required.0, 0, "SYSTEM_DEFAULT demanded {required:?}");
4972 // Therefore it is admissible against a backend that advertises nothing.
4973 assert!(
4974 QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
4975 .validate_against(QoSPolicyMask(0))
4976 .is_ok()
4977 );
4978 }
4979
4980 /// phase-428 W9 — the roadmap predicted that deriving zenoh's mask would
4981 /// drop `DEPTH` and that "nothing that works today breaks". The second
4982 /// half is FALSE, and this is the measurement that says so.
4983 ///
4984 /// `QOS_PROFILE_DEFAULT` states `depth: 10`, so it REQUIRES `DEPTH`; a
4985 /// mask without that bit refuses it, and that profile is what every
4986 /// `create_publisher` / `create_subscription` with no explicit QoS passes
4987 /// (`nros-node/src/executor/node.rs:238,372`). Withdrawing the bit would
4988 /// therefore refuse every default entity on the backend, not just the ones
4989 /// asking for more depth than the ring holds.
4990 ///
4991 /// So the zenoh shim EARNS the bit instead of withdrawing it (phase-428
4992 /// W9: `shim/qos.rs` reads the depth, grants what the ring can hold, and
4993 /// publishes the GRANTED value to the graph). Which is the honest reading
4994 /// of the mask anyway: the bit says "I will not silently ignore this
4995 /// policy", and a depth lie lives in the VALUE, not in the bit.
4996 #[test]
4997 fn withdrawing_depth_would_refuse_the_default_profile() {
4998 let without_depth = QoSPolicyMask(!QoSPolicyMask::DEPTH.0);
4999 assert!(
5000 QoSProfile::QOS_PROFILE_DEFAULT
5001 .validate_against(without_depth)
5002 .is_err(),
5003 "QOS_PROFILE_DEFAULT states depth 10; a mask without DEPTH must refuse it"
5004 );
5005 // And the same for the two other presets a default caller reaches.
5006 for qos in [
5007 QoSProfile::QOS_PROFILE_SERVICES_DEFAULT,
5008 QoSProfile::QOS_PROFILE_SENSOR_DATA,
5009 ] {
5010 assert!(qos.validate_against(without_depth).is_err());
5011 }
5012 // The sentinel profile is the one that survives, because it states no
5013 // depth at all.
5014 assert!(
5015 QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5016 .validate_against(without_depth)
5017 .is_ok()
5018 );
5019 }
5020
5021 /// phase-428 W9 — the trait default advertises NOTHING, so an
5022 /// implementation that has written no QoS code refuses any stated policy.
5023 ///
5024 /// The default was `CORE` until W9: four policies granted to a backend
5025 /// that had implemented none of them, by the trait itself.
5026 #[test]
5027 fn the_empty_mask_refuses_every_stated_policy_and_nothing_else() {
5028 assert_eq!(QoSPolicyMask::NONE.0, 0);
5029 assert!(
5030 QoSProfile::QOS_PROFILE_DEFAULT
5031 .validate_against(QoSPolicyMask::NONE)
5032 .is_err()
5033 );
5034 assert!(
5035 QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5036 .validate_against(QoSPolicyMask::NONE)
5037 .is_ok(),
5038 "an absence demands nothing, so it is admissible anywhere"
5039 );
5040 }
5041
5042 // ------------------------------------------------------------------
5043 // phase-417 G7 — the refusal NAMES the policy, and the vocabulary it
5044 // names it in is `QoSPolicyMask`'s own constants.
5045 // ------------------------------------------------------------------
5046
5047 /// The table is the twelve bits and nothing else, and a name is never a
5048 /// second authoring of the constant it belongs to.
5049 ///
5050 /// The buildless half of this lives in `check-qos-mask-derivation` (R8),
5051 /// which reads the `pub const` block and compares it against `NAMED`
5052 /// entry for entry. This is the half a `no_std` unit run can assert: every
5053 /// entry is one bit, the bits are distinct, and every name is distinct.
5054 /// A bit added above with no entry here would leave a policy that refuses
5055 /// a create and prints nothing.
5056 #[test]
5057 fn every_named_policy_is_one_distinct_bit_with_one_distinct_name() {
5058 assert_eq!(QoSPolicyMask::NAMED.len(), 12);
5059 let mut seen_bits = 0u32;
5060 for (bit, cstr) in QoSPolicyMask::NAMED {
5061 let name = cstr.to_str().expect("every entry is an ASCII literal");
5062 assert_eq!(
5063 bit.0.count_ones(),
5064 1,
5065 "{name} is not a single policy bit ({:#x})",
5066 bit.0
5067 );
5068 assert_eq!(seen_bits & bit.0, 0, "{name} repeats a bit already named");
5069 seen_bits |= bit.0;
5070 assert_eq!(
5071 bit.policy_name(),
5072 Some(name),
5073 "{name} does not name itself back"
5074 );
5075 // The C entry points hand this pointer straight over, so the NUL
5076 // is load-bearing and the byte before it must be the last of the
5077 // name — a `&str` view that silently dropped a byte would be a
5078 // truncated C string.
5079 assert_eq!(cstr.to_bytes().len(), name.len());
5080 }
5081 // The twelve bits are contiguous from 0, which is what
5082 // `rmw_entity.h`'s `NROS_RMW_QOS_POLICY_*` mirrors.
5083 assert_eq!(seen_bits, (1 << 12) - 1);
5084 }
5085
5086 /// `None` is a real answer in both directions — neither the empty mask nor
5087 /// a union is a policy, so neither may borrow a neighbour's name.
5088 #[test]
5089 fn neither_the_empty_mask_nor_a_union_names_a_policy() {
5090 assert_eq!(QoSPolicyMask::NONE.policy_name(), None);
5091 assert_eq!(QoSPolicyMask::CORE.policy_name(), None);
5092 assert_eq!(
5093 (QoSPolicyMask::DEADLINE | QoSPolicyMask::LIFESPAN).policy_name(),
5094 None
5095 );
5096 // And an error that is not a QoS refusal names no policy either.
5097 assert_eq!(TransportError::Timeout.qos_policy_name(), None);
5098 assert_eq!(
5099 TransportError::IncompatibleQos(QoSPolicyMask::NONE).qos_policy_name(),
5100 None,
5101 "a refusal relayed across `rmw_ret_t` identified no policy"
5102 );
5103 }
5104
5105 /// Several missing policies still produce ONE name, the same one every
5106 /// time — otherwise two surfaces could report different policies for the
5107 /// same refusal and a test could not assert either.
5108 #[test]
5109 fn a_refusal_missing_several_policies_names_the_lowest_one() {
5110 // DEFAULT states reliability, durability(volatile), history and depth.
5111 let qos = QoSProfile::QOS_PROFILE_DEFAULT;
5112 assert_eq!(
5113 qos.validate_against(QoSPolicyMask::NONE),
5114 Err(TransportError::IncompatibleQos(QoSPolicyMask::RELIABILITY)),
5115 "RELIABILITY is bit 0, so it is the reported one"
5116 );
5117 // Grant reliability and the next-lowest missing policy is reported.
5118 assert_eq!(
5119 qos.validate_against(QoSPolicyMask::RELIABILITY)
5120 .unwrap_err()
5121 .qos_policy_name(),
5122 Some("DURABILITY_VOLATILE")
5123 );
5124 // Grant everything the profile states and it is admitted.
5125 assert!(qos.validate_against(qos.required_policies()).is_ok());
5126 }
5127
5128 /// The policy a caller stated is the policy the refusal names — not a
5129 /// policy the profile never asked for. `DEADLINE` is the interesting one
5130 /// because it is stated by a VALUE rather than by a variant.
5131 #[test]
5132 fn the_refusal_names_the_policy_the_caller_actually_stated() {
5133 let qos = QoSProfile {
5134 deadline_ms: 250,
5135 ..QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5136 };
5137 assert_eq!(qos.required_policies(), QoSPolicyMask::DEADLINE);
5138 assert_eq!(
5139 qos.validate_against(QoSPolicyMask::NONE)
5140 .unwrap_err()
5141 .qos_policy_name(),
5142 Some("DEADLINE")
5143 );
5144 }
5145
5146 /// The relaxation is per FIELD, not all-or-nothing: a profile that states
5147 /// SOME policies still demands exactly those.
5148 #[test]
5149 fn a_partly_stated_profile_demands_only_what_it_states() {
5150 let qos = QoSProfile {
5151 reliability: QoSReliabilityPolicy::BestEffort,
5152 ..QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5153 };
5154 let required = qos.required_policies();
5155 assert!(required.contains(QoSPolicyMask::RELIABILITY));
5156 assert!(!required.contains(QoSPolicyMask::HISTORY));
5157 assert!(!required.contains(QoSPolicyMask::DEPTH));
5158 assert!(!required.contains(QoSPolicyMask::DURABILITY_VOLATILE));
5159 assert!(!required.contains(QoSPolicyMask::DURABILITY_TRANSIENT_LOCAL));
5160 }
5161
5162 /// A fully concrete profile is unaffected by the relaxation — this is the
5163 /// guard on "do not silently stop demanding what the caller asked for".
5164 #[test]
5165 fn concrete_profiles_still_demand_their_core_policies() {
5166 let required = QoSProfile::QOS_PROFILE_DEFAULT.required_policies();
5167 assert!(required.contains(QoSPolicyMask::RELIABILITY));
5168 assert!(required.contains(QoSPolicyMask::DURABILITY_VOLATILE));
5169 assert!(required.contains(QoSPolicyMask::HISTORY));
5170 assert!(required.contains(QoSPolicyMask::DEPTH));
5171 // And a backend missing one still rejects it.
5172 let missing = QoSPolicyMask(required.0 & !QoSPolicyMask::DEPTH.0);
5173 // phase-417 G7 — and it says WHICH policy. `DEPTH` is the only bit
5174 // dropped, so there is exactly one right answer here.
5175 assert_eq!(
5176 QoSProfile::QOS_PROFILE_DEFAULT.validate_against(missing),
5177 Err(TransportError::IncompatibleQos(QoSPolicyMask::DEPTH))
5178 );
5179 }
5180
5181 /// issue 0829 — resolution replaces ONLY the sentinel fields, and the
5182 /// answer is the backend's, not a constant this crate bakes.
5183 #[test]
5184 fn resolve_system_default_fills_absences_and_touches_nothing_else() {
5185 // Two backends, two different answers to the SAME sentinel — the whole
5186 // reason no concrete `QOS_PROFILE_SYSTEM_DEFAULT` could be right.
5187 const CYCLONE: QoSSystemDefaults = QoSSystemDefaults {
5188 reliability: QoSReliabilityPolicy::Reliable,
5189 durability: QoSDurabilityPolicy::Volatile,
5190 history: QoSHistoryPolicy::KeepLast,
5191 depth: 1,
5192 };
5193 const ZENOH: QoSSystemDefaults = QoSSystemDefaults {
5194 depth: 4,
5195 ..CYCLONE
5196 };
5197
5198 let a = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.resolve_system_default(&CYCLONE);
5199 let b = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.resolve_system_default(&ZENOH);
5200 assert_eq!(a.reliability, QoSReliabilityPolicy::Reliable);
5201 assert_eq!(a.history, QoSHistoryPolicy::KeepLast);
5202 assert_eq!(a.durability, QoSDurabilityPolicy::Volatile);
5203 assert_eq!(a.depth, 1);
5204 assert_eq!(b.depth, 4);
5205 assert!(!a.has_unresolved_system_default());
5206 assert!(!b.has_unresolved_system_default());
5207
5208 // A STATED policy is never overridden — resolution fills an absence.
5209 let stated = QoSProfile {
5210 reliability: QoSReliabilityPolicy::BestEffort,
5211 history: QoSHistoryPolicy::KeepAll,
5212 durability: QoSDurabilityPolicy::TransientLocal,
5213 depth: 7,
5214 ..QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5215 };
5216 let resolved = stated.resolve_system_default(&CYCLONE);
5217 assert_eq!(resolved, stated, "resolution overrode a stated policy");
5218
5219 // Idempotent.
5220 assert_eq!(a.resolve_system_default(&ZENOH), a);
5221 }
5222
5223 #[test]
5224 fn services_default_validates_and_rejects_missing_policy() {
5225 // Phase 193.5 — the service-create path (node `create_service*_sized` +
5226 // the typed-arena `register_service*_on`) runs `validate_against` on the
5227 // caller's profile, exactly like pub/sub. A backend advertising the
5228 // profile's required policies admits it; dropping any required bit (here
5229 // RELIABILITY) rejects it with `IncompatibleQos` — no silent downgrade.
5230 let qos = QoSProfile::services_default();
5231 let required = qos.required_policies();
5232 assert!(qos.validate_against(required).is_ok());
5233 assert!(qos.validate_against(QoSPolicyMask(u32::MAX)).is_ok());
5234 let missing = QoSPolicyMask(required.0 & !QoSPolicyMask::RELIABILITY.0);
5235 // phase-417 G7 — RELIABILITY is the bit dropped and RELIABILITY is
5236 // what the refusal has to name.
5237 assert_eq!(
5238 qos.validate_against(missing),
5239 Err(TransportError::IncompatibleQos(QoSPolicyMask::RELIABILITY))
5240 );
5241 assert_eq!(
5242 qos.validate_against(missing).unwrap_err().qos_policy_name(),
5243 Some("RELIABILITY")
5244 );
5245 }
5246
5247 /// issue 0793 — this asserted `TransientLocal`, which PINNED THE DEFECT:
5248 /// upstream `rmw_qos_profile_parameters` is KEEP_LAST(1000) + RELIABLE +
5249 /// **VOLATILE** (`/opt/ros/<distro>/include/rmw/rmw/qos_profiles.h`), and
5250 /// our own second copy of the profile, `nros::qos::PARAMETERS`, was already
5251 /// correct. The test agreed with the wrong copy, so the disagreement between
5252 /// the two survived every run.
5253 #[test]
5254 fn test_qos_profile_parameters() {
5255 let qos = QoSProfile::QOS_PROFILE_PARAMETERS;
5256 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
5257 assert_eq!(
5258 qos.durability,
5259 QoSDurabilityPolicy::Volatile,
5260 "rmw_qos_profile_parameters is VOLATILE; transient-local would make \
5261 every parameter server retain for late joiners, which ROS 2 does not do"
5262 );
5263 assert_eq!(qos.depth, 1000);
5264 }
5265
5266 #[test]
5267 fn test_qos_profile_clock() {
5268 let qos = QoSProfile::QOS_PROFILE_CLOCK;
5269 assert_eq!(qos.reliability, QoSReliabilityPolicy::BestEffort);
5270 assert_eq!(qos.depth, 1);
5271 }
5272
5273 /// phase-428 W10 — this asserted `KeepAll`, which PINNED THE DEFECT, in
5274 /// exactly the shape issue 0793 had already been caught in one profile
5275 /// over: upstream `rmw_qos_profile_parameter_events` is KEEP_LAST(1000) +
5276 /// RELIABLE + VOLATILE, byte-identical to `rmw_qos_profile_parameters`, and
5277 /// our C++ surface (`rclcpp::ParameterEventsQoS`) was already correct — so
5278 /// the two languages shipped different profiles under one name while this
5279 /// test reported green.
5280 ///
5281 /// The old value inverted behaviour rather than weakening it: a RELIABLE
5282 /// writer with KEEP_ALL blocks once history fills, where KEEP_LAST(1000)
5283 /// overwrites.
5284 #[test]
5285 fn test_qos_profile_parameter_events() {
5286 let qos = QoSProfile::QOS_PROFILE_PARAMETER_EVENTS;
5287 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
5288 assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
5289 assert_eq!(
5290 qos.history,
5291 QoSHistoryPolicy::KeepLast,
5292 "rmw_qos_profile_parameter_events is KEEP_LAST; KEEP_ALL makes a \
5293 reliable parameter-event writer BLOCK where upstream overwrites"
5294 );
5295 assert_eq!(qos.depth, 1000);
5296
5297 // Upstream defines the two constants with identical field values.
5298 assert_eq!(qos, QoSProfile::QOS_PROFILE_PARAMETERS);
5299 }
5300
5301 /// phase-428 W10 — the class behind the `PARAMETER_EVENTS` defect, not just
5302 /// the instance. `KEEP_ALL` under RELIABLE is a blocking configuration, and
5303 /// no upstream `rmw_qos_profile_*` nor `rcl_action_qos_profile_*` uses it;
5304 /// every one of them is `KEEP_LAST` or the SYSTEM_DEFAULT sentinel. A NAMED
5305 /// preset that is `KeepAll` is therefore a transcription error by
5306 /// construction — a caller who genuinely wants it says
5307 /// [`QoSProfile::keep_all`].
5308 #[test]
5309 fn no_named_preset_is_keep_all() {
5310 for (name, qos) in [
5311 ("BEST_EFFORT", QoSProfile::BEST_EFFORT),
5312 ("RELIABLE", QoSProfile::RELIABLE),
5313 ("SYSTEM_DEFAULT", QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT),
5314 ("DEFAULT", QoSProfile::QOS_PROFILE_DEFAULT),
5315 ("SENSOR_DATA", QoSProfile::QOS_PROFILE_SENSOR_DATA),
5316 ("SERVICES_DEFAULT", QoSProfile::QOS_PROFILE_SERVICES_DEFAULT),
5317 ("PARAMETERS", QoSProfile::QOS_PROFILE_PARAMETERS),
5318 ("PARAMETER_EVENTS", QoSProfile::QOS_PROFILE_PARAMETER_EVENTS),
5319 (
5320 "ACTION_STATUS_DEFAULT",
5321 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT,
5322 ),
5323 ("CLOCK", QoSProfile::QOS_PROFILE_CLOCK),
5324 ("PX4", QoSProfile::QOS_PROFILE_PX4),
5325 ] {
5326 assert_ne!(
5327 qos.history,
5328 QoSHistoryPolicy::KeepAll,
5329 "preset {name} is KEEP_ALL; no upstream profile is"
5330 );
5331 }
5332 }
5333
5334 #[test]
5335 fn test_qos_profile_action_status() {
5336 let qos = QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT;
5337 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
5338 assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
5339 assert_eq!(qos.depth, 1);
5340 }
5341
5342 #[test]
5343 fn test_qos_static_constructors() {
5344 assert_eq!(
5345 QoSProfile::topics_default(),
5346 QoSProfile::QOS_PROFILE_DEFAULT
5347 );
5348 assert_eq!(
5349 QoSProfile::sensor_data_default(),
5350 QoSProfile::QOS_PROFILE_SENSOR_DATA
5351 );
5352 assert_eq!(
5353 QoSProfile::services_default(),
5354 QoSProfile::QOS_PROFILE_SERVICES_DEFAULT
5355 );
5356 assert_eq!(
5357 QoSProfile::parameter_services_default(),
5358 QoSProfile::QOS_PROFILE_PARAMETERS
5359 );
5360 assert_eq!(
5361 QoSProfile::action_status_default(),
5362 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT
5363 );
5364 }
5365
5366 #[test]
5367 fn test_qos_builder_explicit_setters() {
5368 let qos = QoSProfile::new()
5369 .reliability(QoSReliabilityPolicy::Reliable)
5370 .durability(QoSDurabilityPolicy::TransientLocal)
5371 .history(QoSHistoryPolicy::KeepAll)
5372 .depth(100);
5373
5374 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
5375 assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
5376 assert_eq!(qos.history, QoSHistoryPolicy::KeepAll);
5377 assert_eq!(qos.depth, 100);
5378 }
5379
5380 #[test]
5381 fn test_qos_builder_chaining() {
5382 // Test that builder methods can be chained in any order
5383 let qos = QoSProfile::sensor_data_default()
5384 .reliable()
5385 .transient_local()
5386 .keep_last(20);
5387
5388 assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
5389 assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
5390 assert_eq!(qos.history, QoSHistoryPolicy::KeepLast);
5391 assert_eq!(qos.depth, 20);
5392 }
5393
5394 #[test]
5395 fn test_qos_eq_impl() {
5396 // Verify that PartialEq works correctly via derive on QoSProfile
5397 let qos1 = QoSProfile::QOS_PROFILE_DEFAULT;
5398 let qos2 = QoSProfile::topics_default();
5399 // Both should have same values - verify field by field
5400 assert_eq!(qos1.reliability, qos2.reliability);
5401 assert_eq!(qos1.durability, qos2.durability);
5402 assert_eq!(qos1.history, qos2.history);
5403 assert_eq!(qos1.depth, qos2.depth);
5404 }
5405
5406 // --- Locator validation tests ---
5407
5408 #[test]
5409 fn test_locator_protocol_tcp() {
5410 assert_eq!(locator_protocol("tcp/127.0.0.1:7447"), LocatorProtocol::Tcp);
5411 }
5412
5413 #[test]
5414 fn test_locator_protocol_serial() {
5415 assert_eq!(
5416 locator_protocol("serial//dev/ttyUSB0#baudrate=115200"),
5417 LocatorProtocol::Serial
5418 );
5419 }
5420
5421 #[test]
5422 fn test_locator_protocol_unknown() {
5423 assert_eq!(locator_protocol(""), LocatorProtocol::Unknown);
5424 assert_eq!(locator_protocol("http://foo"), LocatorProtocol::Unknown);
5425 assert_eq!(locator_protocol("tls/host:port"), LocatorProtocol::Unknown);
5426 }
5427
5428 #[test]
5429 fn test_locator_protocol_udp() {
5430 assert_eq!(locator_protocol("udp/127.0.0.1:7447"), LocatorProtocol::Udp);
5431 assert_eq!(
5432 locator_protocol("udp/192.168.1.50:2019"),
5433 LocatorProtocol::Udp
5434 );
5435 }
5436
5437 #[test]
5438 fn test_validate_tcp_locator_ok() {
5439 assert!(validate_locator("tcp/127.0.0.1:7447").is_ok());
5440 assert!(validate_locator("tcp/192.168.1.1:7447").is_ok());
5441 }
5442
5443 #[test]
5444 fn test_validate_tcp_locator_missing_port() {
5445 assert!(validate_locator("tcp/127.0.0.1").is_err());
5446 }
5447
5448 #[test]
5449 fn test_validate_serial_locator_ok() {
5450 assert!(validate_locator("serial//dev/ttyUSB0#baudrate=115200").is_ok());
5451 assert!(validate_locator("serial//dev/ttyACM0#baudrate=9600").is_ok());
5452 assert!(validate_locator("serial/uart1#baudrate=921600").is_ok());
5453 }
5454
5455 #[test]
5456 fn test_validate_serial_locator_empty_device() {
5457 assert!(validate_locator("serial/").is_err());
5458 }
5459
5460 #[test]
5461 fn test_validate_serial_locator_missing_baudrate() {
5462 assert!(validate_locator("serial//dev/ttyUSB0").is_err());
5463 }
5464
5465 #[test]
5466 fn test_validate_serial_locator_invalid_baudrate() {
5467 assert!(validate_locator("serial//dev/ttyUSB0#baudrate=abc").is_err());
5468 }
5469
5470 #[test]
5471 fn test_validate_unknown_protocol() {
5472 assert!(validate_locator("http://foo").is_err());
5473 assert!(validate_locator("tls/host:port").is_err());
5474 }
5475
5476 #[test]
5477 fn test_validate_udp_locator_ok() {
5478 assert!(validate_locator("udp/127.0.0.1:7447").is_ok());
5479 assert!(validate_locator("udp/192.168.1.50:2019").is_ok());
5480 }
5481
5482 #[test]
5483 fn test_validate_udp_locator_missing_port() {
5484 assert!(validate_locator("udp/127.0.0.1").is_err());
5485 }
5486
5487 // Phase 233.2 — the PX4 companion QoS profile must be BEST_EFFORT +
5488 // TRANSIENT_LOCAL + KEEP_LAST so it matches PX4's uxrce_dds_client endpoints.
5489 #[test]
5490 fn px4_qos_profile_matches_uxrce_dds_client() {
5491 let q = QoSProfile::px4();
5492 assert_eq!(q.reliability, QoSReliabilityPolicy::BestEffort);
5493 // VOLATILE — PX4's /fmu/out writers are volatile; a TRANSIENT_LOCAL
5494 // reader silently fails to match (verified against real PX4 SITL).
5495 assert_eq!(q.durability, QoSDurabilityPolicy::Volatile);
5496 assert_eq!(q.history, QoSHistoryPolicy::KeepLast);
5497 assert_eq!(q, QoSProfile::QOS_PROFILE_PX4);
5498 // Depth is tunable via the builder without losing the PX4 policies.
5499 let deep = QoSProfile::px4().keep_last(5);
5500 assert_eq!(deep.depth, 5);
5501 assert_eq!(deep.reliability, QoSReliabilityPolicy::BestEffort);
5502 assert_eq!(deep.durability, QoSDurabilityPolicy::Volatile);
5503 }
5504
5505 // --- RmwConfig Tests ---
5506
5507 #[test]
5508 fn test_rmw_config_default() {
5509 let config = RmwConfig::default();
5510 assert_eq!(config.locator, "tcp/127.0.0.1:7447");
5511 assert_eq!(config.mode, SessionMode::Client);
5512 assert_eq!(config.domain_id, 0);
5513 assert_eq!(config.node_name, "node");
5514 assert_eq!(config.namespace, "");
5515 }
5516
5517 #[test]
5518 fn test_rmw_config_custom() {
5519 let config = RmwConfig {
5520 locator: "tcp/192.168.1.1:7447",
5521 mode: SessionMode::Peer,
5522 domain_id: 42,
5523 node_name: "talker",
5524 namespace: "/ns1",
5525 properties: &[("agent_port", "2019")],
5526 };
5527 assert_eq!(config.locator, "tcp/192.168.1.1:7447");
5528 assert_eq!(config.mode, SessionMode::Peer);
5529 assert_eq!(config.domain_id, 42);
5530 assert_eq!(config.node_name, "talker");
5531 assert_eq!(config.namespace, "/ns1");
5532 assert_eq!(config.properties.len(), 1);
5533 assert_eq!(config.properties[0].0, "agent_port");
5534 }
5535
5536 #[test]
5537 fn test_rmw_config_is_copy() {
5538 let config = RmwConfig::default();
5539 let config2 = config; // Copy
5540 assert_eq!(config.locator, config2.locator);
5541 assert_eq!(config.domain_id, config2.domain_id);
5542 }
5543
5544 #[test]
5545 fn test_rmw_config_clone() {
5546 let config = RmwConfig::default();
5547 let cloned = RmwConfig { ..config };
5548 assert_eq!(cloned.locator, config.locator);
5549 assert_eq!(cloned.node_name, config.node_name);
5550 }
5551}
5552
5553#[cfg(test)]
5554mod rx_buffer_hint_tests {
5555 use super::*;
5556
5557 /// issue 0896 / phase-402 — the hint must SURVIVE being set on `TopicInfo`.
5558 ///
5559 /// This is the seam every path funnels through: the C register writes it
5560 /// here, `nros-rmw-cffi` reads `topic.rx_buffer_hint` back out when
5561 /// building `rmw_subscription_options_t`, and a size-classing backend
5562 /// routes on it. A default that silently stayed 0 is the original defect,
5563 /// so the round trip is worth an assertion rather than an assumption.
5564 #[test]
5565 fn a_hint_set_on_topic_info_is_readable_back() {
5566 let t = TopicInfo::new("/chatter", "std_msgs/msg/Int32", "").with_rx_buffer_hint(4096);
5567 assert_eq!(t.rx_buffer_hint, 4096);
5568 }
5569
5570 /// Zero is "no opinion", not "zero bytes". The C options struct uses 0 as
5571 /// its unset sentinel and the executor only calls the setter when the
5572 /// caller stated something, so the default must stay 0 for the
5573 /// backend-default path to remain reachable.
5574 #[test]
5575 fn the_default_hint_is_zero_meaning_no_opinion() {
5576 let t = TopicInfo::new("/chatter", "std_msgs/msg/Int32", "");
5577 assert_eq!(t.rx_buffer_hint, 0);
5578 }
5579
5580 /// The builder must not disturb the rest of the descriptor — it is applied
5581 /// AFTER domain/namespace/node in the C path, so a setter that reset a
5582 /// field would drop identity that discovery depends on.
5583 #[test]
5584 fn setting_the_hint_preserves_the_rest_of_the_descriptor() {
5585 let make = || {
5586 TopicInfo::new("/chatter", "std_msgs/msg/Int32", "hash")
5587 .with_domain(7)
5588 .with_namespace("/ns")
5589 };
5590 let base = make();
5591 let hinted = make().with_rx_buffer_hint(1234);
5592 assert_eq!(hinted.name, base.name);
5593 assert_eq!(hinted.type_name, base.type_name);
5594 assert_eq!(hinted.type_hash, base.type_hash);
5595 assert_eq!(hinted.domain_id, base.domain_id);
5596 assert_eq!(hinted.namespace, base.namespace);
5597 assert_eq!(hinted.rx_buffer_hint, 1234);
5598 }
5599}
5600
5601#[cfg(test)]
5602mod granted_qos_tests {
5603 use super::*;
5604
5605 /// A backend with no read-back answers an ABSENCE, and the trait default
5606 /// is where that is decided for every backend that does not override it.
5607 ///
5608 /// The tempting defaults are both wrong and both silent: the REQUEST
5609 /// ("you got what you asked for", which is the claim this accessor exists
5610 /// to stop being made for free) and `QOS_PROFILE_DEFAULT` ("RELIABLE
5611 /// KEEP_LAST(10)", a profile nobody chose).
5612 struct Mute;
5613
5614 impl Publisher for Mute {
5615 type Error = TransportError;
5616 fn publish_raw(&self, _data: &[u8]) -> Result<(), TransportError> {
5617 Ok(())
5618 }
5619 fn buffer_error(&self) -> TransportError {
5620 TransportError::BufferTooSmall
5621 }
5622 fn serialization_error(&self) -> TransportError {
5623 TransportError::SerializationError
5624 }
5625 }
5626
5627 #[test]
5628 fn the_default_read_back_is_an_absence_not_the_request() {
5629 let answered = Publisher::actual_qos(&Mute);
5630 assert_eq!(answered, QoSProfile::QOS_PROFILE_UNKNOWN);
5631 assert!(answered.has_unknown());
5632 assert_ne!(
5633 answered,
5634 QoSProfile::QOS_PROFILE_DEFAULT,
5635 "a backend that cannot say must not answer a profile nobody chose"
5636 );
5637 }
5638
5639 /// The phase-467 RMW gap-closure design study's Q1 — the trait default is
5640 /// a REFUSAL, never a gid.
5641 ///
5642 /// `Mute` overrides nothing, so it stands for every backend that has not
5643 /// implemented the accessor. The tempting default is `Ok([0u8; 24])`: it
5644 /// compiles, it needs no error plumbing, and it is indistinguishable from
5645 /// a real gid a backend zero-padded — which is the RFC-0089 Part I shape
5646 /// this method exists on the loud side of.
5647 #[test]
5648 fn the_default_gid_is_a_refusal_not_zeros() {
5649 let answered = Publisher::get_gid(&Mute);
5650 assert_eq!(answered, Err(TransportError::Unsupported));
5651 assert_ne!(answered, Ok([0u8; nros_core::PUBLISHER_GID_SIZE]));
5652 }
5653
5654 /// Row 7 of the same study — the ONE spelling of "the application owns
5655 /// the assertion", which two backends had written out by hand.
5656 ///
5657 /// `Unknown` is deliberately not manual: it is what a `*_get_actual_qos`
5658 /// read-back writes for a policy the backend could not report, and an
5659 /// absence is not a request. Reading it as manual would make a backend
5660 /// with no read-back refuse an assertion nobody asked for.
5661 #[test]
5662 fn only_the_two_manual_kinds_are_manual() {
5663 assert!(QoSLivelinessPolicy::ManualByNode.is_manual());
5664 assert!(QoSLivelinessPolicy::ManualByTopic.is_manual());
5665 assert!(!QoSLivelinessPolicy::Automatic.is_manual());
5666 assert!(!QoSLivelinessPolicy::None.is_manual());
5667 assert!(!QoSLivelinessPolicy::Unknown.is_manual());
5668 }
5669
5670 /// `UNKNOWN` and `SYSTEM_DEFAULT` are DIFFERENT absences and the ABI keeps
5671 /// them apart: "nobody asked" is resolvable by the backend, "I cannot say"
5672 /// is not. A profile that confused them would let a read-back round-trip
5673 /// into a create call and silently pick policies.
5674 #[test]
5675 fn unknown_is_not_the_system_default_sentinel() {
5676 assert_ne!(
5677 QoSProfile::QOS_PROFILE_UNKNOWN,
5678 QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
5679 );
5680 assert!(QoSProfile::QOS_PROFILE_UNKNOWN.has_unknown());
5681 assert!(!QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.has_unknown());
5682 }
5683
5684 /// The round trip is REFUSED. Feeding a read-back straight back into a
5685 /// create is the one way `UNKNOWN` reaches a request, and it must fail
5686 /// loudly rather than resolve to whatever the backend picks.
5687 #[test]
5688 fn a_read_back_profile_cannot_be_used_as_a_request() {
5689 // A mask that supports everything, so the refusal cannot be an
5690 // accident of a narrow backend.
5691 let everything = QoSPolicyMask(u32::MAX);
5692 assert_eq!(
5693 QoSProfile::QOS_PROFILE_UNKNOWN.validate_against(everything),
5694 Err(TransportError::IncompatibleQos(QoSPolicyMask::NONE)),
5695 "UNKNOWN is an absence; nothing can grant it, so no policy is named"
5696 );
5697 // And exactly one unknown policy is enough — a partial read-back is
5698 // the common case, not the exotic one.
5699 let mut partial = QoSProfile::QOS_PROFILE_DEFAULT;
5700 partial.durability = QoSDurabilityPolicy::Unknown;
5701 assert_eq!(
5702 partial.validate_against(everything),
5703 Err(TransportError::IncompatibleQos(QoSPolicyMask::NONE))
5704 );
5705 // The same profile without the sentinel is admitted, so the refusal
5706 // above is about `UNKNOWN` and not about the mask.
5707 assert_eq!(
5708 QoSProfile::QOS_PROFILE_DEFAULT.validate_against(everything),
5709 Ok(())
5710 );
5711 }
5712
5713 /// `required_policies` cannot express `UNKNOWN`, which is why the refusal
5714 /// lives one level up. Asserted so that a future attempt to smuggle it in
5715 /// as an unsatisfiable mask bit has to change a test that says why not.
5716 #[test]
5717 fn unknown_demands_no_capability_bit() {
5718 let mut p = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT;
5719 p.durability = QoSDurabilityPolicy::Unknown;
5720 p.liveliness_kind = QoSLivelinessPolicy::Unknown;
5721 assert_eq!(p.required_policies().0, QoSPolicyMask(0).0);
5722 }
5723}
5724
5725#[cfg(test)]
5726mod best_available_tests {
5727 //! issue 1973 — the ONE best-available rule, asserted against upstream's
5728 //! `rmw_dds_common` behaviour case by case.
5729 use super::*;
5730
5731 const ALL: QoSPolicyMask = QoSPolicyMask(u32::MAX);
5732
5733 fn peer(
5734 rel: QoSReliabilityPolicy,
5735 dur: QoSDurabilityPolicy,
5736 liv: QoSLivelinessPolicy,
5737 deadline_ms: u32,
5738 lease_ms: u32,
5739 ) -> QoSProfile {
5740 let mut q = QoSProfile::QOS_PROFILE_DEFAULT;
5741 q.reliability = rel;
5742 q.durability = dur;
5743 q.liveliness_kind = liv;
5744 q.deadline_ms = deadline_ms;
5745 q.liveliness_lease_ms = lease_ms;
5746 q
5747 }
5748
5749 fn fold(peers: &[QoSProfile]) -> BestAvailablePeers {
5750 let mut f = BestAvailablePeers::NONE;
5751 for p in peers {
5752 f.observe(p);
5753 }
5754 f
5755 }
5756
5757 #[test]
5758 fn the_profile_matches_upstream_and_asks_for_resolution() {
5759 let q = QoSProfile::best_available();
5760 assert_eq!(q, QoSProfile::QOS_PROFILE_BEST_AVAILABLE);
5761 assert!(q.has_best_available());
5762 assert_eq!((q.history, q.depth), (QoSHistoryPolicy::KeepLast, 10));
5763 assert_eq!(q.lifespan_ms, 0, "lifespan has no best-available spelling");
5764 assert!(!QoSProfile::QOS_PROFILE_DEFAULT.has_best_available());
5765 }
5766
5767 /// A best-available field demands nothing of the backend: the resolver
5768 /// clamps to the backend's mask, so validation of the REQUEST cannot fail
5769 /// on it.
5770 #[test]
5771 fn a_best_available_request_requires_no_policy_bit() {
5772 let q = QoSProfile::best_available();
5773 assert_eq!(
5774 q.required_policies(),
5775 QoSPolicyMask::HISTORY | QoSPolicyMask::DEPTH
5776 );
5777 assert!(
5778 q.validate_against(QoSPolicyMask::HISTORY | QoSPolicyMask::DEPTH)
5779 .is_ok()
5780 );
5781 }
5782
5783 #[test]
5784 fn subscription_with_no_publishers_gets_upstreams_nothing_discovered_answer() {
5785 let q = QoSProfile::best_available().resolve_best_available(
5786 BestAvailableRole::Subscription,
5787 &BestAvailablePeers::NONE,
5788 ALL,
5789 );
5790 assert_eq!(q.reliability, QoSReliabilityPolicy::BestEffort);
5791 assert_eq!(q.durability, QoSDurabilityPolicy::Volatile);
5792 assert_eq!(q.liveliness_kind, QoSLivelinessPolicy::Automatic);
5793 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (0, 0));
5794 assert!(!q.has_best_available());
5795 }
5796
5797 #[test]
5798 fn publisher_with_no_subscriptions_gets_the_highest_service() {
5799 let q = QoSProfile::best_available().resolve_best_available(
5800 BestAvailableRole::Publisher,
5801 &BestAvailablePeers::NONE,
5802 ALL,
5803 );
5804 assert_eq!(q.reliability, QoSReliabilityPolicy::Reliable);
5805 assert_eq!(q.durability, QoSDurabilityPolicy::TransientLocal);
5806 assert_eq!(q.liveliness_kind, QoSLivelinessPolicy::Automatic);
5807 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (0, 0));
5808 }
5809
5810 #[test]
5811 fn subscription_takes_the_strictest_value_every_publisher_offers() {
5812 use QoSDurabilityPolicy as D;
5813 use QoSLivelinessPolicy as L;
5814 use QoSReliabilityPolicy as R;
5815 // All reliable + transient-local + manual: the subscription may ask
5816 // for all three; largest finite deadline / lease.
5817 let all = fold(&[
5818 peer(R::Reliable, D::TransientLocal, L::ManualByTopic, 100, 10),
5819 peer(R::Reliable, D::TransientLocal, L::ManualByTopic, 300, 30),
5820 ]);
5821 let q = QoSProfile::best_available().resolve_best_available(
5822 BestAvailableRole::Subscription,
5823 &all,
5824 ALL,
5825 );
5826 assert_eq!(q.reliability, R::Reliable);
5827 assert_eq!(q.durability, D::TransientLocal);
5828 assert_eq!(q.liveliness_kind, L::ManualByTopic);
5829 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (300, 30));
5830
5831 // One best-effort volatile automatic publisher with no deadline drags
5832 // each policy down — and an infinite window makes the deadline default.
5833 let mixed = fold(&[
5834 peer(R::Reliable, D::TransientLocal, L::ManualByTopic, 100, 0),
5835 peer(
5836 R::BestEffort,
5837 D::Volatile,
5838 L::Automatic,
5839 DURATION_INFINITE_MS,
5840 0,
5841 ),
5842 ]);
5843 let q = QoSProfile::best_available().resolve_best_available(
5844 BestAvailableRole::Subscription,
5845 &mixed,
5846 ALL,
5847 );
5848 assert_eq!(q.reliability, R::BestEffort);
5849 assert_eq!(q.durability, D::Volatile);
5850 assert_eq!(q.liveliness_kind, L::Automatic);
5851 assert_eq!(
5852 q.deadline_ms, 100,
5853 "infinite reads as no finite window, like upstream's DEFAULT"
5854 );
5855 }
5856
5857 #[test]
5858 fn publisher_follows_any_manual_subscription_and_the_smallest_window() {
5859 use QoSDurabilityPolicy as D;
5860 use QoSLivelinessPolicy as L;
5861 use QoSReliabilityPolicy as R;
5862 let subs = fold(&[
5863 peer(R::BestEffort, D::Volatile, L::Automatic, 300, 0),
5864 peer(R::BestEffort, D::Volatile, L::ManualByTopic, 100, 40),
5865 ]);
5866 let q = QoSProfile::best_available().resolve_best_available(
5867 BestAvailableRole::Publisher,
5868 &subs,
5869 ALL,
5870 );
5871 assert_eq!(
5872 q.reliability,
5873 R::Reliable,
5874 "reliable serves every subscription"
5875 );
5876 assert_eq!(
5877 q.durability,
5878 D::TransientLocal,
5879 "transient-local serves every subscription"
5880 );
5881 assert_eq!(q.liveliness_kind, L::ManualByTopic);
5882 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (100, 40));
5883 }
5884
5885 /// A peer the backend could not read contributes its least demanding
5886 /// value — never counted reliable — so a subscription stays compatible.
5887 #[test]
5888 fn an_unreadable_peer_counts_as_least_demanding() {
5889 let unknown = fold(&[QoSProfile::QOS_PROFILE_UNKNOWN]);
5890 let q = QoSProfile::best_available().resolve_best_available(
5891 BestAvailableRole::Subscription,
5892 &unknown,
5893 ALL,
5894 );
5895 assert_eq!(q.reliability, QoSReliabilityPolicy::BestEffort);
5896 assert_eq!(q.durability, QoSDurabilityPolicy::Volatile);
5897 }
5898
5899 #[test]
5900 fn services_take_the_services_default_profile() {
5901 let q = QoSProfile::best_available().resolve_best_available(
5902 BestAvailableRole::Service,
5903 &BestAvailablePeers::NONE,
5904 ALL,
5905 );
5906 let svc = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
5907 assert_eq!(q.reliability, svc.reliability);
5908 assert_eq!(q.durability, svc.durability);
5909 assert_eq!(q.liveliness_kind, svc.liveliness_kind);
5910 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (0, 0));
5911 }
5912
5913 /// The choice never lands on a value the backend would refuse: it steps
5914 /// down to the next one the mask honours, and to the sentinel when the
5915 /// policy is not honoured at all.
5916 #[test]
5917 fn the_choice_is_clamped_to_what_the_backend_honours() {
5918 let mask = QoSPolicyMask::RELIABILITY
5919 | QoSPolicyMask::DURABILITY_VOLATILE
5920 | QoSPolicyMask::LIVELINESS_AUTOMATIC;
5921 let subs = fold(&[peer(
5922 QoSReliabilityPolicy::Reliable,
5923 QoSDurabilityPolicy::TransientLocal,
5924 QoSLivelinessPolicy::ManualByTopic,
5925 100,
5926 40,
5927 )]);
5928 let q = QoSProfile::best_available().resolve_best_available(
5929 BestAvailableRole::Publisher,
5930 &subs,
5931 mask,
5932 );
5933 assert_eq!(q.durability, QoSDurabilityPolicy::Volatile);
5934 assert_eq!(q.liveliness_kind, QoSLivelinessPolicy::Automatic);
5935 assert_eq!((q.deadline_ms, q.liveliness_lease_ms), (0, 0));
5936 assert!(
5937 q.validate_against(mask | QoSPolicyMask::HISTORY | QoSPolicyMask::DEPTH)
5938 .is_ok()
5939 );
5940
5941 let q = QoSProfile::best_available().resolve_best_available(
5942 BestAvailableRole::Publisher,
5943 &subs,
5944 QoSPolicyMask::NONE,
5945 );
5946 assert_eq!(q.reliability, QoSReliabilityPolicy::SystemDefault);
5947 assert_eq!(q.durability, QoSDurabilityPolicy::SystemDefault);
5948 assert_eq!(q.liveliness_kind, QoSLivelinessPolicy::None);
5949 }
5950
5951 /// Fields the caller STATED are never touched — only the best-available
5952 /// ones resolve.
5953 #[test]
5954 fn stated_fields_are_left_alone() {
5955 let q = QoSProfile::best_available().best_effort();
5956 let r =
5957 q.resolve_best_available(BestAvailableRole::Publisher, &BestAvailablePeers::NONE, ALL);
5958 assert_eq!(r.reliability, QoSReliabilityPolicy::BestEffort);
5959 assert_eq!(r.durability, QoSDurabilityPolicy::TransientLocal);
5960 let plain = QoSProfile::QOS_PROFILE_SENSOR_DATA;
5961 assert_eq!(
5962 plain.resolve_best_available(
5963 BestAvailableRole::Subscription,
5964 &BestAvailablePeers::NONE,
5965 ALL
5966 ),
5967 plain
5968 );
5969 }
5970
5971 /// No finite duration lowers onto the best-available sentinel.
5972 #[test]
5973 fn a_finite_duration_cannot_collide_with_the_sentinel() {
5974 use core::time::Duration;
5975 assert_eq!(
5976 duration_to_qos_ms(Duration::from_millis(DURATION_BEST_AVAILABLE_MS as u64)),
5977 Err(TransportError::InvalidArgument)
5978 );
5979 assert_eq!(
5980 duration_to_qos_ms(Duration::from_millis(DURATION_BEST_AVAILABLE_MS as u64 - 1)),
5981 Ok(DURATION_BEST_AVAILABLE_MS - 1)
5982 );
5983 }
5984}