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`] / [`Subscriber`] — pub/sub data transport
8//! - [`ServiceServerTrait`] / [`ServiceClientTrait`] — 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    /// Keep last N messages (where N is defined in QosSettings)
335    #[default]
336    KeepLast,
337    /// Keep all messages (up to resource limits)
338    KeepAll,
339}
340
341/// QoS reliability policy
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
343pub enum QosReliabilityPolicy {
344    /// Reliable delivery (retransmit if needed).
345    ///
346    /// Default — matches ROS 2 `rmw_qos_profile_default` and the
347    /// `QosSettings::default()` / `QOS_PROFILE_DEFAULT` aggregates.
348    #[default]
349    Reliable,
350    /// Best-effort delivery (no retransmits)
351    BestEffort,
352}
353
354/// QoS durability policy
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
356pub enum QosDurabilityPolicy {
357    /// Messages are discarded when subscriber disconnects
358    #[default]
359    Volatile,
360    /// Messages are persisted for late-joining subscribers
361    TransientLocal,
362}
363
364/// QoS liveliness policy. Matches DDS `LIVELINESS` semantics.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
366#[repr(u8)]
367pub enum QosLivelinessPolicy {
368    /// No liveliness assertion or tracking. Default for entities
369    /// that don't care about liveliness.
370    #[default]
371    None = 0,
372    /// Backend's keepalive task asserts liveliness automatically.
373    Automatic = 1,
374    /// Application calls `assert_liveliness()` per topic explicitly.
375    ManualByTopic = 2,
376    /// Application calls `assert_liveliness()` at the node level.
377    ManualByNode = 3,
378}
379
380/// Phase 211.H — which side of a topic a [`QosOverride`] targets.
381/// Mirrors the `<role>` segment of a ROS 2
382/// `qos_overrides.<topic>.<role>.<policy>` launch parameter.
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum QosOverrideRole {
385    /// `qos_overrides.<topic>.publisher.*`
386    Publisher,
387    /// `qos_overrides.<topic>.subscription.*`
388    Subscription,
389}
390
391/// Phase 211.H — a single policy value a [`QosOverride`] sets. A typed enum
392/// (not a string) so the codegen that bakes these from the plan catches an
393/// unknown policy / mistyped value at generation time rather than silently
394/// no-op-ing at runtime.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum QosOverrideValue {
397    /// `.reliability` → Reliable / BestEffort.
398    Reliability(QosReliabilityPolicy),
399    /// `.durability` → Volatile / TransientLocal.
400    Durability(QosDurabilityPolicy),
401    /// `.history` → KeepLast / KeepAll.
402    History(QosHistoryPolicy),
403    /// `.depth` → KeepLast depth.
404    Depth(u32),
405}
406
407/// Phase 211.H — one per-topic QoS override, lowered from a ROS 2
408/// `qos_overrides.<topic>.<role>.<policy>` launch parameter by the planner and
409/// baked into a `&'static [QosOverride]` table by the entry codegen. The node
410/// folds the matching entries into the entity's [`QosSettings`] at
411/// `create_publisher` / `create_subscription` time (setup-time, single
412/// linear scan, no alloc), *before* the backend-compat `validate_against` —
413/// so an override the active RMW can't honour still errors loudly, never a
414/// silent downgrade.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct QosOverride {
417    /// The resolved (remapped) topic name the override targets, e.g.
418    /// `"/chatter"`. Matched exactly against the entity's topic.
419    pub topic: &'static str,
420    /// Publisher or subscription side.
421    pub role: QosOverrideRole,
422    /// The policy + value to set.
423    pub value: QosOverrideValue,
424}
425
426/// Full DDS-shaped QoS profile. Matches the field set of upstream
427/// `rmw_qos_profile_t`.
428///
429/// Backends advertise per-policy support via
430/// [`Session::supported_qos_policies`]; entities created with a
431/// profile the active backend can't honour return
432/// [`TransportError::IncompatibleQos`] synchronously at create time
433/// — no silent downgrade.
434///
435/// Zero-valued time-window fields ("off") mean infinite — the policy
436/// is effectively disabled for the entity.
437#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438pub struct QosSettings {
439    /// History policy
440    pub history: QosHistoryPolicy,
441    /// Reliability policy
442    pub reliability: QosReliabilityPolicy,
443    /// Durability policy
444    pub durability: QosDurabilityPolicy,
445    /// Liveliness policy
446    pub liveliness_kind: QosLivelinessPolicy,
447    /// History depth (only used if history is KeepLast)
448    pub depth: u32,
449    /// Subscriber max-inter-arrival / publisher offered-rate, ms.
450    /// `0` = infinite (no deadline check).
451    pub deadline_ms: u32,
452    /// Sample expiry, ms. Subscribers filter samples older than this.
453    /// `0` = infinite (no expiry).
454    pub lifespan_ms: u32,
455    /// Liveliness lease, ms. `0` = infinite.
456    pub liveliness_lease_ms: u32,
457    /// If `true`, topic-name encoding skips the `/rt/` ROS prefix.
458    pub avoid_ros_namespace_conventions: bool,
459    /// Phase 282 (#145) — publisher-side "express" hint: this publisher's
460    /// samples bypass transport tx batching (zenoh: the wire EXPRESS flag; a
461    /// batching zenoh-pico session sends them immediately instead of queueing
462    /// them for the next flush). A transport hint, not a DDS policy — no RxO
463    /// matching, no backend-compat validation; ignored by subscriptions and by
464    /// backends without a batching concept.
465    pub tx_express: bool,
466}
467
468impl Default for QosSettings {
469    fn default() -> Self {
470        Self::QOS_PROFILE_DEFAULT
471    }
472}
473
474impl QosSettings {
475    /// Phase 211.H — fold the plan's `qos_overrides` matching `topic` + `role`
476    /// into this profile, returning the overridden profile. Setup-time only
477    /// (called from `create_publisher`/`create_subscription`): a single linear
478    /// scan over the baked `&'static` table, no alloc, RT-safe. Later entries
479    /// win on a duplicate `(topic, role, policy)` (last-write), matching the
480    /// planner's sorted, de-conflicted emit. Non-matching entries are ignored,
481    /// so passing the whole node table to every entity is cheap + correct.
482    #[must_use]
483    pub fn apply_overrides(
484        mut self,
485        topic: &str,
486        role: QosOverrideRole,
487        overrides: &[QosOverride],
488    ) -> Self {
489        for ovr in overrides {
490            if ovr.topic == topic && ovr.role == role {
491                match ovr.value {
492                    QosOverrideValue::Reliability(r) => self.reliability = r,
493                    QosOverrideValue::Durability(d) => self.durability = d,
494                    QosOverrideValue::History(h) => self.history = h,
495                    QosOverrideValue::Depth(d) => self.depth = d,
496                }
497            }
498        }
499        self
500    }
501}
502
503impl QosSettings {
504    /// Internal const builder. Extended-policy fields default to
505    /// "off" (zero) and `liveliness_kind = Automatic` (the upstream
506    /// `rmw_qos_profile_default` choice).
507    const fn build(
508        reliability: QosReliabilityPolicy,
509        durability: QosDurabilityPolicy,
510        history: QosHistoryPolicy,
511        depth: u32,
512    ) -> Self {
513        Self {
514            history,
515            reliability,
516            durability,
517            liveliness_kind: QosLivelinessPolicy::Automatic,
518            depth,
519            deadline_ms: 0,
520            lifespan_ms: 0,
521            liveliness_lease_ms: 0,
522            avoid_ros_namespace_conventions: false,
523            tx_express: false,
524        }
525    }
526
527    /// Create new QoS settings with defaults (matches `QOS_PROFILE_DEFAULT`:
528    /// Reliable, Volatile, KeepLast(10)).
529    pub const fn new() -> Self {
530        Self::QOS_PROFILE_DEFAULT
531    }
532
533    /// Best-effort QoS (for real-time)
534    pub const BEST_EFFORT: Self = Self::build(
535        QosReliabilityPolicy::BestEffort,
536        QosDurabilityPolicy::Volatile,
537        QosHistoryPolicy::KeepLast,
538        1,
539    );
540
541    /// Reliable QoS
542    pub const RELIABLE: Self = Self::build(
543        QosReliabilityPolicy::Reliable,
544        QosDurabilityPolicy::Volatile,
545        QosHistoryPolicy::KeepLast,
546        10,
547    );
548
549    /// System default QoS profile (matches rmw_qos_profile_system_default)
550    pub const QOS_PROFILE_SYSTEM_DEFAULT: Self = Self::build(
551        QosReliabilityPolicy::Reliable,
552        QosDurabilityPolicy::Volatile,
553        QosHistoryPolicy::KeepLast,
554        1,
555    );
556
557    /// Default QoS profile (matches rmw_qos_profile_default)
558    pub const QOS_PROFILE_DEFAULT: Self = Self::build(
559        QosReliabilityPolicy::Reliable,
560        QosDurabilityPolicy::Volatile,
561        QosHistoryPolicy::KeepLast,
562        10,
563    );
564
565    /// Sensor data QoS profile (matches rmw_qos_profile_sensor_data)
566    pub const QOS_PROFILE_SENSOR_DATA: Self = Self::build(
567        QosReliabilityPolicy::BestEffort,
568        QosDurabilityPolicy::Volatile,
569        QosHistoryPolicy::KeepLast,
570        5,
571    );
572
573    /// Services default QoS profile (matches rmw_qos_profile_services_default)
574    pub const QOS_PROFILE_SERVICES_DEFAULT: Self = Self::build(
575        QosReliabilityPolicy::Reliable,
576        QosDurabilityPolicy::Volatile,
577        QosHistoryPolicy::KeepLast,
578        10,
579    );
580
581    /// Parameters QoS profile (matches rmw_qos_profile_parameters)
582    pub const QOS_PROFILE_PARAMETERS: Self = Self::build(
583        QosReliabilityPolicy::Reliable,
584        QosDurabilityPolicy::TransientLocal,
585        QosHistoryPolicy::KeepLast,
586        1000,
587    );
588
589    /// Clock QoS profile - same as sensor data but with depth 1
590    pub const QOS_PROFILE_CLOCK: Self = Self::build(
591        QosReliabilityPolicy::BestEffort,
592        QosDurabilityPolicy::Volatile,
593        QosHistoryPolicy::KeepLast,
594        1,
595    );
596
597    /// Parameter events QoS profile (matches rmw_qos_profile_parameter_events)
598    pub const QOS_PROFILE_PARAMETER_EVENTS: Self = Self::build(
599        QosReliabilityPolicy::Reliable,
600        QosDurabilityPolicy::Volatile,
601        QosHistoryPolicy::KeepAll,
602        0, // Not used with KeepAll
603    );
604
605    /// Action status default QoS profile (matches rcl_action_qos_profile_status_default)
606    pub const QOS_PROFILE_ACTION_STATUS_DEFAULT: Self = Self::build(
607        QosReliabilityPolicy::Reliable,
608        QosDurabilityPolicy::TransientLocal,
609        QosHistoryPolicy::KeepLast,
610        1,
611    );
612
613    /// PX4 companion QoS profile (Phase 233 / RFC-0039 Track B). Matches the
614    /// QoS PX4's `uxrce_dds_client` uses on `/fmu/out/*` and `/fmu/in/*` —
615    /// `BEST_EFFORT` + `VOLATILE` + `KEEP_LAST(1)`. A nano-ros node talking to
616    /// the same `MicroXRCEAgent` must use this (a reliable or
617    /// `TRANSIENT_LOCAL` reader will not match PX4's volatile best-effort
618    /// writers). Verified against real PX4 SITL (`nros-px4-sitl-test`):
619    /// `TRANSIENT_LOCAL` durability silently fails to match `/fmu/out/*`.
620    /// Adjust depth via `.keep_last(n)` for higher-rate streams.
621    pub const QOS_PROFILE_PX4: Self = Self::build(
622        QosReliabilityPolicy::BestEffort,
623        QosDurabilityPolicy::Volatile,
624        QosHistoryPolicy::KeepLast,
625        1,
626    );
627
628    // --- Static constructor methods (matching rclrs API) ---
629
630    /// Get the default QoS profile for ordinary topics
631    pub const fn topics_default() -> Self {
632        Self::QOS_PROFILE_DEFAULT
633    }
634
635    /// The PX4 companion QoS profile ([`QOS_PROFILE_PX4`](Self::QOS_PROFILE_PX4))
636    /// — use for `/fmu/out/*` subscriptions and `/fmu/in/*` publications against
637    /// a `MicroXRCEAgent`.
638    pub const fn px4() -> Self {
639        Self::QOS_PROFILE_PX4
640    }
641
642    /// Get the default QoS profile for sensor data topics
643    pub const fn sensor_data_default() -> Self {
644        Self::QOS_PROFILE_SENSOR_DATA
645    }
646
647    /// Get the default QoS profile for services
648    pub const fn services_default() -> Self {
649        Self::QOS_PROFILE_SERVICES_DEFAULT
650    }
651
652    /// Get the default QoS profile for parameter services
653    pub const fn parameters_default() -> Self {
654        Self::QOS_PROFILE_PARAMETERS
655    }
656
657    /// Get the default QoS profile for parameter events
658    pub const fn parameter_events_default() -> Self {
659        Self::QOS_PROFILE_PARAMETER_EVENTS
660    }
661
662    /// Get the system default QoS profile
663    pub const fn system_default() -> Self {
664        Self::QOS_PROFILE_SYSTEM_DEFAULT
665    }
666
667    /// Get the default QoS profile for action status topics
668    pub const fn action_status_default() -> Self {
669        Self::QOS_PROFILE_ACTION_STATUS_DEFAULT
670    }
671
672    /// Get the default QoS profile for clock topics
673    pub const fn clock_default() -> Self {
674        Self::QOS_PROFILE_CLOCK
675    }
676
677    // --- Builder methods ---
678
679    /// Set history to keep last N messages
680    pub const fn keep_last(mut self, depth: u32) -> Self {
681        self.history = QosHistoryPolicy::KeepLast;
682        self.depth = depth;
683        self
684    }
685
686    /// Set history to keep all messages
687    pub const fn keep_all(mut self) -> Self {
688        self.history = QosHistoryPolicy::KeepAll;
689        self
690    }
691
692    /// Set reliability to reliable
693    pub const fn reliable(mut self) -> Self {
694        self.reliability = QosReliabilityPolicy::Reliable;
695        self
696    }
697
698    /// Set reliability to best-effort
699    pub const fn best_effort(mut self) -> Self {
700        self.reliability = QosReliabilityPolicy::BestEffort;
701        self
702    }
703
704    /// Set durability to volatile
705    pub const fn volatile(mut self) -> Self {
706        self.durability = QosDurabilityPolicy::Volatile;
707        self
708    }
709
710    /// Set durability to transient local
711    pub const fn transient_local(mut self) -> Self {
712        self.durability = QosDurabilityPolicy::TransientLocal;
713        self
714    }
715
716    /// Set reliability policy explicitly
717    pub const fn reliability(mut self, policy: QosReliabilityPolicy) -> Self {
718        self.reliability = policy;
719        self
720    }
721
722    /// Set durability policy explicitly
723    pub const fn durability(mut self, policy: QosDurabilityPolicy) -> Self {
724        self.durability = policy;
725        self
726    }
727
728    /// Set history policy explicitly
729    pub const fn history(mut self, policy: QosHistoryPolicy) -> Self {
730        self.history = policy;
731        self
732    }
733
734    /// Set history depth explicitly
735    pub const fn depth(mut self, depth: u32) -> Self {
736        self.depth = depth;
737        self
738    }
739
740    /// Phase 282 (#145) — mark this publisher's samples "express": they
741    /// bypass transport tx batching (sent immediately even when the batching
742    /// knob is on). A transport hint for control-tier / latency-sensitive
743    /// topics; ignored on subscriptions and by backends without batching.
744    pub const fn tx_express(mut self, express: bool) -> Self {
745        self.tx_express = express;
746        self
747    }
748
749    /// Get history depth (for backwards compatibility)
750    pub const fn history_depth(&self) -> u8 {
751        if self.depth > 255 {
752            255
753        } else {
754            self.depth as u8
755        }
756    }
757}
758
759/// Transport session configuration
760#[derive(Debug, Clone)]
761pub struct TransportConfig<'a> {
762    /// Peer locator (e.g., "tcp/192.168.1.1:7447" or "serial//dev/ttyUSB0#baudrate=115200")
763    pub locator: Option<&'a str>,
764    /// Session mode: client, peer, or router
765    pub mode: SessionMode,
766    /// Additional transport properties (key-value pairs)
767    ///
768    /// These are passed through to the underlying transport backend.
769    /// For zenoh-pico, recognized keys include:
770    /// - `"multicast_scouting"` - Enable/disable multicast scouting (`"true"` or `"false"`)
771    /// - `"scouting_timeout_ms"` - Scouting timeout in milliseconds
772    /// - `"multicast_locator"` - Multicast group address
773    /// - `"listen"` - Listen endpoint (e.g., `"tcp/0.0.0.0:0"`)
774    /// - `"add_timestamp"` - Add timestamps to messages (`"true"` or `"false"`)
775    pub properties: &'a [(&'a str, &'a str)],
776    /// Node name for ROS 2 graph discovery liveliness token.
777    ///
778    /// Empty string (`""`) means no node-liveliness token is declared (preserves
779    /// the pre-#104 behaviour). Non-empty causes the session to declare a
780    /// `@ros2_lv/<domain>/<zid>/0/0/NN/%/<ns>/<node>` token on open.
781    pub node_name: &'a str,
782    /// Node namespace for the liveliness token (e.g., `""` or `"/ns1"`).
783    ///
784    /// Empty string is treated as root `"/"` by the keyexpr builder.
785    pub namespace: &'a str,
786    /// ROS 2 domain ID used in the liveliness token key expression.
787    pub domain_id: u32,
788}
789
790impl Default for TransportConfig<'_> {
791    fn default() -> Self {
792        Self {
793            locator: None,
794            mode: SessionMode::Client,
795            properties: &[],
796            node_name: "",
797            namespace: "",
798            domain_id: 0,
799        }
800    }
801}
802
803/// Middleware-agnostic session configuration.
804///
805/// `RmwConfig` provides a uniform interface that any RMW backend can
806/// interpret. Backends map the universal fields to their own connection
807/// parameters and interpret `properties` for anything backend-specific.
808///
809/// # Examples
810///
811/// ```
812/// use nros_rmw::{RmwConfig, SessionMode};
813///
814/// let config = RmwConfig {
815///     locator: "tcp/192.168.1.1:7447",
816///     mode: SessionMode::Client,
817///     domain_id: 0,
818///     node_name: "talker",
819///     namespace: "",
820///     properties: &[],
821/// };
822/// ```
823#[derive(Debug, Clone, Copy)]
824pub struct RmwConfig<'a> {
825    /// Middleware-specific connection string.
826    ///
827    /// - zenoh: `"tcp/192.168.1.1:7447"` or `"udp/224.0.0.224:7447"`
828    /// - XRCE-DDS: `"udp/192.168.1.1:2019"`
829    pub locator: &'a str,
830    /// Session mode (zenoh: client/peer; XRCE-DDS: always client)
831    pub mode: SessionMode,
832    /// ROS 2 domain ID (maps to DDS domain or zenoh key prefix)
833    pub domain_id: u32,
834    /// Node name (e.g., `"talker"`)
835    pub node_name: &'a str,
836    /// Node namespace (e.g., `""` or `"/ns1"`)
837    pub namespace: &'a str,
838    /// Backend-specific key/value properties.
839    ///
840    /// Uniform escape hatch for backend-specific tuning that doesn't fit
841    /// the universal fields above. Each backend documents the keys it
842    /// understands; unknown keys are ignored. Passing `&[]` is always
843    /// valid.
844    ///
845    /// Examples:
846    /// - zenoh: `"tls.root_ca"`, `"scouting.multicast.enabled"`
847    /// - XRCE-DDS: `"agent_port"`, `"client_key"`
848    pub properties: &'a [(&'a str, &'a str)],
849}
850
851impl Default for RmwConfig<'_> {
852    fn default() -> Self {
853        Self {
854            locator: "tcp/127.0.0.1:7447",
855            mode: SessionMode::Client,
856            domain_id: 0,
857            node_name: "node",
858            namespace: "",
859            properties: &[],
860        }
861    }
862}
863
864/// Locator transport protocol
865#[derive(Debug, Clone, Copy, PartialEq, Eq)]
866pub enum LocatorProtocol {
867    /// TCP transport (e.g., "tcp/127.0.0.1:7447")
868    Tcp,
869    /// UDP transport (e.g., "udp/192.168.1.50:2019" — common for XRCE-DDS)
870    Udp,
871    /// Serial/UART transport (e.g., "serial//dev/ttyUSB0#baudrate=115200")
872    Serial,
873    /// Unknown protocol
874    Unknown,
875}
876
877/// Parse the protocol from a locator string
878pub fn locator_protocol(locator: &str) -> LocatorProtocol {
879    if locator.starts_with("tcp/") {
880        LocatorProtocol::Tcp
881    } else if locator.starts_with("udp/") {
882        LocatorProtocol::Udp
883    } else if locator.starts_with("serial/") {
884        LocatorProtocol::Serial
885    } else {
886        LocatorProtocol::Unknown
887    }
888}
889
890/// Validate a locator string format.
891///
892/// Returns `Ok(())` if the locator is well-formed, or an error message describing
893/// the problem. This provides early feedback before zenoh-pico or XRCE-DDS rejects
894/// a bad locator.
895///
896/// Supported formats:
897/// - TCP: `tcp/<host>:<port>` (e.g., `tcp/127.0.0.1:7447`)
898/// - UDP: `udp/<host>:<port>` (e.g., `udp/192.168.1.50:2019`)
899/// - Serial: `serial/<device>#baudrate=<rate>` (e.g., `serial//dev/ttyUSB0#baudrate=115200`)
900pub fn validate_locator(locator: &str) -> Result<(), &'static str> {
901    match locator_protocol(locator) {
902        LocatorProtocol::Tcp => {
903            let rest = &locator[4..]; // skip "tcp/"
904            if !rest.contains(':') {
905                return Err("TCP locator must contain host:port (e.g., tcp/127.0.0.1:7447)");
906            }
907            Ok(())
908        }
909        LocatorProtocol::Udp => {
910            let rest = &locator[4..]; // skip "udp/"
911            if !rest.contains(':') {
912                return Err("UDP locator must contain host:port (e.g., udp/192.168.1.50:2019)");
913            }
914            Ok(())
915        }
916        LocatorProtocol::Serial => {
917            let rest = &locator[7..]; // skip "serial/"
918            if rest.is_empty() {
919                return Err(
920                    "serial locator must specify device (e.g., serial//dev/ttyUSB0#baudrate=115200)",
921                );
922            }
923            if !rest.contains("#baudrate=") {
924                return Err(
925                    "serial locator must include #baudrate=RATE (e.g., serial//dev/ttyUSB0#baudrate=115200)",
926                );
927            }
928            // Validate baudrate is numeric
929            if let Some(baud_str) = rest.split("#baudrate=").nth(1) {
930                let baud_str = baud_str.split('#').next().unwrap_or(baud_str);
931                if baud_str.parse::<u32>().is_err() {
932                    return Err("serial baudrate must be a number");
933                }
934            }
935            Ok(())
936        }
937        LocatorProtocol::Unknown => {
938            Err("unknown locator protocol (expected tcp/, udp/, or serial/)")
939        }
940    }
941}
942
943/// Session mode
944#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
945pub enum SessionMode {
946    /// Connect as client to a router
947    #[default]
948    Client,
949    /// Connect as peer for peer-to-peer communication
950    Peer,
951}
952
953/// Transport session trait — the per-process anchor an RMW backend
954/// gives to the executor.
955///
956/// # Threading
957///
958/// `&mut self` on every method means the executor serialises all
959/// session calls onto a single thread. A backend may rely on this
960/// — no internal locking is required for `create_*` / `close` /
961/// `drive_io`. **Publisher / subscriber / service handles created
962/// from the session, however, are typically used from worker
963/// threads** and must carry their own synchronisation (see the
964/// [`Publisher`] / [`Subscriber`] trait docs).
965///
966/// # Calling pattern
967///
968/// 1. Open the session (backend-specific factory; not on this trait).
969/// 2. `create_*` for every entity at startup. Creating entities mid-
970///    flight after `drive_io` has run is allowed but not common.
971/// 3. The executor calls `drive_io` periodically. Worker threads
972///    publish / receive in parallel.
973/// 4. `close` once at shutdown. Entities must be dropped first.
974pub trait Session {
975    /// Error type for this session
976    type Error;
977    /// Publisher handle type
978    type PublisherHandle;
979    /// Subscriber handle type
980    type SubscriberHandle;
981    /// Service server handle type
982    type ServiceServerHandle;
983    /// Service client handle type
984    type ServiceClientHandle;
985
986    /// Create a publisher bound to this session.
987    ///
988    /// May allocate transport resources (zenoh declarations, DDS
989    /// writers). Returns a handle that outlives the call but not the
990    /// session — drop the handle before `close()`.
991    fn create_publisher(
992        &mut self,
993        topic: &TopicInfo,
994        qos: QosSettings,
995    ) -> Result<Self::PublisherHandle, Self::Error>;
996
997    /// Create a subscriber bound to this session.
998    ///
999    /// Subscribers may start receiving immediately after creation if
1000    /// the transport supports late-joining publishers. Late messages
1001    /// are buffered up to the QoS depth.
1002    fn create_subscriber(
1003        &mut self,
1004        topic: &TopicInfo,
1005        qos: QosSettings,
1006    ) -> Result<Self::SubscriberHandle, Self::Error>;
1007
1008    /// Create a service server bound to this session. Replies are
1009    /// matched to requests by the sequence number returned from
1010    /// [`ServiceServerTrait::try_recv_request`].
1011    ///
1012    /// `qos` is applied to both the request and reply endpoints (a
1013    /// service is two DDS topics; rmw uses one profile for both). The
1014    /// default is [`QosSettings::services_default`]
1015    /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
1016    fn create_service_server(
1017        &mut self,
1018        service: &ServiceInfo,
1019        qos: QosSettings,
1020    ) -> Result<Self::ServiceServerHandle, Self::Error>;
1021
1022    /// Create a service client bound to this session.
1023    ///
1024    /// `qos` is applied to both the request and reply endpoints (a
1025    /// service is two DDS topics; rmw uses one profile for both). The
1026    /// default is [`QosSettings::services_default`]
1027    /// (RELIABLE+VOLATILE+KEEP_LAST(10)).
1028    fn create_service_client(
1029        &mut self,
1030        service: &ServiceInfo,
1031        qos: QosSettings,
1032    ) -> Result<Self::ServiceClientHandle, Self::Error>;
1033
1034    /// Close the session, releasing transport resources. All entity
1035    /// handles created from this session must already be dropped.
1036    fn close(&mut self) -> Result<(), Self::Error>;
1037
1038    /// Drive transport I/O (poll network, dispatch callbacks).
1039    ///
1040    /// Both zenoh-pico and XRCE-DDS are pull-based: they require the
1041    /// application to periodically call this method to read from the
1042    /// network socket and dispatch incoming messages to subscriber
1043    /// buffers.
1044    ///
1045    /// `timeout_ms` is the maximum time to wait for data (0 = non-blocking;
1046    /// negative values mean "block indefinitely" — see Phase 84.D7 for the
1047    /// planned migration to `core::time::Duration`).
1048    ///
1049    /// **Required**. There is no default body — both shipped backends
1050    /// (zenoh and XRCE) must drive I/O, and a silent no-op default was a
1051    /// trap for third-party implementers. If your backend genuinely
1052    /// receives data via OS callbacks (push-based) and has nothing to do
1053    /// here, return `Ok(())` explicitly.
1054    fn drive_io(&mut self, timeout_ms: i32) -> Result<(), Self::Error>;
1055
1056    /// Phase 109 — report which QoS policies the active backend
1057    /// honours. The runtime validates requested QoS against this mask
1058    /// at entity-create time and returns
1059    /// [`TransportError::IncompatibleQos`] if the requested profile
1060    /// includes a policy the backend can't enforce. **No silent
1061    /// downgrade.**
1062    ///
1063    /// Default returns [`QosPolicyMask::CORE`] — reliability +
1064    /// durability VOLATILE + history + depth. Backends override per
1065    /// supported policy.
1066    fn supported_qos_policies(&self) -> QosPolicyMask {
1067        QosPolicyMask::CORE
1068    }
1069
1070    /// Phase 110.0 — backend's next internal-event deadline in
1071    /// milliseconds from now (lease keepalive, heartbeat, reader
1072    /// ACK-NACK timeout, etc.).
1073    ///
1074    /// The executor caps its `drive_io` timeout against
1075    /// `min(user_timeout, timer_deadline, this)` so quiet links don't
1076    /// wake early, see no user-visible work, and round-trip back into
1077    /// `drive_io`. Returns `None` when the backend has no internal
1078    /// deadlines or chooses not to expose them.
1079    ///
1080    /// Default `None` keeps existing backends working unchanged; opt-in
1081    /// per backend.
1082    fn next_deadline_ms(&self) -> Option<u32> {
1083        None
1084    }
1085
1086    /// Phase 124.B.1 — install (or clear, when `cb.is_none()`) the
1087    /// executor wake callback. The runtime calls this once per
1088    /// session after `open` with `cb` pointing at a runtime-owned
1089    /// function and `ctx` pointing at the executor's wake state.
1090    /// The backend stores `(cb, ctx)` in its per-session state and
1091    /// calls `cb(ctx)` whenever its transport notification path
1092    /// fires (datagram arrival, condvar wake, etc.) — the runtime
1093    /// cb does flag-write + condvar-signal atomically, so a
1094    /// `spin_once` blocked on the wake condvar resumes immediately
1095    /// instead of waiting for the next poll iteration.
1096    ///
1097    /// # Safety
1098    ///
1099    /// When `cb` is `Some`, `ctx` must remain valid until the callback is
1100    /// cleared or the session is closed. The backend may invoke `cb(ctx)` from
1101    /// its transport notification path.
1102    ///
1103    /// Default body: ignore the call. Poll-only backends (XRCE,
1104    /// bare-metal) leave the default in place; the executor still
1105    /// drains them on its deadline-bound cv-wait boundary.
1106    unsafe fn set_wake_callback(
1107        &mut self,
1108        cb: Option<unsafe extern "C" fn(ctx: *mut core::ffi::c_void)>,
1109        ctx: *mut core::ffi::c_void,
1110    ) {
1111        let _ = (cb, ctx);
1112    }
1113
1114    /// Phase 130.4 — does this backend actually honour
1115    /// [`set_wake_callback`]?
1116    ///
1117    /// `true` means the backend installs the callback and will
1118    /// fire it from its async notify path (worker thread, ISR,
1119    /// signalfd, …). `false` (the default) means
1120    /// `set_wake_callback` was a no-op — the executor must drive
1121    /// I/O for the caller's full timeout because no async wake
1122    /// will pre-empt it.
1123    ///
1124    /// The executor uses this to choose between the wake-primitive
1125    /// wait (`NodeWake::wait_ms` / `std::Condvar::wait_timeout_while`)
1126    /// and a direct `drive_io(timeout_ms)`. Poll-only backends
1127    /// (XRCE-DDS-Client, bare-metal smoltcp) return `false`;
1128    /// event-driven backends (zenoh-pico with an RX task that
1129    /// invokes the callback on packet arrival) return `true`.
1130    ///
1131    /// [`set_wake_callback`]: Self::set_wake_callback
1132    fn supports_wake_callback(&self) -> bool {
1133        false
1134    }
1135
1136    /// Phase 124.F.1 — session-level connectivity probe.
1137    ///
1138    /// Sends a wire-level round-trip probe and waits up to
1139    /// `timeout_ms`. `Ok(())` on reply, `Err(TransportError::Timeout)`
1140    /// on no reply, `Err(TransportError::Unsupported)` when the
1141    /// backend can't probe (DDS without participant introspection).
1142    /// Lesson from micro-ROS's `rmw_uros_ping_agent`.
1143    ///
1144    /// Default body: `Err(Unsupported)`. Backends with a native
1145    /// ping API (zenoh: `z_send_ping`; XRCE:
1146    /// `uxr_ping_agent_session_until_timeout`) opt in by overriding.
1147    fn ping_session(&mut self, timeout_ms: i32) -> Result<(), Self::Error>
1148    where
1149        Self::Error: From<TransportError>,
1150    {
1151        let _ = timeout_ms;
1152        Err(TransportError::Unsupported.into())
1153    }
1154}
1155
1156/// Bitmask of QoS policies a backend can honour. See
1157/// [`Session::supported_qos_policies`].
1158///
1159/// `CORE` covers the policies every nano-ros backend implements:
1160/// reliability, durability=VOLATILE, history, depth. Backends opt
1161/// into additional policies by OR-ing the relevant flags.
1162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1163pub struct QosPolicyMask(pub u32);
1164
1165impl QosPolicyMask {
1166    pub const RELIABILITY: Self = Self(1 << 0);
1167    pub const DURABILITY_VOLATILE: Self = Self(1 << 1);
1168    pub const DURABILITY_TRANSIENT_LOCAL: Self = Self(1 << 2);
1169    pub const HISTORY: Self = Self(1 << 3);
1170    pub const DEPTH: Self = Self(1 << 4);
1171    pub const DEADLINE: Self = Self(1 << 5);
1172    pub const LIFESPAN: Self = Self(1 << 6);
1173    pub const LIVELINESS_AUTOMATIC: Self = Self(1 << 7);
1174    pub const LIVELINESS_MANUAL_BY_TOPIC: Self = Self(1 << 8);
1175    pub const LIVELINESS_MANUAL_BY_NODE: Self = Self(1 << 9);
1176    pub const LIVELINESS_LEASE: Self = Self(1 << 10);
1177    pub const AVOID_ROS_NAMESPACE_CONVENTIONS: Self = Self(1 << 11);
1178
1179    /// Policies every nano-ros backend implements.
1180    pub const CORE: Self =
1181        Self(Self::RELIABILITY.0 | Self::DURABILITY_VOLATILE.0 | Self::HISTORY.0 | Self::DEPTH.0);
1182
1183    /// `true` if `self` contains every policy in `other`.
1184    pub const fn contains(self, other: Self) -> bool {
1185        self.0 & other.0 == other.0
1186    }
1187
1188    /// Bitwise OR of two masks.
1189    pub const fn union(self, other: Self) -> Self {
1190        Self(self.0 | other.0)
1191    }
1192}
1193
1194impl core::ops::BitOr for QosPolicyMask {
1195    type Output = Self;
1196    fn bitor(self, rhs: Self) -> Self {
1197        self.union(rhs)
1198    }
1199}
1200
1201impl core::ops::BitOrAssign for QosPolicyMask {
1202    fn bitor_assign(&mut self, rhs: Self) {
1203        self.0 |= rhs.0;
1204    }
1205}
1206
1207impl QosSettings {
1208    /// Compute the set of QoS policies actually requested by this profile.
1209    ///
1210    /// Zero-valued time fields and `LivelinessKind::None` count as "not
1211    /// requesting" the corresponding policy — the cheap default. The
1212    /// `CORE` bits (reliability, durability=VOLATILE, history, depth)
1213    /// are always present because every nano-ros backend honours them.
1214    pub fn required_policies(&self) -> QosPolicyMask {
1215        let mut mask = QosPolicyMask::CORE;
1216        if self.durability == QosDurabilityPolicy::TransientLocal {
1217            mask = QosPolicyMask(
1218                (mask.0 & !QosPolicyMask::DURABILITY_VOLATILE.0)
1219                    | QosPolicyMask::DURABILITY_TRANSIENT_LOCAL.0,
1220            );
1221        }
1222        if self.deadline_ms != 0 {
1223            mask |= QosPolicyMask::DEADLINE;
1224        }
1225        if self.lifespan_ms != 0 {
1226            mask |= QosPolicyMask::LIFESPAN;
1227        }
1228        match self.liveliness_kind {
1229            QosLivelinessPolicy::None => {}
1230            QosLivelinessPolicy::Automatic => mask |= QosPolicyMask::LIVELINESS_AUTOMATIC,
1231            QosLivelinessPolicy::ManualByTopic => mask |= QosPolicyMask::LIVELINESS_MANUAL_BY_TOPIC,
1232            QosLivelinessPolicy::ManualByNode => mask |= QosPolicyMask::LIVELINESS_MANUAL_BY_NODE,
1233        }
1234        if self.liveliness_lease_ms != 0 {
1235            mask |= QosPolicyMask::LIVELINESS_LEASE;
1236        }
1237        if self.avoid_ros_namespace_conventions {
1238            mask |= QosPolicyMask::AVOID_ROS_NAMESPACE_CONVENTIONS;
1239        }
1240        mask
1241    }
1242
1243    /// Returns `Err(TransportError::IncompatibleQos)` if any policy this
1244    /// profile requires is missing from the backend's `supported` mask.
1245    /// Used at entity-create time to enforce the **no silent
1246    /// degradation** contract.
1247    pub fn validate_against(&self, supported: QosPolicyMask) -> Result<(), TransportError> {
1248        if supported.contains(self.required_policies()) {
1249            Ok(())
1250        } else {
1251            Err(TransportError::IncompatibleQos)
1252        }
1253    }
1254}
1255
1256/// Publisher trait for sending messages.
1257///
1258/// # Threading
1259///
1260/// `&self` on `publish_raw` — implementors must allow concurrent
1261/// publishes from multiple threads. Internal locking (or lock-free
1262/// queues) is the backend's responsibility.
1263///
1264/// # Buffer ownership
1265///
1266/// `data` in `publish_raw` is borrowed for the duration of the call.
1267/// The backend must either send it inline or copy into its own
1268/// buffer before returning — the slice is invalid after the call.
1269///
1270/// # Blocking
1271///
1272/// `publish_raw` is expected to be non-blocking on best-effort QoS
1273/// and bounded-blocking on reliable QoS (waiting for outbound queue
1274/// space). Backends should *not* block waiting for ack from a
1275/// matched subscriber.
1276pub trait Publisher {
1277    /// Error type for publish operations
1278    type Error;
1279
1280    /// Publish a CDR-serialised message.
1281    ///
1282    /// Returns once the message has been handed to the transport
1283    /// (queued or fired-and-forgotten depending on QoS). Does **not**
1284    /// wait for delivery.
1285    fn publish_raw(&self, data: &[u8]) -> Result<(), Self::Error>;
1286
1287    /// Phase 128.F.4 — publish with an opaque attachment block.
1288    ///
1289    /// `attachment` rides alongside the payload at the wire layer.
1290    /// Receivers can read it back via
1291    /// [`Subscriber::try_recv_raw_with_attachment`].
1292    ///
1293    /// Primary use case: cross-RMW bridges stamp a `bridge_origin`
1294    /// tag (the source backend's RMW name) so a paired return
1295    /// bridge can deterministically drop echoed frames.
1296    ///
1297    /// Default body delegates to [`publish_raw`](Self::publish_raw)
1298    /// and discards the attachment — backends that do not natively
1299    /// carry attachments (XRCE today, DDS without a user-data hook)
1300    /// see no change. Backends with native attachment support
1301    /// (zenoh-pico's `z_publisher_put_options::attachment`,
1302    /// Cyclone DDS user-data) override to write the bytes onto the
1303    /// wire.
1304    fn publish_raw_with_attachment(
1305        &self,
1306        data: &[u8],
1307        _attachment: &[u8],
1308    ) -> Result<(), Self::Error> {
1309        self.publish_raw(data)
1310    }
1311
1312    /// Phase 124.E.1 — streamed publish.
1313    ///
1314    /// `size_cb` reports the total payload length once; `chunk_cb`
1315    /// fills the slot in chunks. Saves the per-publisher staging
1316    /// buffer when the message is large enough to dominate the
1317    /// device's `.bss`.
1318    ///
1319    /// Default body — the **staging-buffer fallback** (124.E.2).
1320    /// Asks `size_cb` for the total length, fills a stack-allocated
1321    /// `[u8; NROS_MAX_STREAM_CHUNK]` via `chunk_cb`, then forwards
1322    /// to `publish_raw`. Returns `Err(BufferTooSmall)` if the total
1323    /// exceeds the stack cap so the caller can drop back to a
1324    /// regular `publish_raw` with a heap-sized buffer.
1325    ///
1326    /// Concrete backends opt in by overriding to stream straight
1327    /// into the network buffer (zenoh: write into the zenoh-pico
1328    /// outbound buffer; XRCE: micro-CDR streaming APIs).
1329    ///
1330    /// # Safety
1331    ///
1332    /// The caller must ensure `user_ctx` is valid for every invocation of
1333    /// `size_cb` and `chunk_cb` during this call, and that both callbacks obey
1334    /// their out-pointer contracts.
1335    ///
1336    /// `size_cb` and `chunk_cb` may be called from the same thread
1337    /// that called `publish_streamed`. Backends MUST NOT defer the
1338    /// calls past the function return; the caller's `user_ctx`
1339    /// pointer is only guaranteed valid for the duration of the
1340    /// call.
1341    unsafe fn publish_streamed(
1342        &self,
1343        size_cb: unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut core::ffi::c_void),
1344        chunk_cb: unsafe extern "C" fn(
1345            out_buf: *mut u8,
1346            cap: usize,
1347            out_written: *mut usize,
1348            user_ctx: *mut core::ffi::c_void,
1349        ),
1350        user_ctx: *mut core::ffi::c_void,
1351    ) -> Result<(), Self::Error>
1352    where
1353        Self::Error: From<TransportError>,
1354    {
1355        /// Default staging-buffer cap. Stack-allocated, so embedded
1356        /// callers don't pay for it unless they actually invoke this
1357        /// fallback. 4 KiB matches typical RTPS frag-size +
1358        /// micro-XRCE message ceilings.
1359        const STAGE_CAP: usize = 4096;
1360
1361        let mut total = 0usize;
1362        // SAFETY: caller's contract on `size_cb` matches our trait
1363        // doc — fire once with a writable `*mut usize` slot.
1364        unsafe { size_cb(&mut total as *mut usize, user_ctx) };
1365        if total > STAGE_CAP {
1366            return Err(TransportError::BufferTooSmall.into());
1367        }
1368        let mut stage = [0u8; STAGE_CAP];
1369        let mut written_so_far = 0usize;
1370        while written_so_far < total {
1371            let mut chunk_written = 0usize;
1372            let remaining = total - written_so_far;
1373            // SAFETY: `chunk_cb` writes ≤ `cap` bytes to
1374            // `out_buf` and reports the count via `out_written`.
1375            unsafe {
1376                chunk_cb(
1377                    stage.as_mut_ptr().add(written_so_far),
1378                    remaining,
1379                    &mut chunk_written as *mut usize,
1380                    user_ctx,
1381                );
1382            }
1383            if chunk_written == 0 {
1384                // Caller signalled EOF early — treat the partial
1385                // write as a malformed sequence; reporting it as
1386                // BufferTooSmall keeps the surface tight without
1387                // adding a new variant.
1388                return Err(TransportError::BufferTooSmall.into());
1389            }
1390            written_so_far += chunk_written;
1391        }
1392        self.publish_raw(&stage[..total])
1393    }
1394
1395    /// Publish a typed message (serializes automatically)
1396    fn publish<M: RosMessage>(&self, msg: &M, buf: &mut [u8]) -> Result<(), Self::Error> {
1397        use nros_core::CdrWriter;
1398
1399        let mut writer = CdrWriter::new_with_header(buf).map_err(|_| self.buffer_error())?;
1400        msg.serialize(&mut writer)
1401            .map_err(|_| self.serialization_error())?;
1402        let len = writer.position();
1403        self.publish_raw(&buf[..len])
1404    }
1405
1406    /// Return a buffer-too-small error (implementation specific)
1407    fn buffer_error(&self) -> Self::Error;
1408
1409    /// Return a serialization error (implementation specific)
1410    fn serialization_error(&self) -> Self::Error;
1411
1412    /// Phase 108 — `true` if the backend can generate this event for
1413    /// this publisher. Default returns `false`; backends override per
1414    /// supported event kind.
1415    ///
1416    /// Only [`EventKind::LivelinessLost`](crate::event::EventKind::LivelinessLost) and
1417    /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) are publisher-side events;
1418    /// other kinds always return `false` here.
1419    fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
1420        false
1421    }
1422
1423    /// Phase 108 — register a callback fired when the named status
1424    /// event occurs. `deadline_ms` applies to
1425    /// [`EventKind::OfferedDeadlineMissed`](crate::event::EventKind::OfferedDeadlineMissed) only; ignored otherwise.
1426    /// Default impl returns the backend's "unsupported"-shaped error.
1427    ///
1428    /// # Safety
1429    ///
1430    /// `cb` and `user_ctx` must remain valid for the entity's
1431    /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
1432    /// responsible for keeping the closure / context arena alive.
1433    unsafe fn register_event_callback(
1434        &mut self,
1435        _kind: crate::event::EventKind,
1436        _deadline_ms: u32,
1437        _cb: crate::event::EventCallback,
1438        _user_ctx: *mut core::ffi::c_void,
1439    ) -> Result<(), Self::Error> {
1440        Err(self.unsupported_event_error())
1441    }
1442
1443    /// Phase 108 — backend's error variant for "this event kind is
1444    /// not supported." Default impl reuses `serialization_error()`
1445    /// since most backends share an `Unsupported` variant; backends
1446    /// override if they have a distinct `Unsupported` mapping.
1447    fn unsupported_event_error(&self) -> Self::Error {
1448        self.serialization_error()
1449    }
1450
1451    /// Phase 109 — assert this publisher's liveliness manually.
1452    /// Required for publishers configured with
1453    /// `QosLivelinessPolicy::ManualByTopic`. No-op for other
1454    /// liveliness kinds. Default impl returns `Ok(())` (no-op);
1455    /// backends override when they implement manual liveliness.
1456    fn assert_liveliness(&self) -> Result<(), Self::Error> {
1457        Ok(())
1458    }
1459}
1460
1461/// Subscriber trait for receiving messages.
1462///
1463/// # Threading
1464///
1465/// `&mut self` on `try_recv_raw` — the executor takes exclusive
1466/// ownership of the subscriber for the duration of a receive. A
1467/// backend that wants to allow concurrent receives must split into
1468/// per-thread sub-handles internally.
1469///
1470/// # Buffer ownership
1471///
1472/// `buf` is caller-owned. The implementation copies the next ready
1473/// message into `buf` and returns the byte count. The caller may
1474/// re-use or drop `buf` immediately after the call.
1475///
1476/// # Blocking
1477///
1478/// `try_recv_raw` is **non-blocking**: returns `Ok(None)` (or
1479/// equivalent for backends that map empty into a zero-length read)
1480/// when no message is ready. Use [`Session::drive_io`] to wait for
1481/// data; never sleep inside `try_recv_raw`.
1482pub trait Subscriber {
1483    /// Error type for receive operations
1484    type Error;
1485
1486    /// Check if data is available without consuming it.
1487    ///
1488    /// Non-destructive — does not advance the receive cursor.
1489    /// Conservative default returns `true` (always assume data may
1490    /// be available); backends should override with a real check
1491    /// to avoid spurious receive attempts.
1492    fn has_data(&self) -> bool {
1493        true
1494    }
1495
1496    /// Try to receive one message into `buf`.
1497    ///
1498    /// Non-blocking. On success returns `Ok(Some(len))` where `len`
1499    /// is the byte count written into `buf[..len]`. Returns
1500    /// `Ok(None)` if no message is ready. If `buf` is too small the
1501    /// backend may either truncate (and document it) or return an
1502    /// error (preferred).
1503    fn try_recv_raw(&mut self, buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
1504
1505    /// Phase 128.F.4 — receive with attachment bytes alongside the
1506    /// payload.
1507    ///
1508    /// On success returns `Ok(Some((payload_len, attachment_len)))`
1509    /// with the payload written into `buf[..payload_len]` and the
1510    /// attachment (if any) written into
1511    /// `att_buf[..attachment_len]`. `attachment_len == 0` means the
1512    /// incoming sample carried no attachment.
1513    ///
1514    /// Default body falls back to [`try_recv_raw`](Self::try_recv_raw)
1515    /// and reports a 0-length attachment. Backends with native
1516    /// attachment support override to populate `att_buf`. Cross-RMW
1517    /// bridges use the attachment to read the `bridge_origin` tag
1518    /// stamped by the sending side.
1519    fn try_recv_raw_with_attachment(
1520        &mut self,
1521        buf: &mut [u8],
1522        _att_buf: &mut [u8],
1523    ) -> Result<Option<(usize, usize)>, Self::Error> {
1524        match self.try_recv_raw(buf)? {
1525            Some(len) => Ok(Some((len, 0))),
1526            None => Ok(None),
1527        }
1528    }
1529
1530    /// Phase 124.D.1 — burst-take.
1531    ///
1532    /// Drain up to `max_msgs` queued samples into the contiguous
1533    /// `buf` block in one call, with the i-th sample at
1534    /// `buf[i * per_msg_cap .. i * per_msg_cap + out_lens[i]]`.
1535    /// Returns the number of messages actually delivered. Partial
1536    /// drains MUST report the count, not error out.
1537    ///
1538    /// Default body loop-drives `try_recv_raw` so callers can
1539    /// commit to the batched API regardless of backend support.
1540    /// Concrete backends opt in by overriding with a native batch
1541    /// take (zenoh queue drain, `dds_take(max_samples)`).
1542    fn try_recv_sequence(
1543        &mut self,
1544        buf: &mut [u8],
1545        per_msg_cap: usize,
1546        max_msgs: usize,
1547        out_lens: &mut [usize],
1548    ) -> Result<usize, Self::Error> {
1549        if per_msg_cap == 0 || max_msgs == 0 {
1550            return Ok(0);
1551        }
1552        let limit = max_msgs.min(out_lens.len());
1553        let mut count = 0;
1554        for i in 0..limit {
1555            let slot = &mut buf[i * per_msg_cap..(i + 1) * per_msg_cap];
1556            match self.try_recv_raw(slot)? {
1557                Some(len) => {
1558                    out_lens[i] = len;
1559                    count += 1;
1560                }
1561                None => break,
1562            }
1563        }
1564        Ok(count)
1565    }
1566
1567    /// Try to receive a typed message (non-blocking)
1568    fn try_recv<M: RosMessage>(&mut self, buf: &mut [u8]) -> Result<Option<M>, Self::Error> {
1569        use nros_core::CdrReader;
1570
1571        match self.try_recv_raw(buf)? {
1572            Some(len) => {
1573                let mut reader = CdrReader::new_with_header(&buf[..len])
1574                    .map_err(|_| self.deserialization_error())?;
1575                let msg = M::deserialize(&mut reader).map_err(|_| self.deserialization_error())?;
1576                Ok(Some(msg))
1577            }
1578            None => Ok(None),
1579        }
1580    }
1581
1582    /// Process the received message in-place without copying.
1583    ///
1584    /// Calls `f` with a reference to the raw CDR bytes in the subscriber's
1585    /// internal receive buffer, avoiding a copy into a caller-provided buffer.
1586    /// While `f` executes the buffer is exclusively borrowed — any messages
1587    /// arriving from the transport during that time are dropped to prevent
1588    /// data races.
1589    ///
1590    /// Returns `Ok(true)` if a message was available and `f` was called,
1591    /// `Ok(false)` if no message was available.
1592    ///
1593    /// **Default body**: returns `Err(MessageTooLarge)` — the old default
1594    /// silently truncated anything larger than 1 KB into a stack buffer,
1595    /// which broke large messages with no diagnostic. Backends must
1596    /// override this with a real zero-copy path if they advertise support
1597    /// for `process_raw_in_place`; callers that hit the default should
1598    /// use `try_recv_raw` with a caller-sized buffer instead.
1599    fn process_raw_in_place(&mut self, f: impl FnOnce(&[u8])) -> Result<bool, Self::Error>
1600    where
1601        Self::Error: From<TransportError>,
1602    {
1603        let _ = f;
1604        Err(TransportError::MessageTooLarge.into())
1605    }
1606
1607    /// Whether this backend implements the in-place dispatch methods
1608    /// ([`process_raw_in_place`](Subscriber::process_raw_in_place) /
1609    /// [`process_raw_in_place_with_info`](Subscriber::process_raw_in_place_with_info))
1610    /// with a real zero-copy borrow.
1611    ///
1612    /// The executor consults this at subscription registration to choose the
1613    /// **in-place** arena dispatch (borrow + deserialize from the backend slot, no
1614    /// arena buffer) over the **buffered** dispatch (copy into an arena buffer
1615    /// first). Backends that leave the in-place methods at their unsupported
1616    /// default return `false` (the default) and keep the buffered path. (RFC-0038,
1617    /// Phase 231 Wave 0.2.)
1618    fn supports_process_in_place(&self) -> bool {
1619        false
1620    }
1621
1622    /// In-place processing variant that also surfaces publisher metadata.
1623    ///
1624    /// Same borrow contract as
1625    /// [`process_raw_in_place`](Subscriber::process_raw_in_place): `f` receives
1626    /// the raw CDR bytes plus the parsed [`MessageInfo`](nros_core::MessageInfo)
1627    /// — the co-located attachment (publisher GID / sequence / source timestamp),
1628    /// or `None` when no attachment was present — for the duration of the call;
1629    /// the slot is released after `f` returns. `Ok(true)` = a message was
1630    /// available and `f` was called; `Ok(false)` = none ready.
1631    ///
1632    /// **Default body**: returns the unsupported error (mirrors
1633    /// `process_raw_in_place`). Backends that advertise in-place support override
1634    /// this with a real zero-copy path; callers that hit the default should use
1635    /// the buffered [`try_recv_raw_with_info`](Subscriber::try_recv_raw_with_info)
1636    /// path instead. (RFC-0038, Phase 231 Wave 0.1.)
1637    fn process_raw_in_place_with_info(
1638        &mut self,
1639        f: impl FnOnce(&[u8], Option<nros_core::MessageInfo>),
1640    ) -> Result<bool, Self::Error>
1641    where
1642        Self::Error: From<TransportError>,
1643    {
1644        let _ = f;
1645        Err(TransportError::MessageTooLarge.into())
1646    }
1647
1648    /// Try to receive raw data along with publisher metadata.
1649    ///
1650    /// When available, [`MessageInfo`](nros_core::MessageInfo) contains
1651    /// the publisher's GID (Global Identifier) and source timestamp,
1652    /// extracted from a transport-level attachment on the incoming message.
1653    ///
1654    /// Returns `Ok(Some((len, info)))` if data is available, where:
1655    /// - `len` is the number of bytes written to the buffer
1656    /// - `info` is the parsed publisher metadata (if attachment was present)
1657    ///
1658    /// Default: delegates to [`try_recv_raw`](Subscriber::try_recv_raw) with no info.
1659    fn try_recv_raw_with_info(
1660        &mut self,
1661        buf: &mut [u8],
1662    ) -> Result<Option<(usize, Option<nros_core::MessageInfo>)>, Self::Error> {
1663        self.try_recv_raw(buf).map(|opt| opt.map(|len| (len, None)))
1664    }
1665
1666    /// Try to receive raw data with E2E safety validation (CRC + sequence tracking).
1667    ///
1668    /// Returns `Ok(Some((len, status)))` if data is available, where:
1669    /// - `len` is the number of bytes written to the buffer
1670    /// - `status` is the integrity validation result
1671    ///
1672    /// Default: delegates to `try_recv_raw` with no CRC info.
1673    #[cfg(feature = "safety-e2e")]
1674    fn try_recv_validated(
1675        &mut self,
1676        buf: &mut [u8],
1677    ) -> Result<Option<(usize, crate::IntegrityStatus)>, Self::Error> {
1678        self.try_recv_raw(buf).map(|opt| {
1679            opt.map(|len| {
1680                (
1681                    len,
1682                    crate::IntegrityStatus {
1683                        gap: 0,
1684                        duplicate: false,
1685                        crc_valid: None,
1686                    },
1687                )
1688            })
1689        })
1690    }
1691
1692    /// Register an async waker to be notified when data arrives.
1693    ///
1694    /// Called from `Future::poll()` implementations to store the waker.
1695    /// The transport backend calls `waker.wake()` from its receive callback
1696    /// when new data is available, enabling event-driven async without
1697    /// busy-polling.
1698    ///
1699    /// Default: no-op (backends that don't support waking simply ignore this).
1700    fn register_waker(&self, _waker: &core::task::Waker) {}
1701
1702    /// Return a deserialization error (implementation specific)
1703    fn deserialization_error(&self) -> Self::Error;
1704
1705    /// Phase 108 — `true` if the backend can generate this event for
1706    /// this subscriber. Default returns `false`; backends override per
1707    /// supported event kind.
1708    ///
1709    /// Subscriber-side event kinds:
1710    /// [`EventKind::LivelinessChanged`](crate::event::EventKind::LivelinessChanged),
1711    /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed),
1712    /// [`EventKind::MessageLost`](crate::event::EventKind::MessageLost).
1713    /// Publisher kinds always return `false` here.
1714    fn supports_event(&self, _kind: crate::event::EventKind) -> bool {
1715        false
1716    }
1717
1718    /// Phase 108 — register a callback fired when the named status
1719    /// event occurs. `deadline_ms` applies to
1720    /// [`EventKind::RequestedDeadlineMissed`](crate::event::EventKind::RequestedDeadlineMissed) only; ignored otherwise.
1721    /// Default impl returns the backend's "unsupported"-shaped error.
1722    ///
1723    /// # Safety
1724    ///
1725    /// `cb` and `user_ctx` must remain valid for the entity's
1726    /// lifetime. Caller (typically `nros-node`'s typed wrapper) is
1727    /// responsible for keeping the closure / context arena alive.
1728    unsafe fn register_event_callback(
1729        &mut self,
1730        _kind: crate::event::EventKind,
1731        _deadline_ms: u32,
1732        _cb: crate::event::EventCallback,
1733        _user_ctx: *mut core::ffi::c_void,
1734    ) -> Result<(), Self::Error> {
1735        Err(self.unsupported_event_error())
1736    }
1737
1738    /// Phase 108 — backend's error variant for "this event kind is
1739    /// not supported." Default reuses `deserialization_error()` for
1740    /// backends that don't have a distinct `Unsupported` mapping.
1741    fn unsupported_event_error(&self) -> Self::Error {
1742        self.deserialization_error()
1743    }
1744}
1745
1746/// Service request from a client
1747pub struct ServiceRequest<'a> {
1748    /// Raw request data (CDR encoded)
1749    pub data: &'a [u8],
1750    /// Sequence number for request/response matching
1751    pub sequence_number: i64,
1752}
1753
1754// ============================================================================
1755// Phase 99 — zero-copy raw API: SlotLending / SlotBorrowing
1756// ============================================================================
1757//
1758// Backends that can lend a slot directly into their outbound buffer
1759// (zenoh-pico w/ unstable-zenoh-api, XRCE-DDS via uxr_prepare_output_stream,
1760// full DDS w/ SHM transport) implement these traits. Backends that cannot
1761// (uORB, default zenoh-pico) do NOT impl them — `EmbeddedRawPublisher` then
1762// falls back to its per-publisher arena and memcpys at commit time. Both
1763// paths land at `Publisher::publish_raw` for the actual wire write; only
1764// the user-side copy is eliminated when lending is available.
1765//
1766// Selection is **compile-time** via the `lending` Cargo feature. Each
1767// backend crate forwards its own `lending` feature to `nros-rmw/lending`
1768// when it can satisfy the trait. nros-node's `rmw-lending` aggregates.
1769// User opting `nros/rmw-lending` w/ a non-lending backend (e.g. uORB)
1770// gets a clear compile error from the unsatisfied trait bound on the
1771// concrete `RmwPublisher`.
1772
1773/// Backend can lend a writable slot into its outbound buffer.
1774///
1775/// The returned slot's lifetime is tied to `&self`; user fills it in
1776/// place, then calls [`commit_slot`](Self::commit_slot) to publish.
1777/// Dropping the slot without commit is a no-op (slot returned to free
1778/// pool); concurrent loan attempts that would exceed backend capacity
1779/// return [`TransportError::WouldBlock`] — never block.
1780#[cfg(feature = "lending")]
1781pub trait SlotLending: Publisher {
1782    /// Backend-owned writable slot. Holds a `&'a mut [u8]` and any
1783    /// state needed for commit_slot.
1784    type Slot<'a>: AsMut<[u8]> + 'a
1785    where
1786        Self: 'a;
1787
1788    /// Reserve a writable slot of `len` bytes from the backend's
1789    /// outbound buffer. Returns `Ok(None)` if the backend has no slot
1790    /// available (full); never blocks.
1791    fn try_lend_slot(&self, len: usize) -> Result<Option<Self::Slot<'_>>, Self::Error>;
1792
1793    /// Commit a previously-lent slot. Consumes the slot and triggers
1794    /// the actual wire write. Returns `Err` on backend send failure;
1795    /// the slot's bytes are lost in that case (caller must re-lend +
1796    /// re-fill to retry).
1797    fn commit_slot(&self, slot: Self::Slot<'_>) -> Result<(), Self::Error>;
1798}
1799
1800/// Backend can lend a read-only view into its receive buffer.
1801///
1802/// The returned view's lifetime is tied to `&mut self` (subscriber-
1803/// exclusive); dropping the view releases any backend lock and lets
1804/// the next message advance into the buffer.
1805#[cfg(feature = "lending")]
1806pub trait SlotBorrowing: Subscriber {
1807    /// Backend-owned read-only view. Holds a `&'a [u8]` and any state
1808    /// needed to release the borrow on Drop.
1809    type View<'a>: AsRef<[u8]> + 'a
1810    where
1811        Self: 'a;
1812
1813    /// Try to borrow the next available message in place. Returns
1814    /// `Ok(None)` if no message is ready; never blocks.
1815    fn try_borrow(&mut self) -> Result<Option<Self::View<'_>>, Self::Error>;
1816}
1817
1818/// Service server trait for handling requests.
1819///
1820/// # Threading
1821///
1822/// `&mut self` on `try_recv_request` and `send_reply` — the executor
1823/// owns the server while a request is being handled. Handler bodies
1824/// run synchronously on the executor thread; long handlers should
1825/// dispatch work to a worker queue and reply later via the recorded
1826/// `sequence_number`.
1827///
1828/// # Calling pattern
1829///
1830/// 1. Executor calls `try_recv_request(buf)`.
1831/// 2. If `Some(req)` returned, decode, run handler, encode reply.
1832/// 3. `send_reply(req.sequence_number, &reply_buf)`.
1833///
1834/// `sequence_number` is the canonical request → reply correlation
1835/// token; backends derive it from the wire-level metadata (zenoh
1836/// query id, DDS sample identity).
1837pub trait ServiceServerTrait {
1838    /// Error type for service operations
1839    type Error;
1840
1841    /// Check if a request is available without consuming it.
1842    ///
1843    /// Non-destructive. Default returns `true` (always assume one
1844    /// may be available); backends should override with a real
1845    /// check.
1846    fn has_request(&self) -> bool {
1847        true
1848    }
1849
1850    /// Phase 122.3.c.6.e — register a `Waker` for event-driven
1851    /// service servers. Mirrors the matching method on
1852    /// `SubscriberTrait` / `ServiceClientTrait`. Backends that
1853    /// surface incoming-request notifications wake `waker` when
1854    /// `has_request()` flips true. Default: no-op (backends without
1855    /// wake support ignore — caller falls back to polling).
1856    fn register_waker(&self, _waker: &core::task::Waker) {}
1857
1858    /// Try to receive a service request into `buf` (non-blocking).
1859    ///
1860    /// On success returns a `ServiceRequest` that borrows from
1861    /// `buf`. The borrow is released when the returned struct is
1862    /// dropped — typically before `send_reply` is called, since
1863    /// `send_reply` takes `&mut self`.
1864    fn try_recv_request<'a>(
1865        &mut self,
1866        buf: &'a mut [u8],
1867    ) -> Result<Option<ServiceRequest<'a>>, Self::Error>;
1868
1869    /// Send a reply for the given sequence number. Non-blocking
1870    /// from the application's perspective; the backend may queue
1871    /// the reply for transport-level transmission.
1872    fn send_reply(&mut self, sequence_number: i64, data: &[u8]) -> Result<(), Self::Error>;
1873
1874    /// Handle a service request with typed messages
1875    fn handle_request<S: RosService>(
1876        &mut self,
1877        req_buf: &mut [u8],
1878        reply_buf: &mut [u8],
1879        handler: impl FnOnce(&S::Request) -> S::Reply,
1880    ) -> Result<bool, Self::Error>
1881    where
1882        Self::Error: From<TransportError>,
1883    {
1884        use nros_core::{CdrReader, CdrWriter};
1885
1886        // First, try to receive a request and extract necessary data.
1887        // Capture the data slice's offset within `req_buf` so we can
1888        // re-borrow it after the `ServiceRequest` (which holds a
1889        // borrow into `req_buf`) is dropped. Some backends prepend a
1890        // header (DDS: 8-byte sequence number) and place the CDR
1891        // payload at a non-zero offset in the buffer; others (zenoh)
1892        // put it at offset 0. Reading from offset 0 unconditionally
1893        // would feed the prefix bytes to the CDR deserializer and
1894        // silently corrupt the request.
1895        let buf_start = req_buf.as_ptr() as usize;
1896        let (data_offset, data_len, sequence_number) = match self.try_recv_request(req_buf)? {
1897            Some(request) => {
1898                let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
1899                (offset, request.data.len(), request.sequence_number)
1900            }
1901            None => return Ok(false),
1902        };
1903
1904        // Deserialize request from the captured offset.
1905        let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
1906            .map_err(|_| TransportError::DeserializationError)?;
1907        let req = S::Request::deserialize(&mut reader)
1908            .map_err(|_| TransportError::DeserializationError)?;
1909
1910        // Call handler
1911        let reply = handler(&req);
1912
1913        // Serialize reply
1914        let mut writer =
1915            CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
1916        reply
1917            .serialize(&mut writer)
1918            .map_err(|_| TransportError::SerializationError)?;
1919        let len = writer.position();
1920
1921        // Send reply (now we can borrow self mutably again)
1922        self.send_reply(sequence_number, &reply_buf[..len])?;
1923        Ok(true)
1924    }
1925
1926    /// Handle a service request where the handler returns `Box<S::Reply>`
1927    ///
1928    /// Identical to `handle_request` but the handler returns a heap-allocated reply.
1929    /// This is needed for services with large response types (e.g., parameter services
1930    /// where `Vec<ParameterValue, 64>` is ~1MB+) that would overflow the stack.
1931    #[cfg(feature = "alloc")]
1932    fn handle_request_boxed<S: RosService>(
1933        &mut self,
1934        req_buf: &mut [u8],
1935        reply_buf: &mut [u8],
1936        handler: impl FnOnce(&S::Request) -> alloc::boxed::Box<S::Reply>,
1937    ) -> Result<bool, Self::Error>
1938    where
1939        Self::Error: From<TransportError>,
1940    {
1941        use nros_core::{CdrReader, CdrWriter};
1942
1943        let buf_start = req_buf.as_ptr() as usize;
1944        let (data_offset, data_len, sequence_number) = match self.try_recv_request(req_buf)? {
1945            Some(request) => {
1946                let offset = (request.data.as_ptr() as usize).saturating_sub(buf_start);
1947                (offset, request.data.len(), request.sequence_number)
1948            }
1949            None => return Ok(false),
1950        };
1951
1952        let mut reader = CdrReader::new_with_header(&req_buf[data_offset..data_offset + data_len])
1953            .map_err(|_| TransportError::DeserializationError)?;
1954        let req = S::Request::deserialize(&mut reader)
1955            .map_err(|_| TransportError::DeserializationError)?;
1956
1957        let reply = handler(&req);
1958
1959        let mut writer =
1960            CdrWriter::new_with_header(reply_buf).map_err(|_| TransportError::BufferTooSmall)?;
1961        reply
1962            .serialize(&mut writer)
1963            .map_err(|_| TransportError::SerializationError)?;
1964        let len = writer.position();
1965
1966        self.send_reply(sequence_number, &reply_buf[..len])?;
1967        Ok(true)
1968    }
1969}
1970
1971/// Service client trait for sending requests.
1972///
1973/// # Threading
1974///
1975/// `&mut self` on every method — the client is single-owner. For
1976/// fan-out request patterns, create one client per worker thread.
1977///
1978/// # Calling pattern
1979///
1980/// All in-tree backends route blocking waits through the executor:
1981///
1982/// 1. `send_request_raw(buf)` — non-blocking; returns once the
1983///    request is queued for transmission.
1984/// 2. The executor's `drive_io` runs.
1985/// 3. `try_recv_reply_raw(buf)` — non-blocking; returns
1986///    `Ok(Some(len))` when the reply is back.
1987///
1988/// The deprecated [`call_raw`](Self::call_raw) blocking path is
1989/// kept for backwards compatibility but should not be called.
1990pub trait ServiceClientTrait {
1991    /// Error type for service operations
1992    type Error;
1993
1994    /// Send a service request and wait for reply (blocking).
1995    ///
1996    /// **Deprecated — do not call.** The default body returns `Timeout`
1997    /// immediately without polling. Use `Client::call` →
1998    /// `Promise::wait(executor, timeout_ms)` which lets the executor
1999    /// drive I/O while waiting instead of busy-looping on
2000    /// `try_recv_reply_raw` with no sleep (which starves the transport
2001    /// on FreeRTOS / Zephyr single-threaded schedulers).
2002    ///
2003    /// Backends that still need an internal blocking path should
2004    /// override this with a real sleep-between-polls implementation,
2005    /// but all in-tree backends (zenoh, XRCE) route blocking waits
2006    /// through the executor.
2007    #[deprecated(note = "use Client::call → Promise::wait with an executor instead")]
2008    fn call_raw(&mut self, request: &[u8], _reply_buf: &mut [u8]) -> Result<usize, Self::Error>
2009    where
2010        Self::Error: From<TransportError>,
2011    {
2012        let _ = request;
2013        Err(TransportError::Timeout.into())
2014    }
2015
2016    /// Send a service request without waiting for a reply (non-blocking).
2017    ///
2018    /// The caller must subsequently poll [`try_recv_reply_raw`](Self::try_recv_reply_raw)
2019    /// to retrieve the reply.
2020    fn send_request_raw(&mut self, request: &[u8]) -> Result<(), Self::Error>;
2021
2022    /// Poll for a reply to the most recently sent request (non-blocking).
2023    ///
2024    /// Returns `Ok(Some(len))` when a reply has arrived, `Ok(None)` if not yet
2025    /// available, or `Err` on failure.
2026    fn try_recv_reply_raw(&mut self, reply_buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
2027
2028    /// Send a typed service request without waiting for a reply (non-blocking).
2029    ///
2030    /// Serializes the request into `req_buf` and calls [`send_request_raw`](Self::send_request_raw).
2031    fn send_request<S: RosService>(
2032        &mut self,
2033        request: &S::Request,
2034        req_buf: &mut [u8],
2035    ) -> Result<(), Self::Error>
2036    where
2037        Self::Error: From<TransportError>,
2038    {
2039        use nros_core::CdrWriter;
2040
2041        let mut writer =
2042            CdrWriter::new_with_header(req_buf).map_err(|_| TransportError::BufferTooSmall)?;
2043        request
2044            .serialize(&mut writer)
2045            .map_err(|_| TransportError::SerializationError)?;
2046        let req_len = writer.position();
2047
2048        self.send_request_raw(&req_buf[..req_len])
2049    }
2050
2051    /// Poll for a typed reply to the most recently sent request (non-blocking).
2052    ///
2053    /// Calls [`try_recv_reply_raw`](Self::try_recv_reply_raw) and deserializes if available.
2054    fn try_recv_reply<S: RosService>(
2055        &mut self,
2056        reply_buf: &mut [u8],
2057    ) -> Result<Option<S::Reply>, Self::Error>
2058    where
2059        Self::Error: From<TransportError>,
2060    {
2061        use nros_core::CdrReader;
2062
2063        match self.try_recv_reply_raw(reply_buf)? {
2064            Some(len) => {
2065                let mut reader = CdrReader::new_with_header(&reply_buf[..len])
2066                    .map_err(|_| TransportError::DeserializationError)?;
2067                let reply = S::Reply::deserialize(&mut reader)
2068                    .map_err(|_| TransportError::DeserializationError)?;
2069                Ok(Some(reply))
2070            }
2071            None => Ok(None),
2072        }
2073    }
2074
2075    /// Register an async waker to be notified when a reply arrives.
2076    ///
2077    /// Called from `Future::poll()` implementations to store the waker.
2078    /// The transport backend calls `waker.wake()` from its reply callback
2079    /// when a response is available, enabling event-driven async without
2080    /// busy-polling.
2081    ///
2082    /// Default: no-op (backends that don't support waking simply ignore this).
2083    fn register_waker(&self, _waker: &core::task::Waker) {}
2084
2085    /// Begin a server-discovery query on this client (non-blocking).
2086    ///
2087    /// Models `rclcpp::ClientBase::wait_for_service` machinery: the backend
2088    /// fires off a discovery probe (typically a Zenoh liveliness query
2089    /// against the matching server's wildcarded liveliness keyexpr) and
2090    /// the caller polls [`poll_server_discovery`](Self::poll_server_discovery)
2091    /// to collect the result.
2092    ///
2093    /// Default impl: no-op success. Backends without a discovery channel
2094    /// (or those that always assume the server is reachable) can leave
2095    /// this default and have `poll_server_discovery` return
2096    /// `Ok(Some(true))` immediately.
2097    fn start_server_discovery(&mut self, _timeout_ms: u32) -> Result<(), Self::Error> {
2098        Ok(())
2099    }
2100
2101    /// Poll an in-flight server-discovery query.
2102    ///
2103    /// - `Ok(Some(true))` — at least one matching server has reported
2104    ///   back; safe to send the first request.
2105    /// - `Ok(Some(false))` — discovery query finished without finding
2106    ///   any matching server (timeout / no-replies).
2107    /// - `Ok(None)` — query still in flight.
2108    /// - `Err(_)` — transport-level failure unrelated to server presence.
2109    ///
2110    /// Default impl: returns `Ok(Some(true))` (i.e., "server is always
2111    /// assumed reachable"). The Zenoh backend overrides this with a
2112    /// liveliness-token check.
2113    fn poll_server_discovery(&mut self) -> Result<Option<bool>, Self::Error> {
2114        Ok(Some(true))
2115    }
2116
2117    /// Synchronous, non-blocking check of whether a matching server is
2118    /// currently visible.
2119    ///
2120    /// Mirrors `rclcpp::ClientBase::service_is_ready`. Backends that lack
2121    /// discovery should keep the default `true` so existing call sites
2122    /// don't regress.
2123    ///
2124    /// Default impl: always `true`.
2125    fn is_server_ready(&self) -> bool {
2126        true
2127    }
2128
2129    /// Phase 124.C.1 — graph-aware server-availability probe.
2130    ///
2131    /// Returns `Ok(true)` if at least one matching server has been
2132    /// discovered, `Ok(false)` if none yet, or `Err(_)` if the
2133    /// backend cannot answer (e.g. XRCE — micro-XRCE-DDS-Client has
2134    /// no participant enumeration). Distinct from
2135    /// [`is_server_ready`](Self::is_server_ready), which collapses
2136    /// "don't know" and "no server" into the same `false` answer.
2137    ///
2138    /// User-facing surface: `Client<S>::server_available()` in Rust,
2139    /// `nros_client_server_available()` in C/C++. Clients use this
2140    /// to gate the first `call_raw` so a startup-ordering race
2141    /// (client opens before server's discovery announcement lands)
2142    /// doesn't surface as a request-side timeout.
2143    ///
2144    /// Default impl: `Err(TransportError::Unsupported)` — backends
2145    /// that support graph introspection (zenoh queryable interest,
2146    /// DDS built-in topic readers) opt in by overriding.
2147    fn server_available(&self) -> Result<bool, Self::Error>
2148    where
2149        Self::Error: From<TransportError>,
2150    {
2151        Err(TransportError::Unsupported.into())
2152    }
2153
2154    /// Call a service with typed messages (blocking).
2155    ///
2156    /// **Deprecated — do not call.** The default body returns `Timeout`
2157    /// immediately without polling. Use `Client::call` on the executor
2158    /// instead, which drives I/O while waiting. See
2159    /// [`call_raw`](Self::call_raw) for the same reasoning.
2160    #[deprecated(note = "use Client::call → Promise::wait with an executor instead")]
2161    fn call<S: RosService>(
2162        &mut self,
2163        request: &S::Request,
2164        req_buf: &mut [u8],
2165        _reply_buf: &mut [u8],
2166    ) -> Result<S::Reply, Self::Error>
2167    where
2168        Self::Error: From<TransportError>,
2169    {
2170        use nros_core::CdrWriter;
2171
2172        // Serialize request so the error surface matches the old impl
2173        // for the "bad request" path; but skip the receive busy-loop.
2174        let mut writer =
2175            CdrWriter::new_with_header(req_buf).map_err(|_| TransportError::BufferTooSmall)?;
2176        request
2177            .serialize(&mut writer)
2178            .map_err(|_| TransportError::SerializationError)?;
2179        Err(TransportError::Timeout.into())
2180    }
2181}
2182
2183/// Transport backend trait (legacy).
2184///
2185/// Use [`Rmw`] for new code. This trait is retained for backward compatibility
2186/// with existing code that uses [`TransportConfig`] directly.
2187pub trait Transport {
2188    /// Error type for this transport
2189    type Error;
2190    /// Session type for this transport
2191    type Session: Session;
2192
2193    /// Open a new session with the given configuration
2194    fn open(config: &TransportConfig) -> Result<Self::Session, Self::Error>;
2195}
2196
2197/// Factory trait for compile-time middleware selection.
2198///
2199/// Embedded crates select a backend via feature flag:
2200/// ```rust,ignore
2201/// #[cfg(feature = "rmw-cffi")]
2202/// type DefaultRmw = nros_rmw_cffi::CffiRmw;
2203/// ```
2204///
2205/// Each backend provides its own `Rmw` implementation that bridges
2206/// from the middleware-agnostic [`RmwConfig`] to backend-specific
2207/// initialization.
2208///
2209/// Phase 84.E2: `open` consumes `self`. Backends carry their own
2210/// configuration (agent addresses, serial ports, TLS CA slots)
2211/// inside the factory value and hand that over to the session at
2212/// `open` time. All in-repo backends also implement
2213/// [`Default`]; most callers spell this as
2214/// `BackendRmw::default().open(&config)`.
2215pub trait Rmw {
2216    /// Session type returned by [`open`](Rmw::open)
2217    type Session: Session;
2218    /// Error type for session creation
2219    type Error: core::fmt::Debug;
2220
2221    /// Open a new middleware session with the given configuration.
2222    ///
2223    /// The backend maps [`RmwConfig`] fields to its own connection
2224    /// parameters (e.g., zenoh locator and session mode, XRCE-DDS
2225    /// agent address). Any backend-specific pre-open state stored
2226    /// on `self` (e.g. configured agent IP / port) is moved into the
2227    /// returned `Session`.
2228    fn open(self, config: &RmwConfig) -> Result<Self::Session, Self::Error>;
2229}
2230
2231#[cfg(test)]
2232mod tests {
2233    use super::*;
2234
2235    #[test]
2236    fn test_topic_info() {
2237        let topic = TopicInfo::new("/chatter", "std_msgs::msg::dds_::String_", "abc123");
2238        assert_eq!(topic.name, "/chatter");
2239        assert_eq!(topic.domain_id, 0);
2240    }
2241
2242    #[test]
2243    fn qos_apply_overrides_matches_topic_and_role() {
2244        // Default is Reliable / Volatile / KeepLast(10).
2245        static OVERRIDES: &[QosOverride] = &[
2246            QosOverride {
2247                topic: "/chatter",
2248                role: QosOverrideRole::Publisher,
2249                value: QosOverrideValue::Reliability(QosReliabilityPolicy::BestEffort),
2250            },
2251            QosOverride {
2252                topic: "/chatter",
2253                role: QosOverrideRole::Publisher,
2254                value: QosOverrideValue::Depth(5),
2255            },
2256            QosOverride {
2257                topic: "/scan",
2258                role: QosOverrideRole::Subscription,
2259                value: QosOverrideValue::Durability(QosDurabilityPolicy::TransientLocal),
2260            },
2261        ];
2262
2263        // Matching topic + publisher role → reliability + depth applied.
2264        let pub_qos = QosSettings::default().apply_overrides(
2265            "/chatter",
2266            QosOverrideRole::Publisher,
2267            OVERRIDES,
2268        );
2269        assert_eq!(pub_qos.reliability, QosReliabilityPolicy::BestEffort);
2270        assert_eq!(pub_qos.depth, 5);
2271        assert_eq!(pub_qos.durability, QosDurabilityPolicy::Volatile); // untouched
2272
2273        // Same topic but subscription role → publisher overrides DON'T apply;
2274        // the /scan override is for a different topic → also no change.
2275        let sub_qos = QosSettings::default().apply_overrides(
2276            "/chatter",
2277            QosOverrideRole::Subscription,
2278            OVERRIDES,
2279        );
2280        assert_eq!(sub_qos, QosSettings::default());
2281
2282        // The /scan subscription override applies only to /scan+subscription.
2283        let scan_qos = QosSettings::default().apply_overrides(
2284            "/scan",
2285            QosOverrideRole::Subscription,
2286            OVERRIDES,
2287        );
2288        assert_eq!(scan_qos.durability, QosDurabilityPolicy::TransientLocal);
2289
2290        // Empty table → identity (the zero-override fast path).
2291        assert_eq!(
2292            QosSettings::default().apply_overrides("/x", QosOverrideRole::Publisher, &[]),
2293            QosSettings::default()
2294        );
2295    }
2296
2297    #[test]
2298    fn test_qos_defaults() {
2299        let qos = QosSettings::default();
2300        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2301    }
2302
2303    #[test]
2304    fn test_action_info() {
2305        let action = ActionInfo::new(
2306            "/fibonacci",
2307            "example_interfaces::action::dds_::Fibonacci_",
2308            "abc123",
2309        );
2310        assert_eq!(action.name, "/fibonacci");
2311        assert_eq!(action.domain_id, 0);
2312    }
2313
2314    #[test]
2315    fn test_action_info_with_domain() {
2316        let action = ActionInfo::new(
2317            "/fibonacci",
2318            "example_interfaces::action::dds_::Fibonacci_",
2319            "abc123",
2320        )
2321        .with_domain(42);
2322        assert_eq!(action.domain_id, 42);
2323    }
2324
2325    #[test]
2326    fn test_action_send_goal_key() {
2327        let action = ActionInfo::new(
2328            "/fibonacci",
2329            "example_interfaces::action::dds_::Fibonacci_",
2330            "abc123",
2331        )
2332        .with_domain(0);
2333
2334        let key: heapless::String<256> = action.send_goal_key();
2335        // ActionInfo returns the sub-entity name with leading slash for ROS 2 compatibility
2336        assert_eq!(key.as_str(), "/fibonacci/_action/send_goal");
2337    }
2338
2339    #[test]
2340    fn test_action_feedback_key() {
2341        let action = ActionInfo::new(
2342            "/fibonacci",
2343            "example_interfaces::action::dds_::Fibonacci_",
2344            "abc123",
2345        )
2346        .with_domain(0);
2347
2348        let key: heapless::String<256> = action.feedback_key();
2349        assert_eq!(key.as_str(), "/fibonacci/_action/feedback");
2350    }
2351
2352    #[test]
2353    fn test_action_all_sub_names() {
2354        let action = ActionInfo::new(
2355            "/fibonacci",
2356            "example_interfaces::action::dds_::Fibonacci_",
2357            "abc123",
2358        )
2359        .with_domain(0);
2360
2361        let cancel: heapless::String<256> = action.cancel_goal_key();
2362        assert_eq!(cancel.as_str(), "/fibonacci/_action/cancel_goal");
2363
2364        let result: heapless::String<256> = action.get_result_key();
2365        assert_eq!(result.as_str(), "/fibonacci/_action/get_result");
2366
2367        let status: heapless::String<256> = action.status_key();
2368        assert_eq!(status.as_str(), "/fibonacci/_action/status");
2369    }
2370
2371    // --- QoS Profile Tests ---
2372
2373    #[test]
2374    fn test_qos_profile_sensor_data() {
2375        let qos = QosSettings::QOS_PROFILE_SENSOR_DATA;
2376        assert_eq!(qos.reliability, QosReliabilityPolicy::BestEffort);
2377        assert_eq!(qos.durability, QosDurabilityPolicy::Volatile);
2378        assert_eq!(qos.history, QosHistoryPolicy::KeepLast);
2379        assert_eq!(qos.depth, 5);
2380    }
2381
2382    #[test]
2383    fn test_qos_profile_default() {
2384        let qos = QosSettings::QOS_PROFILE_DEFAULT;
2385        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2386        assert_eq!(qos.durability, QosDurabilityPolicy::Volatile);
2387        assert_eq!(qos.depth, 10);
2388    }
2389
2390    #[test]
2391    fn test_qos_profile_services_default() {
2392        let qos = QosSettings::QOS_PROFILE_SERVICES_DEFAULT;
2393        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2394        assert_eq!(qos.durability, QosDurabilityPolicy::Volatile);
2395    }
2396
2397    #[test]
2398    fn services_default_validates_and_rejects_missing_policy() {
2399        // Phase 193.5 — the service-create path (node `create_service*_sized` +
2400        // the typed-arena `register_service*_on`) runs `validate_against` on the
2401        // caller's profile, exactly like pub/sub. A backend advertising the
2402        // profile's required policies admits it; dropping any required bit (here
2403        // RELIABILITY) rejects it with `IncompatibleQos` — no silent downgrade.
2404        let qos = QosSettings::services_default();
2405        let required = qos.required_policies();
2406        assert!(qos.validate_against(required).is_ok());
2407        assert!(qos.validate_against(QosPolicyMask(u32::MAX)).is_ok());
2408        let missing = QosPolicyMask(required.0 & !QosPolicyMask::RELIABILITY.0);
2409        assert_eq!(
2410            qos.validate_against(missing),
2411            Err(TransportError::IncompatibleQos)
2412        );
2413    }
2414
2415    #[test]
2416    fn test_qos_profile_parameters() {
2417        let qos = QosSettings::QOS_PROFILE_PARAMETERS;
2418        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2419        assert_eq!(qos.durability, QosDurabilityPolicy::TransientLocal);
2420        assert_eq!(qos.depth, 1000);
2421    }
2422
2423    #[test]
2424    fn test_qos_profile_clock() {
2425        let qos = QosSettings::QOS_PROFILE_CLOCK;
2426        assert_eq!(qos.reliability, QosReliabilityPolicy::BestEffort);
2427        assert_eq!(qos.depth, 1);
2428    }
2429
2430    #[test]
2431    fn test_qos_profile_parameter_events() {
2432        let qos = QosSettings::QOS_PROFILE_PARAMETER_EVENTS;
2433        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2434        assert_eq!(qos.history, QosHistoryPolicy::KeepAll);
2435    }
2436
2437    #[test]
2438    fn test_qos_profile_action_status() {
2439        let qos = QosSettings::QOS_PROFILE_ACTION_STATUS_DEFAULT;
2440        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2441        assert_eq!(qos.durability, QosDurabilityPolicy::TransientLocal);
2442        assert_eq!(qos.depth, 1);
2443    }
2444
2445    #[test]
2446    fn test_qos_static_constructors() {
2447        assert_eq!(
2448            QosSettings::topics_default(),
2449            QosSettings::QOS_PROFILE_DEFAULT
2450        );
2451        assert_eq!(
2452            QosSettings::sensor_data_default(),
2453            QosSettings::QOS_PROFILE_SENSOR_DATA
2454        );
2455        assert_eq!(
2456            QosSettings::services_default(),
2457            QosSettings::QOS_PROFILE_SERVICES_DEFAULT
2458        );
2459        assert_eq!(
2460            QosSettings::parameters_default(),
2461            QosSettings::QOS_PROFILE_PARAMETERS
2462        );
2463        assert_eq!(
2464            QosSettings::action_status_default(),
2465            QosSettings::QOS_PROFILE_ACTION_STATUS_DEFAULT
2466        );
2467    }
2468
2469    #[test]
2470    fn test_qos_builder_explicit_setters() {
2471        let qos = QosSettings::new()
2472            .reliability(QosReliabilityPolicy::Reliable)
2473            .durability(QosDurabilityPolicy::TransientLocal)
2474            .history(QosHistoryPolicy::KeepAll)
2475            .depth(100);
2476
2477        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2478        assert_eq!(qos.durability, QosDurabilityPolicy::TransientLocal);
2479        assert_eq!(qos.history, QosHistoryPolicy::KeepAll);
2480        assert_eq!(qos.depth, 100);
2481    }
2482
2483    #[test]
2484    fn test_qos_builder_chaining() {
2485        // Test that builder methods can be chained in any order
2486        let qos = QosSettings::sensor_data_default()
2487            .reliable()
2488            .transient_local()
2489            .keep_last(20);
2490
2491        assert_eq!(qos.reliability, QosReliabilityPolicy::Reliable);
2492        assert_eq!(qos.durability, QosDurabilityPolicy::TransientLocal);
2493        assert_eq!(qos.history, QosHistoryPolicy::KeepLast);
2494        assert_eq!(qos.depth, 20);
2495    }
2496
2497    #[test]
2498    fn test_qos_eq_impl() {
2499        // Verify that PartialEq works correctly via derive on QosSettings
2500        let qos1 = QosSettings::QOS_PROFILE_DEFAULT;
2501        let qos2 = QosSettings::topics_default();
2502        // Both should have same values - verify field by field
2503        assert_eq!(qos1.reliability, qos2.reliability);
2504        assert_eq!(qos1.durability, qos2.durability);
2505        assert_eq!(qos1.history, qos2.history);
2506        assert_eq!(qos1.depth, qos2.depth);
2507    }
2508
2509    // --- Locator validation tests ---
2510
2511    #[test]
2512    fn test_locator_protocol_tcp() {
2513        assert_eq!(locator_protocol("tcp/127.0.0.1:7447"), LocatorProtocol::Tcp);
2514    }
2515
2516    #[test]
2517    fn test_locator_protocol_serial() {
2518        assert_eq!(
2519            locator_protocol("serial//dev/ttyUSB0#baudrate=115200"),
2520            LocatorProtocol::Serial
2521        );
2522    }
2523
2524    #[test]
2525    fn test_locator_protocol_unknown() {
2526        assert_eq!(locator_protocol(""), LocatorProtocol::Unknown);
2527        assert_eq!(locator_protocol("http://foo"), LocatorProtocol::Unknown);
2528        assert_eq!(locator_protocol("tls/host:port"), LocatorProtocol::Unknown);
2529    }
2530
2531    #[test]
2532    fn test_locator_protocol_udp() {
2533        assert_eq!(locator_protocol("udp/127.0.0.1:7447"), LocatorProtocol::Udp);
2534        assert_eq!(
2535            locator_protocol("udp/192.168.1.50:2019"),
2536            LocatorProtocol::Udp
2537        );
2538    }
2539
2540    #[test]
2541    fn test_validate_tcp_locator_ok() {
2542        assert!(validate_locator("tcp/127.0.0.1:7447").is_ok());
2543        assert!(validate_locator("tcp/192.168.1.1:7447").is_ok());
2544    }
2545
2546    #[test]
2547    fn test_validate_tcp_locator_missing_port() {
2548        assert!(validate_locator("tcp/127.0.0.1").is_err());
2549    }
2550
2551    #[test]
2552    fn test_validate_serial_locator_ok() {
2553        assert!(validate_locator("serial//dev/ttyUSB0#baudrate=115200").is_ok());
2554        assert!(validate_locator("serial//dev/ttyACM0#baudrate=9600").is_ok());
2555        assert!(validate_locator("serial/uart1#baudrate=921600").is_ok());
2556    }
2557
2558    #[test]
2559    fn test_validate_serial_locator_empty_device() {
2560        assert!(validate_locator("serial/").is_err());
2561    }
2562
2563    #[test]
2564    fn test_validate_serial_locator_missing_baudrate() {
2565        assert!(validate_locator("serial//dev/ttyUSB0").is_err());
2566    }
2567
2568    #[test]
2569    fn test_validate_serial_locator_invalid_baudrate() {
2570        assert!(validate_locator("serial//dev/ttyUSB0#baudrate=abc").is_err());
2571    }
2572
2573    #[test]
2574    fn test_validate_unknown_protocol() {
2575        assert!(validate_locator("http://foo").is_err());
2576        assert!(validate_locator("tls/host:port").is_err());
2577    }
2578
2579    #[test]
2580    fn test_validate_udp_locator_ok() {
2581        assert!(validate_locator("udp/127.0.0.1:7447").is_ok());
2582        assert!(validate_locator("udp/192.168.1.50:2019").is_ok());
2583    }
2584
2585    #[test]
2586    fn test_validate_udp_locator_missing_port() {
2587        assert!(validate_locator("udp/127.0.0.1").is_err());
2588    }
2589
2590    // Phase 233.2 — the PX4 companion QoS profile must be BEST_EFFORT +
2591    // TRANSIENT_LOCAL + KEEP_LAST so it matches PX4's uxrce_dds_client endpoints.
2592    #[test]
2593    fn px4_qos_profile_matches_uxrce_dds_client() {
2594        let q = QosSettings::px4();
2595        assert_eq!(q.reliability, QosReliabilityPolicy::BestEffort);
2596        // VOLATILE — PX4's /fmu/out writers are volatile; a TRANSIENT_LOCAL
2597        // reader silently fails to match (verified against real PX4 SITL).
2598        assert_eq!(q.durability, QosDurabilityPolicy::Volatile);
2599        assert_eq!(q.history, QosHistoryPolicy::KeepLast);
2600        assert_eq!(q, QosSettings::QOS_PROFILE_PX4);
2601        // Depth is tunable via the builder without losing the PX4 policies.
2602        let deep = QosSettings::px4().keep_last(5);
2603        assert_eq!(deep.depth, 5);
2604        assert_eq!(deep.reliability, QosReliabilityPolicy::BestEffort);
2605        assert_eq!(deep.durability, QosDurabilityPolicy::Volatile);
2606    }
2607
2608    // --- RmwConfig Tests ---
2609
2610    #[test]
2611    fn test_rmw_config_default() {
2612        let config = RmwConfig::default();
2613        assert_eq!(config.locator, "tcp/127.0.0.1:7447");
2614        assert_eq!(config.mode, SessionMode::Client);
2615        assert_eq!(config.domain_id, 0);
2616        assert_eq!(config.node_name, "node");
2617        assert_eq!(config.namespace, "");
2618    }
2619
2620    #[test]
2621    fn test_rmw_config_custom() {
2622        let config = RmwConfig {
2623            locator: "tcp/192.168.1.1:7447",
2624            mode: SessionMode::Peer,
2625            domain_id: 42,
2626            node_name: "talker",
2627            namespace: "/ns1",
2628            properties: &[("agent_port", "2019")],
2629        };
2630        assert_eq!(config.locator, "tcp/192.168.1.1:7447");
2631        assert_eq!(config.mode, SessionMode::Peer);
2632        assert_eq!(config.domain_id, 42);
2633        assert_eq!(config.node_name, "talker");
2634        assert_eq!(config.namespace, "/ns1");
2635        assert_eq!(config.properties.len(), 1);
2636        assert_eq!(config.properties[0].0, "agent_port");
2637    }
2638
2639    #[test]
2640    fn test_rmw_config_is_copy() {
2641        let config = RmwConfig::default();
2642        let config2 = config; // Copy
2643        assert_eq!(config.locator, config2.locator);
2644        assert_eq!(config.domain_id, config2.domain_id);
2645    }
2646
2647    #[test]
2648    fn test_rmw_config_clone() {
2649        let config = RmwConfig::default();
2650        let cloned = RmwConfig { ..config };
2651        assert_eq!(cloned.locator, config.locator);
2652        assert_eq!(cloned.node_name, config.node_name);
2653    }
2654}