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 nros_core::{Deserialize, RosMessage, RosService, Serialize};
12
13/// Topic information for pub/sub
14#[derive(Debug, Clone)]
15pub struct TopicInfo<'a> {
16    /// Topic name (e.g., "/chatter")
17    pub name: &'a str,
18    /// ROS type name (e.g., "std_msgs::msg::dds_::String_")
19    pub type_name: &'a str,
20    /// Type hash for compatibility checking
21    pub type_hash: &'a str,
22    /// Domain ID (default: 0)
23    pub domain_id: u32,
24    /// Node name for liveliness token generation.
25    /// `None` means no node association — no liveliness token will be declared.
26    pub node_name: Option<&'a str>,
27    /// Node namespace for liveliness token generation (default: "/").
28    /// In ROS 2, "/" is the root namespace and the standard default.
29    pub namespace: &'a str,
30    /// Phase 231 (RFC-0038) — receive-buffer size hint, bytes. The executor sets
31    /// this from the subscription's `RX_BUF`; a backend may use it to route the
32    /// subscription to a size-class receive buffer (zenoh-pico: small vs large).
33    /// `0` = unset (backend picks its default). Ignored by backends that don't
34    /// size-class their receive storage.
35    pub rx_buffer_hint: usize,
36    /// phase-279 (#145) — publisher-side "express" hint: this topic's samples
37    /// bypass transport tx batching (zenoh: the wire EXPRESS flag; a batching
38    /// zenoh-pico session sends them immediately instead of queueing them for
39    /// the next flush). For control-tier / latency-sensitive topics. Ignored by
40    /// subscriptions and by backends without a batching concept.
41    pub tx_express: bool,
42}
43
44impl<'a> TopicInfo<'a> {
45    /// Create new topic info
46    pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
47        Self {
48            name,
49            type_name,
50            type_hash,
51            domain_id: 0,
52            node_name: None,
53            namespace: "/",
54            rx_buffer_hint: 0,
55            tx_express: false,
56        }
57    }
58
59    /// Create topic info with custom domain ID
60    pub const fn with_domain(mut self, domain_id: u32) -> Self {
61        self.domain_id = domain_id;
62        self
63    }
64
65    /// Phase 231 (RFC-0038) — set the receive-buffer size hint (bytes) used by
66    /// size-classing backends (zenoh-pico) to route the subscription's receive
67    /// buffer to the small or large class.
68    pub const fn with_rx_buffer_hint(mut self, hint: usize) -> Self {
69        self.rx_buffer_hint = hint;
70        self
71    }
72
73    /// phase-279 (#145) — mark this publisher's samples express: they bypass
74    /// transport tx batching (sent immediately even when `ZPICO_TX_BATCH` is
75    /// on). For control-tier / latency-sensitive topics.
76    pub const fn with_tx_express(mut self, express: bool) -> Self {
77        self.tx_express = express;
78        self
79    }
80
81    /// Set the node name for liveliness token generation
82    pub const fn with_node_name(mut self, node_name: &'a str) -> Self {
83        self.node_name = Some(node_name);
84        self
85    }
86
87    /// Set the node namespace for liveliness token generation
88    pub const fn with_namespace(mut self, namespace: &'a str) -> Self {
89        self.namespace = namespace;
90        self
91    }
92}
93
94/// Service information for service client/server
95#[derive(Debug, Clone)]
96pub struct ServiceInfo<'a> {
97    /// Service name (e.g., "/add_two_ints")
98    pub name: &'a str,
99    /// ROS service type name (e.g., "example_interfaces::srv::dds_::AddTwoInts_")
100    pub type_name: &'a str,
101    /// Type hash for compatibility checking
102    pub type_hash: &'a str,
103    /// Domain ID (default: 0)
104    pub domain_id: u32,
105    /// Node name for liveliness token generation.
106    /// `None` means no node association — no liveliness token will be declared.
107    pub node_name: Option<&'a str>,
108    /// Node namespace for liveliness token generation (default: "/").
109    /// In ROS 2, "/" is the root namespace and the standard default.
110    pub namespace: &'a str,
111}
112
113/// Action information for action client/server
114///
115/// Actions in ROS 2 use 5 communication channels:
116/// - `send_goal` service: `<action_name>/_action/send_goal`
117/// - `cancel_goal` service: `<action_name>/_action/cancel_goal`
118/// - `get_result` service: `<action_name>/_action/get_result`
119/// - `feedback` topic: `<action_name>/_action/feedback`
120/// - `status` topic: `<action_name>/_action/status`
121#[derive(Debug, Clone)]
122pub struct ActionInfo<'a> {
123    /// Action name (e.g., "/fibonacci")
124    pub name: &'a str,
125    /// ROS action type name (e.g., "example_interfaces::action::dds_::Fibonacci_")
126    pub type_name: &'a str,
127    /// Type hash for compatibility checking
128    pub type_hash: &'a str,
129    /// Domain ID (default: 0)
130    pub domain_id: u32,
131}
132
133impl<'a> ActionInfo<'a> {
134    /// Create new action info
135    pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
136        Self {
137            name,
138            type_name,
139            type_hash,
140            domain_id: 0,
141        }
142    }
143
144    /// Create action info with custom domain ID
145    pub const fn with_domain(mut self, domain_id: u32) -> Self {
146        self.domain_id = domain_id;
147        self
148    }
149
150    /// Generate the send_goal service name
151    /// Returns: `<action>/_action/send_goal`
152    pub fn send_goal_key<const N: usize>(&self) -> heapless::String<N> {
153        self.sub_name::<N>("send_goal")
154    }
155
156    /// Generate the cancel_goal service name
157    /// Returns: `<action>/_action/cancel_goal`
158    pub fn cancel_goal_key<const N: usize>(&self) -> heapless::String<N> {
159        self.sub_name::<N>("cancel_goal")
160    }
161
162    /// Generate the get_result service name
163    /// Returns: `<action>/_action/get_result`
164    pub fn get_result_key<const N: usize>(&self) -> heapless::String<N> {
165        self.sub_name::<N>("get_result")
166    }
167
168    /// Generate the feedback topic name
169    /// Returns: `<action>/_action/feedback`
170    pub fn feedback_key<const N: usize>(&self) -> heapless::String<N> {
171        self.sub_name::<N>("feedback")
172    }
173
174    /// Generate the status topic name
175    /// Returns: `<action>/_action/status`
176    pub fn status_key<const N: usize>(&self) -> heapless::String<N> {
177        self.sub_name::<N>("status")
178    }
179
180    /// Generate a sub-entity name for an action component
181    /// Returns: `<action>/_action/<suffix>` (e.g., `fibonacci/_action/send_goal`)
182    ///
183    /// The caller is responsible for constructing the full key expression
184    /// by wrapping this name in a `ServiceInfo` or `TopicInfo` with the
185    /// correct sub-service/sub-topic type name.
186    fn sub_name<const N: usize>(&self, suffix: &str) -> heapless::String<N> {
187        let mut name = heapless::String::new();
188        let action_stripped = self.name.trim_matches('/');
189        let _ = core::fmt::write(
190            &mut name,
191            format_args!("/{}/_action/{}", action_stripped, suffix),
192        );
193        name
194    }
195}
196
197impl<'a> ServiceInfo<'a> {
198    /// Create new service info
199    pub const fn new(name: &'a str, type_name: &'a str, type_hash: &'a str) -> Self {
200        Self {
201            name,
202            type_name,
203            type_hash,
204            domain_id: 0,
205            node_name: None,
206            namespace: "/",
207        }
208    }
209
210    /// Create service info with custom domain ID
211    pub const fn with_domain(mut self, domain_id: u32) -> Self {
212        self.domain_id = domain_id;
213        self
214    }
215
216    /// Set the node name for liveliness token generation
217    pub const fn with_node_name(mut self, node_name: &'a str) -> Self {
218        self.node_name = Some(node_name);
219        self
220    }
221
222    /// Set the node namespace for liveliness token generation
223    pub const fn with_namespace(mut self, namespace: &'a str) -> Self {
224        self.namespace = namespace;
225        self
226    }
227}
228
229/// Transport error types.
230///
231/// No longer `Copy` — the `Backend` / `BackendDynamic` variants carry a
232/// string diagnostic, which can't be `Copy`. Rust callers that used to
233/// copy a `TransportError` value repeatedly now need `.clone()` or
234/// `ref` in match arms. C/C++ callers are unaffected — both map
235/// `TransportError` to integer codes (`nros_ret_t` / `ErrorCode`)
236/// before crossing the FFI boundary.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum TransportError {
239    /// Failed to connect to transport
240    ConnectionFailed,
241    /// Connection was closed
242    Disconnected,
243    /// Failed to create publisher
244    PublisherCreationFailed,
245    /// Failed to create subscriber
246    SubscriberCreationFailed,
247    /// Failed to create service server
248    ServiceServerCreationFailed,
249    /// Failed to create service client
250    ServiceClientCreationFailed,
251    /// Failed to publish message
252    PublishFailed,
253    /// Failed to send service request
254    ServiceRequestFailed,
255    /// Failed to send service reply
256    ServiceReplyFailed,
257    /// Serialization error
258    SerializationError,
259    /// Deserialization error
260    DeserializationError,
261    /// Buffer too small
262    BufferTooSmall,
263    /// Incoming message exceeded the static buffer capacity
264    MessageTooLarge,
265    /// Timeout waiting for message
266    Timeout,
267    /// Invalid configuration
268    InvalidConfig,
269    /// Resource (slot, buffer, queue) momentarily unavailable. Retry.
270    /// Phase 99: returned by `try_loan` when arena slots are full and
271    /// by `try_borrow` when no message is ready (alternative to
272    /// `Ok(None)` for backends that prefer the error variant).
273    WouldBlock,
274    /// Requested allocation exceeds backend capacity. Phase 99:
275    /// `try_loan(len)` returns this when `len` > arena slot size.
276    TooLarge,
277    /// Failed to start background tasks
278    TaskStartFailed,
279    /// Failed to poll for incoming messages
280    PollFailed,
281    /// Failed to send keepalive
282    KeepaliveFailed,
283    /// Failed to send join message
284    JoinFailed,
285    /// Caller supplied a NULL pointer, an out-of-range value, or an
286    /// inconsistent argument combination. Phase 102.1.
287    InvalidArgument,
288    /// The backend does not implement this operation. Optional
289    /// callbacks return this; the runtime then falls back to a
290    /// default path. Phase 102.1.
291    Unsupported,
292    /// Memory allocation failed. Returned by backends on `std` /
293    /// `alloc`-equipped platforms when heap allocation fails.
294    /// Bare-metal backends generally do not produce this — they
295    /// preallocate at session-open time. Phase 102.1.
296    BadAlloc,
297    /// Publisher and subscriber QoS profiles do not match in a way
298    /// the backend cannot reconcile. Phase 102.1.
299    IncompatibleQos,
300    /// Topic, service, or action name failed validation. Phase 102.1.
301    TopicNameInvalid,
302    /// A request referenced a node that does not exist in this
303    /// session. Phase 102.1.
304    NodeNameNonExistent,
305    /// The backend does not support loaned messages on this entity,
306    /// or the loan slot is currently in use. Phase 102.1.
307    LoanNotSupported,
308    /// No data was available on a non-blocking receive. Distinct
309    /// from `Timeout`: fires immediately, not after a bounded wait.
310    /// Phase 102.1.
311    NoData,
312    /// Phase 115.A.2 — caller passed a versioned vtable struct
313    /// (e.g. `NrosTransportOps`) with an `abi_version` the runtime
314    /// doesn't know. Maps to `NROS_RMW_RET_INCOMPATIBLE_ABI` at
315    /// the C boundary.
316    IncompatibleAbi,
317    /// Backend-specific error with a `'static` diagnostic string.
318    ///
319    /// Useful for zenoh-pico / XRCE-DDS return codes that map to a
320    /// fixed set of known messages. `no_std`-compatible.
321    Backend(&'static str),
322    /// Backend-specific error with an owned diagnostic string.
323    ///
324    /// Available only with the `alloc` feature. Use this when the
325    /// diagnostic is formatted at runtime (e.g. from a C error code
326    /// plus a socket address).
327    #[cfg(feature = "alloc")]
328    BackendDynamic(alloc::string::String),
329}
330
331/// QoS history policy
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
333pub enum QoSHistoryPolicy {
334    /// **The caller did not state a history policy** — the backend picks.
335    /// Upstream's `RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT`; lowers to
336    /// `NROS_RMW_HISTORY_SYSTEM_DEFAULT` (0) across the C ABI.
337    ///
338    /// See [`QoSSystemDefaults`] for how a backend resolves it. Listed
339    /// first to match rclrs's enumerator order.
340    SystemDefault,
341    /// Keep last N messages (where N is defined in QoSProfile)
342    #[default]
343    KeepLast,
344    /// Keep all messages (up to resource limits)
345    KeepAll,
346}
347
348/// QoS reliability policy
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
350pub enum QoSReliabilityPolicy {
351    /// **The caller did not state a reliability policy** — the backend picks.
352    /// Upstream's `RMW_QOS_POLICY_RELIABILITY_SYSTEM_DEFAULT`; lowers to
353    /// `NROS_RMW_RELIABILITY_SYSTEM_DEFAULT` (0) across the C ABI.
354    ///
355    /// See [`QoSSystemDefaults`].
356    SystemDefault,
357    /// Reliable delivery (retransmit if needed).
358    ///
359    /// Default — matches ROS 2 `rmw_qos_profile_default` and the
360    /// `QoSProfile::default()` / `QOS_PROFILE_DEFAULT` aggregates.
361    #[default]
362    Reliable,
363    /// Best-effort delivery (no retransmits)
364    BestEffort,
365}
366
367/// QoS durability policy
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
369pub enum QoSDurabilityPolicy {
370    /// **The caller did not state a durability policy** — the backend picks.
371    /// Upstream's `RMW_QOS_POLICY_DURABILITY_SYSTEM_DEFAULT`; lowers to
372    /// `NROS_RMW_DURABILITY_SYSTEM_DEFAULT` (0) across the C ABI.
373    ///
374    /// See [`QoSSystemDefaults`].
375    SystemDefault,
376    /// Messages are discarded when subscriber disconnects
377    #[default]
378    Volatile,
379    /// Messages are persisted for late-joining subscribers
380    TransientLocal,
381}
382
383/// QoS liveliness policy. Matches DDS `LIVELINESS` semantics.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
385#[repr(u8)]
386pub enum QoSLivelinessPolicy {
387    /// No liveliness assertion or tracking. Default for entities
388    /// that don't care about liveliness.
389    #[default]
390    None = 0,
391    /// Backend's keepalive task asserts liveliness automatically.
392    Automatic = 1,
393    /// Application calls `assert_liveliness()` at the node level.
394    ///
395    /// Phase 376 W5/B2 — this was 3 and `ManualByTopic` was 2, the opposite of
396    /// upstream. The discriminant IS the C ABI value (`liveliness_kind` is
397    /// written with `as u8`), and the cyclonedds backend turns it into a real
398    /// DDS liveliness kind that a ROS peer matches on, so the swap was visible
399    /// on the wire and nowhere else.
400    ManualByNode = 2,
401    /// Application calls `assert_liveliness()` per topic explicitly.
402    ManualByTopic = 3,
403}
404
405/// Phase 211.H — which side of a topic a [`QoSOverride`] targets.
406/// Mirrors the `<role>` segment of a ROS 2
407/// `qos_overrides.<topic>.<role>.<policy>` launch parameter.
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum QoSOverrideRole {
410    /// `qos_overrides.<topic>.publisher.*`
411    Publisher,
412    /// `qos_overrides.<topic>.subscription.*`
413    Subscription,
414}
415
416/// Phase 211.H — a single policy value a [`QoSOverride`] sets. A typed enum
417/// (not a string) so the codegen that bakes these from the plan catches an
418/// unknown policy / mistyped value at generation time rather than silently
419/// no-op-ing at runtime.
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum QoSOverrideValue {
422    /// `.reliability` → Reliable / BestEffort.
423    Reliability(QoSReliabilityPolicy),
424    /// `.durability` → Volatile / TransientLocal.
425    Durability(QoSDurabilityPolicy),
426    /// `.history` → KeepLast / KeepAll.
427    History(QoSHistoryPolicy),
428    /// `.depth` → KeepLast depth.
429    Depth(u32),
430    /// Issue 0303 — `.deadline` → [`QoSProfile::deadline_ms`]. `0` /
431    /// [`DURATION_INFINITE_MS`] = no deadline check.
432    Deadline(u32),
433    /// Issue 0303 — `.lifespan` → [`QoSProfile::lifespan_ms`].
434    Lifespan(u32),
435    /// Issue 0303 — `.liveliness` → [`QoSProfile::liveliness_kind`].
436    Liveliness(QoSLivelinessPolicy),
437    /// Issue 0303 — `.liveliness_lease_duration` →
438    /// [`QoSProfile::liveliness_lease_ms`].
439    LivelinessLease(u32),
440}
441
442/// Issue 0303 — the wire form a baked QoS override travels in:
443/// `(topic, role, policy, value)`, all primitives.
444///
445/// One numbering for every language: `nros_qos_override_t` (C),
446/// `nros_cpp_qos_override_t` (C++), `RuntimeCtx::qos_overrides` (the Rust
447/// entry bake) and `NodeRecord::qos_overrides` are all THIS tuple. Codes rather
448/// than [`QoSOverride`] because the table crosses the C ABI and rides
449/// `nros-platform`, which sits below this crate in the layer graph.
450///
451/// `topic` is `&'static` because every producer bakes a literal.
452pub type QoSOverrideCode = (&'static str, u8, u8, u32);
453
454/// `role` codes for [`QoSOverrideCode`].
455pub mod qos_override_role {
456    /// The override targets publishers on the topic.
457    pub const PUBLISHER: u8 = 0;
458    /// The override targets subscriptions on the topic.
459    pub const SUBSCRIPTION: u8 = 1;
460}
461
462/// `policy` codes for [`QoSOverrideCode`].
463///
464/// Append-only: these numbers are baked into shipped images and mirrored in
465/// two C headers. Never renumber — add.
466pub mod qos_override_policy {
467    /// value `0` = best_effort, `1` = reliable.
468    pub const RELIABILITY: u8 = 0;
469    /// value `0` = volatile, `1` = transient_local.
470    pub const DURABILITY: u8 = 1;
471    /// value `0` = keep_last, `1` = keep_all.
472    pub const HISTORY: u8 = 2;
473    /// value = the KeepLast depth.
474    pub const DEPTH: u8 = 3;
475    /// value = milliseconds (issue 0303).
476    pub const DEADLINE: u8 = 4;
477    /// value = milliseconds (issue 0303).
478    pub const LIFESPAN: u8 = 5;
479    /// value = [`super::QoSLivelinessPolicy`] discriminant (issue 0303).
480    pub const LIVELINESS: u8 = 6;
481    /// value = milliseconds (issue 0303).
482    pub const LIVELINESS_LEASE: u8 = 7;
483}
484
485/// Decode one [`QoSOverrideCode`] into a typed [`QoSOverride`].
486///
487/// `None` for an unrecognised role or policy code. THE one decoder: before
488/// issue 0303 this match existed four times (nros-node, nros-c, nros-cpp, and
489/// the executor's node record), each with a silent catch-all, so adding a
490/// policy meant finding all four — and the two FFI copies had already been
491/// forgotten once.
492pub fn decode_qos_override(code: &QoSOverrideCode) -> Option<QoSOverride> {
493    let (topic, role, policy, value) = *code;
494    decode_qos_override_parts(topic, role, policy, value)
495}
496
497/// [`decode_qos_override`] over loose parts — for the FFI paths, which read the
498/// fields out of a `#[repr(C)]` struct rather than a tuple.
499pub fn decode_qos_override_parts(
500    topic: &'static str,
501    role: u8,
502    policy: u8,
503    value: u32,
504) -> Option<QoSOverride> {
505    Some(QoSOverride {
506        topic,
507        role: decode_qos_override_role(role)?,
508        value: decode_qos_override_value(policy, value)?,
509    })
510}
511
512/// Decode a `role` code. `None` for an unrecognised one.
513pub fn decode_qos_override_role(role: u8) -> Option<QoSOverrideRole> {
514    match role {
515        qos_override_role::PUBLISHER => Some(QoSOverrideRole::Publisher),
516        qos_override_role::SUBSCRIPTION => Some(QoSOverrideRole::Subscription),
517        _ => None,
518    }
519}
520
521/// Decode a `(policy, value)` code pair. `None` for an unrecognised policy or
522/// an out-of-range enum value.
523///
524/// Split out from [`decode_qos_override`] for the FFI paths: they have already
525/// matched the topic against a `*const c_char`, so they need the VALUE without
526/// a `&'static str` to build a whole [`QoSOverride`] around.
527pub fn decode_qos_override_value(policy: u8, value: u32) -> Option<QoSOverrideValue> {
528    let out = match policy {
529        qos_override_policy::RELIABILITY => QoSOverrideValue::Reliability(if value == 0 {
530            QoSReliabilityPolicy::BestEffort
531        } else {
532            QoSReliabilityPolicy::Reliable
533        }),
534        qos_override_policy::DURABILITY => QoSOverrideValue::Durability(if value == 1 {
535            QoSDurabilityPolicy::TransientLocal
536        } else {
537            QoSDurabilityPolicy::Volatile
538        }),
539        qos_override_policy::HISTORY => QoSOverrideValue::History(if value == 1 {
540            QoSHistoryPolicy::KeepAll
541        } else {
542            QoSHistoryPolicy::KeepLast
543        }),
544        qos_override_policy::DEPTH => QoSOverrideValue::Depth(value),
545        qos_override_policy::DEADLINE => QoSOverrideValue::Deadline(value),
546        qos_override_policy::LIFESPAN => QoSOverrideValue::Lifespan(value),
547        // The encoder is `nros_orchestration_ir::qos_override`; both ends name
548        // the variant rather than its number, so W5/B2's renumbering moved them
549        // together instead of silently swapping two policies.
550        qos_override_policy::LIVELINESS => QoSOverrideValue::Liveliness(match value {
551            v if v == QoSLivelinessPolicy::None as u32 => QoSLivelinessPolicy::None,
552            v if v == QoSLivelinessPolicy::Automatic as u32 => QoSLivelinessPolicy::Automatic,
553            v if v == QoSLivelinessPolicy::ManualByNode as u32 => QoSLivelinessPolicy::ManualByNode,
554            v if v == QoSLivelinessPolicy::ManualByTopic as u32 => {
555                QoSLivelinessPolicy::ManualByTopic
556            }
557            _ => return None,
558        }),
559        qos_override_policy::LIVELINESS_LEASE => QoSOverrideValue::LivelinessLease(value),
560        _ => return None,
561    };
562    Some(out)
563}
564
565/// Phase 211.H — one per-topic QoS override, lowered from a ROS 2
566/// `qos_overrides.<topic>.<role>.<policy>` launch parameter by the planner and
567/// baked into a `&'static [QoSOverride]` table by the entry codegen. The node
568/// folds the matching entries into the entity's [`QoSProfile`] at
569/// `create_publisher` / `create_subscription` time (setup-time, single
570/// linear scan, no alloc), *before* the backend-compat `validate_against` —
571/// so an override the active RMW can't honour still errors loudly, never a
572/// silent downgrade.
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub struct QoSOverride {
575    /// The resolved (remapped) topic name the override targets, e.g.
576    /// `"/chatter"`. Matched exactly against the entity's topic.
577    pub topic: &'static str,
578    /// Publisher or subscription side.
579    pub role: QoSOverrideRole,
580    /// The policy + value to set.
581    pub value: QoSOverrideValue,
582}
583
584/// Phase-301 (issue 0241) — explicit "infinite" spelling for the u32
585/// millisecond QoS duration fields (`deadline_ms`, `lifespan_ms`,
586/// `liveliness_lease_ms`). Semantically identical to `0` (unset /
587/// no-check) at every check site; exists so a caller can distinguish
588/// "default" from "deliberately infinite". Mirrors the C header's
589/// `NROS_RMW_DURATION_INFINITE_MS`.
590pub const DURATION_INFINITE_MS: u32 = u32::MAX;
591
592/// Phase-301 (issue 0241) — lower a [`core::time::Duration`] into a u32
593/// millisecond QoS field. Boundary contract:
594///
595/// - zero stays `0` (unset / no-check);
596/// - sub-millisecond remainders CEIL to the next ms (rounding down
597///   would silently turn a real deadline into "no deadline");
598/// - values at or past [`DURATION_INFINITE_MS`] ms are a create-time
599///   error, never a clamp (infinite is requested via the sentinel or
600///   `0`, not by a huge finite duration).
601pub fn duration_to_qos_ms(d: core::time::Duration) -> Result<u32, TransportError> {
602    if d.is_zero() {
603        return Ok(0);
604    }
605    let mut ms = d.as_millis();
606    if !d.subsec_nanos().is_multiple_of(1_000_000) {
607        ms += 1;
608    }
609    if ms >= DURATION_INFINITE_MS as u128 {
610        return Err(TransportError::InvalidArgument);
611    }
612    Ok(ms as u32)
613}
614
615/// Full DDS-shaped QoS profile. Matches the field set of upstream
616/// `rmw_qos_profile_t`.
617///
618/// Backends advertise per-policy support via
619/// [`Session::supported_qos_policies`]; entities created with a
620/// profile the active backend can't honour return
621/// [`TransportError::IncompatibleQos`] synchronously at create time
622/// — no silent downgrade.
623///
624/// Zero-valued time-window fields ("off") mean infinite — the policy
625/// is effectively disabled for the entity.
626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub struct QoSProfile {
628    /// History policy
629    pub history: QoSHistoryPolicy,
630    /// Reliability policy
631    pub reliability: QoSReliabilityPolicy,
632    /// Durability policy
633    pub durability: QoSDurabilityPolicy,
634    /// Liveliness policy
635    pub liveliness_kind: QoSLivelinessPolicy,
636    /// History depth (only used if history is KeepLast)
637    pub depth: u32,
638    /// Subscription max-inter-arrival / publisher offered-rate, ms.
639    /// `0` = infinite (no deadline check).
640    pub deadline_ms: u32,
641    /// Sample expiry, ms. Subscribers filter samples older than this.
642    /// `0` = infinite (no expiry).
643    pub lifespan_ms: u32,
644    /// Liveliness lease, ms. `0` = infinite.
645    pub liveliness_lease_ms: u32,
646    /// If `true`, topic-name encoding skips the `/rt/` ROS prefix.
647    pub avoid_ros_namespace_conventions: bool,
648    /// Phase 282 (#145) — publisher-side "express" hint: this publisher's
649    /// samples bypass transport tx batching (zenoh: the wire EXPRESS flag; a
650    /// batching zenoh-pico session sends them immediately instead of queueing
651    /// them for the next flush). A transport hint, not a DDS policy — no RxO
652    /// matching, no backend-compat validation; ignored by subscriptions and by
653    /// backends without a batching concept.
654    pub tx_express: bool,
655}
656
657impl Default for QoSProfile {
658    fn default() -> Self {
659        Self::QOS_PROFILE_DEFAULT
660    }
661}
662
663/// The depth sentinel — "the caller did not state a queue depth".
664///
665/// issue 0829. Upstream spells it `RMW_QOS_POLICY_DEPTH_SYSTEM_DEFAULT = 0`
666/// (`rmw/include/rmw/types.h`); depth is `size_t` there and `uint16_t` in our
667/// C ABI, but 0 is 0 in both. The value was already free on every backend:
668/// Cyclone REJECTS `KEEP_LAST` with depth 0 outright
669/// (`validate_history_qospolicy`, `ddsi_plist.c:2603-2604`), the XRCE client
670/// already reads it as "unstated" and drops the field from the wire
671/// (`create_entities_bin.c:148`), and both `read_entity_qos`
672/// (`nros-rmw-cyclonedds/src/qos.cpp:138`) and `report_qos_downgrade`
673/// (`nros-rmw-cffi/src/lib.rs:1903`) already treat a 0 read-back as "no
674/// answer" rather than as an answer.
675pub const DEPTH_SYSTEM_DEFAULT: u32 = 0;
676
677/// What ONE backend resolves the `SYSTEM_DEFAULT` sentinel to.
678///
679/// issue 0829. `rmw_qos_profile_system_default` is an absence, and the RMW
680/// fills it — which means the answer is per backend and there is no constant
681/// this crate could bake. Each backend declares its own `QoSSystemDefaults`
682/// and applies it with [`QoSProfile::resolve_system_default`] at its create
683/// entry, **before anything is derived from the QoS**.
684///
685/// That ordering is load-bearing on the zenoh path: the profile is serialised
686/// into the liveliness-token keyexpr that a ROS `rmw_zenoh_cpp` peer parses
687/// out of the graph (`nros-rmw-zenoh/src/keyexpr.rs`). Upstream resolves in
688/// `best_available_qos` before the entity and its token exist, so its tokens
689/// never carry a sentinel; ours must not either, or we advertise `0:0:0,0` to
690/// peers as though it were a policy.
691///
692/// The values a backend picks should mirror **the corresponding upstream
693/// RMW**, not the raw middleware default — interop with a ROS peer is the
694/// requirement, and the two differ. Leaving `dds_qset_reliability` unset gives
695/// Cyclone's own reader default of BEST_EFFORT (`ddsi_plist.c:3470`), where
696/// `rmw_cyclonedds_cpp` deliberately picks RELIABLE.
697#[derive(Debug, Clone, Copy, PartialEq, Eq)]
698pub struct QoSSystemDefaults {
699    /// Concrete reliability. Must not itself be `SystemDefault`.
700    pub reliability: QoSReliabilityPolicy,
701    /// Concrete durability. Must not itself be `SystemDefault`.
702    pub durability: QoSDurabilityPolicy,
703    /// Concrete history. Must not itself be `SystemDefault`.
704    pub history: QoSHistoryPolicy,
705    /// Concrete queue depth. `0` means the backend genuinely has no answer and
706    /// defers further down the stack — the XRCE case, where the client encodes
707    /// exactly that (`optional_history_depth = false`) and the Agent's DDS
708    /// layer resolves it.
709    pub depth: u32,
710}
711
712impl QoSProfile {
713    /// Phase 211.H — fold the plan's `qos_overrides` matching `topic` + `role`
714    /// into this profile, returning the overridden profile. Setup-time only
715    /// (called from `create_publisher`/`create_subscription`): a single linear
716    /// scan over the baked `&'static` table, no alloc, RT-safe. Later entries
717    /// win on a duplicate `(topic, role, policy)` (last-write), matching the
718    /// planner's sorted, de-conflicted emit. Non-matching entries are ignored,
719    /// so passing the whole node table to every entity is cheap + correct.
720    #[must_use]
721    pub fn apply_overrides(
722        mut self,
723        topic: &str,
724        role: QoSOverrideRole,
725        overrides: &[QoSOverride],
726    ) -> Self {
727        for ovr in overrides {
728            if ovr.topic == topic && ovr.role == role {
729                self.apply_override_value(ovr.value);
730            }
731        }
732        self
733    }
734
735    /// Issue 0303 — apply ONE decoded override value. The single place a
736    /// policy maps to the field it sets; `apply_overrides` and the FFI folds
737    /// both go through it, so a new policy cannot reach some paths only.
738    pub fn apply_override_value(&mut self, value: QoSOverrideValue) {
739        match value {
740            QoSOverrideValue::Reliability(r) => self.reliability = r,
741            QoSOverrideValue::Durability(d) => self.durability = d,
742            QoSOverrideValue::History(h) => self.history = h,
743            QoSOverrideValue::Depth(d) => self.depth = d,
744            QoSOverrideValue::Deadline(ms) => self.deadline_ms = ms,
745            QoSOverrideValue::Lifespan(ms) => self.lifespan_ms = ms,
746            QoSOverrideValue::Liveliness(k) => self.liveliness_kind = k,
747            QoSOverrideValue::LivelinessLease(ms) => self.liveliness_lease_ms = ms,
748        }
749    }
750
751    /// Issue 0303 — fold baked [`QoSOverrideCode`]s for one `(topic, role)`.
752    /// Unrecognised codes are skipped; the producers reject them at BAKE time
753    /// (`nros_orchestration_ir::qos_override`), so a code reaching here that
754    /// this build does not know is an older image running newer data, not a
755    /// user error to diagnose at runtime.
756    pub fn apply_override_codes(
757        self,
758        topic: &str,
759        role: QoSOverrideRole,
760        codes: &[QoSOverrideCode],
761    ) -> Self {
762        let mut qos = self;
763        for code in codes {
764            if let Some(ovr) = decode_qos_override(code) {
765                qos = qos.apply_overrides(topic, role, core::slice::from_ref(&ovr));
766            }
767        }
768        qos
769    }
770}
771
772impl QoSProfile {
773    /// Internal const builder. Extended-policy fields default to
774    /// "off" (zero) and `liveliness_kind = Automatic` (the upstream
775    /// `rmw_qos_profile_default` choice).
776    const fn build(
777        reliability: QoSReliabilityPolicy,
778        durability: QoSDurabilityPolicy,
779        history: QoSHistoryPolicy,
780        depth: u32,
781    ) -> Self {
782        Self {
783            history,
784            reliability,
785            durability,
786            liveliness_kind: QoSLivelinessPolicy::Automatic,
787            depth,
788            deadline_ms: 0,
789            lifespan_ms: 0,
790            liveliness_lease_ms: 0,
791            avoid_ros_namespace_conventions: false,
792            tx_express: false,
793        }
794    }
795
796    /// Create new QoS settings with defaults (matches `QOS_PROFILE_DEFAULT`:
797    /// Reliable, Volatile, KeepLast(10)).
798    pub const fn new() -> Self {
799        Self::QOS_PROFILE_DEFAULT
800    }
801
802    /// Best-effort QoS (for real-time)
803    pub const BEST_EFFORT: Self = Self::build(
804        QoSReliabilityPolicy::BestEffort,
805        QoSDurabilityPolicy::Volatile,
806        QoSHistoryPolicy::KeepLast,
807        1,
808    );
809
810    /// Reliable QoS
811    pub const RELIABLE: Self = Self::build(
812        QoSReliabilityPolicy::Reliable,
813        QoSDurabilityPolicy::Volatile,
814        QoSHistoryPolicy::KeepLast,
815        10,
816    );
817
818    /// `rmw_qos_profile_system_default` — **an absence, not a profile.**
819    ///
820    /// issue 0829. Every field is the sentinel: upstream's constant names no
821    /// concrete policy at all, and the two reference RMWs fill the absence with
822    /// different numbers — `rmw_cyclonedds_cpp`'s `create_readwrite_qos` folds
823    /// `RMW_QOS_POLICY_DEPTH_SYSTEM_DEFAULT` to `KEEP_LAST(1)`, while
824    /// `rmw_zenoh_cpp`'s `QoS::QoS()` fills it from
825    /// `RMW_ZENOH_DEFAULT_HISTORY_DEPTH`, which is 42, over a comment stating
826    /// the contract outright: *"If the depth field in the qos profile is set to
827    /// 0, the RMW implementation has the liberty to assign a default depth."*
828    ///
829    /// So no baked number can be right. This carried a concrete
830    /// `Reliable / Volatile / KeepLast(1)` until 2026-09-03, and
831    /// `nros::qos::SYSTEM_DEFAULT` carried a concrete depth **10**, which is how
832    /// one name shipped two queue depths. Both are gone; the backend resolves
833    /// this at its create entry via [`QoSProfile::resolve_system_default`].
834    ///
835    /// `liveliness_kind` is [`QoSLivelinessPolicy::None`], which IS the
836    /// sentinel on that policy — it lowers to
837    /// `NROS_RMW_LIVELINESS_SYSTEM_DEFAULT` (0), the two having collapsed onto
838    /// one value in phase-376 W5/B2.
839    pub const QOS_PROFILE_SYSTEM_DEFAULT: Self = Self {
840        history: QoSHistoryPolicy::SystemDefault,
841        reliability: QoSReliabilityPolicy::SystemDefault,
842        durability: QoSDurabilityPolicy::SystemDefault,
843        liveliness_kind: QoSLivelinessPolicy::None,
844        depth: DEPTH_SYSTEM_DEFAULT,
845        deadline_ms: 0,
846        lifespan_ms: 0,
847        liveliness_lease_ms: 0,
848        avoid_ros_namespace_conventions: false,
849        tx_express: false,
850    };
851
852    /// Default QoS profile (matches rmw_qos_profile_default)
853    pub const QOS_PROFILE_DEFAULT: Self = Self::build(
854        QoSReliabilityPolicy::Reliable,
855        QoSDurabilityPolicy::Volatile,
856        QoSHistoryPolicy::KeepLast,
857        10,
858    );
859
860    /// Sensor data QoS profile (matches rmw_qos_profile_sensor_data)
861    pub const QOS_PROFILE_SENSOR_DATA: Self = Self::build(
862        QoSReliabilityPolicy::BestEffort,
863        QoSDurabilityPolicy::Volatile,
864        QoSHistoryPolicy::KeepLast,
865        5,
866    );
867
868    /// Services default QoS profile (matches rmw_qos_profile_services_default)
869    pub const QOS_PROFILE_SERVICES_DEFAULT: Self = Self::build(
870        QoSReliabilityPolicy::Reliable,
871        QoSDurabilityPolicy::Volatile,
872        QoSHistoryPolicy::KeepLast,
873        10,
874    );
875
876    /// Parameters QoS profile (matches rmw_qos_profile_parameters)
877    /// Mirrors `rmw_qos_profile_parameters`: KEEP_LAST(1000), RELIABLE,
878    /// **VOLATILE**.
879    ///
880    /// issue 0793 — this said `TransientLocal` until 2026-08-25, disagreeing
881    /// both with upstream (`/opt/ros/<distro>/include/rmw/rmw/qos_profiles.h`)
882    /// and with our own second copy of the same profile, `nros::qos::PARAMETERS`,
883    /// which was already correct. Two copies of one profile that disagree is the
884    /// defect; the wrong one being the one named after the upstream constant is
885    /// what made it hard to see.
886    pub const QOS_PROFILE_PARAMETERS: Self = Self::build(
887        QoSReliabilityPolicy::Reliable,
888        QoSDurabilityPolicy::Volatile,
889        QoSHistoryPolicy::KeepLast,
890        1000,
891    );
892
893    /// Clock QoS profile - same as sensor data but with depth 1
894    pub const QOS_PROFILE_CLOCK: Self = Self::build(
895        QoSReliabilityPolicy::BestEffort,
896        QoSDurabilityPolicy::Volatile,
897        QoSHistoryPolicy::KeepLast,
898        1,
899    );
900
901    /// Parameter events QoS profile (matches rmw_qos_profile_parameter_events)
902    pub const QOS_PROFILE_PARAMETER_EVENTS: Self = Self::build(
903        QoSReliabilityPolicy::Reliable,
904        QoSDurabilityPolicy::Volatile,
905        QoSHistoryPolicy::KeepAll,
906        0, // Not used with KeepAll
907    );
908
909    /// Action status default QoS profile (matches rcl_action_qos_profile_status_default)
910    pub const QOS_PROFILE_ACTION_STATUS_DEFAULT: Self = Self::build(
911        QoSReliabilityPolicy::Reliable,
912        QoSDurabilityPolicy::TransientLocal,
913        QoSHistoryPolicy::KeepLast,
914        1,
915    );
916
917    /// PX4 companion QoS profile (Phase 233 / RFC-0039 Track B). Matches the
918    /// QoS PX4's `uxrce_dds_client` uses on `/fmu/out/*` and `/fmu/in/*` —
919    /// `BEST_EFFORT` + `VOLATILE` + `KEEP_LAST(1)`. A nano-ros node talking to
920    /// the same `MicroXRCEAgent` must use this (a reliable or
921    /// `TRANSIENT_LOCAL` reader will not match PX4's volatile best-effort
922    /// writers). Verified against real PX4 SITL (`nros-px4-sitl-test`):
923    /// `TRANSIENT_LOCAL` durability silently fails to match `/fmu/out/*`.
924    /// Adjust depth via `.keep_last(n)` for higher-rate streams.
925    pub const QOS_PROFILE_PX4: Self = Self::build(
926        QoSReliabilityPolicy::BestEffort,
927        QoSDurabilityPolicy::Volatile,
928        QoSHistoryPolicy::KeepLast,
929        1,
930    );
931
932    // --- Static constructor methods (matching rclrs API) ---
933
934    /// Get the default QoS profile for ordinary topics
935    pub const fn topics_default() -> Self {
936        Self::QOS_PROFILE_DEFAULT
937    }
938
939    /// The PX4 companion QoS profile ([`QOS_PROFILE_PX4`](Self::QOS_PROFILE_PX4))
940    /// — use for `/fmu/out/*` subscriptions and `/fmu/in/*` publications against
941    /// a `MicroXRCEAgent`.
942    pub const fn px4() -> Self {
943        Self::QOS_PROFILE_PX4
944    }
945
946    /// Get the default QoS profile for sensor data topics
947    pub const fn sensor_data_default() -> Self {
948        Self::QOS_PROFILE_SENSOR_DATA
949    }
950
951    /// Get the default QoS profile for services
952    pub const fn services_default() -> Self {
953        Self::QOS_PROFILE_SERVICES_DEFAULT
954    }
955
956    /// Get the default QoS profile for parameter services
957    pub const fn parameters_default() -> Self {
958        Self::QOS_PROFILE_PARAMETERS
959    }
960
961    /// Get the default QoS profile for parameter events
962    pub const fn parameter_events_default() -> Self {
963        Self::QOS_PROFILE_PARAMETER_EVENTS
964    }
965
966    /// Get the system default QoS profile
967    pub const fn system_default() -> Self {
968        Self::QOS_PROFILE_SYSTEM_DEFAULT
969    }
970
971    /// Get the default QoS profile for action status topics
972    pub const fn action_status_default() -> Self {
973        Self::QOS_PROFILE_ACTION_STATUS_DEFAULT
974    }
975
976    /// Get the default QoS profile for clock topics
977    pub const fn clock_default() -> Self {
978        Self::QOS_PROFILE_CLOCK
979    }
980
981    // --- Builder methods ---
982
983    /// Set history to keep last N messages
984    pub const fn keep_last(mut self, depth: u32) -> Self {
985        self.history = QoSHistoryPolicy::KeepLast;
986        self.depth = depth;
987        self
988    }
989
990    /// Set history to keep all messages
991    pub const fn keep_all(mut self) -> Self {
992        self.history = QoSHistoryPolicy::KeepAll;
993        self
994    }
995
996    /// Set reliability to reliable
997    pub const fn reliable(mut self) -> Self {
998        self.reliability = QoSReliabilityPolicy::Reliable;
999        self
1000    }
1001
1002    /// Set reliability to best-effort
1003    pub const fn best_effort(mut self) -> Self {
1004        self.reliability = QoSReliabilityPolicy::BestEffort;
1005        self
1006    }
1007
1008    /// Set durability to volatile
1009    pub const fn volatile(mut self) -> Self {
1010        self.durability = QoSDurabilityPolicy::Volatile;
1011        self
1012    }
1013
1014    /// Set durability to transient local
1015    pub const fn transient_local(mut self) -> Self {
1016        self.durability = QoSDurabilityPolicy::TransientLocal;
1017        self
1018    }
1019
1020    /// Set reliability policy explicitly
1021    pub const fn reliability(mut self, policy: QoSReliabilityPolicy) -> Self {
1022        self.reliability = policy;
1023        self
1024    }
1025
1026    /// Set durability policy explicitly
1027    pub const fn durability(mut self, policy: QoSDurabilityPolicy) -> Self {
1028        self.durability = policy;
1029        self
1030    }
1031
1032    /// Set history policy explicitly
1033    pub const fn history(mut self, policy: QoSHistoryPolicy) -> Self {
1034        self.history = policy;
1035        self
1036    }
1037
1038    /// Set history depth explicitly
1039    pub const fn depth(mut self, depth: u32) -> Self {
1040        self.depth = depth;
1041        self
1042    }
1043
1044    /// Phase 282 (#145) — mark this publisher's samples "express": they
1045    /// bypass transport tx batching (sent immediately even when the batching
1046    /// knob is on). A transport hint for control-tier / latency-sensitive
1047    /// topics; ignored on subscriptions and by backends without batching.
1048    pub const fn tx_express(mut self, express: bool) -> Self {
1049        self.tx_express = express;
1050        self
1051    }
1052
1053    /// Get history depth (for backwards compatibility)
1054    pub const fn history_depth(&self) -> u8 {
1055        if self.depth > 255 {
1056            255
1057        } else {
1058            self.depth as u8
1059        }
1060    }
1061}
1062
1063/// Transport session configuration
1064#[derive(Debug, Clone)]
1065pub struct TransportConfig<'a> {
1066    /// Peer locator (e.g., "tcp/192.168.1.1:7447" or "serial//dev/ttyUSB0#baudrate=115200")
1067    pub locator: Option<&'a str>,
1068    /// Session mode: client, peer, or router
1069    pub mode: SessionMode,
1070    /// Additional transport properties (key-value pairs)
1071    ///
1072    /// These are passed through to the underlying transport backend.
1073    /// For zenoh-pico, recognized keys include:
1074    /// - `"multicast_scouting"` - Enable/disable multicast scouting (`"true"` or `"false"`)
1075    /// - `"scouting_timeout_ms"` - Scouting timeout in milliseconds
1076    /// - `"multicast_locator"` - Multicast group address
1077    /// - `"listen"` - Listen endpoint (e.g., `"tcp/0.0.0.0:0"`)
1078    /// - `"add_timestamp"` - Add timestamps to messages (`"true"` or `"false"`)
1079    pub properties: &'a [(&'a str, &'a str)],
1080    /// Node name for ROS 2 graph discovery liveliness token.
1081    ///
1082    /// Empty string (`""`) means no node-liveliness token is declared (preserves
1083    /// the pre-#104 behaviour). Non-empty causes the session to declare a
1084    /// `@ros2_lv/<domain>/<zid>/0/0/NN/%/<ns>/<node>` token on open.
1085    pub node_name: &'a str,
1086    /// Node namespace for the liveliness token (e.g., `""` or `"/ns1"`).
1087    ///
1088    /// Empty string is treated as root `"/"` by the keyexpr builder.
1089    pub namespace: &'a str,
1090    /// ROS 2 domain ID used in the liveliness token key expression.
1091    pub domain_id: u32,
1092}
1093
1094impl Default for TransportConfig<'_> {
1095    fn default() -> Self {
1096        Self {
1097            locator: None,
1098            mode: SessionMode::Client,
1099            properties: &[],
1100            node_name: "",
1101            namespace: "",
1102            domain_id: 0,
1103        }
1104    }
1105}
1106
1107/// Middleware-agnostic session configuration.
1108///
1109/// `RmwConfig` provides a uniform interface that any RMW backend can
1110/// interpret. Backends map the universal fields to their own connection
1111/// parameters and interpret `properties` for anything backend-specific.
1112///
1113/// # Examples
1114///
1115/// ```
1116/// use nros_rmw::{RmwConfig, SessionMode};
1117///
1118/// let config = RmwConfig {
1119///     locator: "tcp/192.168.1.1:7447",
1120///     mode: SessionMode::Client,
1121///     domain_id: 0,
1122///     node_name: "talker",
1123///     namespace: "",
1124///     properties: &[],
1125/// };
1126/// ```
1127#[derive(Debug, Clone, Copy)]
1128pub struct RmwConfig<'a> {
1129    /// Middleware-specific connection string.
1130    ///
1131    /// - zenoh: `"tcp/192.168.1.1:7447"` or `"udp/224.0.0.224:7447"`
1132    /// - XRCE-DDS: `"udp/192.168.1.1:2019"`
1133    pub locator: &'a str,
1134    /// Session mode (zenoh: client/peer; XRCE-DDS: always client)
1135    pub mode: SessionMode,
1136    /// ROS 2 domain ID (maps to DDS domain or zenoh key prefix)
1137    pub domain_id: u32,
1138    /// Node name (e.g., `"talker"`)
1139    pub node_name: &'a str,
1140    /// Node namespace (e.g., `""` or `"/ns1"`)
1141    pub namespace: &'a str,
1142    /// Backend-specific key/value properties.
1143    ///
1144    /// Uniform escape hatch for backend-specific tuning that doesn't fit
1145    /// the universal fields above. Each backend documents the keys it
1146    /// understands; unknown keys are ignored. Passing `&[]` is always
1147    /// valid.
1148    ///
1149    /// Examples:
1150    /// - zenoh: `"tls.root_ca"`, `"scouting.multicast.enabled"`
1151    /// - XRCE-DDS: `"agent_port"`, `"client_key"`
1152    pub properties: &'a [(&'a str, &'a str)],
1153}
1154
1155impl Default for RmwConfig<'_> {
1156    fn default() -> Self {
1157        Self {
1158            locator: "tcp/127.0.0.1:7447",
1159            mode: SessionMode::Client,
1160            domain_id: 0,
1161            node_name: "node",
1162            namespace: "",
1163            properties: &[],
1164        }
1165    }
1166}
1167
1168/// Locator transport protocol
1169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1170pub enum LocatorProtocol {
1171    /// TCP transport (e.g., "tcp/127.0.0.1:7447")
1172    Tcp,
1173    /// UDP transport (e.g., "udp/192.168.1.50:2019" — common for XRCE-DDS)
1174    Udp,
1175    /// Serial/UART transport (e.g., "serial//dev/ttyUSB0#baudrate=115200")
1176    Serial,
1177    /// Unknown protocol
1178    Unknown,
1179}
1180
1181/// Parse the protocol from a locator string
1182pub fn locator_protocol(locator: &str) -> LocatorProtocol {
1183    if locator.starts_with("tcp/") {
1184        LocatorProtocol::Tcp
1185    } else if locator.starts_with("udp/") {
1186        LocatorProtocol::Udp
1187    } else if locator.starts_with("serial/") {
1188        LocatorProtocol::Serial
1189    } else {
1190        LocatorProtocol::Unknown
1191    }
1192}
1193
1194/// Validate a locator string format.
1195///
1196/// Returns `Ok(())` if the locator is well-formed, or an error message describing
1197/// the problem. This provides early feedback before zenoh-pico or XRCE-DDS rejects
1198/// a bad locator.
1199///
1200/// Supported formats:
1201/// - TCP: `tcp/<host>:<port>` (e.g., `tcp/127.0.0.1:7447`)
1202/// - UDP: `udp/<host>:<port>` (e.g., `udp/192.168.1.50:2019`)
1203/// - Serial: `serial/<device>#baudrate=<rate>` (e.g., `serial//dev/ttyUSB0#baudrate=115200`)
1204pub fn validate_locator(locator: &str) -> Result<(), &'static str> {
1205    match locator_protocol(locator) {
1206        LocatorProtocol::Tcp => {
1207            let rest = &locator[4..]; // skip "tcp/"
1208            if !rest.contains(':') {
1209                return Err("TCP locator must contain host:port (e.g., tcp/127.0.0.1:7447)");
1210            }
1211            Ok(())
1212        }
1213        LocatorProtocol::Udp => {
1214            let rest = &locator[4..]; // skip "udp/"
1215            if !rest.contains(':') {
1216                return Err("UDP locator must contain host:port (e.g., udp/192.168.1.50:2019)");
1217            }
1218            Ok(())
1219        }
1220        LocatorProtocol::Serial => {
1221            let rest = &locator[7..]; // skip "serial/"
1222            if rest.is_empty() {
1223                return Err(
1224                    "serial locator must specify device (e.g., serial//dev/ttyUSB0#baudrate=115200)",
1225                );
1226            }
1227            if !rest.contains("#baudrate=") {
1228                return Err(
1229                    "serial locator must include #baudrate=RATE (e.g., serial//dev/ttyUSB0#baudrate=115200)",
1230                );
1231            }
1232            // Validate baudrate is numeric
1233            if let Some(baud_str) = rest.split("#baudrate=").nth(1) {
1234                let baud_str = baud_str.split('#').next().unwrap_or(baud_str);
1235                if baud_str.parse::<u32>().is_err() {
1236                    return Err("serial baudrate must be a number");
1237                }
1238            }
1239            Ok(())
1240        }
1241        LocatorProtocol::Unknown => {
1242            Err("unknown locator protocol (expected tcp/, udp/, or serial/)")
1243        }
1244    }
1245}
1246
1247/// Session mode
1248#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1249pub enum SessionMode {
1250    /// Connect as client to a router
1251    #[default]
1252    Client,
1253    /// Connect as peer for peer-to-peer communication
1254    Peer,
1255}
1256
1257/// Transport session trait — the per-process anchor an RMW backend
1258/// gives to the executor.
1259///
1260/// # Threading
1261///
1262/// `&mut self` on every method means the executor serialises all
1263/// session calls onto a single thread. A backend may rely on this
1264/// — no internal locking is required for `create_*` / `close` /
1265/// `drive_io`. **Publisher / subscriber / service handles created
1266/// from the session, however, are typically used from worker
1267/// threads** and must carry their own synchronisation (see the
1268/// [`Publisher`] / [`Subscription`] trait docs).
1269///
1270/// # Calling pattern
1271///
1272/// 1. Open the session (backend-specific factory; not on this trait).
1273/// 2. `create_*` for every entity at startup. Creating entities mid-
1274///    flight after `drive_io` has run is allowed but not common.
1275/// 3. The executor calls `drive_io` periodically. Worker threads
1276///    publish / receive in parallel.
1277/// 4. `close` once at shutdown. Entities must be dropped first.
1278pub trait Session {
1279    /// RFC-0088 — the serialization format this backend speaks, as ROS 2's
1280    /// `rmw_get_serialization_format()` reports it ("One middleware can only
1281    /// have one encoding").
1282    ///
1283    /// Defaulted to CDR because every backend in tree except uORB speaks it,
1284    /// and a backend that speaks something else says so by overriding these
1285    /// two. They travel together: `SERIALIZATION_FORMAT` is the identity that
1286    /// crosses images (bridge config, tooling, the vtable slot) and
1287    /// `SERIALIZATION_FORMAT_ID` is the image-local discriminant used for the
1288    /// one-byte comparison a bridge makes at construction.
1289    const SERIALIZATION_FORMAT: &'static str =
1290        nros_serdes::format::SerializationFormatId::Cdr.as_str();
1291
1292    /// Image-local discriminant for [`Self::SERIALIZATION_FORMAT`]. Never
1293    /// persisted, never compared across images — see `nros_serdes::format`.
1294    const SERIALIZATION_FORMAT_ID: nros_serdes::format::SerializationFormatId =
1295        nros_serdes::format::SerializationFormatId::Cdr;
1296
1297    /// RFC-0088 — this session's serialization format, as ROS 2's
1298    /// `rmw_get_serialization_format()` reports it.
1299    ///
1300    /// **Per session, not per process.** ROS 2's function takes no handle
1301    /// because one process links one middleware; an `Executor::open_multi`
1302    /// image links two, so the answer must be asked of the session.
1303    ///
1304    /// The default answers from [`Self::SERIALIZATION_FORMAT`], which is right
1305    /// for any backend whose format is a compile-time fact. A session that
1306    /// dispatches to a backend chosen at run time — the C-ABI adapter, whose
1307    /// vtable carries the answer — overrides this to ask the backend.
1308    fn serialization_format(&self) -> &'static str {
1309        Self::SERIALIZATION_FORMAT
1310    }
1311
1312    /// Error type for this session
1313    type Error;
1314    /// Publisher handle type
1315    type PublisherHandle;
1316    /// Subscription handle type
1317    type SubscriptionHandle;
1318    /// Service server handle type
1319    type ServiceHandle;
1320    /// Service client handle type
1321    type ClientHandle;
1322
1323    /// Create a publisher bound to this session.
1324    ///
1325    /// May allocate transport resources (zenoh declarations, DDS
1326    /// writers). Returns a handle that outlives the call but not the
1327    /// session — drop the handle before `close()`.
1328    fn create_publisher(
1329        &mut self,
1330        topic: &TopicInfo,
1331        qos: QoSProfile,
1332    ) -> Result<Self::PublisherHandle, Self::Error>;
1333
1334    /// Create a subscriber bound to this session.
1335    ///
1336    /// Subscribers may start receiving immediately after creation if
1337    /// the transport supports late-joining publishers. Late messages
1338    /// are buffered up to the QoS depth.
1339    fn create_subscription(
1340        &mut self,
1341        topic: &TopicInfo,
1342        qos: QoSProfile,
1343    ) -> Result<Self::SubscriptionHandle, Self::Error>;
1344
1345    /// Create a service server bound to this session. Replies are
1346    /// matched to requests by the sequence number returned from
1347    /// [`ServiceTrait::take_request`].
1348    ///
1349    /// `qos` is applied to both the request and reply endpoints (a
1350    /// service is two DDS topics; rmw uses one profile for both). The
1351    /// default is [`QoSProfile::services_default`]
1352    /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
1353    fn create_service(
1354        &mut self,
1355        service: &ServiceInfo,
1356        qos: QoSProfile,
1357    ) -> Result<Self::ServiceHandle, Self::Error>;
1358
1359    /// Create a service client bound to this session.
1360    ///
1361    /// `qos` is applied to both the request and reply endpoints (a
1362    /// service is two DDS topics; rmw uses one profile for both). The
1363    /// default is [`QoSProfile::services_default`]
1364    /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
1365    fn create_client(
1366        &mut self,
1367        service: &ServiceInfo,
1368        qos: QoSProfile,
1369    ) -> Result<Self::ClientHandle, Self::Error>;
1370
1371    /// Close the session, releasing transport resources. All entity
1372    /// handles created from this session must already be dropped.
1373    fn close(&mut self) -> Result<(), Self::Error>;
1374
1375    /// Drive transport I/O (poll network, dispatch callbacks).
1376    ///
1377    /// Both zenoh-pico and XRCE-DDS are pull-based: they require the
1378    /// application to periodically call this method to read from the
1379    /// network socket and dispatch incoming messages to subscriber
1380    /// buffers.
1381    ///
1382    /// `timeout_ms` is the maximum time to wait for data (0 = non-blocking;
1383    /// negative values mean "block indefinitely" — see Phase 84.D7 for the
1384    /// planned migration to `core::time::Duration`).
1385    ///
1386    /// **Required**. There is no default body — both shipped backends
1387    /// (zenoh and XRCE) must drive I/O, and a silent no-op default was a
1388    /// trap for third-party implementers. If your backend genuinely
1389    /// receives data via OS callbacks (push-based) and has nothing to do
1390    /// here, return `Ok(())` explicitly.
1391    fn drive_io(&mut self, timeout_ms: i32) -> Result<(), Self::Error>;
1392
1393    /// Phase 109 — report which QoS policies the active backend
1394    /// honours. The runtime validates requested QoS against this mask
1395    /// at entity-create time and returns
1396    /// [`TransportError::IncompatibleQos`] if the requested profile
1397    /// includes a policy the backend can't enforce. **No silent
1398    /// downgrade.**
1399    ///
1400    /// Default returns [`QoSPolicyMask::CORE`] — reliability +
1401    /// durability VOLATILE + history + depth. Backends override per
1402    /// supported policy.
1403    fn supported_qos_policies(&self) -> QoSPolicyMask {
1404        QoSPolicyMask::CORE
1405    }
1406
1407    /// Phase 110.0 — backend's next internal-event deadline in
1408    /// milliseconds from now (lease keepalive, heartbeat, reader
1409    /// ACK-NACK timeout, etc.).
1410    ///
1411    /// The executor caps its `drive_io` timeout against
1412    /// `min(user_timeout, timer_deadline, this)` so quiet links don't
1413    /// wake early, see no user-visible work, and round-trip back into
1414    /// `drive_io`. Returns `None` when the backend has no internal
1415    /// deadlines or chooses not to expose them.
1416    ///
1417    /// Default `None` keeps existing backends working unchanged; opt-in
1418    /// per backend.
1419    fn next_deadline_ms(&self) -> Option<u32> {
1420        None
1421    }
1422
1423    /// Phase 124.B.1 — install (or clear, when `cb.is_none()`) the
1424    /// executor wake callback. The runtime calls this once per
1425    /// session after `open` with `cb` pointing at a runtime-owned
1426    /// function and `ctx` pointing at the executor's wake state.
1427    /// The backend stores `(cb, ctx)` in its per-session state and
1428    /// calls `cb(ctx)` whenever its transport notification path
1429    /// fires (datagram arrival, condvar wake, etc.) — the runtime
1430    /// cb does flag-write + condvar-signal atomically, so a
1431    /// `spin_once` blocked on the wake condvar resumes immediately
1432    /// instead of waiting for the next poll iteration.
1433    ///
1434    /// # Safety
1435    ///
1436    /// When `cb` is `Some`, `ctx` must remain valid until the callback is
1437    /// cleared or the session is closed. The backend may invoke `cb(ctx)` from
1438    /// its transport notification path.
1439    ///
1440    /// Default body: ignore the call. Poll-only backends (XRCE,
1441    /// bare-metal) leave the default in place; the executor still
1442    /// drains them on its deadline-bound cv-wait boundary.
1443    unsafe fn set_wake_callback(
1444        &mut self,
1445        cb: Option<unsafe extern "C" fn(ctx: *mut core::ffi::c_void)>,
1446        ctx: *mut core::ffi::c_void,
1447    ) {
1448        let _ = (cb, ctx);
1449    }
1450
1451    /// Phase 130.4 — does this backend actually honour
1452    /// [`set_wake_callback`]?
1453    ///
1454    /// `true` means the backend installs the callback and will
1455    /// fire it from its async notify path (worker thread, ISR,
1456    /// signalfd, …). `false` (the default) means
1457    /// `set_wake_callback` was a no-op — the executor must drive
1458    /// I/O for the caller's full timeout because no async wake
1459    /// will pre-empt it.
1460    ///
1461    /// The executor uses this to choose between the wake-primitive
1462    /// wait (`NodeWake::wait_ms` / `std::Condvar::wait_timeout_while`)
1463    /// and a direct `drive_io(timeout_ms)`. Poll-only backends
1464    /// (XRCE-DDS-Client, bare-metal smoltcp) return `false`;
1465    /// event-driven backends (zenoh-pico with an RX task that
1466    /// invokes the callback on packet arrival) return `true`.
1467    ///
1468    /// [`set_wake_callback`]: Self::set_wake_callback
1469    fn supports_wake_callback(&self) -> bool {
1470        false
1471    }
1472
1473    /// Phase 124.F.1 — session-level connectivity probe.
1474    ///
1475    /// Sends a wire-level round-trip probe and waits up to
1476    /// `timeout_ms`. `Ok(())` on reply, `Err(TransportError::Timeout)`
1477    /// on no reply, `Err(TransportError::Unsupported)` when the
1478    /// backend can't probe (DDS without participant introspection).
1479    /// Lesson from micro-ROS's `rmw_uros_ping_agent`.
1480    ///
1481    /// Default body: `Err(Unsupported)`. Backends with a native
1482    /// ping API (zenoh: `z_send_ping`; XRCE:
1483    /// `uxr_ping_agent_session_until_timeout`) opt in by overriding.
1484    fn ping_session(&mut self, timeout_ms: i32) -> Result<(), Self::Error>
1485    where
1486        Self::Error: From<TransportError>,
1487    {
1488        let _ = timeout_ms;
1489        Err(TransportError::Unsupported.into())
1490    }
1491
1492    /// phase-381 W3 — enumerate the nodes this session can see.
1493    ///
1494    /// A VISITOR, not a returned collection, because upstream's
1495    /// `rcutils_string_array_t` allocates two levels deep and there is no
1496    /// allocator at this seam. A caller-provided buffer is worse than it looks:
1497    /// the graph has no bound the CALLER can know. So the backend streams from
1498    /// state it already holds, peak extra RAM is one entry, and a caller with a
1499    /// bound stops early by returning `false`.
1500    ///
1501    /// `namespace` and `name` are ROS names. `enclave` is `None` where the
1502    /// backend does not track one — which is what lets this one method answer
1503    /// both `rmw_get_node_names` and `rmw_get_node_names_with_enclaves`.
1504    ///
1505    /// Every string is BORROWED for the duration of the call.
1506    ///
1507    /// **Must not block on the wire, and takes no timeout.** It reports what has
1508    /// ALREADY arrived, so the first call after startup legitimately returns a
1509    /// partial graph — a backend feeds its view from `drive_io`. Letting this
1510    /// block was considered and rejected: it would stall the executor's only
1511    /// thread inside an introspection call, on a runtime whose premise is that
1512    /// there is no other thread to do the work.
1513    ///
1514    /// Default body: `Err(Unsupported)` — a backend with no graph (XRCE) says
1515    /// so, and the runtime can tell that from an empty graph.
1516    fn get_node_names(
1517        &mut self,
1518        visit: &mut dyn FnMut(&str, &str, Option<&str>) -> bool,
1519    ) -> Result<(), Self::Error>
1520    where
1521        Self::Error: From<TransportError>,
1522    {
1523        let _ = visit;
1524        Err(TransportError::Unsupported.into())
1525    }
1526
1527    /// phase-381 W3 — how many publishers this session can see on `topic_name`.
1528    ///
1529    /// `topic_name` is a ROS name (`/chatter`); the backend mangles as needed.
1530    /// Same warm-up caveat as [`Self::get_node_names`]: a count reflects what
1531    /// has already been discovered, so it can be low right after startup and is
1532    /// never a proof of absence.
1533    ///
1534    /// Default body: `Err(Unsupported)` — distinct from `Ok(0)`, which claims
1535    /// there are none.
1536    fn count_publishers(&mut self, topic_name: &str) -> Result<usize, Self::Error>
1537    where
1538        Self::Error: From<TransportError>,
1539    {
1540        let _ = topic_name;
1541        Err(TransportError::Unsupported.into())
1542    }
1543
1544    /// phase-381 W3 — how many subscribers this session can see on `topic_name`.
1545    /// See [`Self::count_publishers`] for the caveats.
1546    fn count_subscribers(&mut self, topic_name: &str) -> Result<usize, Self::Error>
1547    where
1548        Self::Error: From<TransportError>,
1549    {
1550        let _ = topic_name;
1551        Err(TransportError::Unsupported.into())
1552    }
1553
1554    /// phase-381 W3 — every topic, with the types published or subscribed on it.
1555    ///
1556    /// A visitor for the same reason as [`Self::get_node_names`], and one call
1557    /// per distinct NAME: the contract hands over a name and the types on it,
1558    /// so a topic carrying two types is one visit with two entries, not two
1559    /// visits.
1560    ///
1561    /// `types_count` may legitimately be 0 on a partially discovered graph —
1562    /// reporting the name without a type beats dropping it.
1563    ///
1564    /// Default body: `Err(Unsupported)`.
1565    fn get_topic_names_and_types(
1566        &mut self,
1567        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
1568    ) -> Result<(), Self::Error>
1569    where
1570        Self::Error: From<TransportError>,
1571    {
1572        let _ = visit;
1573        Err(TransportError::Unsupported.into())
1574    }
1575
1576    /// phase-381 W3 — every service, with its types. As
1577    /// [`Self::get_topic_names_and_types`], over servers and clients.
1578    fn get_service_names_and_types(
1579        &mut self,
1580        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
1581    ) -> Result<(), Self::Error>
1582    where
1583        Self::Error: From<TransportError>,
1584    {
1585        let _ = visit;
1586        Err(TransportError::Unsupported.into())
1587    }
1588
1589    /// phase-381 W3 — what ONE named node publishes / subscribes / serves /
1590    /// calls.
1591    ///
1592    /// `node_name` and `node_namespace` are ROS names; a node the graph has not
1593    /// discovered yields no visits, which is NOT an error — see
1594    /// [`Self::get_node_names`] for why an empty answer is "not seen yet".
1595    ///
1596    /// `kind` selects which of the four upstream `*_by_node` questions this
1597    /// answers. One method rather than four because the four differ ONLY by
1598    /// which entity kind they keep, and four trait methods would be four copies
1599    /// of one filter.
1600    ///
1601    /// The trait keeps the ABI's `subscriber` vocabulary because
1602    /// `rmw_get_subscriber_names_and_types_by_node` is what upstream rmw calls
1603    /// it and RFC-0054 makes the C headers the SSoT. The USER-facing spelling
1604    /// is per language and settled at that layer: rcl says `subscriber`, rclcpp
1605    /// and rclrs say `subscription`.
1606    ///
1607    /// Default body: `Err(Unsupported)`.
1608    fn get_names_and_types_by_node(
1609        &mut self,
1610        kind: GraphEntityKind,
1611        node_name: &str,
1612        node_namespace: &str,
1613        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
1614    ) -> Result<(), Self::Error>
1615    where
1616        Self::Error: From<TransportError>,
1617    {
1618        let _ = (kind, node_name, node_namespace, visit);
1619        Err(TransportError::Unsupported.into())
1620    }
1621
1622    /// phase-381 W3 — the endpoints on one topic, with the QoS each GRANTED.
1623    ///
1624    /// `publishers` selects `rmw_get_publishers_info_by_topic` (`true`) or
1625    /// `rmw_get_subscriptions_info_by_topic` (`false`).
1626    ///
1627    /// The granted profile is the whole reason a consumer asks: "why is nothing
1628    /// arriving" is usually a QoS incompatibility, and the REQUESTED profile
1629    /// cannot answer it. A backend that cannot read a remote's granted QoS says
1630    /// so per field rather than echoing the request back — see
1631    /// `rmw_topic_endpoint_info_t`.
1632    ///
1633    /// Default body: `Err(Unsupported)`.
1634    fn get_endpoint_info_by_topic(
1635        &mut self,
1636        publishers: bool,
1637        topic_name: &str,
1638        visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool,
1639    ) -> Result<(), Self::Error>
1640    where
1641        Self::Error: From<TransportError>,
1642    {
1643        let _ = (publishers, topic_name, visit);
1644        Err(TransportError::Unsupported.into())
1645    }
1646}
1647
1648/// Which entity kind a `*_by_node` graph query keeps — phase-381 W3.
1649///
1650/// Named for upstream rmw's four `*_by_node` slots. `Subscriber` carries rmw's
1651/// word, not rclcpp's `subscription`; the user-facing spelling is chosen per
1652/// language one layer up.
1653#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1654pub enum GraphEntityKind {
1655    Publisher,
1656    Subscriber,
1657    Service,
1658    Client,
1659}
1660
1661/// One discovered endpoint on a topic — phase-381 W3, the Rust view of
1662/// `rmw_topic_endpoint_info_t`.
1663///
1664/// Every string BORROWS from the backend's own state for the duration of the
1665/// visit. A caller that needs one afterwards copies it; that is what lets the
1666/// graph stream without an allocator.
1667#[derive(Debug, Clone)]
1668pub struct GraphEndpointInfo<'a> {
1669    /// Node that owns the endpoint.
1670    pub node_name: &'a str,
1671    /// That node's namespace.
1672    pub node_namespace: &'a str,
1673    /// Fully-qualified type on the wire, e.g. `"std_msgs/msg/Int32"`.
1674    pub topic_type: &'a str,
1675    /// `true` for a publisher, `false` for a subscription.
1676    pub is_publisher: bool,
1677    /// The endpoint's 24-byte identity; all-zero when the backend has none.
1678    pub endpoint_gid: [u8; 24],
1679}
1680
1681// No `qos` field, deliberately. The C seam carries one and fills it with the
1682// ABI's `*_UNKNOWN` sentinels, which is the contract for a policy a backend
1683// cannot determine (see `publisher_get_actual_qos`: write UNKNOWN and return
1684// OK; `UNSUPPORTED` means no read-back AT ALL).
1685//
1686// No backend can fill it today. zenoh's liveliness token carries the DECLARING
1687// side's own profile, not a negotiated grant, and this seam promises the
1688// granted one — reporting the declaration would be the plausible wrong answer
1689// the slot exists to avoid, since "why is nothing arriving" is usually a QoS
1690// mismatch and the requested profile cannot show it. Cyclone can read a real
1691// grant and will need this; adding a field to a Rust struct then is additive
1692// and not an ABI change, which is why carrying an always-`None` field now
1693// buys nothing.
1694
1695/// Bitmask of QoS policies a backend can honour. See
1696/// [`Session::supported_qos_policies`].
1697///
1698/// `CORE` covers the policies every nano-ros backend implements:
1699/// reliability, durability=VOLATILE, history, depth. Backends opt
1700/// into additional policies by OR-ing the relevant flags.
1701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1702pub struct QoSPolicyMask(pub u32);
1703
1704impl QoSPolicyMask {
1705    pub const RELIABILITY: Self = Self(1 << 0);
1706    pub const DURABILITY_VOLATILE: Self = Self(1 << 1);
1707    pub const DURABILITY_TRANSIENT_LOCAL: Self = Self(1 << 2);
1708    pub const HISTORY: Self = Self(1 << 3);
1709    pub const DEPTH: Self = Self(1 << 4);
1710    pub const DEADLINE: Self = Self(1 << 5);
1711    pub const LIFESPAN: Self = Self(1 << 6);
1712    pub const LIVELINESS_AUTOMATIC: Self = Self(1 << 7);
1713    pub const LIVELINESS_MANUAL_BY_TOPIC: Self = Self(1 << 8);
1714    pub const LIVELINESS_MANUAL_BY_NODE: Self = Self(1 << 9);
1715    pub const LIVELINESS_LEASE: Self = Self(1 << 10);
1716    pub const AVOID_ROS_NAMESPACE_CONVENTIONS: Self = Self(1 << 11);
1717
1718    /// Policies every nano-ros backend implements.
1719    pub const CORE: Self =
1720        Self(Self::RELIABILITY.0 | Self::DURABILITY_VOLATILE.0 | Self::HISTORY.0 | Self::DEPTH.0);
1721
1722    /// `true` if `self` contains every policy in `other`.
1723    pub const fn contains(self, other: Self) -> bool {
1724        self.0 & other.0 == other.0
1725    }
1726
1727    /// Bitwise OR of two masks.
1728    pub const fn union(self, other: Self) -> Self {
1729        Self(self.0 | other.0)
1730    }
1731}
1732
1733impl core::ops::BitOr for QoSPolicyMask {
1734    type Output = Self;
1735    fn bitor(self, rhs: Self) -> Self {
1736        self.union(rhs)
1737    }
1738}
1739
1740impl core::ops::BitOrAssign for QoSPolicyMask {
1741    fn bitor_assign(&mut self, rhs: Self) {
1742        self.0 |= rhs.0;
1743    }
1744}
1745
1746impl QoSProfile {
1747    /// Compute the set of QoS policies actually requested by this profile.
1748    ///
1749    /// Zero-valued time fields and `LivelinessKind::None` count as "not
1750    /// requesting" the corresponding policy — the cheap default.
1751    ///
1752    /// issue 0829 — the four `CORE` bits (reliability, durability, history,
1753    /// depth) used to be added UNCONDITIONALLY, on the reasoning that every
1754    /// nano-ros backend honours them. That is true and still not the right
1755    /// test: this function answers *what did the caller ASK FOR*, and a
1756    /// `SYSTEM_DEFAULT` policy asks for nothing. Starting from `CORE` made
1757    /// `QOS_PROFILE_SYSTEM_DEFAULT` demand all four, so a backend that could
1758    /// not honour one would reject the profile with `IncompatibleQos` — for
1759    /// requesting nothing. The pattern was already here for the extended
1760    /// policies three lines down (a zero `deadline_ms` declines its bit); it
1761    /// just never reached the four CORE ones.
1762    ///
1763    /// This RELAXES the mask, so it can only turn a rejection into an
1764    /// acceptance, never the reverse — and it flips no verdict today: every
1765    /// `supported_qos_policies` impl in the tree returns at least `CORE`
1766    /// (`traits.rs` default impl, `nros-node/src/mock.rs:259`,
1767    /// `nros-rmw-cffi/src/lib.rs:2580`, `nros-rmw-zenoh/src/shim/session.rs:1245`),
1768    /// so a CORE bit has never been the reason for a failure.
1769    pub fn required_policies(&self) -> QoSPolicyMask {
1770        let mut mask = QoSPolicyMask(0);
1771        if self.reliability != QoSReliabilityPolicy::SystemDefault {
1772            mask |= QoSPolicyMask::RELIABILITY;
1773        }
1774        if self.history != QoSHistoryPolicy::SystemDefault {
1775            mask |= QoSPolicyMask::HISTORY;
1776        }
1777        // Depth's sentinel is a VALUE, not a variant: `DEPTH_SYSTEM_DEFAULT`
1778        // is 0, the same 0 that `KEEP_ALL` profiles carry to mean "depth is
1779        // not used here" (`QOS_PROFILE_PARAMETER_EVENTS`). Both mean the
1780        // caller did not ask for a depth, so both decline the bit.
1781        if self.depth != DEPTH_SYSTEM_DEFAULT {
1782            mask |= QoSPolicyMask::DEPTH;
1783        }
1784        match self.durability {
1785            QoSDurabilityPolicy::SystemDefault => {}
1786            QoSDurabilityPolicy::Volatile => mask |= QoSPolicyMask::DURABILITY_VOLATILE,
1787            QoSDurabilityPolicy::TransientLocal => {
1788                mask |= QoSPolicyMask::DURABILITY_TRANSIENT_LOCAL
1789            }
1790        }
1791        // Phase-301 (issue 0241): DURATION_INFINITE_MS reads the same as 0
1792        // (infinite = no check) at every duration check site.
1793        if self.deadline_ms != 0 && self.deadline_ms != DURATION_INFINITE_MS {
1794            mask |= QoSPolicyMask::DEADLINE;
1795        }
1796        if self.lifespan_ms != 0 && self.lifespan_ms != DURATION_INFINITE_MS {
1797            mask |= QoSPolicyMask::LIFESPAN;
1798        }
1799        match self.liveliness_kind {
1800            QoSLivelinessPolicy::None => {}
1801            QoSLivelinessPolicy::Automatic => mask |= QoSPolicyMask::LIVELINESS_AUTOMATIC,
1802            QoSLivelinessPolicy::ManualByTopic => mask |= QoSPolicyMask::LIVELINESS_MANUAL_BY_TOPIC,
1803            QoSLivelinessPolicy::ManualByNode => mask |= QoSPolicyMask::LIVELINESS_MANUAL_BY_NODE,
1804        }
1805        if self.liveliness_lease_ms != 0 && self.liveliness_lease_ms != DURATION_INFINITE_MS {
1806            mask |= QoSPolicyMask::LIVELINESS_LEASE;
1807        }
1808        if self.avoid_ros_namespace_conventions {
1809            mask |= QoSPolicyMask::AVOID_ROS_NAMESPACE_CONVENTIONS;
1810        }
1811        mask
1812    }
1813
1814    /// Returns `Err(TransportError::IncompatibleQos)` if any policy this
1815    /// profile requires is missing from the backend's `supported` mask.
1816    /// Used at entity-create time to enforce the **no silent
1817    /// degradation** contract.
1818    pub fn validate_against(&self, supported: QoSPolicyMask) -> Result<(), TransportError> {
1819        if supported.contains(self.required_policies()) {
1820            Ok(())
1821        } else {
1822            Err(TransportError::IncompatibleQos)
1823        }
1824    }
1825
1826    /// Replace every `SYSTEM_DEFAULT` field with the backend's own answer.
1827    ///
1828    /// issue 0829 — a backend calls this at its create entry, **before**
1829    /// anything is derived from the profile (see [`QoSSystemDefaults`] for why
1830    /// the ordering matters on the zenoh path). Fields the caller DID state
1831    /// are left exactly as they are: this resolves an absence, it never
1832    /// overrides a request.
1833    ///
1834    /// Idempotent, and safe to call on a fully concrete profile — a profile
1835    /// with no sentinel in it is returned unchanged.
1836    #[must_use]
1837    pub const fn resolve_system_default(mut self, defaults: &QoSSystemDefaults) -> Self {
1838        if matches!(self.reliability, QoSReliabilityPolicy::SystemDefault) {
1839            self.reliability = defaults.reliability;
1840        }
1841        if matches!(self.durability, QoSDurabilityPolicy::SystemDefault) {
1842            self.durability = defaults.durability;
1843        }
1844        if matches!(self.history, QoSHistoryPolicy::SystemDefault) {
1845            self.history = defaults.history;
1846        }
1847        if self.depth == DEPTH_SYSTEM_DEFAULT {
1848            self.depth = defaults.depth;
1849        }
1850        self
1851    }
1852
1853    /// `true` if any field is still the `SYSTEM_DEFAULT` sentinel.
1854    ///
1855    /// Depth is deliberately NOT part of this test: a `KEEP_ALL` profile
1856    /// legitimately carries depth 0 forever (`QOS_PROFILE_PARAMETER_EVENTS`),
1857    /// and a backend that resolves the sentinel depth to 0 — XRCE does, on
1858    /// purpose — leaves a resolved profile reading 0 here. This asks about the
1859    /// three POLICY fields, where the sentinel is a distinct variant and so
1860    /// cannot be confused with a stated value.
1861    #[must_use]
1862    pub const fn has_unresolved_system_default(&self) -> bool {
1863        matches!(self.reliability, QoSReliabilityPolicy::SystemDefault)
1864            || matches!(self.durability, QoSDurabilityPolicy::SystemDefault)
1865            || matches!(self.history, QoSHistoryPolicy::SystemDefault)
1866    }
1867}
1868
1869/// Publisher trait for sending messages.
1870///
1871/// # Threading
1872///
1873/// `&self` on `publish_raw` — implementors must allow concurrent
1874/// publishes from multiple threads. Internal locking (or lock-free
1875/// queues) is the backend's responsibility.
1876///
1877/// # Buffer ownership
1878///
1879/// `data` in `publish_raw` is borrowed for the duration of the call.
1880/// The backend must either send it inline or copy into its own
1881/// buffer before returning — the slice is invalid after the call.
1882///
1883/// # Blocking
1884///
1885/// `publish_raw` is expected to be non-blocking on best-effort QoS
1886/// and bounded-blocking on reliable QoS (waiting for outbound queue
1887/// space). Backends should *not* block waiting for ack from a
1888/// matched subscriber.
1889pub trait Publisher {
1890    /// Error type for publish operations
1891    type Error;
1892
1893    /// Publish a CDR-serialised message.
1894    ///
1895    /// Returns once the message has been handed to the transport
1896    /// (queued or fired-and-forgotten depending on QoS). Does **not**
1897    /// wait for delivery.
1898    fn publish_raw(&self, data: &[u8]) -> Result<(), Self::Error>;
1899
1900    /// Phase 128.F.4 — publish with an opaque attachment block.
1901    ///
1902    /// `attachment` rides alongside the payload at the wire layer.
1903    /// Receivers can read it back via
1904    /// [`Subscription::take_serialized_with_attachment`].
1905    ///
1906    /// Primary use case: cross-RMW bridges stamp a `bridge_origin`
1907    /// tag (the source backend's RMW name) so a paired return
1908    /// bridge can deterministically drop echoed frames.
1909    ///
1910    /// Default body delegates to [`publish_raw`](Self::publish_raw)
1911    /// and discards the attachment — backends that do not natively
1912    /// carry attachments (XRCE today, DDS without a user-data hook)
1913    /// see no change. Backends with native attachment support
1914    /// (zenoh-pico's `z_publisher_put_options::attachment`,
1915    /// Cyclone DDS user-data) override to write the bytes onto the
1916    /// wire.
1917    fn publish_raw_with_attachment(
1918        &self,
1919        data: &[u8],
1920        _attachment: &[u8],
1921    ) -> Result<(), Self::Error> {
1922        self.publish_raw(data)
1923    }
1924
1925    /// Phase 124.E.1 — streamed publish.
1926    ///
1927    /// `size_cb` reports the total payload length once; `chunk_cb`
1928    /// fills the slot in chunks. Saves the per-publisher staging
1929    /// buffer when the message is large enough to dominate the
1930    /// device's `.bss`.
1931    ///
1932    /// Default body — the **staging-buffer fallback** (124.E.2).
1933    /// Asks `size_cb` for the total length, fills a stack-allocated
1934    /// `[u8; NROS_MAX_STREAM_CHUNK]` via `chunk_cb`, then forwards
1935    /// to `publish_raw`. Returns `Err(BufferTooSmall)` if the total
1936    /// exceeds the stack cap so the caller can drop back to a
1937    /// regular `publish_raw` with a heap-sized buffer.
1938    ///
1939    /// Concrete backends opt in by overriding to stream straight
1940    /// into the network buffer (zenoh: write into the zenoh-pico
1941    /// outbound buffer; XRCE: micro-CDR streaming APIs).
1942    ///
1943    /// # Safety
1944    ///
1945    /// The caller must ensure `user_ctx` is valid for every invocation of
1946    /// `size_cb` and `chunk_cb` during this call, and that both callbacks obey
1947    /// their out-pointer contracts.
1948    ///
1949    /// `size_cb` and `chunk_cb` may be called from the same thread
1950    /// that called `publish_streamed`. Backends MUST NOT defer the
1951    /// calls past the function return; the caller's `user_ctx`
1952    /// pointer is only guaranteed valid for the duration of the
1953    /// call.
1954    unsafe fn publish_streamed(
1955        &self,
1956        size_cb: unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut core::ffi::c_void),
1957        chunk_cb: unsafe extern "C" fn(
1958            out_buf: *mut u8,
1959            cap: usize,
1960            out_written: *mut usize,
1961            user_ctx: *mut core::ffi::c_void,
1962        ),
1963        user_ctx: *mut core::ffi::c_void,
1964    ) -> Result<(), Self::Error>
1965    where
1966        Self::Error: From<TransportError>,
1967    {
1968        /// Default staging-buffer cap. Stack-allocated, so embedded
1969        /// callers don't pay for it unless they actually invoke this
1970        /// fallback. 4 KiB matches typical RTPS frag-size +
1971        /// micro-XRCE message ceilings.
1972        const STAGE_CAP: usize = 4096;
1973
1974        let mut total = 0usize;
1975        // SAFETY: caller's contract on `size_cb` matches our trait
1976        // doc — fire once with a writable `*mut usize` slot.
1977        unsafe { size_cb(&mut total as *mut usize, user_ctx) };
1978        if total > STAGE_CAP {
1979            return Err(TransportError::BufferTooSmall.into());
1980        }
1981        let mut stage = [0u8; STAGE_CAP];
1982        let mut written_so_far = 0usize;
1983        while written_so_far < total {
1984            let mut chunk_written = 0usize;
1985            let remaining = total - written_so_far;
1986            // SAFETY: `chunk_cb` writes ≤ `cap` bytes to
1987            // `out_buf` and reports the count via `out_written`.
1988            unsafe {
1989                chunk_cb(
1990                    stage.as_mut_ptr().add(written_so_far),
1991                    remaining,
1992                    &mut chunk_written as *mut usize,
1993                    user_ctx,
1994                );
1995            }
1996            if chunk_written == 0 {
1997                // Caller signalled EOF early — treat the partial
1998                // write as a malformed sequence; reporting it as
1999                // BufferTooSmall keeps the surface tight without
2000                // adding a new variant.
2001                return Err(TransportError::BufferTooSmall.into());
2002            }
2003            written_so_far += chunk_written;
2004        }
2005        self.publish_raw(&stage[..total])
2006    }
2007
2008    /// Publish a typed message (serializes automatically)
2009    fn publish<M: RosMessage>(&self, msg: &M, buf: &mut [u8]) -> Result<(), Self::Error> {
2010        use nros_core::CdrWriter;
2011
2012        let mut writer = CdrWriter::new_with_header(buf).map_err(|_| self.buffer_error())?;
2013        msg.serialize(&mut writer)
2014            .map_err(|_| self.serialization_error())?;
2015        let len = writer.position();
2016        self.publish_raw(&buf[..len])
2017    }
2018
2019    /// Return a buffer-too-small error (implementation specific)
2020    fn buffer_error(&self) -> Self::Error;
2021
2022    /// Return a serialization error (implementation specific)
2023    fn serialization_error(&self) -> Self::Error;
2024
2025    /// Phase 108 — `true` if the backend can generate this event for
2026    /// this publisher. Default returns `false`; backends override per
2027    /// supported event kind.
2028    ///
2029    /// Only [`EventKind::LivelinessLost`](crate::event::EventKind::LivelinessLost) and
2030    /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) are publisher-side events;
2031    /// other kinds always return `false` here.
2032    fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
2033        false
2034    }
2035
2036    /// Phase 108 — register a callback fired when the named status
2037    /// event occurs. `deadline_ms` applies to
2038    /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) only; ignored otherwise.
2039    /// Default impl returns the backend's "unsupported"-shaped error.
2040    ///
2041    /// # Safety
2042    ///
2043    /// `cb` and `user_ctx` must remain valid for the entity's
2044    /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
2045    /// responsible for keeping the closure / context arena alive.
2046    unsafe fn register_event_callback(
2047        &mut self,
2048        _kind: crate::event::EventKind,
2049        _deadline_ms: u32,
2050        _cb: crate::event::EventCallback,
2051        _user_ctx: *mut core::ffi::c_void,
2052    ) -> Result<(), Self::Error> {
2053        Err(self.unsupported_event_error())
2054    }
2055
2056    /// Phase 108 — backend's error variant for "this event kind is
2057    /// not supported." Default impl reuses `serialization_error()`
2058    /// since most backends share an `Unsupported` variant; backends
2059    /// override if they have a distinct `Unsupported` mapping.
2060    fn unsupported_event_error(&self) -> Self::Error {
2061        self.serialization_error()
2062    }
2063
2064    /// Phase 109 — assert this publisher's liveliness manually.
2065    /// Required for publishers configured with
2066    /// `QoSLivelinessPolicy::ManualByTopic`. No-op for other
2067    /// liveliness kinds. Default impl returns `Ok(())` (no-op);
2068    /// backends override when they implement manual liveliness.
2069    fn assert_liveliness(&self) -> Result<(), Self::Error> {
2070        Ok(())
2071    }
2072}
2073
2074/// Subscription trait for receiving messages.
2075///
2076/// # Threading
2077///
2078/// `&mut self` on `take_serialized` — the executor takes exclusive
2079/// ownership of the subscriber for the duration of a receive. A
2080/// backend that wants to allow concurrent receives must split into
2081/// per-thread sub-handles internally.
2082///
2083/// # Buffer ownership
2084///
2085/// `buf` is caller-owned. The implementation copies the next ready
2086/// message into `buf` and returns the byte count. The caller may
2087/// re-use or drop `buf` immediately after the call.
2088///
2089/// # Blocking
2090///
2091/// `take_serialized` is **non-blocking**: returns `Ok(None)` (or
2092/// equivalent for backends that map empty into a zero-length read)
2093/// when no message is ready. Use [`Session::drive_io`] to wait for
2094/// data; never sleep inside `take_serialized`.
2095pub trait Subscription {
2096    /// Error type for receive operations
2097    type Error;
2098
2099    /// Check if data is available without consuming it.
2100    ///
2101    /// Non-destructive — does not advance the receive cursor.
2102    /// Conservative default returns `true` (always assume data may
2103    /// be available); backends should override with a real check
2104    /// to avoid spurious receive attempts.
2105    fn has_data(&self) -> bool {
2106        true
2107    }
2108
2109    /// Try to receive one message into `buf`.
2110    ///
2111    /// Non-blocking. On success returns `Ok(Some(len))` where `len`
2112    /// is the byte count written into `buf[..len]`. Returns
2113    /// `Ok(None)` if no message is ready. If `buf` is too small the
2114    /// backend may either truncate (and document it) or return an
2115    /// error (preferred).
2116    fn take_serialized(&mut self, buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
2117
2118    /// Phase 128.F.4 — receive with attachment bytes alongside the
2119    /// payload.
2120    ///
2121    /// On success returns `Ok(Some((payload_len, attachment_len)))`
2122    /// with the payload written into `buf[..payload_len]` and the
2123    /// attachment (if any) written into
2124    /// `att_buf[..attachment_len]`. `attachment_len == 0` means the
2125    /// incoming sample carried no attachment.
2126    ///
2127    /// Default body falls back to [`take_serialized`](Self::take_serialized)
2128    /// and reports a 0-length attachment. Backends with native
2129    /// attachment support override to populate `att_buf`. Cross-RMW
2130    /// bridges use the attachment to read the `bridge_origin` tag
2131    /// stamped by the sending side.
2132    fn take_serialized_with_attachment(
2133        &mut self,
2134        buf: &mut [u8],
2135        _att_buf: &mut [u8],
2136    ) -> Result<Option<(usize, usize)>, Self::Error> {
2137        match self.take_serialized(buf)? {
2138            Some(len) => Ok(Some((len, 0))),
2139            None => Ok(None),
2140        }
2141    }
2142
2143    /// Phase 124.D.1 — burst-take.
2144    ///
2145    /// Drain up to `max_msgs` queued samples into the contiguous
2146    /// `buf` block in one call, with the i-th sample at
2147    /// `buf[i * per_msg_cap .. i * per_msg_cap + out_lens[i]]`.
2148    /// Returns the number of messages actually delivered. Partial
2149    /// drains MUST report the count, not error out.
2150    ///
2151    /// Default body loop-drives `take_serialized` so callers can
2152    /// commit to the batched API regardless of backend support.
2153    /// Concrete backends opt in by overriding with a native batch
2154    /// take (zenoh queue drain, `dds_take(max_samples)`).
2155    fn take_sequence(
2156        &mut self,
2157        buf: &mut [u8],
2158        per_msg_cap: usize,
2159        max_msgs: usize,
2160        out_lens: &mut [usize],
2161    ) -> Result<usize, Self::Error> {
2162        if per_msg_cap == 0 || max_msgs == 0 {
2163            return Ok(0);
2164        }
2165        let limit = max_msgs.min(out_lens.len());
2166        let mut count = 0;
2167        for i in 0..limit {
2168            let slot = &mut buf[i * per_msg_cap..(i + 1) * per_msg_cap];
2169            // Issue 0971 — NOT `take_serialized(slot)?`. The `?` propagates the
2170            // error and DISCARDS `count`, which is precisely what the doc
2171            // comment above forbids ("Partial drains MUST report the count, not
2172            // error out") and what `c3af8c1d1` removed from the two concrete
2173            // implementations. A backend that does not override this body got
2174            // the original defect back through the default.
2175            //
2176            // The shape is the one that fix established: a drain that has
2177            // already taken messages reports the COUNT, and the error is
2178            // delivered by the NEXT call — the caller sees every message it was
2179            // handed, then the reason the drain stopped. With nothing taken
2180            // there is no count to protect, so the error goes out immediately.
2181            //
2182            // Parking is per-implementation state, which a default body has no
2183            // place to keep. So it does the half it can do correctly: it
2184            // returns the partial count and lets the error surface on the
2185            // caller's next `take_serialized`, which is where an unconsumed
2186            // backend error still sits.
2187            match self.take_serialized(slot) {
2188                Ok(Some(len)) => {
2189                    out_lens[i] = len;
2190                    count += 1;
2191                }
2192                Ok(None) => break,
2193                Err(e) => {
2194                    if count == 0 {
2195                        return Err(e);
2196                    }
2197                    break;
2198                }
2199            }
2200        }
2201        Ok(count)
2202    }
2203
2204    /// Try to receive a typed message (non-blocking)
2205    fn take<M: RosMessage>(&mut self, buf: &mut [u8]) -> Result<Option<M>, Self::Error> {
2206        use nros_core::CdrReader;
2207
2208        match self.take_serialized(buf)? {
2209            Some(len) => {
2210                let mut reader = CdrReader::new_with_header(&buf[..len])
2211                    .map_err(|_| self.deserialization_error())?;
2212                let msg = M::deserialize(&mut reader).map_err(|_| self.deserialization_error())?;
2213                Ok(Some(msg))
2214            }
2215            None => Ok(None),
2216        }
2217    }
2218
2219    /// Process the received message in-place without copying.
2220    ///
2221    /// Calls `f` with a reference to the raw CDR bytes in the subscriber's
2222    /// internal receive buffer, avoiding a copy into a caller-provided buffer.
2223    /// While `f` executes the buffer is exclusively borrowed — any messages
2224    /// arriving from the transport during that time are dropped to prevent
2225    /// data races.
2226    ///
2227    /// Returns `Ok(true)` if a message was available and `f` was called,
2228    /// `Ok(false)` if no message was available.
2229    ///
2230    /// **Default body**: returns `Err(MessageTooLarge)` — the old default
2231    /// silently truncated anything larger than 1 KB into a stack buffer,
2232    /// which broke large messages with no diagnostic. Backends must
2233    /// override this with a real zero-copy path if they advertise support
2234    /// for `process_raw_in_place`; callers that hit the default should
2235    /// use `take_serialized` with a caller-sized buffer instead.
2236    fn process_raw_in_place(&mut self, f: impl FnOnce(&[u8])) -> Result<bool, Self::Error>
2237    where
2238        Self::Error: From<TransportError>,
2239    {
2240        let _ = f;
2241        Err(TransportError::MessageTooLarge.into())
2242    }
2243
2244    /// Whether this backend implements the in-place dispatch methods
2245    /// ([`process_raw_in_place`](Subscription::process_raw_in_place) /
2246    /// [`process_raw_in_place_with_info`](Subscription::process_raw_in_place_with_info))
2247    /// with a real zero-copy borrow.
2248    ///
2249    /// The executor consults this at subscription registration to choose the
2250    /// **in-place** arena dispatch (borrow + deserialize from the backend slot, no
2251    /// arena buffer) over the **buffered** dispatch (copy into an arena buffer
2252    /// first). Backends that leave the in-place methods at their unsupported
2253    /// default return `false` (the default) and keep the buffered path. (RFC-0038,
2254    /// Phase 231 Wave 0.2.)
2255    fn supports_process_in_place(&self) -> bool {
2256        false
2257    }
2258
2259    /// In-place processing variant that also surfaces publisher metadata.
2260    ///
2261    /// Same borrow contract as
2262    /// [`process_raw_in_place`](Subscription::process_raw_in_place): `f` receives
2263    /// the raw CDR bytes plus the parsed [`MessageInfo`](nros_core::MessageInfo)
2264    /// — the co-located attachment (publisher GID / sequence / source timestamp),
2265    /// or `None` when no attachment was present — for the duration of the call;
2266    /// the slot is released after `f` returns. `Ok(true)` = a message was
2267    /// available and `f` was called; `Ok(false)` = none ready.
2268    ///
2269    /// **Default body**: returns the unsupported error (mirrors
2270    /// `process_raw_in_place`). Backends that advertise in-place support override
2271    /// this with a real zero-copy path; callers that hit the default should use
2272    /// the buffered [`take_serialized_with_info`](Subscription::take_serialized_with_info)
2273    /// path instead. (RFC-0038, Phase 231 Wave 0.1.)
2274    fn process_raw_in_place_with_info(
2275        &mut self,
2276        f: impl FnOnce(&[u8], Option<nros_core::MessageInfo>),
2277    ) -> Result<bool, Self::Error>
2278    where
2279        Self::Error: From<TransportError>,
2280    {
2281        let _ = f;
2282        Err(TransportError::MessageTooLarge.into())
2283    }
2284
2285    /// Try to receive raw data along with publisher metadata.
2286    ///
2287    /// When available, [`MessageInfo`](nros_core::MessageInfo) contains
2288    /// the publisher's GID (Global Identifier) and source timestamp,
2289    /// extracted from a transport-level attachment on the incoming message.
2290    ///
2291    /// Returns `Ok(Some((len, info)))` if data is available, where:
2292    /// - `len` is the number of bytes written to the buffer
2293    /// - `info` is the parsed publisher metadata (if attachment was present)
2294    ///
2295    /// Default: delegates to [`take_serialized`](Subscription::take_serialized) with no info.
2296    fn take_serialized_with_info(
2297        &mut self,
2298        buf: &mut [u8],
2299    ) -> Result<Option<(usize, Option<nros_core::MessageInfo>)>, Self::Error> {
2300        self.take_serialized(buf)
2301            .map(|opt| opt.map(|len| (len, None)))
2302    }
2303
2304    /// Try to receive raw data with E2E safety validation (CRC + sequence tracking).
2305    ///
2306    /// Returns `Ok(Some((len, status)))` if data is available, where:
2307    /// - `len` is the number of bytes written to the buffer
2308    /// - `status` is the integrity validation result
2309    ///
2310    /// Default: delegates to `take_serialized` with no CRC info.
2311    #[cfg(feature = "safety-e2e")]
2312    fn take_validated(
2313        &mut self,
2314        buf: &mut [u8],
2315    ) -> Result<Option<(usize, crate::IntegrityStatus)>, Self::Error> {
2316        self.take_serialized(buf).map(|opt| {
2317            opt.map(|len| {
2318                (
2319                    len,
2320                    crate::IntegrityStatus {
2321                        gap: 0,
2322                        duplicate: false,
2323                        crc_valid: None,
2324                    },
2325                )
2326            })
2327        })
2328    }
2329
2330    /// Register an async waker to be notified when data arrives.
2331    ///
2332    /// Called from `Future::poll()` implementations to store the waker.
2333    /// The transport backend calls `waker.wake()` from its receive callback
2334    /// when new data is available, enabling event-driven async without
2335    /// busy-polling.
2336    ///
2337    /// Default: no-op (backends that don't support waking simply ignore this).
2338    fn register_waker(&self, _waker: &core::task::Waker) {}
2339
2340    /// Return a deserialization error (implementation specific)
2341    fn deserialization_error(&self) -> Self::Error;
2342
2343    /// Phase 108 — `true` if the backend can generate this event for
2344    /// this subscriber. Default returns `false`; backends override per
2345    /// supported event kind.
2346    ///
2347    /// Subscription-side event kinds:
2348    /// [`EventKind::LivelinessChanged`](crate::event::EventKind::LivelinessChanged),
2349    /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed),
2350    /// [`EventKind::MessageLost`](crate::event::EventKind::MessageLost).
2351    /// Publisher kinds always return `false` here.
2352    fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
2353        false
2354    }
2355
2356    /// Phase 108 — register a callback fired when the named status
2357    /// event occurs. `deadline_ms` applies to
2358    /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed) only; ignored otherwise.
2359    /// Default impl returns the backend's "unsupported"-shaped error.
2360    ///
2361    /// # Safety
2362    ///
2363    /// `cb` and `user_ctx` must remain valid for the entity's
2364    /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
2365    /// responsible for keeping the closure / context arena alive.
2366    unsafe fn register_event_callback(
2367        &mut self,
2368        _kind: crate::event::EventKind,
2369        _deadline_ms: u32,
2370        _cb: crate::event::EventCallback,
2371        _user_ctx: *mut core::ffi::c_void,
2372    ) -> Result<(), Self::Error> {
2373        Err(self.unsupported_event_error())
2374    }
2375
2376    /// Phase 108 — backend's error variant for "this event kind is
2377    /// not supported." Default reuses `deserialization_error()` for
2378    /// backends that don't have a distinct `Unsupported` mapping.
2379    fn unsupported_event_error(&self) -> Self::Error {
2380        self.deserialization_error()
2381    }
2382}
2383
2384/// Service request from a client
2385pub struct ServiceRequest<'a> {
2386    /// Raw request data (CDR encoded)
2387    pub data: &'a [u8],
2388    /// Sequence number for request/response matching
2389    pub sequence_number: i64,
2390}
2391
2392// ============================================================================
2393// Phase 99 — zero-copy raw API: SlotLending / SlotBorrowing
2394// ============================================================================
2395//
2396// Backends that can lend a slot directly into their outbound buffer
2397// (zenoh-pico w/ unstable-zenoh-api, XRCE-DDS via uxr_prepare_output_stream,
2398// full DDS w/ SHM transport) implement these traits. Backends that cannot
2399// (uORB, default zenoh-pico) do NOT impl them — `EmbeddedRawPublisher` then
2400// falls back to its per-publisher arena and memcpys at commit time. Both
2401// paths land at `Publisher::publish_raw` for the actual wire write; only
2402// the user-side copy is eliminated when lending is available.
2403//
2404// Selection is **compile-time** via the `lending` Cargo feature. Each
2405// backend crate forwards its own `lending` feature to `nros-rmw/lending`
2406// when it can satisfy the trait. nros-node's `rmw-lending` aggregates.
2407// User opting `nros/rmw-lending` w/ a non-lending backend (e.g. uORB)
2408// gets a clear compile error from the unsatisfied trait bound on the
2409// concrete `RmwPublisher`.
2410
2411/// Backend can lend a writable slot into its outbound buffer.
2412///
2413/// The returned slot's lifetime is tied to `&self`; user fills it in
2414/// place, then calls [`commit_slot`](Self::commit_slot) to publish.
2415/// Dropping the slot without commit is a no-op (slot returned to free
2416/// pool); concurrent loan attempts that would exceed backend capacity
2417/// return [`TransportError::WouldBlock`] — never block.
2418#[cfg(feature = "lending")]
2419pub trait SlotLending: Publisher {
2420    /// Backend-owned writable slot. Holds a `&'a mut [u8]` and any
2421    /// state needed for commit_slot.
2422    type Slot<'a>: AsMut<[u8]> + 'a
2423    where
2424        Self: 'a;
2425
2426    /// Reserve a writable slot of `len` bytes from the backend's
2427    /// outbound buffer. Returns `Ok(None)` if the backend has no slot
2428    /// available (full); never blocks.
2429    fn try_lend_slot(&self, len: usize) -> Result<Option<Self::Slot<'_>>, Self::Error>;
2430
2431    /// Commit a previously-lent slot. Consumes the slot and triggers
2432    /// the actual wire write. Returns `Err` on backend send failure;
2433    /// the slot's bytes are lost in that case (caller must re-lend +
2434    /// re-fill to retry).
2435    fn commit_slot(&self, slot: Self::Slot<'_>) -> Result<(), Self::Error>;
2436}
2437
2438/// Backend can lend a read-only view into its receive buffer.
2439///
2440/// The returned view's lifetime is tied to `&mut self` (subscriber-
2441/// exclusive); dropping the view releases any backend lock and lets
2442/// the next message advance into the buffer.
2443#[cfg(feature = "lending")]
2444pub trait SlotBorrowing: Subscription {
2445    /// Backend-owned read-only view. Holds a `&'a [u8]` and any state
2446    /// needed to release the borrow on Drop.
2447    type View<'a>: AsRef<[u8]> + 'a
2448    where
2449        Self: 'a;
2450
2451    /// Try to borrow the next available message in place. Returns
2452    /// `Ok(None)` if no message is ready; never blocks.
2453    fn try_borrow(&mut self) -> Result<Option<Self::View<'_>>, Self::Error>;
2454}
2455
2456/// Service server trait for handling requests.
2457///
2458/// # Threading
2459///
2460/// `&mut self` on `take_request` and `send_response` — the executor
2461/// owns the server while a request is being handled. Handler bodies
2462/// run synchronously on the executor thread; long handlers should
2463/// dispatch work to a worker queue and reply later via the recorded
2464/// `sequence_number`.
2465///
2466/// # Calling pattern
2467///
2468/// 1. Executor calls `take_request(buf)`.
2469/// 2. If `Some(req)` returned, decode, run handler, encode reply.
2470/// 3. `send_response(req.sequence_number, &reply_buf)`.
2471///
2472/// `sequence_number` is the canonical request → reply correlation
2473/// token; backends derive it from the wire-level metadata (zenoh
2474/// query id, DDS sample identity).
2475pub trait ServiceTrait {
2476    /// Error type for service operations
2477    type Error;
2478
2479    /// Check if a request is available without consuming it.
2480    ///
2481    /// Non-destructive. Default returns `true` (always assume one
2482    /// may be available); backends should override with a real
2483    /// check.
2484    fn has_request(&self) -> bool {
2485        true
2486    }
2487
2488    /// Phase 122.3.c.6.e — register a `Waker` for event-driven
2489    /// service servers. Mirrors the matching method on
2490    /// `SubscriberTrait` / `ClientTrait`. Backends that
2491    /// surface incoming-request notifications wake `waker` when
2492    /// `has_request()` flips true. Default: no-op (backends without
2493    /// wake support ignore — caller falls back to polling).
2494    fn register_waker(&self, _waker: &core::task::Waker) {}
2495
2496    /// Try to receive a service request into `buf` (non-blocking).
2497    ///
2498    /// On success returns a `ServiceRequest` that borrows from
2499    /// `buf`. The borrow is released when the returned struct is
2500    /// dropped — typically before `send_response` is called, since
2501    /// `send_response` takes `&mut self`.
2502    fn take_request<'a>(
2503        &mut self,
2504        buf: &'a mut [u8],
2505    ) -> Result<Option<ServiceRequest<'a>>, Self::Error>;
2506
2507    /// Send a reply for the given sequence number. Non-blocking
2508    /// from the application's perspective; the backend may queue
2509    /// the reply for transport-level transmission.
2510    fn send_response(&mut self, sequence_number: i64, data: &[u8]) -> Result<(), Self::Error>;
2511
2512    /// Handle a service request with typed messages
2513    fn handle_request<S: RosService>(
2514        &mut self,
2515        req_buf: &mut [u8],
2516        reply_buf: &mut [u8],
2517        handler: impl FnOnce(&S::Request) -> S::Reply,
2518    ) -> Result<bool, Self::Error>
2519    where
2520        Self::Error: From<TransportError>,
2521    {
2522        use nros_core::{CdrReader, CdrWriter};
2523
2524        // First, try to receive a request and extract necessary data.
2525        // Capture the data slice's offset within `req_buf` so we can
2526        // re-borrow it after the `ServiceRequest` (which holds a
2527        // borrow into `req_buf`) is dropped. Some backends prepend a
2528        // header (DDS: 8-byte sequence number) and place the CDR
2529        // payload at a non-zero offset in the buffer; others (zenoh)
2530        // put it at offset 0. Reading from offset 0 unconditionally
2531        // would feed the prefix bytes to the CDR deserializer and
2532        // silently corrupt the request.
2533        let buf_start = req_buf.as_ptr() as usize;
2534        let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
2535            Some(request) => {
2536                let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
2537                (offset, request.data.len(), request.sequence_number)
2538            }
2539            None => return Ok(false),
2540        };
2541
2542        // Deserialize request from the captured offset.
2543        let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
2544            .map_err(|_| TransportError::DeserializationError)?;
2545        let req = S::Request::deserialize(&mut reader)
2546            .map_err(|_| TransportError::DeserializationError)?;
2547
2548        // Call handler
2549        let reply = handler(&req);
2550
2551        // Serialize reply
2552        let mut writer =
2553            CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
2554        reply
2555            .serialize(&mut writer)
2556            .map_err(|_| TransportError::SerializationError)?;
2557        let len = writer.position();
2558
2559        // Send reply (now we can borrow self mutably again)
2560        self.send_response(sequence_number, &reply_buf[..len])?;
2561        Ok(true)
2562    }
2563
2564    /// Handle a service request where the handler returns `Box<S::Reply>`
2565    ///
2566    /// Identical to `handle_request` but the handler returns a heap-allocated reply.
2567    /// This is needed for services with large response types (e.g., parameter services
2568    /// where `Vec<ParameterValue, 64>` is ~1MB+) that would overflow the stack.
2569    #[cfg(feature = "alloc")]
2570    fn handle_request_boxed<S: RosService>(
2571        &mut self,
2572        req_buf: &mut [u8],
2573        reply_buf: &mut [u8],
2574        handler: impl FnOnce(&S::Request) -> alloc::boxed::Box<S::Reply>,
2575    ) -> Result<bool, Self::Error>
2576    where
2577        Self::Error: From<TransportError>,
2578    {
2579        use nros_core::{CdrReader, CdrWriter};
2580
2581        let buf_start = req_buf.as_ptr() as usize;
2582        let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
2583            Some(request) => {
2584                let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
2585                (offset, request.data.len(), request.sequence_number)
2586            }
2587            None => return Ok(false),
2588        };
2589
2590        let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
2591            .map_err(|_| TransportError::DeserializationError)?;
2592        let req = S::Request::deserialize(&mut reader)
2593            .map_err(|_| TransportError::DeserializationError)?;
2594
2595        let reply = handler(&req);
2596
2597        let mut writer =
2598            CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
2599        reply
2600            .serialize(&mut writer)
2601            .map_err(|_| TransportError::SerializationError)?;
2602        let len = writer.position();
2603
2604        self.send_response(sequence_number, &reply_buf[..len])?;
2605        Ok(true)
2606    }
2607
2608    /// Handle one request by STREAMING: the handler reads fields off the wire
2609    /// and writes the reply's fields straight back, so neither the request nor
2610    /// the reply is ever materialised as a value.
2611    ///
2612    /// phase-382 W1'. `handle_request_boxed` above boxes the REPLY because the
2613    /// parameter responses are enormous — `GetParametersResponse` measures
2614    /// 1,176,072 bytes. It does not box the REQUEST, which is deserialized by
2615    /// value into a stack local one line above the handler, and
2616    /// `SetParametersRequest` measures **1,192,968 bytes**. So every
2617    /// `ros2 param set` against a node put a 1.19 MB local on the calling
2618    /// task's stack — larger than the reply the boxing exists for, on every
2619    /// platform, with `param-services` live on Zephyr.
2620    ///
2621    /// Streaming removes both, and removes the `alloc` requirement with them:
2622    /// the whole value is never needed, because serialisation happens on the
2623    /// line after construction. Three things make it safe rather than clever:
2624    ///
2625    /// * **No `rcl_interfaces` message uses a DHEADER** — every `Serialize` impl
2626    ///   is plain sequential CDR, so hand-written field writes are byte-identical
2627    ///   to the generated ones. (If a future message gains XCDR2 extensibility
2628    ///   this stops being true for THAT message; see RFC-0055.)
2629    /// * `req_buf` and `reply_buf` are disjoint, so a handler can hold the
2630    ///   reader and the writer at once.
2631    /// * `CdrReader::read_string` borrows out of `req_buf` rather than copying,
2632    ///   so a handler can look a name up without a buffer of its own.
2633    ///
2634    /// The cost is that the hand-written writes can drift from the generated
2635    /// `Serialize`. Guard it with a round-trip test that deserialises the
2636    /// streamed bytes back into the generated type — the by-value handler makes
2637    /// a good test-only oracle.
2638    ///
2639    /// No `alloc`, deliberately: this is the seam that lets `param-services` and
2640    /// `lifecycle-services` build without it.
2641    fn handle_request_raw(
2642        &mut self,
2643        req_buf: &mut [u8],
2644        reply_buf: &mut [u8],
2645        handler: impl FnOnce(
2646            &mut nros_core::CdrReader<'_>,
2647            &mut nros_core::CdrWriter<'_>,
2648        ) -> Result<(), TransportError>,
2649    ) -> Result<bool, Self::Error>
2650    where
2651        Self::Error: From<TransportError>,
2652    {
2653        use nros_core::{CdrReader, CdrWriter};
2654
2655        let buf_start = req_buf.as_ptr() as usize;
2656        let (data_offset, data_len, sequence_number) = match self.take_request(req_buf)? {
2657            Some(request) => {
2658                let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
2659                (offset, request.data.len(), request.sequence_number)
2660            }
2661            None => return Ok(false),
2662        };
2663
2664        // Split the borrow so the reader (over `req_buf`) and the writer (over
2665        // `reply_buf`) can be live at the same time. They are separate
2666        // parameters, so this is disjoint by construction.
2667        let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
2668            .map_err(|_| TransportError::DeserializationError)?;
2669        let mut writer =
2670            CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
2671
2672        handler(&mut reader, &mut writer)?;
2673        let len = writer.position();
2674
2675        self.send_response(sequence_number, &reply_buf[..len])?;
2676        Ok(true)
2677    }
2678}
2679
2680/// Service client trait for sending requests.
2681///
2682/// # Threading
2683///
2684/// `&mut self` on every method — the client is single-owner. For
2685/// fan-out request patterns, create one client per worker thread.
2686///
2687/// # Calling pattern
2688///
2689/// All in-tree backends route blocking waits through the executor:
2690///
2691/// 1. `send_request_raw(buf)` — non-blocking; returns once the
2692///    request is queued for transmission.
2693/// 2. The executor's `drive_io` runs.
2694/// 3. `take_response_raw(buf)` — non-blocking; returns
2695///    `Ok(Some(len))` when the reply is back.
2696///
2697/// Phase-301 (issue 0240): the deprecated blocking `call_raw` path is
2698/// DELETED — `send_request_raw` + `take_response_raw` is the one
2699/// request/reply path, and both are required for service-capable
2700/// backends.
2701pub trait ClientTrait {
2702    /// Error type for service operations
2703    type Error;
2704
2705    /// Send a service request without waiting for a reply (non-blocking).
2706    ///
2707    /// Returns the SEQUENCE ID the backend assigned. The caller must
2708    /// subsequently poll [`take_response_raw`](Self::take_response_raw) and
2709    /// match that id against the one the reply carries.
2710    ///
2711    /// Issue 0778 — this returned `()` until 2026-08-25, and every backend
2712    /// computed an id and discarded it. With nothing to correlate by, a client
2713    /// with two calls outstanding could not tell the replies apart, so each
2714    /// backend picked a policy: cyclonedds abandoned the older request, zenoh
2715    /// took the first reply. Both are wrong for `send_goal` and
2716    /// `SetParameters`, which travel this path and are not idempotent.
2717    fn send_request_raw(&mut self, request: &[u8]) -> Result<i64, Self::Error>;
2718
2719    /// Poll for a reply (non-blocking).
2720    ///
2721    /// Returns `Ok(Some((len, sequence_id)))` when a reply has arrived,
2722    /// `Ok(None)` if not yet available, or `Err` on failure. The
2723    /// `sequence_id` is the one [`send_request_raw`](Self::send_request_raw)
2724    /// returned for the request this answers.
2725    ///
2726    /// It used to say "a reply to the MOST RECENTLY sent request", which was
2727    /// the single-outstanding-call assumption written into the contract.
2728    fn take_response_raw(
2729        &mut self,
2730        reply_buf: &mut [u8],
2731    ) -> Result<Option<(usize, i64)>, Self::Error>;
2732
2733    /// Send a typed service request without waiting for a reply (non-blocking).
2734    ///
2735    /// Serializes the request into `req_buf` and calls [`send_request_raw`](Self::send_request_raw).
2736    fn send_request<S: RosService>(
2737        &mut self,
2738        request: &S::Request,
2739        req_buf: &mut [u8],
2740    ) -> Result<(), Self::Error>
2741    where
2742        Self::Error: From<TransportError>,
2743    {
2744        use nros_core::CdrWriter;
2745
2746        let mut writer =
2747            CdrWriter::new_with_header(req_buf).map_err(|_| TransportError::BufferTooSmall)?;
2748        request
2749            .serialize(&mut writer)
2750            .map_err(|_| TransportError::SerializationError)?;
2751        let req_len = writer.position();
2752
2753        self.send_request_raw(&req_buf[..req_len]).map(|_seq| ())
2754    }
2755
2756    /// Poll for a typed reply to the most recently sent request (non-blocking).
2757    ///
2758    /// Calls [`take_response_raw`](Self::take_response_raw) and deserializes if available.
2759    fn take_response<S: RosService>(
2760        &mut self,
2761        reply_buf: &mut [u8],
2762    ) -> Result<Option<S::Reply>, Self::Error>
2763    where
2764        Self::Error: From<TransportError>,
2765    {
2766        use nros_core::CdrReader;
2767
2768        match self.take_response_raw(reply_buf)? {
2769            Some((len, _seq)) => {
2770                let mut reader = CdrReader::new_with_header(&reply_buf[..len])
2771                    .map_err(|_| TransportError::DeserializationError)?;
2772                let reply = S::Reply::deserialize(&mut reader)
2773                    .map_err(|_| TransportError::DeserializationError)?;
2774                Ok(Some(reply))
2775            }
2776            None => Ok(None),
2777        }
2778    }
2779
2780    /// Register an async waker to be notified when a reply arrives.
2781    ///
2782    /// Called from `Future::poll()` implementations to store the waker.
2783    /// The transport backend calls `waker.wake()` from its reply callback
2784    /// when a response is available, enabling event-driven async without
2785    /// busy-polling.
2786    ///
2787    /// Default: no-op (backends that don't support waking simply ignore this).
2788    fn register_waker(&self, _waker: &core::task::Waker) {}
2789
2790    /// Begin a server-discovery query on this client (non-blocking).
2791    ///
2792    /// Models `rclcpp::ClientBase::wait_for_service` machinery: the backend
2793    /// fires off a discovery probe (typically a Zenoh liveliness query
2794    /// against the matching server's wildcarded liveliness keyexpr) and
2795    /// the caller polls [`poll_server_discovery`](Self::poll_server_discovery)
2796    /// to collect the result.
2797    ///
2798    /// Default impl: no-op success. Backends without a discovery channel
2799    /// (or those that always assume the server is reachable) can leave
2800    /// this default and have `poll_server_discovery` return
2801    /// `Ok(Some(true))` immediately.
2802    fn start_server_discovery(&mut self, _timeout_ms: u32) -> Result<(), Self::Error> {
2803        Ok(())
2804    }
2805
2806    /// Poll an in-flight server-discovery query.
2807    ///
2808    /// - `Ok(Some(true))` — at least one matching server has reported
2809    ///   back; safe to send the first request.
2810    /// - `Ok(Some(false))` — discovery query finished without finding
2811    ///   any matching server (timeout / no-replies).
2812    /// - `Ok(None)` — query still in flight.
2813    /// - `Err(_)` — transport-level failure unrelated to server presence.
2814    ///
2815    /// Default impl: returns `Ok(Some(true))` (i.e., "server is always
2816    /// assumed reachable"). The Zenoh backend overrides this with a
2817    /// liveliness-token check.
2818    fn poll_server_discovery(&mut self) -> Result<Option<bool>, Self::Error> {
2819        Ok(Some(true))
2820    }
2821
2822    /// Whether a matching service server is currently discoverable.
2823    ///
2824    /// Mirrors `rclcpp::ClientBase::service_is_ready` — the NAME is upstream's.
2825    /// The SHAPE is `rcl`'s: `rcl_service_server_is_available(node, client,
2826    /// bool *is_available)` returns `RCL_RET_OK` "if the check was made
2827    /// successfully (regardless of the service readiness)", i.e. the return
2828    /// code says whether the CHECK worked and the out-param carries the ANSWER.
2829    /// rclcpp collapses that to a bare `bool` and moves the error to
2830    /// exceptions; RFC-0018 forbids exceptions, so `Result<bool, _>` is how the
2831    /// same contract is expressed here (phase-379 W6, RFC-0036).
2832    ///
2833    /// Returns `Ok(true)` if at least one matching server has been
2834    /// discovered, `Ok(false)` if none yet, or `Err(_)` if the
2835    /// backend cannot answer (e.g. XRCE — micro-XRCE-DDS-Client has
2836    /// no participant enumeration). Distinct from
2837    /// [`is_server_ready`](Self::is_server_ready), which collapses
2838    /// "don't know" and "no server" into the same `false` answer.
2839    ///
2840    /// User-facing surface: `Client<S>::server_available()` in Rust,
2841    /// `nros_client_server_available()` in C/C++. Clients use this
2842    /// to gate the first request so a startup-ordering race
2843    /// (client opens before server's discovery announcement lands)
2844    /// doesn't surface as a request-side timeout.
2845    ///
2846    /// Default impl: `Err(TransportError::Unsupported)` — backends
2847    /// that support graph introspection (zenoh queryable interest,
2848    /// DDS built-in topic readers) opt in by overriding.
2849    fn service_is_ready(&self) -> Result<bool, Self::Error>
2850    where
2851        Self::Error: From<TransportError>,
2852    {
2853        Err(TransportError::Unsupported.into())
2854    }
2855}
2856
2857/// Transport backend trait (legacy).
2858///
2859/// Use [`Rmw`] for new code. This trait is retained for backward compatibility
2860/// with existing code that uses [`TransportConfig`] directly.
2861pub trait Transport {
2862    /// Error type for this transport
2863    type Error;
2864    /// Session type for this transport
2865    type Session: Session;
2866
2867    /// Open a new session with the given configuration
2868    fn open(config: &TransportConfig) -> Result<Self::Session, Self::Error>;
2869}
2870
2871/// Factory trait for compile-time middleware selection.
2872///
2873/// Embedded crates select a backend via feature flag:
2874/// ```rust,ignore
2875/// #[cfg(feature = "rmw-cffi")]
2876/// type DefaultRmw = nros_rmw_cffi::CffiRmw;
2877/// ```
2878///
2879/// Each backend provides its own `Rmw` implementation that bridges
2880/// from the middleware-agnostic [`RmwConfig`] to backend-specific
2881/// initialization.
2882///
2883/// Phase 84.E2: `open` consumes `self`. Backends carry their own
2884/// configuration (agent addresses, serial ports, TLS CA slots)
2885/// inside the factory value and hand that over to the session at
2886/// `open` time. All in-repo backends also implement
2887/// [`Default`]; most callers spell this as
2888/// `BackendRmw::default().open(&config)`.
2889pub trait Rmw {
2890    /// Session type returned by [`open`](Rmw::open)
2891    type Session: Session;
2892    /// Error type for session creation
2893    type Error: core::fmt::Debug;
2894
2895    /// Open a new middleware session with the given configuration.
2896    ///
2897    /// The backend maps [`RmwConfig`] fields to its own connection
2898    /// parameters (e.g., zenoh locator and session mode, XRCE-DDS
2899    /// agent address). Any backend-specific pre-open state stored
2900    /// on `self` (e.g. configured agent IP / port) is moved into the
2901    /// returned `Session`.
2902    fn open(self, config: &RmwConfig) -> Result<Self::Session, Self::Error>;
2903}
2904
2905#[cfg(test)]
2906mod tests {
2907    use super::*;
2908
2909    /// Issue 0971 — the DEFAULT `take_sequence` body must report a partial
2910    /// count rather than discard it, which is what its own doc comment says and
2911    /// what `?` did not do.
2912    ///
2913    /// This is the default's negative control: a backend that takes two
2914    /// messages and then errors must hand the caller those two. Before the fix
2915    /// the `?` threw them away and the caller could not tell two messages had
2916    /// been consumed — they were gone from the queue and absent from the
2917    /// result.
2918    mod take_sequence_default {
2919        use super::*;
2920
2921        #[derive(Debug, PartialEq)]
2922        struct Boom;
2923
2924        /// Yields `n` one-byte messages, then errors forever.
2925        struct ThenErrors {
2926            left: usize,
2927        }
2928
2929        impl Subscription for ThenErrors {
2930            type Error = Boom;
2931
2932            fn take_serialized(&mut self, buf: &mut [u8]) -> Result<Option<usize>, Self::Error> {
2933                if self.left == 0 {
2934                    return Err(Boom);
2935                }
2936                self.left -= 1;
2937                buf[0] = 0xAB;
2938                Ok(Some(1))
2939            }
2940
2941            fn deserialization_error(&self) -> Self::Error {
2942                Boom
2943            }
2944        }
2945
2946        #[test]
2947        fn a_partial_drain_reports_its_count_instead_of_the_error() {
2948            let mut sub = ThenErrors { left: 2 };
2949            let mut buf = [0u8; 4 * 8];
2950            let mut lens = [0usize; 4];
2951
2952            // Two messages are available, the third call errors.
2953            let got = sub.take_sequence(&mut buf, 8, 4, &mut lens);
2954
2955            assert_eq!(
2956                got,
2957                Ok(2),
2958                "a drain that took messages must report them; `?` discarded the \
2959                 count and the caller lost two consumed messages"
2960            );
2961            assert_eq!(&lens[..2], &[1, 1]);
2962        }
2963
2964        #[test]
2965        fn an_error_with_nothing_taken_is_returned_immediately() {
2966            let mut sub = ThenErrors { left: 0 };
2967            let mut buf = [0u8; 4 * 8];
2968            let mut lens = [0usize; 4];
2969
2970            // No count to protect, so the caller should see the error itself
2971            // rather than an ambiguous `Ok(0)`.
2972            assert_eq!(sub.take_sequence(&mut buf, 8, 4, &mut lens), Err(Boom));
2973        }
2974    }
2975
2976    #[test]
2977    fn test_topic_info() {
2978        let topic = TopicInfo::new("/chatter", "std_msgs::msg::dds_::String_", "abc123");
2979        assert_eq!(topic.name, "/chatter");
2980        assert_eq!(topic.domain_id, 0);
2981    }
2982
2983    #[test]
2984    fn qos_apply_overrides_matches_topic_and_role() {
2985        // Default is Reliable / Volatile / KeepLast(10).
2986        static OVERRIDES: &[QoSOverride] = &[
2987            QoSOverride {
2988                topic: "/chatter",
2989                role: QoSOverrideRole::Publisher,
2990                value: QoSOverrideValue::Reliability(QoSReliabilityPolicy::BestEffort),
2991            },
2992            QoSOverride {
2993                topic: "/chatter",
2994                role: QoSOverrideRole::Publisher,
2995                value: QoSOverrideValue::Depth(5),
2996            },
2997            QoSOverride {
2998                topic: "/scan",
2999                role: QoSOverrideRole::Subscription,
3000                value: QoSOverrideValue::Durability(QoSDurabilityPolicy::TransientLocal),
3001            },
3002        ];
3003
3004        // Matching topic + publisher role → reliability + depth applied.
3005        let pub_qos = QoSProfile::default().apply_overrides(
3006            "/chatter",
3007            QoSOverrideRole::Publisher,
3008            OVERRIDES,
3009        );
3010        assert_eq!(pub_qos.reliability, QoSReliabilityPolicy::BestEffort);
3011        assert_eq!(pub_qos.depth, 5);
3012        assert_eq!(pub_qos.durability, QoSDurabilityPolicy::Volatile); // untouched
3013
3014        // Same topic but subscription role → publisher overrides DON'T apply;
3015        // the /scan override is for a different topic → also no change.
3016        let sub_qos = QoSProfile::default().apply_overrides(
3017            "/chatter",
3018            QoSOverrideRole::Subscription,
3019            OVERRIDES,
3020        );
3021        assert_eq!(sub_qos, QoSProfile::default());
3022
3023        // The /scan subscription override applies only to /scan+subscription.
3024        let scan_qos = QoSProfile::default().apply_overrides(
3025            "/scan",
3026            QoSOverrideRole::Subscription,
3027            OVERRIDES,
3028        );
3029        assert_eq!(scan_qos.durability, QoSDurabilityPolicy::TransientLocal);
3030
3031        // Empty table → identity (the zero-override fast path).
3032        assert_eq!(
3033            QoSProfile::default().apply_overrides("/x", QoSOverrideRole::Publisher, &[]),
3034            QoSProfile::default()
3035        );
3036    }
3037
3038    #[test]
3039    fn test_qos_defaults() {
3040        let qos = QoSProfile::default();
3041        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3042    }
3043
3044    #[test]
3045    fn test_action_info() {
3046        let action = ActionInfo::new(
3047            "/fibonacci",
3048            "example_interfaces::action::dds_::Fibonacci_",
3049            "abc123",
3050        );
3051        assert_eq!(action.name, "/fibonacci");
3052        assert_eq!(action.domain_id, 0);
3053    }
3054
3055    #[test]
3056    fn test_action_info_with_domain() {
3057        let action = ActionInfo::new(
3058            "/fibonacci",
3059            "example_interfaces::action::dds_::Fibonacci_",
3060            "abc123",
3061        )
3062        .with_domain(42);
3063        assert_eq!(action.domain_id, 42);
3064    }
3065
3066    #[test]
3067    fn test_action_send_goal_key() {
3068        let action = ActionInfo::new(
3069            "/fibonacci",
3070            "example_interfaces::action::dds_::Fibonacci_",
3071            "abc123",
3072        )
3073        .with_domain(0);
3074
3075        let key: heapless::String<256> = action.send_goal_key();
3076        // ActionInfo returns the sub-entity name with leading slash for ROS 2 compatibility
3077        assert_eq!(key.as_str(), "/fibonacci/_action/send_goal");
3078    }
3079
3080    #[test]
3081    fn test_action_feedback_key() {
3082        let action = ActionInfo::new(
3083            "/fibonacci",
3084            "example_interfaces::action::dds_::Fibonacci_",
3085            "abc123",
3086        )
3087        .with_domain(0);
3088
3089        let key: heapless::String<256> = action.feedback_key();
3090        assert_eq!(key.as_str(), "/fibonacci/_action/feedback");
3091    }
3092
3093    #[test]
3094    fn test_action_all_sub_names() {
3095        let action = ActionInfo::new(
3096            "/fibonacci",
3097            "example_interfaces::action::dds_::Fibonacci_",
3098            "abc123",
3099        )
3100        .with_domain(0);
3101
3102        let cancel: heapless::String<256> = action.cancel_goal_key();
3103        assert_eq!(cancel.as_str(), "/fibonacci/_action/cancel_goal");
3104
3105        let result: heapless::String<256> = action.get_result_key();
3106        assert_eq!(result.as_str(), "/fibonacci/_action/get_result");
3107
3108        let status: heapless::String<256> = action.status_key();
3109        assert_eq!(status.as_str(), "/fibonacci/_action/status");
3110    }
3111
3112    // --- QoS Profile Tests ---
3113
3114    #[test]
3115    fn test_qos_profile_sensor_data() {
3116        let qos = QoSProfile::QOS_PROFILE_SENSOR_DATA;
3117        assert_eq!(qos.reliability, QoSReliabilityPolicy::BestEffort);
3118        assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
3119        assert_eq!(qos.history, QoSHistoryPolicy::KeepLast);
3120        assert_eq!(qos.depth, 5);
3121    }
3122
3123    #[test]
3124    fn test_qos_profile_default() {
3125        let qos = QoSProfile::QOS_PROFILE_DEFAULT;
3126        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3127        assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
3128        assert_eq!(qos.depth, 10);
3129    }
3130
3131    #[test]
3132    fn test_qos_profile_services_default() {
3133        let qos = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
3134        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3135        assert_eq!(qos.durability, QoSDurabilityPolicy::Volatile);
3136    }
3137
3138    /// issue 0829 — `SYSTEM_DEFAULT` asks for NOTHING, so it demands no policy.
3139    ///
3140    /// `required_policies` started from `QoSPolicyMask::CORE` unconditionally,
3141    /// which made the sentinel profile demand reliability, durability, history
3142    /// AND depth — so a backend that could not honour one would answer
3143    /// `IncompatibleQos` to a profile that requested none of them.
3144    #[test]
3145    fn system_default_profile_requires_no_policy() {
3146        let required = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.required_policies();
3147        assert_eq!(required.0, 0, "SYSTEM_DEFAULT demanded {required:?}");
3148        // Therefore it is admissible against a backend that advertises nothing.
3149        assert!(
3150            QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
3151                .validate_against(QoSPolicyMask(0))
3152                .is_ok()
3153        );
3154    }
3155
3156    /// The relaxation is per FIELD, not all-or-nothing: a profile that states
3157    /// SOME policies still demands exactly those.
3158    #[test]
3159    fn a_partly_stated_profile_demands_only_what_it_states() {
3160        let qos = QoSProfile {
3161            reliability: QoSReliabilityPolicy::BestEffort,
3162            ..QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
3163        };
3164        let required = qos.required_policies();
3165        assert!(required.contains(QoSPolicyMask::RELIABILITY));
3166        assert!(!required.contains(QoSPolicyMask::HISTORY));
3167        assert!(!required.contains(QoSPolicyMask::DEPTH));
3168        assert!(!required.contains(QoSPolicyMask::DURABILITY_VOLATILE));
3169        assert!(!required.contains(QoSPolicyMask::DURABILITY_TRANSIENT_LOCAL));
3170    }
3171
3172    /// A fully concrete profile is unaffected by the relaxation — this is the
3173    /// guard on "do not silently stop demanding what the caller asked for".
3174    #[test]
3175    fn concrete_profiles_still_demand_their_core_policies() {
3176        let required = QoSProfile::QOS_PROFILE_DEFAULT.required_policies();
3177        assert!(required.contains(QoSPolicyMask::RELIABILITY));
3178        assert!(required.contains(QoSPolicyMask::DURABILITY_VOLATILE));
3179        assert!(required.contains(QoSPolicyMask::HISTORY));
3180        assert!(required.contains(QoSPolicyMask::DEPTH));
3181        // And a backend missing one still rejects it.
3182        let missing = QoSPolicyMask(required.0 & !QoSPolicyMask::DEPTH.0);
3183        assert_eq!(
3184            QoSProfile::QOS_PROFILE_DEFAULT.validate_against(missing),
3185            Err(TransportError::IncompatibleQos)
3186        );
3187    }
3188
3189    /// issue 0829 — resolution replaces ONLY the sentinel fields, and the
3190    /// answer is the backend's, not a constant this crate bakes.
3191    #[test]
3192    fn resolve_system_default_fills_absences_and_touches_nothing_else() {
3193        // Two backends, two different answers to the SAME sentinel — the whole
3194        // reason no concrete `QOS_PROFILE_SYSTEM_DEFAULT` could be right.
3195        const CYCLONE: QoSSystemDefaults = QoSSystemDefaults {
3196            reliability: QoSReliabilityPolicy::Reliable,
3197            durability: QoSDurabilityPolicy::Volatile,
3198            history: QoSHistoryPolicy::KeepLast,
3199            depth: 1,
3200        };
3201        const ZENOH: QoSSystemDefaults = QoSSystemDefaults {
3202            depth: 4,
3203            ..CYCLONE
3204        };
3205
3206        let a = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.resolve_system_default(&CYCLONE);
3207        let b = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT.resolve_system_default(&ZENOH);
3208        assert_eq!(a.reliability, QoSReliabilityPolicy::Reliable);
3209        assert_eq!(a.history, QoSHistoryPolicy::KeepLast);
3210        assert_eq!(a.durability, QoSDurabilityPolicy::Volatile);
3211        assert_eq!(a.depth, 1);
3212        assert_eq!(b.depth, 4);
3213        assert!(!a.has_unresolved_system_default());
3214        assert!(!b.has_unresolved_system_default());
3215
3216        // A STATED policy is never overridden — resolution fills an absence.
3217        let stated = QoSProfile {
3218            reliability: QoSReliabilityPolicy::BestEffort,
3219            history: QoSHistoryPolicy::KeepAll,
3220            durability: QoSDurabilityPolicy::TransientLocal,
3221            depth: 7,
3222            ..QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT
3223        };
3224        let resolved = stated.resolve_system_default(&CYCLONE);
3225        assert_eq!(resolved, stated, "resolution overrode a stated policy");
3226
3227        // Idempotent.
3228        assert_eq!(a.resolve_system_default(&ZENOH), a);
3229    }
3230
3231    #[test]
3232    fn services_default_validates_and_rejects_missing_policy() {
3233        // Phase 193.5 — the service-create path (node `create_service*_sized` +
3234        // the typed-arena `register_service*_on`) runs `validate_against` on the
3235        // caller's profile, exactly like pub/sub. A backend advertising the
3236        // profile's required policies admits it; dropping any required bit (here
3237        // RELIABILITY) rejects it with `IncompatibleQos` — no silent downgrade.
3238        let qos = QoSProfile::services_default();
3239        let required = qos.required_policies();
3240        assert!(qos.validate_against(required).is_ok());
3241        assert!(qos.validate_against(QoSPolicyMask(u32::MAX)).is_ok());
3242        let missing = QoSPolicyMask(required.0 & !QoSPolicyMask::RELIABILITY.0);
3243        assert_eq!(
3244            qos.validate_against(missing),
3245            Err(TransportError::IncompatibleQos)
3246        );
3247    }
3248
3249    /// issue 0793 — this asserted `TransientLocal`, which PINNED THE DEFECT:
3250    /// upstream `rmw_qos_profile_parameters` is KEEP_LAST(1000) + RELIABLE +
3251    /// **VOLATILE** (`/opt/ros/<distro>/include/rmw/rmw/qos_profiles.h`), and
3252    /// our own second copy of the profile, `nros::qos::PARAMETERS`, was already
3253    /// correct. The test agreed with the wrong copy, so the disagreement between
3254    /// the two survived every run.
3255    #[test]
3256    fn test_qos_profile_parameters() {
3257        let qos = QoSProfile::QOS_PROFILE_PARAMETERS;
3258        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3259        assert_eq!(
3260            qos.durability,
3261            QoSDurabilityPolicy::Volatile,
3262            "rmw_qos_profile_parameters is VOLATILE; transient-local would make \
3263             every parameter server retain for late joiners, which ROS 2 does not do"
3264        );
3265        assert_eq!(qos.depth, 1000);
3266    }
3267
3268    #[test]
3269    fn test_qos_profile_clock() {
3270        let qos = QoSProfile::QOS_PROFILE_CLOCK;
3271        assert_eq!(qos.reliability, QoSReliabilityPolicy::BestEffort);
3272        assert_eq!(qos.depth, 1);
3273    }
3274
3275    #[test]
3276    fn test_qos_profile_parameter_events() {
3277        let qos = QoSProfile::QOS_PROFILE_PARAMETER_EVENTS;
3278        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3279        assert_eq!(qos.history, QoSHistoryPolicy::KeepAll);
3280    }
3281
3282    #[test]
3283    fn test_qos_profile_action_status() {
3284        let qos = QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT;
3285        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3286        assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
3287        assert_eq!(qos.depth, 1);
3288    }
3289
3290    #[test]
3291    fn test_qos_static_constructors() {
3292        assert_eq!(
3293            QoSProfile::topics_default(),
3294            QoSProfile::QOS_PROFILE_DEFAULT
3295        );
3296        assert_eq!(
3297            QoSProfile::sensor_data_default(),
3298            QoSProfile::QOS_PROFILE_SENSOR_DATA
3299        );
3300        assert_eq!(
3301            QoSProfile::services_default(),
3302            QoSProfile::QOS_PROFILE_SERVICES_DEFAULT
3303        );
3304        assert_eq!(
3305            QoSProfile::parameters_default(),
3306            QoSProfile::QOS_PROFILE_PARAMETERS
3307        );
3308        assert_eq!(
3309            QoSProfile::action_status_default(),
3310            QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT
3311        );
3312    }
3313
3314    #[test]
3315    fn test_qos_builder_explicit_setters() {
3316        let qos = QoSProfile::new()
3317            .reliability(QoSReliabilityPolicy::Reliable)
3318            .durability(QoSDurabilityPolicy::TransientLocal)
3319            .history(QoSHistoryPolicy::KeepAll)
3320            .depth(100);
3321
3322        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3323        assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
3324        assert_eq!(qos.history, QoSHistoryPolicy::KeepAll);
3325        assert_eq!(qos.depth, 100);
3326    }
3327
3328    #[test]
3329    fn test_qos_builder_chaining() {
3330        // Test that builder methods can be chained in any order
3331        let qos = QoSProfile::sensor_data_default()
3332            .reliable()
3333            .transient_local()
3334            .keep_last(20);
3335
3336        assert_eq!(qos.reliability, QoSReliabilityPolicy::Reliable);
3337        assert_eq!(qos.durability, QoSDurabilityPolicy::TransientLocal);
3338        assert_eq!(qos.history, QoSHistoryPolicy::KeepLast);
3339        assert_eq!(qos.depth, 20);
3340    }
3341
3342    #[test]
3343    fn test_qos_eq_impl() {
3344        // Verify that PartialEq works correctly via derive on QoSProfile
3345        let qos1 = QoSProfile::QOS_PROFILE_DEFAULT;
3346        let qos2 = QoSProfile::topics_default();
3347        // Both should have same values - verify field by field
3348        assert_eq!(qos1.reliability, qos2.reliability);
3349        assert_eq!(qos1.durability, qos2.durability);
3350        assert_eq!(qos1.history, qos2.history);
3351        assert_eq!(qos1.depth, qos2.depth);
3352    }
3353
3354    // --- Locator validation tests ---
3355
3356    #[test]
3357    fn test_locator_protocol_tcp() {
3358        assert_eq!(locator_protocol("tcp/127.0.0.1:7447"), LocatorProtocol::Tcp);
3359    }
3360
3361    #[test]
3362    fn test_locator_protocol_serial() {
3363        assert_eq!(
3364            locator_protocol("serial//dev/ttyUSB0#baudrate=115200"),
3365            LocatorProtocol::Serial
3366        );
3367    }
3368
3369    #[test]
3370    fn test_locator_protocol_unknown() {
3371        assert_eq!(locator_protocol(""), LocatorProtocol::Unknown);
3372        assert_eq!(locator_protocol("http://foo"), LocatorProtocol::Unknown);
3373        assert_eq!(locator_protocol("tls/host:port"), LocatorProtocol::Unknown);
3374    }
3375
3376    #[test]
3377    fn test_locator_protocol_udp() {
3378        assert_eq!(locator_protocol("udp/127.0.0.1:7447"), LocatorProtocol::Udp);
3379        assert_eq!(
3380            locator_protocol("udp/192.168.1.50:2019"),
3381            LocatorProtocol::Udp
3382        );
3383    }
3384
3385    #[test]
3386    fn test_validate_tcp_locator_ok() {
3387        assert!(validate_locator("tcp/127.0.0.1:7447").is_ok());
3388        assert!(validate_locator("tcp/192.168.1.1:7447").is_ok());
3389    }
3390
3391    #[test]
3392    fn test_validate_tcp_locator_missing_port() {
3393        assert!(validate_locator("tcp/127.0.0.1").is_err());
3394    }
3395
3396    #[test]
3397    fn test_validate_serial_locator_ok() {
3398        assert!(validate_locator("serial//dev/ttyUSB0#baudrate=115200").is_ok());
3399        assert!(validate_locator("serial//dev/ttyACM0#baudrate=9600").is_ok());
3400        assert!(validate_locator("serial/uart1#baudrate=921600").is_ok());
3401    }
3402
3403    #[test]
3404    fn test_validate_serial_locator_empty_device() {
3405        assert!(validate_locator("serial/").is_err());
3406    }
3407
3408    #[test]
3409    fn test_validate_serial_locator_missing_baudrate() {
3410        assert!(validate_locator("serial//dev/ttyUSB0").is_err());
3411    }
3412
3413    #[test]
3414    fn test_validate_serial_locator_invalid_baudrate() {
3415        assert!(validate_locator("serial//dev/ttyUSB0#baudrate=abc").is_err());
3416    }
3417
3418    #[test]
3419    fn test_validate_unknown_protocol() {
3420        assert!(validate_locator("http://foo").is_err());
3421        assert!(validate_locator("tls/host:port").is_err());
3422    }
3423
3424    #[test]
3425    fn test_validate_udp_locator_ok() {
3426        assert!(validate_locator("udp/127.0.0.1:7447").is_ok());
3427        assert!(validate_locator("udp/192.168.1.50:2019").is_ok());
3428    }
3429
3430    #[test]
3431    fn test_validate_udp_locator_missing_port() {
3432        assert!(validate_locator("udp/127.0.0.1").is_err());
3433    }
3434
3435    // Phase 233.2 — the PX4 companion QoS profile must be BEST_EFFORT +
3436    // TRANSIENT_LOCAL + KEEP_LAST so it matches PX4's uxrce_dds_client endpoints.
3437    #[test]
3438    fn px4_qos_profile_matches_uxrce_dds_client() {
3439        let q = QoSProfile::px4();
3440        assert_eq!(q.reliability, QoSReliabilityPolicy::BestEffort);
3441        // VOLATILE — PX4's /fmu/out writers are volatile; a TRANSIENT_LOCAL
3442        // reader silently fails to match (verified against real PX4 SITL).
3443        assert_eq!(q.durability, QoSDurabilityPolicy::Volatile);
3444        assert_eq!(q.history, QoSHistoryPolicy::KeepLast);
3445        assert_eq!(q, QoSProfile::QOS_PROFILE_PX4);
3446        // Depth is tunable via the builder without losing the PX4 policies.
3447        let deep = QoSProfile::px4().keep_last(5);
3448        assert_eq!(deep.depth, 5);
3449        assert_eq!(deep.reliability, QoSReliabilityPolicy::BestEffort);
3450        assert_eq!(deep.durability, QoSDurabilityPolicy::Volatile);
3451    }
3452
3453    // --- RmwConfig Tests ---
3454
3455    #[test]
3456    fn test_rmw_config_default() {
3457        let config = RmwConfig::default();
3458        assert_eq!(config.locator, "tcp/127.0.0.1:7447");
3459        assert_eq!(config.mode, SessionMode::Client);
3460        assert_eq!(config.domain_id, 0);
3461        assert_eq!(config.node_name, "node");
3462        assert_eq!(config.namespace, "");
3463    }
3464
3465    #[test]
3466    fn test_rmw_config_custom() {
3467        let config = RmwConfig {
3468            locator: "tcp/192.168.1.1:7447",
3469            mode: SessionMode::Peer,
3470            domain_id: 42,
3471            node_name: "talker",
3472            namespace: "/ns1",
3473            properties: &[("agent_port", "2019")],
3474        };
3475        assert_eq!(config.locator, "tcp/192.168.1.1:7447");
3476        assert_eq!(config.mode, SessionMode::Peer);
3477        assert_eq!(config.domain_id, 42);
3478        assert_eq!(config.node_name, "talker");
3479        assert_eq!(config.namespace, "/ns1");
3480        assert_eq!(config.properties.len(), 1);
3481        assert_eq!(config.properties[0].0, "agent_port");
3482    }
3483
3484    #[test]
3485    fn test_rmw_config_is_copy() {
3486        let config = RmwConfig::default();
3487        let config2 = config; // Copy
3488        assert_eq!(config.locator, config2.locator);
3489        assert_eq!(config.domain_id, config2.domain_id);
3490    }
3491
3492    #[test]
3493    fn test_rmw_config_clone() {
3494        let config = RmwConfig::default();
3495        let cloned = RmwConfig { ..config };
3496        assert_eq!(cloned.locator, config.locator);
3497        assert_eq!(cloned.node_name, config.node_name);
3498    }
3499}
3500
3501#[cfg(test)]
3502mod rx_buffer_hint_tests {
3503    use super::*;
3504
3505    /// issue 0896 / phase-402 — the hint must SURVIVE being set on `TopicInfo`.
3506    ///
3507    /// This is the seam every path funnels through: the C register writes it
3508    /// here, `nros-rmw-cffi` reads `topic.rx_buffer_hint` back out when
3509    /// building `rmw_subscription_options_t`, and a size-classing backend
3510    /// routes on it. A default that silently stayed 0 is the original defect,
3511    /// so the round trip is worth an assertion rather than an assumption.
3512    #[test]
3513    fn a_hint_set_on_topic_info_is_readable_back() {
3514        let t = TopicInfo::new("/chatter", "std_msgs/msg/Int32", "").with_rx_buffer_hint(4096);
3515        assert_eq!(t.rx_buffer_hint, 4096);
3516    }
3517
3518    /// Zero is "no opinion", not "zero bytes". The C options struct uses 0 as
3519    /// its unset sentinel and the executor only calls the setter when the
3520    /// caller stated something, so the default must stay 0 for the
3521    /// backend-default path to remain reachable.
3522    #[test]
3523    fn the_default_hint_is_zero_meaning_no_opinion() {
3524        let t = TopicInfo::new("/chatter", "std_msgs/msg/Int32", "");
3525        assert_eq!(t.rx_buffer_hint, 0);
3526    }
3527
3528    /// The builder must not disturb the rest of the descriptor — it is applied
3529    /// AFTER domain/namespace/node in the C path, so a setter that reset a
3530    /// field would drop identity that discovery depends on.
3531    #[test]
3532    fn setting_the_hint_preserves_the_rest_of_the_descriptor() {
3533        let make = || {
3534            TopicInfo::new("/chatter", "std_msgs/msg/Int32", "hash")
3535                .with_domain(7)
3536                .with_namespace("/ns")
3537        };
3538        let base = make();
3539        let hinted = make().with_rx_buffer_hint(1234);
3540        assert_eq!(hinted.name, base.name);
3541        assert_eq!(hinted.type_name, base.type_name);
3542        assert_eq!(hinted.type_hash, base.type_hash);
3543        assert_eq!(hinted.domain_id, base.domain_id);
3544        assert_eq!(hinted.namespace, base.namespace);
3545        assert_eq!(hinted.rx_buffer_hint, 1234);
3546    }
3547}