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}