Skip to main content

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}