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