Skip to main content

nros_node/executor/
node.rs

1//! Node — borrows the session to create typed entities.
2
3use core::marker::PhantomData;
4
5use nros_core::{RosAction, RosMessage, RosService};
6use nros_rmw::{ActionInfo, QoSProfile, ServiceInfo, Session as _, TopicInfo, TransportError};
7
8use crate::{
9    rmw_type_registry::{MessageForRmw, register_type},
10    session,
11};
12
13use super::{
14    handles::{
15        ActionClient, ActionClientCallback, ActionServer, EmbeddedPublisher, EmbeddedServiceClient,
16        EmbeddedServiceServer, ServiceClientCallback, Subscription,
17    },
18    types::NodeError,
19};
20
21/// Validate a profile against what the backend honours, and SAY WHICH POLICY
22/// when it does not — phase-417 G7.
23///
24/// Six `create_*` paths ran `qos.validate_against(…).map_err(NodeError::Transport)`
25/// and that was the whole diagnostic: the C caller saw `RCLC_RET_REJECTED`, the
26/// C++ caller `NROS_CPP_RET_NOT_ALLOWED`, and neither could learn WHICH policy
27/// the backend declined — while `rmw_vtable.h`'s `supported_qos_policies` block
28/// told the reader those creates fail "naming a policy". This is the one place
29/// that sentence becomes true for a caller who only gets a return code.
30///
31/// ONE helper rather than a log line per site, for the reason CLAUDE.md's "fix
32/// the CLASS" rule gives: six copies of a diagnostic are six places for the
33/// wording, the logger name and the fallback to drift, and this family has
34/// already paid that twice (the sizes-header mirror, the Zephyr unset-variable
35/// guard).
36///
37/// The policy NAME comes from [`nros_rmw::QoSPolicyMask::NAMED`] by way of
38/// `TransportError::qos_policy_name`, so this crate states no policy vocabulary
39/// of its own, and the token a C++ caller reads out of
40/// `nros::qos_policy_kind_to_cstr` is the same byte string that appears here.
41pub(crate) fn validate_qos_or_report(
42    qos: &QoSProfile,
43    supported: nros_rmw::QoSPolicyMask,
44    kind: &str,
45    name: &str,
46) -> Result<(), NodeError> {
47    match qos.validate_against(supported) {
48        Ok(()) => Ok(()),
49        Err(err) => {
50            nros_log::log_error!(
51                nros_log::get_logger("nros_node"),
52                "qos: {} '{}' refused — the backend does not honour {}",
53                kind,
54                name,
55                // `None` is a real answer, not a missing one: a refusal
56                // relayed from a C backend crosses `rmw_ret_t`, which has no
57                // room for a mask, so no policy was identified. Naming a
58                // neighbouring one would be the plausible wrong answer.
59                err.qos_policy_name()
60                    .unwrap_or("a QoS policy it did not name")
61            );
62            Err(NodeError::Transport(err))
63        }
64    }
65}
66
67// ============================================================================
68// Node
69// ============================================================================
70
71/// Backend-agnostic node — borrows the session to create typed entities.
72pub struct NodeHandle<'a> {
73    name: heapless::String<64>,
74    namespace: heapless::String<64>,
75    session: &'a mut session::ConcreteSession,
76    /// The executor's remap rules, both tiers (issue 1971: the launch table in
77    /// flash plus the runtime table). Consulted only to SUBSTITUTE a name some
78    /// rule matches — see `NodeHandle::remapped`.
79    remaps: super::spin::RemapView<'a>,
80    domain_id: u32,
81    /// Phase 211.H — per-node QoS overrides lowered from the launch
82    /// `qos_overrides.<topic>.<role>.<policy>` params and baked into a
83    /// `&'static` table by the entry codegen. Folded into each entity's
84    /// `QoSProfile` at `create_publisher`/`create_subscription` time
85    /// (setup-time, no alloc). Empty (`&[]`) by default → zero cost for
86    /// systems without overrides.
87    qos_overrides: &'static [nros_rmw::QoSOverride],
88    /// RFC-0052 W3b.4 — baked contract-monitor table (mirror of
89    /// `qos_overrides`): `create_publisher` attaches the matching
90    /// endpoint's counter cell so `publish` can bump it lock-free.
91    monitors: &'static [crate::executor::monitor::MonitorSpec],
92    /// W3b.5 — baked subscriber age-contract table + epoch clock;
93    /// `create_subscription` attaches the matching endpoint's age cell.
94    age_monitors: &'static [crate::executor::monitor::AgeMonitorSpec],
95    epoch_us_fn: Option<fn() -> u64>,
96}
97
98impl<'a> NodeHandle<'a> {
99    /// Create a new node (called by Executor::create_node).
100    pub(crate) fn new(
101        name: heapless::String<64>,
102        namespace: heapless::String<64>,
103        session: &'a mut session::ConcreteSession,
104        remaps: super::spin::RemapView<'a>,
105        domain_id: u32,
106    ) -> Self {
107        Self {
108            name,
109            namespace,
110            session,
111            remaps,
112            domain_id,
113            qos_overrides: &[],
114            monitors: &[],
115            age_monitors: &[],
116            epoch_us_fn: None,
117        }
118    }
119
120    /// Phase 211.H — install the plan's QoS-override table on this node. Called
121    /// by the generated entry BEFORE the component constructs its entities, so
122    /// `create_publisher`/`create_subscription` fold the matching overrides in.
123    /// The table is `&'static` (codegen bakes it as a `static`), so there is no
124    /// lifetime to thread and no runtime allocation. Plan = authority: an
125    /// override for a topic the entity creates is applied transparently (the
126    /// user's `create_publisher(topic)` call is unchanged, matching rclcpp).
127    pub fn set_qos_overrides(&mut self, overrides: &'static [nros_rmw::QoSOverride]) {
128        self.qos_overrides = overrides;
129    }
130
131    /// RFC-0052 W3b.4 — install the executor's monitor table on this node
132    /// (called by the entry glue / fixture alongside `set_qos_overrides`).
133    pub fn set_monitors(&mut self, monitors: &'static [crate::executor::monitor::MonitorSpec]) {
134        self.monitors = monitors;
135    }
136
137    /// W3b.5 — install the subscriber age-contract table + epoch clock
138    /// (auto-seeded from the executor's `set_age_table` / config epoch).
139    pub fn set_age_monitors(
140        &mut self,
141        table: &'static [crate::executor::monitor::AgeMonitorSpec],
142        epoch_us: Option<fn() -> u64>,
143    ) {
144        self.age_monitors = table;
145        self.epoch_us_fn = epoch_us;
146    }
147
148    /// The installed QoS-override table (empty unless the entry set one).
149    #[must_use]
150    pub fn qos_overrides(&self) -> &'static [nros_rmw::QoSOverride] {
151        self.qos_overrides
152    }
153
154    /// Get the node name.
155    pub fn name(&self) -> &str {
156        &self.name
157    }
158
159    /// RFC-0088 — the serialization format this node's entities speak.
160    ///
161    /// The counterpart to ROS 2's `rmw_get_serialization_format()`, asked of
162    /// the node rather than the process: an `Executor::open_multi` image holds
163    /// two sessions and has no single answer, which is the case the reserved
164    /// vtable slot was cut for.
165    ///
166    /// A single-backend image already knows this at compile time — see
167    /// [`crate::session::IMAGE_SERIALIZATION_FORMAT`], which is what the
168    /// entity-creation assertions compare against. Use this accessor when the
169    /// answer must be a value: a bridge, a diagnostic, a tool.
170    ///
171    /// **`None` means the backend has not said** — the same discipline the C
172    /// entry point `rclc_node_get_serialization_format` has always kept by
173    /// returning NULL, and the one `rmw_vtable.h` states for the slot
174    /// underneath. It is not a synonym for `"cdr"`.
175    #[must_use]
176    pub fn serialization_format(&self) -> Option<&'static str> {
177        nros_rmw::Session::serialization_format(&*self.session)
178    }
179
180    /// The RMW backend behind this node, by name — the counterpart to ROS 2's
181    /// `rmw_get_implementation_identifier()` (rclpy's
182    /// `get_rmw_implementation_identifier()`), asked of the node because an
183    /// `Executor::open_multi` image holds two sessions.
184    ///
185    /// The nano-ros backend name (`"zenoh"`, `"cyclonedds"`, `"xrce"`,
186    /// `"uorb"`), never a stock `rmw_*_cpp` name: a zenoh image speaks
187    /// `rmw_zenoh_cpp`'s wire but is not that implementation, and code that
188    /// branches on the identifier is asking about the implementation. A
189    /// backend reached through the C ABI that states no name answers the name
190    /// it was registered under. `None` = the backend has not said.
191    #[must_use]
192    pub fn rmw_implementation_identifier(&self) -> Option<&'static str> {
193        nros_rmw::Session::implementation_identifier(&*self.session)
194    }
195
196    /// Phase 88.12 — return the [`nros_log::Logger`] NAMED FOR THIS NODE.
197    ///
198    /// Loggers are interned in nros-log's bounded global table
199    /// ([`nros_log::MAX_LOGGERS`] slots). If the caller has
200    /// pre-registered a `'static Logger` whose name matches this
201    /// node's name (via [`nros_log::register_logger`]), this method
202    /// returns that exact reference — so subsequent `nros_*!` calls
203    /// share per-logger runtime threshold state with any other call
204    /// site that resolves the same name.
205    ///
206    /// phase-427 W5 — otherwise one is CREATED under the node's name, in
207    /// nros-log's bounded arena. It used to answer
208    /// [`nros_log::DEFAULT_LOGGER`] instead, which kept the API total by
209    /// making every node in an image share the name `"nros"`: no record could
210    /// say which node emitted it, and `set_level` on one node's handle moved
211    /// every other node's bar with it. [`nros_log::resolve_logger`] is the one
212    /// spelling of this resolution; the catch-all remains the fallback for an
213    /// exhausted arena, and it reports itself when it fires.
214    ///
215    /// ```ignore
216    /// // Pre-register if you want a `'static` logger you can also name
217    /// // from elsewhere in the image:
218    /// static MY_NODE_LOGGER: nros_log::Logger =
219    ///     nros_log::Logger::new("my_node");
220    /// nros_log::register_logger(&MY_NODE_LOGGER);
221    ///
222    /// // Inside any node-creating code:
223    /// let logger = node.logger();
224    /// nros_log::log_info!(logger, "started; domain = {}", node.domain_id());
225    /// ```
226    #[must_use]
227    pub fn logger(&self) -> &'static nros_log::Logger {
228        nros_log::resolve_node_logger(self.name())
229    }
230
231    /// Get the domain ID.
232    pub fn domain_id(&self) -> u32 {
233        self.domain_id
234    }
235
236    /// Set the domain ID.
237    pub fn set_domain_id(&mut self, domain_id: u32) {
238        self.domain_id = domain_id;
239    }
240
241    /// Get a mutable reference to the underlying session.
242    pub fn session_mut(&mut self) -> &mut session::ConcreteSession {
243        self.session
244    }
245
246    // ------------------------------------------------------------------
247    // Routing-info builders (Phase 91.F)
248    //
249    // Every `create_*` below threads the same node identity (domain_id +
250    // name + namespace) into a TopicInfo / ServiceInfo / ActionInfo. The
251    // shape repeats verbatim ~12 times across this file. Centralised
252    // here so a future change to the routing-info shape (e.g. adding a
253    // security context) updates one site instead of twelve, and
254    // so the per-`create_*` function bodies focus on the parts that
255    // actually differ between them.
256    // ------------------------------------------------------------------
257
258    // Associated fns (NOT `&self` methods) so the returned `*Info`
259    // value's borrow tracks only the explicit `&str` arguments, not
260    // the whole `Node`. A `&self` form would block the immediately-
261    // following `self.session.create_*(&info, …)` mut borrow on the
262    // `name` / `namespace` reborrow held inside the returned `*Info`,
263    // because going through a method call hides the field-disjoint
264    // path that lets `&self.name` + `&mut self.session` coexist.
265    fn topic_info<'b>(
266        domain_id: u32,
267        node_name: &'b str,
268        namespace: &'b str,
269        topic_name: &'b str,
270        type_name: &'b str,
271        type_hash: &'b str,
272    ) -> TopicInfo<'b> {
273        TopicInfo::new(topic_name, type_name, type_hash)
274            .with_domain(domain_id)
275            .with_node_name(node_name)
276            .with_namespace(namespace)
277    }
278
279    fn service_info<'b>(
280        domain_id: u32,
281        node_name: &'b str,
282        namespace: &'b str,
283        service_name: &'b str,
284        type_name: &'b str,
285        type_hash: &'b str,
286    ) -> ServiceInfo<'b> {
287        ServiceInfo::new(service_name, type_name, type_hash)
288            .with_domain(domain_id)
289            .with_node_name(node_name)
290            .with_namespace(namespace)
291    }
292
293    fn action_info<'b>(
294        domain_id: u32,
295        action_name: &'b str,
296        type_name: &'b str,
297        type_hash: &'b str,
298    ) -> ActionInfo<'b> {
299        // Action root only needs the domain — per-channel ServiceInfo /
300        // TopicInfo derived from action_info.{send_goal,cancel_goal,...}_key()
301        // carry the full node identity via service_info() / topic_info().
302        ActionInfo::new(action_name, type_name, type_hash).with_domain(domain_id)
303    }
304
305    /// The name a rule in either tier sends `source` to, or `None` when no
306    /// rule matches — in which case the caller passes `source` through
307    /// UNCHANGED, exactly as this handle always has.
308    ///
309    /// Launch rules (`Executor::declare_remap`) are authoritative and
310    /// `--ros-args` rules are the fallback, through
311    /// [`crate::names::remap_layered`] — the same two tiers
312    /// `Executor::resolve_entity_name_for` gives the C and C++ roads and
313    /// `ExecutorSink` gives Rust components.
314    ///
315    /// Substitution-only, deliberately. This handle has never expanded a name
316    /// (`~`/relative) before handing it to the backend, and doing so now for
317    /// every entity would move wire names nobody asked to move. A matched rule
318    /// yields a fully-qualified name, which is what every other road hands the
319    /// backend too.
320    ///
321    /// Before this the handle consulted no remap at all: a rule declared on
322    /// the executor reached C, C++ and component entities and silently missed
323    /// every entity created through `Executor::create_node`'s handle.
324    fn remapped(&self, source: &str) -> Option<crate::names::ResolvedName> {
325        crate::names::remap_layered(
326            source,
327            &self.name,
328            &self.namespace,
329            self.remaps.launch_rules(&self.name, &self.namespace),
330            self.remaps.argv_rules(&self.name, &self.namespace),
331        )
332    }
333
334    // -- Publishers --
335
336    /// Create a publisher for the given topic.
337    pub fn create_publisher<M: MessageForRmw>(
338        &mut self,
339        topic_name: &str,
340    ) -> Result<EmbeddedPublisher<M>, NodeError> {
341        self.create_publisher_with_qos::<M>(topic_name, QoSProfile::default())
342    }
343
344    /// Create a publisher with custom QoS settings.
345    pub fn create_publisher_with_qos<M: MessageForRmw>(
346        &mut self,
347        topic_name: &str,
348        qos: QoSProfile,
349    ) -> Result<EmbeddedPublisher<M>, NodeError> {
350        // RFC-0088 / phase-421 W1 — the message's declared format must be the
351        // one the linked backend speaks. Universal: the const lives on
352        // `RosMessage`, which `MessageForRmw` requires under every backend.
353        crate::format_check::assert_message_format::<M>();
354        // Phase 212.K.7.6.b — under `rmw-cyclonedds`, ensure the runtime
355        // type-descriptor exists before the cffi vtable creates the
356        // entity. No-op for other RMWs.
357        register_type::<M>()?;
358        // Phase 211.H — fold any plan qos_overrides for this topic+publisher
359        // into the profile (setup-time, no alloc) BEFORE validation, so an
360        // override the backend can't honour still errors loudly below.
361        let qos = qos.apply_overrides(
362            topic_name,
363            nros_rmw::QoSOverrideRole::Publisher,
364            self.qos_overrides,
365        );
366        // Phase 108.B — synchronous QoS validation against backend's
367        // `supported_qos_policies()` mask. No silent downgrade.
368        let remapped = self.remapped(topic_name);
369        let topic = Self::topic_info(
370            self.domain_id,
371            &self.name,
372            &self.namespace,
373            remapped.as_deref().unwrap_or(topic_name),
374            <M as RosMessage>::TYPE_NAME,
375            <M as RosMessage>::TYPE_HASH,
376        );
377        // issue 1608 -- the publisher half of the declared-QoS check. BEFORE
378        // validation, so a policy the contract raises this publisher to is
379        // one the backend is asked whether it can honour.
380        let qos = crate::declared_qos::honour_publisher(topic.type_name, topic.name, qos)?;
381        validate_qos_or_report(
382            &qos,
383            nros_rmw::Session::supported_qos_policies(self.session),
384            "publisher",
385            topic_name,
386        )?;
387        let handle = self
388            .session
389            .create_publisher(&topic, qos)
390            .map_err(|_| NodeError::Transport(TransportError::PublisherCreationFailed))?;
391        // RFC-0052 W3b.4 — attach the contracted endpoint's counter cell
392        // (exact topic-name match against the baked table; None = free).
393        let monitor = self
394            .monitors
395            .iter()
396            .find(|m| m.topic == topic_name)
397            .map(|m| m.cell);
398        Ok(EmbeddedPublisher {
399            handle,
400            event_regs: crate::executor::handles::empty_event_regs(),
401            monitor,
402            epoch: self.epoch_us_fn,
403            _phantom: PhantomData,
404        })
405    }
406
407    /// Create a typeless publisher for non-ROS wire formats (e.g. PX4 uORB
408    /// raw POD bytes, custom binary protocols). The caller supplies the
409    /// `type_name` and `type_hash` strings used by backends that need them
410    /// for liveliness/discovery; backends that don't (uORB) can pass any
411    /// stable string.
412    pub fn create_publisher_raw(
413        &mut self,
414        topic_name: &str,
415        type_name: &str,
416        type_hash: &str,
417    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
418        self.create_publisher_raw_with_qos(topic_name, type_name, type_hash, QoSProfile::default())
419    }
420
421    /// Typeless publisher with custom QoS.
422    pub fn create_publisher_raw_with_qos(
423        &mut self,
424        topic_name: &str,
425        type_name: &str,
426        type_hash: &str,
427        qos: QoSProfile,
428    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
429        // Phase 211.H — apply plan qos_overrides (publisher side) before validate.
430        let qos = qos.apply_overrides(
431            topic_name,
432            nros_rmw::QoSOverrideRole::Publisher,
433            self.qos_overrides,
434        );
435        let remapped = self.remapped(topic_name);
436        let topic = Self::topic_info(
437            self.domain_id,
438            &self.name,
439            &self.namespace,
440            remapped.as_deref().unwrap_or(topic_name),
441            type_name,
442            type_hash,
443        );
444        // issue 1608 -- see `create_publisher_with_qos`.
445        let qos = crate::declared_qos::honour_publisher(topic.type_name, topic.name, qos)?;
446        validate_qos_or_report(
447            &qos,
448            nros_rmw::Session::supported_qos_policies(self.session),
449            "publisher",
450            topic_name,
451        )?;
452        let handle = self
453            .session
454            .create_publisher(&topic, qos)
455            .map_err(|_| NodeError::Transport(TransportError::PublisherCreationFailed))?;
456        Ok(crate::executor::handles::EmbeddedRawPublisher {
457            handle,
458            arena: crate::executor::handles::TxArena::new(),
459            event_regs: crate::executor::handles::empty_event_regs(),
460            // Issue 1676 — the typed path's rule, verbatim.
461            monitor: self
462                .monitors
463                .iter()
464                .find(|m| m.topic == topic_name)
465                .map(|m| m.cell),
466        })
467    }
468
469    /// Phase 189.M1 — the customizable publisher **builder** (the `clone` tier;
470    /// see `docs/design/0022-entity-api-tiers.md`). Pick a mode with `.typed::<M>()`
471    /// or `.generic(type, hash)`, set knobs (`.qos`), then `.build()`. The
472    /// convenient `create_publisher` / `create_publisher_raw` are the `fork`
473    /// tier — sugar over this with defaults.
474    pub fn publisher<'t>(&mut self, topic: &'t str) -> PublisherBuilder<'_, 'a, 't> {
475        PublisherBuilder {
476            node: self,
477            topic,
478            qos: QoSProfile::default(),
479        }
480    }
481
482    // -- Subscriptions --
483
484    /// Create a subscription for the given topic.
485    pub fn create_subscription<M: MessageForRmw>(
486        &mut self,
487        topic_name: &str,
488    ) -> Result<Subscription<M>, NodeError> {
489        self.create_subscription_sized::<M, { crate::config::DEFAULT_RX_BUF_SIZE }>(topic_name)
490    }
491
492    /// Create a subscription with custom buffer size.
493    pub fn create_subscription_sized<M: MessageForRmw, const RX_BUF: usize>(
494        &mut self,
495        topic_name: &str,
496    ) -> Result<Subscription<M, RX_BUF>, NodeError> {
497        self.create_subscription_with_qos::<M, RX_BUF>(topic_name, QoSProfile::default())
498    }
499
500    /// Create a subscription with custom QoS and buffer size.
501    pub fn create_subscription_with_qos<M: MessageForRmw, const RX_BUF: usize>(
502        &mut self,
503        topic_name: &str,
504        qos: QoSProfile,
505    ) -> Result<Subscription<M, RX_BUF>, NodeError> {
506        // RFC-0088 / phase-421 W1 — the message's declared format must be the
507        // one the linked backend speaks. Universal: the const lives on
508        // `RosMessage`, which `MessageForRmw` requires under every backend.
509        crate::format_check::assert_message_format::<M>();
510        // Phase 212.K.7.6.b — see `create_publisher_with_qos`.
511        register_type::<M>()?;
512        // Phase 211.H — apply plan qos_overrides (subscription side) before validate.
513        let qos = qos.apply_overrides(
514            topic_name,
515            nros_rmw::QoSOverrideRole::Subscription,
516            self.qos_overrides,
517        );
518        validate_qos_or_report(
519            &qos,
520            nros_rmw::Session::supported_qos_policies(self.session),
521            "subscription",
522            topic_name,
523        )?;
524        let remapped = self.remapped(topic_name);
525        let topic = Self::topic_info(
526            self.domain_id,
527            &self.name,
528            &self.namespace,
529            remapped.as_deref().unwrap_or(topic_name),
530            <M as RosMessage>::TYPE_NAME,
531            <M as RosMessage>::TYPE_HASH,
532        );
533        // phase-454 W10/W13 — see the same line in `executor/spin.rs`.
534        let qos = crate::declared_qos::honour(topic.type_name, topic.name, qos)?;
535        // phase-519 W3b — and the declared arrival rate, likewise.
536        let topic = crate::declared_qos::with_declared_arrival_rate(topic);
537        let handle = self
538            .session
539            .create_subscription(&topic, qos)
540            .map_err(NodeError::Transport)?;
541        // W3b.5 — attach the contracted endpoint's age cell (stamped
542        // types only). Issue 2215: with no epoch source the take is still
543        // recorded, unaged.
544        let age_mon = match <M as RosMessage>::STAMP_OFFSET {
545            Some(_) => self
546                .age_monitors
547                .iter()
548                .find(|a| a.topic == topic_name)
549                .map(|a| (a.cell, self.epoch_us_fn)),
550            None => None,
551        };
552        Ok(Subscription {
553            handle,
554            buffer: [0u8; RX_BUF],
555            event_regs: crate::executor::handles::empty_event_regs(),
556            age_mon,
557            _phantom: PhantomData,
558        })
559    }
560
561    /// Create a typeless subscription. Caller decodes raw bytes themselves.
562    pub fn create_subscription_raw(
563        &mut self,
564        topic_name: &str,
565        type_name: &str,
566        type_hash: &str,
567    ) -> Result<crate::executor::handles::RawSubscription, NodeError> {
568        self.create_subscription_raw_sized::<{ crate::config::DEFAULT_RX_BUF_SIZE }>(
569            topic_name, type_name, type_hash,
570        )
571    }
572
573    /// Typeless subscription with custom buffer size.
574    pub fn create_subscription_raw_sized<const RX_BUF: usize>(
575        &mut self,
576        topic_name: &str,
577        type_name: &str,
578        type_hash: &str,
579    ) -> Result<crate::executor::handles::RawSubscription<RX_BUF>, NodeError> {
580        // Phase 211.H — apply plan qos_overrides (subscription side) before
581        // validate, mirroring `create_publisher_raw_with_qos`. The raw entity
582        // paths honour node overrides exactly like the typed ones — an
583        // override the active RMW can't meet errors loudly, never silently.
584        let qos = QoSProfile::default().apply_overrides(
585            topic_name,
586            nros_rmw::QoSOverrideRole::Subscription,
587            self.qos_overrides,
588        );
589        validate_qos_or_report(
590            &qos,
591            nros_rmw::Session::supported_qos_policies(self.session),
592            "subscription",
593            topic_name,
594        )?;
595        let remapped = self.remapped(topic_name);
596        let topic = Self::topic_info(
597            self.domain_id,
598            &self.name,
599            &self.namespace,
600            remapped.as_deref().unwrap_or(topic_name),
601            type_name,
602            type_hash,
603        );
604        // phase-454 W10/W13 — the raw path costs the arena exactly what
605        // the typed one does, so leaving it out would make the declared depth
606        // enforceable everywhere except in the images that decode bytes
607        // themselves.
608        let qos = crate::declared_qos::honour(topic.type_name, topic.name, qos)?;
609        // phase-519 W3b — and the declared arrival rate, likewise.
610        let topic = crate::declared_qos::with_declared_arrival_rate(topic);
611        let handle = self
612            .session
613            .create_subscription(&topic, qos)
614            .map_err(NodeError::Transport)?;
615        Ok(crate::executor::handles::RawSubscription {
616            handle,
617            buffer: [0u8; RX_BUF],
618            event_regs: crate::executor::handles::empty_event_regs(),
619        })
620    }
621
622    // -- Services --
623
624    /// Create a service server.
625    pub fn create_service<Svc: RosService>(
626        &mut self,
627        service_name: &str,
628    ) -> Result<EmbeddedServiceServer<Svc>, NodeError>
629    where
630        Svc::Request: MessageForRmw,
631        Svc::Reply: MessageForRmw,
632    {
633        self.create_service_sized::<Svc, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }>(service_name, QoSProfile::services_default())
634    }
635
636    /// Phase 193.2b — service server with an explicit QoS profile (applied to
637    /// both the request + reply endpoints; rclcpp's `create_service(name, qos)`).
638    pub fn create_service_with_qos<Svc: RosService>(
639        &mut self,
640        service_name: &str,
641        qos: QoSProfile,
642    ) -> Result<EmbeddedServiceServer<Svc>, NodeError>
643    where
644        Svc::Request: MessageForRmw,
645        Svc::Reply: MessageForRmw,
646    {
647        self.create_service_sized::<Svc, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }>(service_name, qos)
648    }
649
650    /// Create a service server with custom buffer sizes + QoS.
651    pub fn create_service_sized<Svc: RosService, const REQ_BUF: usize, const REPLY_BUF: usize>(
652        &mut self,
653        service_name: &str,
654        qos: QoSProfile,
655    ) -> Result<EmbeddedServiceServer<Svc, REQ_BUF, REPLY_BUF>, NodeError>
656    where
657        Svc::Request: MessageForRmw,
658        Svc::Reply: MessageForRmw,
659    {
660        // Phase 212.K.7.6.b — register both halves of the service round-trip
661        // under cyclonedds. No-op for other RMWs.
662        register_type::<Svc::Request>()?;
663        register_type::<Svc::Reply>()?;
664        // Phase 193.5 — validate the service profile against the backend's
665        // supported policies (mirrors pub/sub); no silent downgrade. RELIABLE is
666        // effectively required for request/reply, so a backend that only honours
667        // a fixed profile rejects an incompatible request here.
668        validate_qos_or_report(
669            &qos,
670            nros_rmw::Session::supported_qos_policies(self.session),
671            "service",
672            service_name,
673        )?;
674        let remapped = self.remapped(service_name);
675        let info = Self::service_info(
676            self.domain_id,
677            &self.name,
678            &self.namespace,
679            remapped.as_deref().unwrap_or(service_name),
680            Svc::SERVICE_NAME,
681            Svc::SERVICE_HASH,
682        );
683        let handle = self
684            .session
685            .create_service(&info, qos)
686            .map_err(NodeError::Transport)?;
687        Ok(EmbeddedServiceServer {
688            handle,
689            req_buffer: [0u8; REQ_BUF],
690            reply_buffer: [0u8; REPLY_BUF],
691            _phantom: PhantomData,
692        })
693    }
694
695    /// Create a service client.
696    pub fn create_client<Svc: RosService>(
697        &mut self,
698        service_name: &str,
699    ) -> Result<EmbeddedServiceClient<Svc>, NodeError>
700    where
701        Svc::Request: MessageForRmw,
702        Svc::Reply: MessageForRmw,
703    {
704        self.create_client_sized::<Svc, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }>(service_name, QoSProfile::services_default())
705    }
706
707    /// Phase 193.2b — service client with an explicit QoS profile.
708    pub fn create_client_with_qos<Svc: RosService>(
709        &mut self,
710        service_name: &str,
711        qos: QoSProfile,
712    ) -> Result<EmbeddedServiceClient<Svc>, NodeError>
713    where
714        Svc::Request: MessageForRmw,
715        Svc::Reply: MessageForRmw,
716    {
717        self.create_client_sized::<Svc, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }>(service_name, qos)
718    }
719
720    /// Create a service client with custom buffer sizes + QoS.
721    pub fn create_client_sized<Svc: RosService, const REQ_BUF: usize, const REPLY_BUF: usize>(
722        &mut self,
723        service_name: &str,
724        qos: QoSProfile,
725    ) -> Result<EmbeddedServiceClient<Svc, REQ_BUF, REPLY_BUF>, NodeError>
726    where
727        Svc::Request: MessageForRmw,
728        Svc::Reply: MessageForRmw,
729    {
730        // Phase 212.K.7.6.b — see `create_service_sized`.
731        register_type::<Svc::Request>()?;
732        register_type::<Svc::Reply>()?;
733        // Phase 193.5 — validate against the backend's supported policies (no
734        // silent downgrade); request/reply effectively requires RELIABLE.
735        validate_qos_or_report(
736            &qos,
737            nros_rmw::Session::supported_qos_policies(self.session),
738            "client",
739            service_name,
740        )?;
741        let remapped = self.remapped(service_name);
742        let info = Self::service_info(
743            self.domain_id,
744            &self.name,
745            &self.namespace,
746            remapped.as_deref().unwrap_or(service_name),
747            Svc::SERVICE_NAME,
748            Svc::SERVICE_HASH,
749        );
750        let handle = self
751            .session
752            .create_client(&info, qos)
753            .map_err(|_| NodeError::Transport(TransportError::ServiceClientCreationFailed))?;
754        Ok(EmbeddedServiceClient {
755            handle,
756            req_buffer: [0u8; REQ_BUF],
757            reply_buffer: [0u8; REPLY_BUF],
758            in_flight: false,
759            pending: nros_rmw::RequestCorrelator::new(),
760            _phantom: PhantomData,
761        })
762    }
763
764    /// Typeless service server. L1 counterpart of [`Self::create_service`]
765    /// for the C / C++ FFI shims and callers that own their own
766    /// scheduler. Returns a [`crate::executor::handles::RawServiceServer`]
767    /// which polls request bytes directly.
768    pub fn create_service_raw(
769        &mut self,
770        service_name: &str,
771        type_name: &str,
772        type_hash: &str,
773    ) -> Result<crate::executor::handles::RawServiceServer, NodeError> {
774        self.create_service_raw_sized::<
775            { crate::config::DEFAULT_RX_BUF_SIZE },
776            { crate::config::DEFAULT_RX_BUF_SIZE },
777        >(service_name, type_name, type_hash)
778    }
779
780    /// Typeless service server with custom buffer sizes.
781    pub fn create_service_raw_sized<const REQ_BUF: usize, const RESP_BUF: usize>(
782        &mut self,
783        service_name: &str,
784        type_name: &str,
785        type_hash: &str,
786    ) -> Result<crate::executor::handles::RawServiceServer<REQ_BUF, RESP_BUF>, NodeError> {
787        let remapped = self.remapped(service_name);
788        let info = Self::service_info(
789            self.domain_id,
790            &self.name,
791            &self.namespace,
792            remapped.as_deref().unwrap_or(service_name),
793            type_name,
794            type_hash,
795        );
796        let handle = self
797            .session
798            .create_service(&info, QoSProfile::services_default())
799            .map_err(NodeError::Transport)?;
800        Ok(crate::executor::handles::RawServiceServer::new(handle))
801    }
802
803    /// Typeless service client. L1 counterpart of [`Self::create_client`].
804    pub fn create_client_raw(
805        &mut self,
806        service_name: &str,
807        type_name: &str,
808        type_hash: &str,
809    ) -> Result<crate::executor::handles::RawServiceClient, NodeError> {
810        self.create_client_raw_sized::<
811            { crate::config::DEFAULT_RX_BUF_SIZE },
812            { crate::config::DEFAULT_RX_BUF_SIZE },
813        >(service_name, type_name, type_hash)
814    }
815
816    /// Typeless service client with custom buffer sizes.
817    pub fn create_client_raw_sized<const REQ_BUF: usize, const REPLY_BUF: usize>(
818        &mut self,
819        service_name: &str,
820        type_name: &str,
821        type_hash: &str,
822    ) -> Result<crate::executor::handles::RawServiceClient<REQ_BUF, REPLY_BUF>, NodeError> {
823        let remapped = self.remapped(service_name);
824        let info = Self::service_info(
825            self.domain_id,
826            &self.name,
827            &self.namespace,
828            remapped.as_deref().unwrap_or(service_name),
829            type_name,
830            type_hash,
831        );
832        let handle = self
833            .session
834            .create_client(&info, QoSProfile::services_default())
835            .map_err(|_| NodeError::Transport(TransportError::ServiceClientCreationFailed))?;
836        Ok(crate::executor::handles::RawServiceClient::new(handle))
837    }
838
839    // -- Actions --
840
841    /// Phase 122.3.c.6 — typeless action server. Builds the 5
842    /// transport channels (`send_goal` / `cancel_goal` / `get_result`
843    /// services + `feedback` / `status` publishers) and returns the
844    /// raw `ActionServerCore` directly. Caller owns scheduling —
845    /// drives `try_recv_goal_request` / `publish_feedback_raw` /
846    /// `complete_goal_raw` / `try_handle_cancel` /
847    /// `try_handle_get_result_raw` on the returned core.
848    pub fn create_action_server_raw(
849        &mut self,
850        action_name: &str,
851        type_name: &str,
852        type_hash: &str,
853    ) -> Result<
854        super::action_core::ActionServerCore<
855            { crate::config::DEFAULT_RX_BUF_SIZE },
856            { crate::config::DEFAULT_RX_BUF_SIZE },
857            { crate::config::DEFAULT_RX_BUF_SIZE },
858            4,
859        >,
860        NodeError,
861    > {
862        self.create_action_server_raw_sized::<
863            { crate::config::DEFAULT_RX_BUF_SIZE },
864            { crate::config::DEFAULT_RX_BUF_SIZE },
865            { crate::config::DEFAULT_RX_BUF_SIZE },
866            4,
867        >(action_name, type_name, type_hash)
868    }
869
870    /// Typeless action server with custom buffer + goal-slot sizes.
871    pub fn create_action_server_raw_sized<
872        const GOAL_BUF: usize,
873        const RESULT_BUF: usize,
874        const FEEDBACK_BUF: usize,
875        const MAX_GOALS: usize,
876    >(
877        &mut self,
878        action_name: &str,
879        type_name: &str,
880        type_hash: &str,
881    ) -> Result<
882        super::action_core::ActionServerCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
883        NodeError,
884    > {
885        let remapped = self.remapped(action_name);
886        let action_name = remapped.as_deref().unwrap_or(action_name);
887        let action_info = Self::action_info(self.domain_id, action_name, type_name, type_hash);
888
889        // Issue 0454 / phase-354 W3 — per-CHANNEL DDS type, not the bare
890        // action type (see `action_channel_type`). These two `_raw_sized`
891        // constructors are the last holders of the phase-338 W3 defect: the
892        // type name is baked into the keyexpr, so `…Fibonacci_` never matches a
893        // peer advertising `…Fibonacci_SendGoal_`.
894        let send_goal_type: heapless::String<256> =
895            super::action_core::action_channel_type(type_name, "SendGoal");
896        let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
897        let send_goal_info = Self::service_info(
898            self.domain_id,
899            &self.name,
900            &self.namespace,
901            &send_goal_keyexpr,
902            &send_goal_type,
903            type_hash,
904        );
905        let send_goal_server = self
906            .session
907            .create_service(&send_goal_info, QoSProfile::services_default())
908            .map_err(|_| NodeError::ActionCreationFailed)?;
909
910        let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
911        let cancel_goal_info = Self::service_info(
912            self.domain_id,
913            &self.name,
914            &self.namespace,
915            &cancel_goal_keyexpr,
916            "action_msgs::srv::dds_::CancelGoal_",
917            type_hash,
918        );
919        let cancel_goal_server = self
920            .session
921            .create_service(&cancel_goal_info, QoSProfile::services_default())
922            .map_err(|_| NodeError::ActionCreationFailed)?;
923
924        let get_result_type: heapless::String<256> =
925            super::action_core::action_channel_type(type_name, "GetResult");
926        let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
927        let get_result_info = Self::service_info(
928            self.domain_id,
929            &self.name,
930            &self.namespace,
931            &get_result_keyexpr,
932            &get_result_type,
933            type_hash,
934        );
935        let get_result_server = self
936            .session
937            .create_service(&get_result_info, QoSProfile::services_default())
938            .map_err(|_| NodeError::ActionCreationFailed)?;
939
940        let feedback_type: heapless::String<256> =
941            super::action_core::action_channel_type(type_name, "FeedbackMessage");
942        let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
943        let feedback_topic = Self::topic_info(
944            self.domain_id,
945            &self.name,
946            &self.namespace,
947            &feedback_keyexpr,
948            &feedback_type,
949            type_hash,
950        );
951        let feedback_publisher = self
952            .session
953            .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
954            .map_err(|_| NodeError::ActionCreationFailed)?;
955
956        let status_keyexpr: heapless::String<256> = action_info.status_key();
957        let status_topic = Self::topic_info(
958            self.domain_id,
959            &self.name,
960            &self.namespace,
961            &status_keyexpr,
962            "action_msgs::msg::dds_::GoalStatusArray_",
963            type_hash,
964        );
965        let status_publisher = self
966            .session
967            .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
968            .map_err(|_| NodeError::ActionCreationFailed)?;
969
970        Ok(super::action_core::ActionServerCore {
971            send_goal_server,
972            cancel_goal_server,
973            get_result_server,
974            feedback_publisher,
975            status_publisher,
976            active_goals: heapless::Vec::new(),
977            completed_results: heapless::Vec::new(),
978            pending_get_results: heapless::Vec::new(),
979            result_slab: [0u8; RESULT_BUF],
980            result_slab_used: 0,
981            goal_buffer: [0u8; GOAL_BUF],
982            feedback_buffer: [0u8; FEEDBACK_BUF],
983            cancel_buffer: [0u8; 256],
984        })
985    }
986
987    /// Phase 122.3.c.6 — typeless action client. Same shape as
988    /// `create_action_server_raw` but builds the 3 service clients
989    /// + 1 feedback subscriber, returns the raw `ActionClientCore`.
990    pub fn create_action_client_raw(
991        &mut self,
992        action_name: &str,
993        type_name: &str,
994        type_hash: &str,
995    ) -> Result<
996        super::action_core::ActionClientCore<
997            { crate::config::DEFAULT_RX_BUF_SIZE },
998            { crate::config::DEFAULT_RX_BUF_SIZE },
999            { crate::config::DEFAULT_RX_BUF_SIZE },
1000        >,
1001        NodeError,
1002    > {
1003        self.create_action_client_raw_sized::<
1004            { crate::config::DEFAULT_RX_BUF_SIZE },
1005            { crate::config::DEFAULT_RX_BUF_SIZE },
1006            { crate::config::DEFAULT_RX_BUF_SIZE },
1007        >(action_name, type_name, type_hash)
1008    }
1009
1010    /// Typeless action client with custom buffer sizes.
1011    pub fn create_action_client_raw_sized<
1012        const GOAL_BUF: usize,
1013        const RESULT_BUF: usize,
1014        const FEEDBACK_BUF: usize,
1015    >(
1016        &mut self,
1017        action_name: &str,
1018        type_name: &str,
1019        type_hash: &str,
1020    ) -> Result<super::action_core::ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, NodeError>
1021    {
1022        let remapped = self.remapped(action_name);
1023        let action_name = remapped.as_deref().unwrap_or(action_name);
1024        let action_info = Self::action_info(self.domain_id, action_name, type_name, type_hash);
1025
1026        // Issue 0454 / phase-354 W3 — per-CHANNEL DDS type, not the bare
1027        // action type (see `action_channel_type`). These two `_raw_sized`
1028        // constructors are the last holders of the phase-338 W3 defect: the
1029        // type name is baked into the keyexpr, so `…Fibonacci_` never matches a
1030        // peer advertising `…Fibonacci_SendGoal_`.
1031        let send_goal_type: heapless::String<256> =
1032            super::action_core::action_channel_type(type_name, "SendGoal");
1033        let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1034        let send_goal_info = Self::service_info(
1035            self.domain_id,
1036            &self.name,
1037            &self.namespace,
1038            &send_goal_keyexpr,
1039            &send_goal_type,
1040            type_hash,
1041        );
1042        let send_goal_client = self
1043            .session
1044            .create_client(&send_goal_info, QoSProfile::services_default())
1045            .map_err(|_| NodeError::ActionCreationFailed)?;
1046
1047        let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1048        let cancel_goal_info = Self::service_info(
1049            self.domain_id,
1050            &self.name,
1051            &self.namespace,
1052            &cancel_goal_keyexpr,
1053            "action_msgs::srv::dds_::CancelGoal_",
1054            type_hash,
1055        );
1056        let cancel_goal_client = self
1057            .session
1058            .create_client(&cancel_goal_info, QoSProfile::services_default())
1059            .map_err(|_| NodeError::ActionCreationFailed)?;
1060
1061        let get_result_type: heapless::String<256> =
1062            super::action_core::action_channel_type(type_name, "GetResult");
1063        let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1064        let get_result_info = Self::service_info(
1065            self.domain_id,
1066            &self.name,
1067            &self.namespace,
1068            &get_result_keyexpr,
1069            &get_result_type,
1070            type_hash,
1071        );
1072        let get_result_client = self
1073            .session
1074            .create_client(&get_result_info, QoSProfile::services_default())
1075            .map_err(|_| NodeError::ActionCreationFailed)?;
1076
1077        let feedback_type: heapless::String<256> =
1078            super::action_core::action_channel_type(type_name, "FeedbackMessage");
1079        let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1080        let feedback_topic = Self::topic_info(
1081            self.domain_id,
1082            &self.name,
1083            &self.namespace,
1084            &feedback_keyexpr,
1085            &feedback_type,
1086            type_hash,
1087        );
1088        let feedback_subscriber = self
1089            .session
1090            .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1091            .map_err(|_| NodeError::ActionCreationFailed)?;
1092
1093        Ok(super::action_core::ActionClientCore::new(
1094            send_goal_client,
1095            cancel_goal_client,
1096            get_result_client,
1097            feedback_subscriber,
1098        ))
1099    }
1100
1101    /// Create an action server.
1102    pub fn create_action_server<A: RosAction>(
1103        &mut self,
1104        action_name: &str,
1105    ) -> Result<ActionServer<A>, NodeError>
1106    where
1107        A::Goal: MessageForRmw,
1108        A::Result: MessageForRmw,
1109        A::Feedback: MessageForRmw,
1110        A::SendGoalRequest: MessageForRmw,
1111        A::SendGoalResponse: MessageForRmw,
1112        A::GetResultRequest: MessageForRmw,
1113        A::GetResultResponse: MessageForRmw,
1114        A::FeedbackMessage: MessageForRmw,
1115    {
1116        self.create_action_server_sized::<A, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, 4>(action_name)
1117    }
1118
1119    /// Create an action server with custom buffer sizes.
1120    pub fn create_action_server_sized<
1121        A: RosAction,
1122        const GOAL_BUF: usize,
1123        const RESULT_BUF: usize,
1124        const FEEDBACK_BUF: usize,
1125        const MAX_GOALS: usize,
1126    >(
1127        &mut self,
1128        action_name: &str,
1129    ) -> Result<ActionServer<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>, NodeError>
1130    where
1131        A::Goal: MessageForRmw,
1132        A::Result: MessageForRmw,
1133        A::Feedback: MessageForRmw,
1134        A::SendGoalRequest: MessageForRmw,
1135        A::SendGoalResponse: MessageForRmw,
1136        A::GetResultRequest: MessageForRmw,
1137        A::GetResultResponse: MessageForRmw,
1138        A::FeedbackMessage: MessageForRmw,
1139    {
1140        // Phase 212.K.7.6.b + K.7.7.c — register the three user-facing
1141        // message types AND the five action-protocol envelope types under
1142        // cyclonedds. No-op for other RMWs. The envelopes are needed
1143        // because the action service shapes (`*_SendGoal_Request`,
1144        // `*_GetResult_Response`, …) are the actual on-wire CDR types,
1145        // and the C++ Cyclone bridge auto-prepends a cdds_request_header_t
1146        // for any TYPE_NAME ending `_Request`/`_Response`/`_Reply`.
1147        register_type::<A::Goal>()?;
1148        register_type::<A::Result>()?;
1149        register_type::<A::Feedback>()?;
1150        register_type::<A::SendGoalRequest>()?;
1151        register_type::<A::SendGoalResponse>()?;
1152        register_type::<A::GetResultRequest>()?;
1153        register_type::<A::GetResultResponse>()?;
1154        register_type::<A::FeedbackMessage>()?;
1155        // issue #234 — also register the fixed `action_msgs` protocol types
1156        // (`CancelGoal_{Request,Response}`, `GoalStatusArray`) the cancel service
1157        // + status publisher created below serialize. They are not `RosAction`
1158        // associated types (they live in `action_msgs`, which `nros-core` cannot
1159        // name), so the generated `impl RosAction::register_protocol_types` — which
1160        // routes them through the generic `nros_rmw::register_type_descriptor` seam —
1161        // registers them. Without this the cancel_goal service + status publisher
1162        // have no Cyclone descriptor → `ActionCreationFailed`. The callback executor
1163        // path (`executor/action.rs`) already did this; this node.rs path — the one
1164        // `create_action_server` materialises through — did not.
1165        A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
1166        let remapped = self.remapped(action_name);
1167        let action_name = remapped.as_deref().unwrap_or(action_name);
1168        let action_info =
1169            Self::action_info(self.domain_id, action_name, A::ACTION_NAME, A::ACTION_HASH);
1170
1171        // Each underlying ServiceInfo / TopicInfo also carries the
1172        // node identity so the Zenoh shim declares a liveliness token
1173        // for it. Without `with_node_name` the shim's
1174        // `declare_entity_liveliness` short-circuits (`node_name.and_then`
1175        // → None) and `wait_for_action_server` has nothing to find.
1176        // Advertise the per-channel service / topic types ROS 2 matches on
1177        // (`<Action>_SendGoal` / `<Action>_GetResult` / `<Action>_FeedbackMessage`),
1178        // not the bare action type — see `action_core::action_service_base_type`.
1179        let send_goal_type = super::action_core::action_service_base_type(
1180            <A::SendGoalRequest as RosMessage>::TYPE_NAME,
1181            A::ACTION_NAME,
1182        );
1183        let get_result_type = super::action_core::action_service_base_type(
1184            <A::GetResultRequest as RosMessage>::TYPE_NAME,
1185            A::ACTION_NAME,
1186        );
1187        let feedback_type = <A::FeedbackMessage as RosMessage>::TYPE_NAME;
1188
1189        let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1190        let send_goal_info = Self::service_info(
1191            self.domain_id,
1192            &self.name,
1193            &self.namespace,
1194            &send_goal_keyexpr,
1195            send_goal_type,
1196            A::SEND_GOAL_SERVICE_HASH,
1197        );
1198        let send_goal_server = self
1199            .session
1200            .create_service(&send_goal_info, QoSProfile::services_default())
1201            .map_err(|_| NodeError::ActionCreationFailed)?;
1202
1203        let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1204        let cancel_goal_info = Self::service_info(
1205            self.domain_id,
1206            &self.name,
1207            &self.namespace,
1208            &cancel_goal_keyexpr,
1209            "action_msgs::srv::dds_::CancelGoal_",
1210            A::CANCEL_GOAL_SERVICE_HASH,
1211        );
1212        let cancel_goal_server = self
1213            .session
1214            .create_service(&cancel_goal_info, QoSProfile::services_default())
1215            .map_err(|_| NodeError::ActionCreationFailed)?;
1216
1217        let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1218        let get_result_info = Self::service_info(
1219            self.domain_id,
1220            &self.name,
1221            &self.namespace,
1222            &get_result_keyexpr,
1223            get_result_type,
1224            A::GET_RESULT_SERVICE_HASH,
1225        );
1226        let get_result_server = self
1227            .session
1228            .create_service(&get_result_info, QoSProfile::services_default())
1229            .map_err(|_| NodeError::ActionCreationFailed)?;
1230
1231        let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1232        let feedback_topic = Self::topic_info(
1233            self.domain_id,
1234            &self.name,
1235            &self.namespace,
1236            &feedback_keyexpr,
1237            feedback_type,
1238            <A::FeedbackMessage as RosMessage>::TYPE_HASH,
1239        );
1240        let feedback_publisher = self
1241            .session
1242            .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
1243            .map_err(|_| NodeError::ActionCreationFailed)?;
1244
1245        let status_keyexpr: heapless::String<256> = action_info.status_key();
1246        let status_topic = Self::topic_info(
1247            self.domain_id,
1248            &self.name,
1249            &self.namespace,
1250            &status_keyexpr,
1251            "action_msgs::msg::dds_::GoalStatusArray_",
1252            A::STATUS_MESSAGE_HASH,
1253        );
1254        let status_publisher = self
1255            .session
1256            .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
1257            .map_err(|_| NodeError::ActionCreationFailed)?;
1258
1259        Ok(ActionServer {
1260            core: super::action_core::ActionServerCore {
1261                send_goal_server,
1262                cancel_goal_server,
1263                get_result_server,
1264                feedback_publisher,
1265                status_publisher,
1266                active_goals: heapless::Vec::new(),
1267                completed_results: heapless::Vec::new(),
1268                pending_get_results: heapless::Vec::new(),
1269                result_slab: [0u8; RESULT_BUF],
1270                result_slab_used: 0,
1271                goal_buffer: [0u8; GOAL_BUF],
1272                feedback_buffer: [0u8; FEEDBACK_BUF],
1273                cancel_buffer: [0u8; 256],
1274            },
1275            typed_goals: heapless::Vec::new(),
1276            completed_goals: heapless::Vec::new(),
1277        })
1278    }
1279
1280    /// Create an action client.
1281    pub fn create_action_client<A: RosAction>(
1282        &mut self,
1283        action_name: &str,
1284    ) -> Result<ActionClient<A>, NodeError>
1285    where
1286        A::Goal: MessageForRmw,
1287        A::Result: MessageForRmw,
1288        A::Feedback: MessageForRmw,
1289        A::SendGoalRequest: MessageForRmw,
1290        A::SendGoalResponse: MessageForRmw,
1291        A::GetResultRequest: MessageForRmw,
1292        A::GetResultResponse: MessageForRmw,
1293        A::FeedbackMessage: MessageForRmw,
1294    {
1295        self.create_action_client_sized::<A, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }>(action_name)
1296    }
1297
1298    /// Create an action client with custom buffer sizes.
1299    pub fn create_action_client_sized<
1300        A: RosAction,
1301        const GOAL_BUF: usize,
1302        const RESULT_BUF: usize,
1303        const FEEDBACK_BUF: usize,
1304    >(
1305        &mut self,
1306        action_name: &str,
1307    ) -> Result<ActionClient<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, NodeError>
1308    where
1309        A::Goal: MessageForRmw,
1310        A::Result: MessageForRmw,
1311        A::Feedback: MessageForRmw,
1312        A::SendGoalRequest: MessageForRmw,
1313        A::SendGoalResponse: MessageForRmw,
1314        A::GetResultRequest: MessageForRmw,
1315        A::GetResultResponse: MessageForRmw,
1316        A::FeedbackMessage: MessageForRmw,
1317    {
1318        // Phase 212.K.7.6.b + K.7.7.c — see `create_action_server_sized`.
1319        register_type::<A::Goal>()?;
1320        register_type::<A::Result>()?;
1321        register_type::<A::Feedback>()?;
1322        register_type::<A::SendGoalRequest>()?;
1323        register_type::<A::SendGoalResponse>()?;
1324        register_type::<A::GetResultRequest>()?;
1325        register_type::<A::GetResultResponse>()?;
1326        register_type::<A::FeedbackMessage>()?;
1327        // issue #234 — register the `action_msgs` protocol types the cancel-goal
1328        // service client below serializes (`CancelGoal_{Request,Response}`; the impl
1329        // also registers `GoalStatusArray`, harmlessly unused client-side) via the
1330        // generic seam. Without it the cancel_goal client has no Cyclone descriptor
1331        // → `ActionCreationFailed`. Mirrors the server path.
1332        A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
1333        let remapped = self.remapped(action_name);
1334        let action_name = remapped.as_deref().unwrap_or(action_name);
1335        let action_info =
1336            Self::action_info(self.domain_id, action_name, A::ACTION_NAME, A::ACTION_HASH);
1337
1338        // Mirror `create_action_server_sized`: thread node identity through
1339        // each underlying ServiceInfo / TopicInfo so the Zenoh shim
1340        // declares the matching client-side liveliness tokens (and so the
1341        // discovery wildcard built from `send_goal_info` ends up in the
1342        // same domain as the server's tokens).
1343        // Same per-channel typing as the server side so the client's requesters
1344        // and feedback reader match a real ROS 2 action server over DDS.
1345        let send_goal_type = super::action_core::action_service_base_type(
1346            <A::SendGoalRequest as RosMessage>::TYPE_NAME,
1347            A::ACTION_NAME,
1348        );
1349        let get_result_type = super::action_core::action_service_base_type(
1350            <A::GetResultRequest as RosMessage>::TYPE_NAME,
1351            A::ACTION_NAME,
1352        );
1353        let feedback_type = <A::FeedbackMessage as RosMessage>::TYPE_NAME;
1354
1355        let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1356        let send_goal_info = Self::service_info(
1357            self.domain_id,
1358            &self.name,
1359            &self.namespace,
1360            &send_goal_keyexpr,
1361            send_goal_type,
1362            A::SEND_GOAL_SERVICE_HASH,
1363        );
1364        let send_goal_client = self
1365            .session
1366            .create_client(&send_goal_info, QoSProfile::services_default())
1367            .map_err(|_| NodeError::ActionCreationFailed)?;
1368
1369        let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1370        let cancel_goal_info = Self::service_info(
1371            self.domain_id,
1372            &self.name,
1373            &self.namespace,
1374            &cancel_goal_keyexpr,
1375            "action_msgs::srv::dds_::CancelGoal_",
1376            A::CANCEL_GOAL_SERVICE_HASH,
1377        );
1378        let cancel_goal_client = self
1379            .session
1380            .create_client(&cancel_goal_info, QoSProfile::services_default())
1381            .map_err(|_| NodeError::ActionCreationFailed)?;
1382
1383        let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1384        let get_result_info = Self::service_info(
1385            self.domain_id,
1386            &self.name,
1387            &self.namespace,
1388            &get_result_keyexpr,
1389            get_result_type,
1390            A::GET_RESULT_SERVICE_HASH,
1391        );
1392        let get_result_client = self
1393            .session
1394            .create_client(&get_result_info, QoSProfile::services_default())
1395            .map_err(|_| NodeError::ActionCreationFailed)?;
1396
1397        let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1398        let feedback_topic = Self::topic_info(
1399            self.domain_id,
1400            &self.name,
1401            &self.namespace,
1402            &feedback_keyexpr,
1403            feedback_type,
1404            <A::FeedbackMessage as RosMessage>::TYPE_HASH,
1405        );
1406        let feedback_subscriber = self
1407            .session
1408            .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1409            .map_err(|_| NodeError::ActionCreationFailed)?;
1410
1411        Ok(ActionClient {
1412            core: super::action_core::ActionClientCore {
1413                send_goal_client,
1414                cancel_goal_client,
1415                get_result_client,
1416                feedback_subscriber,
1417                goal_buffer: [0u8; GOAL_BUF],
1418                result_buffer: [0u8; RESULT_BUF],
1419                feedback_buffer: [0u8; FEEDBACK_BUF],
1420                goal_counter: 0,
1421                in_flight_send_goal: false,
1422                in_flight_cancel: false,
1423                in_flight_get_result: false,
1424                pending_send_goal: super::action_core::ActionRequests::new(),
1425                pending_cancel: super::action_core::ActionRequests::new(),
1426                pending_get_result: super::action_core::ActionRequests::new(),
1427            },
1428            _phantom: PhantomData,
1429        })
1430    }
1431}
1432
1433// ===================================================================
1434// Phase 189.M1 — entity builders (the `clone` tier)
1435// ===================================================================
1436
1437/// Publisher builder — `node.publisher(topic)`. Choose `.typed::<M>()` or
1438/// `.generic(type, hash)`, optionally `.qos(..)`, then `.build()`.
1439pub struct PublisherBuilder<'n, 'a, 't> {
1440    node: &'n mut NodeHandle<'a>,
1441    topic: &'t str,
1442    qos: QoSProfile,
1443}
1444
1445impl<'n, 'a, 't> PublisherBuilder<'n, 'a, 't> {
1446    /// Set the QoS (also settable on the typed/generic builder).
1447    pub fn qos(mut self, qos: QoSProfile) -> Self {
1448        self.qos = qos;
1449        self
1450    }
1451
1452    /// Phase 282 (#145) — mark this publisher "express": its samples bypass
1453    /// transport tx batching (sent immediately even when the batching knob is
1454    /// on). For control-tier / latency-sensitive topics.
1455    pub fn tx_express(mut self, express: bool) -> Self {
1456        self.qos.tx_express = express;
1457        self
1458    }
1459
1460    /// Typed publisher for a ROS message `M` (mirrors rclcpp/rclrs).
1461    pub fn typed<M: MessageForRmw>(self) -> TypedPublisherBuilder<'n, 'a, 't, M> {
1462        TypedPublisherBuilder {
1463            node: self.node,
1464            topic: self.topic,
1465            qos: self.qos,
1466            _phantom: PhantomData,
1467        }
1468    }
1469
1470    /// Generic (type-erased) publisher — the rclcpp `create_generic_publisher`
1471    /// form; raw CDR bytes via `publish_raw`.
1472    pub fn generic(
1473        self,
1474        type_name: &'t str,
1475        type_hash: &'t str,
1476    ) -> GenericPublisherBuilder<'n, 'a, 't> {
1477        GenericPublisherBuilder {
1478            node: self.node,
1479            topic: self.topic,
1480            type_name,
1481            type_hash,
1482            qos: self.qos,
1483        }
1484    }
1485}
1486
1487/// Typed publisher builder (`.typed::<M>()`).
1488pub struct TypedPublisherBuilder<'n, 'a, 't, M> {
1489    node: &'n mut NodeHandle<'a>,
1490    topic: &'t str,
1491    qos: QoSProfile,
1492    _phantom: PhantomData<M>,
1493}
1494
1495impl<'n, 'a, 't, M: MessageForRmw> TypedPublisherBuilder<'n, 'a, 't, M> {
1496    pub fn qos(mut self, qos: QoSProfile) -> Self {
1497        self.qos = qos;
1498        self
1499    }
1500
1501    /// Phase 282 (#145) — see [`PublisherBuilder::tx_express`].
1502    pub fn tx_express(mut self, express: bool) -> Self {
1503        self.qos.tx_express = express;
1504        self
1505    }
1506
1507    pub fn build(self) -> Result<EmbeddedPublisher<M>, NodeError> {
1508        self.node
1509            .create_publisher_with_qos::<M>(self.topic, self.qos)
1510    }
1511}
1512
1513/// Generic (type-erased) publisher builder (`.generic(type, hash)`).
1514pub struct GenericPublisherBuilder<'n, 'a, 't> {
1515    node: &'n mut NodeHandle<'a>,
1516    topic: &'t str,
1517    type_name: &'t str,
1518    type_hash: &'t str,
1519    qos: QoSProfile,
1520}
1521
1522impl<'n, 'a, 't> GenericPublisherBuilder<'n, 'a, 't> {
1523    pub fn qos(mut self, qos: QoSProfile) -> Self {
1524        self.qos = qos;
1525        self
1526    }
1527
1528    /// Phase 282 (#145) — see [`PublisherBuilder::tx_express`].
1529    pub fn tx_express(mut self, express: bool) -> Self {
1530        self.qos.tx_express = express;
1531        self
1532    }
1533
1534    pub fn build(self) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
1535        self.node.create_publisher_raw_with_qos(
1536            self.topic,
1537            self.type_name,
1538            self.type_hash,
1539            self.qos,
1540        )
1541    }
1542}
1543
1544// ============================================================================
1545// Phase 273 (RFC-0047) — CallbackGroup token (rclcpp/rclrs shape)
1546// ============================================================================
1547
1548/// A first-class callback group — a **name-only token** (rclcpp/rclrs shape).
1549///
1550/// Created via [`Node::create_callback_group`].  Passed to the `_in`
1551/// entity-create variants (`create_timer_in_group`, `create_subscription_in_group`,
1552/// `create_publisher_in_group`) to label entities with a group name.
1553///
1554/// The group is just a name — the actual `SchedContext` binding is seeded at
1555/// boot by `Executor::bind_group_sched` (from `system.toml group_tiers`, phase
1556/// 273 W2) and resolved at entity-registration time by
1557/// `apply_node_default_sched` (phase 273 W1).  No concurrency type (Mutually-
1558/// Exclusive vs Reentrant) is stored here — that is RFC-0047 OQ1 follow-up.
1559pub struct CallbackGroup {
1560    name: heapless::String<32>,
1561}
1562
1563impl CallbackGroup {
1564    /// The group name (e.g. `"ctrl"`, `"telem"`).
1565    pub fn name(&self) -> &str {
1566        &self.name
1567    }
1568}
1569
1570/// A ROS 2 node — rclrs's `Node`, and the ONE node type (phase-483 W2).
1571///
1572/// `Executor::create_node(name)` returns it, and so does
1573/// `Executor::node_mut(id)`; a component and a standalone program hold the
1574/// same type. It carries rclrs's methods — `create_publisher`,
1575/// `create_subscription(topic, callback)`, `create_service`, timers, the
1576/// clock, `name` / `namespace` / `logger`, the graph queries — plus RTOS
1577/// extensions with names of their own (`create_polling_*`, the `_sized` and
1578/// `_raw` constructors).
1579///
1580/// ADOPT-BOUNDED: a node is a short-lived `&mut Executor` borrow, not rclrs's `Arc<Node>`.
1581/// Create entities, then drop it before acquiring the next node handle; entity
1582/// handles (`HandleId`, publishers) are owned and outlive it (no allocator —
1583/// see `docs/design/0022-entity-api-tiers.md` §Borrow model).
1584pub struct Node<'e, 's> {
1585    executor: &'e mut super::spin::Executor<'s>,
1586    node_id: super::node_record::NodeId,
1587}
1588
1589// ============================================================================
1590// Parameters -- the node is the receiver (phase-515 R10)
1591// ============================================================================
1592//
1593// rclrs keeps parameter verbs on the node; ours were on the `Executor`, as a
1594// `_on(NodeId, ...)` form plus a bare form meaning the primary node, because
1595// the store is one table the executor owns (phase-426 W1). The store did not
1596// move -- these forward to the `_on` forms with this node's id -- but the
1597// RECEIVER a user writes is the node, as upstream's is, and the executor's
1598// bare forms are deprecated. Executor-wide verbs (`register_parameter_services`,
1599// `params`, `install_argv_params`, `flush_parameter_events`) stay there.
1600#[cfg(feature = "param-store")]
1601impl<'e, 's> Node<'e, 's> {
1602    /// rclrs's `Node::declare_parameter::<T>(name)`: a [`ParameterBuilder`]
1603    /// whose terminal (`mandatory` / `optional` / `read_only`) declares it.
1604    ///
1605    /// ADOPT-BOUNDED: returns `Result`, where rclrs's builder cannot fail. The
1606    /// store is created on first use; `Err(NodeError::NotInitialized)` means the
1607    /// platform heap refused it (issue 1706), which has to be reported.
1608    ///
1609    /// [`ParameterBuilder`]: nros_params::ParameterBuilder
1610    pub fn declare_parameter<'a, T: nros_params::ParameterVariant>(
1611        &'a mut self,
1612        name: &'a str,
1613    ) -> Result<nros_params::ParameterBuilder<'a, 's, T>, NodeError> {
1614        self.executor.parameter_on(self.node_id, name)
1615    }
1616
1617    /// rclrs's `Node::use_undeclared_parameters()`: read and write any name,
1618    /// declared or not.
1619    pub fn use_undeclared_parameters(
1620        &mut self,
1621    ) -> Result<nros_params::Parameters<'_, 's>, NodeError> {
1622        self.executor.use_undeclared_parameters_on(self.node_id)
1623    }
1624
1625    /// Declare `name` with `value` and a descriptor -- rclcpp's
1626    /// `declare_parameter(name, value, descriptor)`. `false` if it exists, the
1627    /// table is full, or the value is outside the descriptor's range.
1628    pub fn declare_parameter_with_descriptor(
1629        &mut self,
1630        name: &str,
1631        value: nros_params::ParameterValue,
1632        descriptor: nros_params::ParameterDescriptor,
1633    ) -> bool {
1634        self.executor
1635            .declare_parameter_with_descriptor_on(self.node_id, name, value, descriptor)
1636    }
1637
1638    /// The current value of `name`, if declared.
1639    pub fn get_parameter(&self, name: &str) -> Option<&nros_params::ParameterValue> {
1640        self.executor.get_parameter_on(self.node_id, name)
1641    }
1642
1643    /// Set `name`, through the same rules a remote set obeys.
1644    pub fn set_parameter(
1645        &mut self,
1646        name: &str,
1647        value: nros_params::ParameterValue,
1648    ) -> Result<(), nros_params::ParameterError> {
1649        self.executor.set_parameter_on(self.node_id, name, value)
1650    }
1651
1652    /// Set several parameters all-or-nothing -- rclcpp's
1653    /// `set_parameters_atomically`.
1654    pub fn set_parameters_atomically<'x, I>(
1655        &mut self,
1656        items: I,
1657    ) -> Result<(), nros_params::ParameterError>
1658    where
1659        I: Iterator<Item = Option<(&'x str, nros_params::ParameterValue)>> + Clone,
1660    {
1661        self.executor
1662            .set_parameters_atomically_on(self.node_id, items)
1663    }
1664
1665    /// Remove a declared parameter -- rclcpp's `undeclare_parameter`.
1666    pub fn undeclare_parameter(&mut self, name: &str) -> bool {
1667        self.executor.undeclare_parameter_on(self.node_id, name)
1668    }
1669
1670    /// Whether a REMOTE set may create a name that was never declared.
1671    pub fn allow_undeclared_parameters(&mut self, allow: bool) {
1672        self.executor
1673            .allow_undeclared_parameters_on(self.node_id, allow)
1674    }
1675
1676    /// The descriptor of `name` -- rclcpp's `describe_parameter`.
1677    pub fn describe_parameter(&self, name: &str) -> Option<&nros_params::ParameterDescriptor> {
1678        self.executor.describe_parameter_on(self.node_id, name)
1679    }
1680
1681    /// The type of `name` -- rclcpp's `get_parameter_type`.
1682    pub fn get_parameter_type(&self, name: &str) -> Option<nros_params::ParameterType> {
1683        self.executor.get_parameter_type_on(self.node_id, name)
1684    }
1685
1686    /// The names of this node's parameters under `prefix` -- rclcpp's
1687    /// `list_parameters`, as an iterator rather than an allocated list.
1688    pub fn list_parameters<'p>(&'p self, prefix: &'p str) -> impl Iterator<Item = &'p str> {
1689        self.executor.list_parameters_on(self.node_id, prefix)
1690    }
1691
1692    /// Set the descriptor's description and constraint text.
1693    pub fn set_parameter_description(
1694        &mut self,
1695        name: &str,
1696        description: &str,
1697        additional_constraints: &str,
1698    ) -> bool {
1699        self.executor.set_parameter_description_on(
1700            self.node_id,
1701            name,
1702            description,
1703            additional_constraints,
1704        )
1705    }
1706
1707    /// Make `name` read-only (or writable again).
1708    pub fn set_parameter_read_only(&mut self, name: &str, read_only: bool) -> bool {
1709        self.executor
1710            .set_parameter_read_only_on(self.node_id, name, read_only)
1711    }
1712
1713    /// Constrain an integer parameter to `[min, max]` in steps of `step`.
1714    pub fn set_parameter_integer_range(
1715        &mut self,
1716        name: &str,
1717        min: i64,
1718        max: i64,
1719        step: i64,
1720    ) -> bool {
1721        self.executor
1722            .set_parameter_integer_range_on(self.node_id, name, min, max, step)
1723    }
1724
1725    /// Constrain a floating-point parameter to `[min, max]` in steps of `step`.
1726    pub fn set_parameter_float_range(&mut self, name: &str, min: f64, max: f64, step: f64) -> bool {
1727        self.executor
1728            .set_parameter_float_range_on(self.node_id, name, min, max, step)
1729    }
1730
1731    /// rclcpp's `add_on_set_parameters_callback`: `callback` may refuse a write
1732    /// to this node's parameters.
1733    pub fn add_on_set_parameters_callback(
1734        &mut self,
1735        callback: nros_params::OnSetParameterFn,
1736        ctx: nros_params::OnSetContext,
1737    ) -> Option<nros_params::OnSetParameterHandle> {
1738        self.executor
1739            .add_on_set_parameters_callback_on(self.node_id, callback, ctx)
1740    }
1741
1742    /// rclcpp's `remove_on_set_parameters_callback`.
1743    pub fn remove_on_set_parameters_callback(
1744        &mut self,
1745        handle: nros_params::OnSetParameterHandle,
1746    ) -> bool {
1747        self.executor.remove_on_set_parameters_callback(handle)
1748    }
1749
1750    // phase-515 R10 stage 1b -- the rest of the per-node parameter surface,
1751    // which only the executor's node-id-keyed forms carried before those moved
1752    // behind `ExecutorFfi`.
1753
1754    /// The `-p` / `--params-file` overrides that apply to this node, one per
1755    /// name — rclcpp's `NodeParametersInterface::get_parameter_overrides()`
1756    /// (issue 2057).
1757    #[cfg(feature = "alloc")]
1758    pub fn argv_param_overrides(
1759        &self,
1760    ) -> impl Iterator<Item = (&str, nros_params::ParameterValue)> + '_ {
1761        self.executor.argv_param_overrides_on(self.node_id)
1762    }
1763
1764    /// [`argv_param_overrides`](Self::argv_param_overrides), one call of `f`
1765    /// per override with the value borrowed; returns how many there were.
1766    #[cfg(feature = "alloc")]
1767    pub fn for_each_argv_param_override(
1768        &self,
1769        f: impl FnMut(&str, &nros_params::ParameterValue),
1770    ) -> usize {
1771        self.executor
1772            .for_each_argv_param_override_on(self.node_id, f)
1773    }
1774
1775    /// rclc's LOCAL typed set: type and on-set hooks are checked, read-only
1776    /// and range are not (rclc's owner of a parameter is not bound by them).
1777    pub fn set_parameter_local(
1778        &mut self,
1779        name: &str,
1780        value: nros_params::ParameterValue,
1781    ) -> Result<(), nros_params::ParameterError> {
1782        self.executor
1783            .set_parameter_local_on(self.node_id, name, value)
1784    }
1785
1786    /// Publish no `/parameter_events` for this node (rclcpp's
1787    /// `start_parameter_event_publisher(false)`).
1788    #[cfg(feature = "param-services")]
1789    pub fn disable_parameter_events(&mut self) {
1790        self.executor.disable_parameter_events_on(self.node_id)
1791    }
1792
1793    /// Publish this node's parameter changes on `/parameter_events`.
1794    #[cfg(feature = "param-services")]
1795    pub fn enable_parameter_events(&mut self) -> Result<(), NodeError> {
1796        self.executor.enable_parameter_events_on(self.node_id)
1797    }
1798
1799    /// A remote `NOT_SET` deletes the parameter (rclc) instead of being
1800    /// refused (rclcpp).
1801    pub fn delete_parameters_on_not_set(&mut self, on: bool) {
1802        self.executor
1803            .delete_parameters_on_not_set_on(self.node_id, on)
1804    }
1805
1806    /// [`set_parameters_atomically`](Self::set_parameters_atomically) with
1807    /// element `i` filled into one scratch value by `fill(i, slot)`.
1808    pub fn set_parameters_atomically_with<'x>(
1809        &mut self,
1810        count: usize,
1811        fill: impl FnMut(usize, &mut nros_params::ParameterValue) -> Option<&'x str>,
1812    ) -> Result<(), nros_params::ParameterError> {
1813        self.executor
1814            .set_parameters_atomically_with_on(self.node_id, count, fill)
1815    }
1816}
1817
1818impl<'e, 's> Node<'e, 's> {
1819    pub(crate) fn new(
1820        executor: &'e mut super::spin::Executor<'s>,
1821        node_id: super::node_record::NodeId,
1822    ) -> Self {
1823        Self { executor, node_id }
1824    }
1825
1826    /// The node's name — rclrs's `Node::name()`.
1827    ///
1828    /// phase-483 W2. Read from the executor's node table, where the name the
1829    /// node was created with (or the launch file's override of it) lives.
1830    #[must_use]
1831    pub fn name(&self) -> &str {
1832        self.executor
1833            .node(self.node_id)
1834            .map(|r| r.name.as_str())
1835            .unwrap_or("")
1836    }
1837
1838    /// The node's namespace — rclrs's `Node::namespace()`. `""` is the root.
1839    #[must_use]
1840    pub fn namespace(&self) -> &str {
1841        self.executor
1842            .node(self.node_id)
1843            .map(|r| r.namespace.as_str())
1844            .unwrap_or("")
1845    }
1846
1847    /// `/<namespace>/<name>` — rclrs's `Node::fully_qualified_name()`.
1848    ///
1849    /// Bounded, where rclrs returns a `String`: no allocator here, and the
1850    /// two halves are each at most 64 bytes.
1851    pub fn fully_qualified_name(&self) -> Result<crate::names::ResolvedName, NodeError> {
1852        crate::names::fully_qualified_name(self.name(), self.namespace())
1853            .map_err(|()| NodeError::NameTooLong)
1854    }
1855
1856    /// The node's ROS domain — rclrs's `Node::domain_id()`. Every node of an
1857    /// executor shares its session, so this is the executor's domain.
1858    #[must_use]
1859    pub fn domain_id(&self) -> u32 {
1860        self.executor.domain_id
1861    }
1862
1863    /// The logger named for this node — rclrs's `Node::logger()`. See
1864    /// [`NodeHandle::logger`] for how the name is resolved.
1865    #[must_use]
1866    pub fn logger(&self) -> &'static nros_log::Logger {
1867        nros_log::resolve_node_logger(self.name())
1868    }
1869
1870    /// The executor this node borrows — for the `nros` facade's declarative
1871    /// constructors, which register into it. Not a user API.
1872    #[doc(hidden)]
1873    pub fn __executor(&mut self) -> &mut super::spin::Executor<'s> {
1874        self.executor
1875    }
1876
1877    /// This node's id in the executor's node table — an RTOS extension, for
1878    /// the APIs that address a node by id (`Executor::node_mut`, the
1879    /// per-node QoS override table).
1880    #[must_use]
1881    pub fn id(&self) -> super::node_record::NodeId {
1882        self.node_id
1883    }
1884
1885    /// The node's clock — rclrs's `Node::get_clock()`, and rclcpp's.
1886    ///
1887    /// phase-417 G6. `cpp:Node::get_clock` has shipped since issue 0789 and
1888    /// only the Rust node lacked the accessor: a component reached the clock
1889    /// through the executor, or constructed one, and the ported line
1890    /// `node.get_clock().now()` had nothing to bind.
1891    ///
1892    /// **A delegation, not a second clock.** [`nros_core::clock::Clock`] is a
1893    /// one-field value naming a time SOURCE — it holds no state of its own, and
1894    /// `now()` on a ROS-time clock reads the process-global override that
1895    /// `install_ros_time_source` installs. So
1896    /// this returns the same answer as the executor's clock by construction
1897    /// rather than by agreement, which is the property the graph forwarders in
1898    /// this family are shaped for.
1899    ///
1900    /// ROS TIME, as rclcpp's and rclrs's node clocks are: with a `/clock`
1901    /// source installed it follows the simulator, and with none it reads the
1902    /// same wall clock `Clock::system()` does (issue 1334 put those two on one
1903    /// expression). A component that specifically wants the monotonic source
1904    /// asks for `Clock::steady()`.
1905    ///
1906    /// By VALUE, where rclcpp hands back a `Clock::SharedPtr`: there is no
1907    /// allocator here (RFC-0022) and the type is `Copy`, so a handle would be
1908    /// a pointer to one `u8`.
1909    pub fn get_clock(&self) -> nros_core::clock::Clock {
1910        nros_core::clock::Clock::ros_time()
1911    }
1912
1913    /// Subscription builder (the `clone` tier). Pick a mode with `.typed::<M>()`
1914    /// or `.generic(type, hash)`, set knobs (`.qos`), then `.build(callback)`.
1915    pub fn subscription<'t>(&mut self, topic: &'t str) -> SubscriptionBuilder<'_, 'e, 't, 's> {
1916        SubscriptionBuilder {
1917            ctx: self,
1918            topic,
1919            qos: QoSProfile::default(),
1920        }
1921    }
1922
1923    /// Publisher builder (the `clone` tier), symmetric with
1924    /// [`subscription`](Self::subscription). Pick `.typed::<M>()` or
1925    /// `.generic(type, hash)`, set `.qos()`, then `.build()`. The returned
1926    /// publisher handle is owned and outlives this `Node` — the bridge
1927    /// builds the dest publisher on one ctx, drops it, then registers the
1928    /// source subscription on another (see `0022-entity-api-tiers.md`).
1929    pub fn publisher<'t>(&mut self, topic: &'t str) -> CtxPublisherBuilder<'_, 'e, 't, 's> {
1930        CtxPublisherBuilder {
1931            ctx: self,
1932            topic,
1933            qos: QoSProfile::default(),
1934        }
1935    }
1936
1937    /// Convenient typed publisher (the `fork` tier — rclcpp/rclrs shape).
1938    ///
1939    /// phase-515 R1 — takes rclrs's `impl IntoPrimitiveOptions`: a bare
1940    /// topic name (default QoS) or one with QoS steps chained on,
1941    /// `"chatter".keep_last(10).reliable()`.
1942    pub fn create_publisher<'o, M: MessageForRmw>(
1943        &mut self,
1944        options: impl crate::IntoPrimitiveOptions<'o>,
1945    ) -> Result<EmbeddedPublisher<M>, NodeError> {
1946        let options = options.into_primitive_options();
1947        self.create_publisher_with_qos::<M>(options.name, options.resolve(QoSProfile::default()))
1948    }
1949
1950    /// Convenient generic (type-erased) publisher — rclcpp `create_generic_*`.
1951    pub fn create_generic_publisher(
1952        &mut self,
1953        topic: &str,
1954        type_name: &str,
1955        type_hash: &str,
1956    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
1957        self.create_generic_publisher_with_qos(topic, type_name, type_hash, QoSProfile::default())
1958    }
1959
1960    /// Issue 0306 — the same, with an explicit profile. The declarative
1961    /// component path needs it: a node that declares
1962    /// `create_publisher_for_topic_with_qos(...)` carries its profile in
1963    /// `EntityMetadata::qos`, and the runtime used to drop it on the floor by
1964    /// calling the default-QoS constructor here.
1965    pub fn create_generic_publisher_with_qos(
1966        &mut self,
1967        topic: &str,
1968        type_name: &str,
1969        type_hash: &str,
1970        qos: QoSProfile,
1971    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
1972        self.executor
1973            .create_publisher_raw_on(self.node_id, topic, type_name, type_hash, qos)
1974    }
1975
1976    /// Convenient typed subscription (the `fork` tier — rclcpp/rclrs shape).
1977    /// Sugar over the builder with default QoS + buffer.
1978    ///
1979    /// phase-515 R1 — `options` is rclrs's `impl IntoPrimitiveOptions`, as on
1980    /// [`create_publisher`](Self::create_publisher). ADOPT-BOUNDED (subscription):
1981    /// returns the entity's `HandleId`, not an owned rclrs `Subscription` — the
1982    /// entity lives in the executor's table, and the id is what cancels it.
1983    pub fn create_subscription<'o, M, F>(
1984        &mut self,
1985        options: impl crate::IntoPrimitiveOptions<'o>,
1986        callback: F,
1987    ) -> Result<super::types::HandleId, NodeError>
1988    where
1989        M: MessageForRmw + 'static,
1990        F: FnMut(&M) + 'static,
1991    {
1992        let options = options.into_primitive_options();
1993        self.executor
1994            .register_subscription_buffered_on::<M, F, { crate::config::DEFAULT_RX_BUF_SIZE }>(
1995                self.node_id,
1996                options.name,
1997                options.resolve(QoSProfile::default()),
1998                callback,
1999                None, // no group — node default
2000                None, // phase-403 W2: the configured default, unchanged
2001            )
2002    }
2003
2004    /// Subscribe `/clock` and install every sample as this image's ROS time —
2005    /// phase-425 W3, the thing `rclcpp`'s `use_sim_time` parameter switches on.
2006    ///
2007    /// After this, `Clock::ros_time().now()` is the simulator's or the bag
2008    /// player's time, and a timer registered with `TimerClockSource::Ros`
2009    /// follows it: it stops while the simulation is paused and tracks the replay
2010    /// rate. Nothing else changes — a wall timer on the same executor keeps its
2011    /// own cadence, which is what a watchdog needs.
2012    ///
2013    /// QoS is `QoSProfile::clock_default()`, i.e. `rclcpp::ClockQoS`: best
2014    /// effort, keep-last 1, volatile. A late subscriber wants the NEXT sample,
2015    /// not a replay of the simulation's history.
2016    ///
2017    /// Returns the subscription handle. Cancelling it stops the source but does
2018    /// NOT clear the override: a node that unsubscribes mid-run keeps the last
2019    /// simulated time rather than jumping back to the wall clock, which every
2020    /// ROS-time timer would otherwise have to absorb as a jump.
2021    ///
2022    /// The override is process-global — one simulated clock per image — so
2023    /// calling this on a second node in the same image adds a second subscriber
2024    /// to the same global, which is redundant rather than wrong.
2025    #[cfg(all(feature = "sim-time", any(has_rmw, test)))]
2026    pub fn install_ros_time_source(&mut self) -> Result<super::types::HandleId, NodeError> {
2027        self.install_ros_time_source_on(crate::time_source::CLOCK_TOPIC)
2028    }
2029
2030    /// [`install_ros_time_source`](Self::install_ros_time_source) against a
2031    /// topic other than `/clock` — a remapped or namespaced clock, which is what
2032    /// a launch file creates when two simulations share a graph.
2033    #[cfg(all(feature = "sim-time", any(has_rmw, test)))]
2034    pub fn install_ros_time_source_on(
2035        &mut self,
2036        topic: &str,
2037    ) -> Result<super::types::HandleId, NodeError> {
2038        // One spelling of the QoS and the sample conversion, on the executor:
2039        // the `use_sim_time` reconciliation (phase-425 W3b) installs the same
2040        // subscription, and two copies would be two places for the QoS to drift.
2041        self.executor.install_ros_time_source(self.node_id, topic)
2042    }
2043
2044    // -----------------------------------------------------------------------
2045    // Phase 273 (RFC-0047) — callback-group API (rclcpp/rclrs shape)
2046    // -----------------------------------------------------------------------
2047
2048    /// Create a named callback group — a thin token wrapping the group name.
2049    ///
2050    /// Pass the returned [`CallbackGroup`] to the `_in` create variants
2051    /// (`create_timer_in_group`, `create_subscription_in_group`, `create_publisher_in_group`)
2052    /// to label entities with this group. The executor binds the entity's
2053    /// callback to the `SchedContext` seeded via `bind_group_sched` for
2054    /// `(node_name, namespace, group_name)` (phase 273 W1/W2).
2055    ///
2056    /// ```ignore
2057    /// let ctrl = node.create_callback_group("ctrl");
2058    /// node.create_timer_in_group(&ctrl, TimerDuration::from_millis(10), || { /* … */ })?;
2059    /// ```
2060    ///
2061    /// Names longer than 32 bytes are silently truncated.
2062    pub fn create_callback_group(&self, name: &str) -> CallbackGroup {
2063        let mut s = heapless::String::<32>::new();
2064        // Silently truncate if the name is too long (defensive; caller
2065        // should use short ASCII group names like "ctrl"/"telem").
2066        for ch in name.chars() {
2067            if s.push(ch).is_err() {
2068                break;
2069            }
2070        }
2071        CallbackGroup { name: s }
2072    }
2073
2074    /// Create a repeating timer **in** a callback group (phase 273, rclcpp shape).
2075    ///
2076    /// The timer's callback is bound to the `SchedContext` associated with
2077    /// `group` in the executor's `group_sched_table` for this node. If no
2078    /// entry was seeded for the group, the node's default `SchedContext` applies
2079    /// (same as `register_timer`). `period` fires the callback repeatedly.
2080    pub fn create_timer_in_group<F>(
2081        &mut self,
2082        group: &CallbackGroup,
2083        period: crate::timer::TimerDuration,
2084        callback: F,
2085    ) -> Result<super::types::HandleId, NodeError>
2086    where
2087        F: FnMut() + 'static,
2088    {
2089        self.executor
2090            .register_timer_on(Some(self.node_id), period, callback, Some(group.name()))
2091    }
2092
2093    /// rclrs's `create_timer_repeating(period, callback)` — phase-515 R4.
2094    ///
2095    /// Fires `callback` every `period`, on this node's default `SchedContext`.
2096    /// rclrs 0.7.0 settled on the same three timer modes ours carries
2097    /// (repeating, one-shot, inert); these three verbs are its spelling of
2098    /// them. `period` is a [`TimerDuration`](crate::timer::TimerDuration) or a
2099    /// `core::time::Duration`.
2100    ///
2101    /// ADOPT-BOUNDED: returns the timer's [`HandleId`](super::types::HandleId),
2102    /// not an rclrs `Timer` object — the timer lives in the executor's static
2103    /// table, so `Executor::cancel_timer` / `reset_timer` / `timer_period_ms`
2104    /// take that id. No `IntoTimerOptions` (a clock choice is
2105    /// [`create_timer_on_clock`](Self::create_timer_on_clock)).
2106    pub fn create_timer_repeating<F>(
2107        &mut self,
2108        period: impl Into<crate::timer::TimerDuration>,
2109        callback: F,
2110    ) -> Result<super::types::HandleId, NodeError>
2111    where
2112        F: FnMut() + 'static,
2113    {
2114        self.executor.register_timer_entry(
2115            Some(self.node_id),
2116            period.into(),
2117            super::TimerClockSource::Steady,
2118            false,
2119            None,
2120            callback,
2121        )
2122    }
2123
2124    /// rclrs's `create_timer_oneshot(period, callback)` — phase-515 R4.
2125    ///
2126    /// Fires `callback` once, `period` after creation, then goes inert;
2127    /// `Executor::reset_timer` re-arms it for one more shot. Same envelope as
2128    /// [`create_timer_repeating`](Self::create_timer_repeating).
2129    pub fn create_timer_oneshot<F>(
2130        &mut self,
2131        period: impl Into<crate::timer::TimerDuration>,
2132        callback: F,
2133    ) -> Result<super::types::HandleId, NodeError>
2134    where
2135        F: FnMut() + 'static,
2136    {
2137        self.executor.register_timer_entry(
2138            Some(self.node_id),
2139            period.into(),
2140            super::TimerClockSource::Steady,
2141            true,
2142            None,
2143            callback,
2144        )
2145    }
2146
2147    /// rclrs's `create_timer_inert(period, callback)` — phase-515 R4.
2148    ///
2149    /// Creates the timer without starting it (rcl's `autostart = false`): it
2150    /// never fires until `Executor::reset_timer` starts it, after which it
2151    /// repeats every `period`. ADOPT-BOUNDED beyond
2152    /// [`create_timer_repeating`](Self::create_timer_repeating)'s envelope:
2153    /// rclrs can later switch an inert timer to one-shot; ours starts as a
2154    /// repeating timer, and a one-shot is created with
2155    /// [`create_timer_oneshot`](Self::create_timer_oneshot) instead.
2156    pub fn create_timer_inert<F>(
2157        &mut self,
2158        period: impl Into<crate::timer::TimerDuration>,
2159        callback: F,
2160    ) -> Result<super::types::HandleId, NodeError>
2161    where
2162        F: FnMut() + 'static,
2163    {
2164        let mut callback = callback;
2165        self.executor.register_timer_entry_with(
2166            Some(self.node_id),
2167            period.into(),
2168            super::TimerClockSource::Steady,
2169            false,
2170            false,
2171            None,
2172            None,
2173            core::ptr::null_mut(),
2174            move |_| move |_: &crate::timer::TimerCallInfo| callback(),
2175        )
2176    }
2177
2178    /// Create a guard condition on this node — phase-417 W4.e, the Rust half of
2179    /// the one creation shape.
2180    ///
2181    /// A guard condition is the cross-thread / ISR wake source: any thread may
2182    /// call [`GuardCondition::trigger`](super::types::GuardCondition::trigger)
2183    /// and the `callback` runs on the executor's next `spin_once`. The flag
2184    /// itself lives in the executor's arena, which is what makes the returned
2185    /// handle `Send + Sync` with no allocation — so the NODE is the owner a
2186    /// caller names and the EXECUTOR is where the entity lands, exactly as for
2187    /// a timer or a subscription.
2188    ///
2189    /// Until 2026-09-18 the only Rust spelling was
2190    /// [`Executor::register_guard_condition`](crate::Executor::register_guard_condition),
2191    /// while C++ created one on the node and C needed three calls. That was the
2192    /// three-owner state RFC-0089 stage 4 exists to remove; the executor form
2193    /// survives as the ours-only primitive this calls.
2194    ///
2195    /// The returned [`HandleId`](super::types::HandleId) is the slot, for
2196    /// `trigger_one` / `bind_handle_to_sched_context`; the
2197    /// [`GuardCondition`](super::types::GuardCondition) is the trigger handle to
2198    /// hand to the other thread.
2199    ///
2200    /// ```ignore
2201    /// let (_id, guard) = node.create_guard_condition(|| { /* woken */ })?;
2202    /// // from an ISR or another task:
2203    /// guard.trigger();
2204    /// ```
2205    pub fn create_guard_condition<F>(
2206        &mut self,
2207        callback: F,
2208    ) -> Result<(super::types::HandleId, super::types::GuardCondition), NodeError>
2209    where
2210        F: FnMut() + 'static,
2211    {
2212        self.executor
2213            .register_guard_condition_on(Some(self.node_id), callback)
2214    }
2215
2216    /// Create a repeating timer on a chosen CLOCK — phase-430 W4, the
2217    /// node-level spelling of
2218    /// [`Executor::register_timer_on_clock`](crate::Executor::register_timer_on_clock)
2219    /// and the Rust counterpart of C++'s `create_timer(clock, period, cb)`.
2220    ///
2221    /// [`create_timer_in_group`](Self::create_timer_in_group) and the executor's
2222    /// `register_timer` are the WALL case: they consume the spin delta the
2223    /// executor already measured, and no simulator can slow them down. A
2224    /// [`TimerClockSource::Ros`](crate::executor::TimerClockSource::Ros) timer instead follows `/clock`, so it stops
2225    /// while the simulator is paused and tracks a bag's replay rate; with no
2226    /// `/clock` source installed it reads system time, the same fallback
2227    /// `rclcpp::Clock` has.
2228    ///
2229    /// The timer is bound to this node, so the node's default `SchedContext`
2230    /// applies — [`create_timer_on_clock_in_group`](Self::create_timer_on_clock_in_group)
2231    /// is the callback-group form.
2232    ///
2233    /// ```ignore
2234    /// node.create_timer_on_clock(
2235    ///     TimerDuration::from_millis(100),
2236    ///     TimerClockSource::Ros,
2237    ///     || { /* fires once per 100 ms of SIMULATED time */ },
2238    /// )?;
2239    /// ```
2240    pub fn create_timer_on_clock<F>(
2241        &mut self,
2242        period: crate::timer::TimerDuration,
2243        source: crate::executor::TimerClockSource,
2244        callback: F,
2245    ) -> Result<super::types::HandleId, NodeError>
2246    where
2247        F: FnMut() + 'static,
2248    {
2249        self.executor.register_timer_entry(
2250            Some(self.node_id),
2251            period,
2252            source,
2253            false,
2254            None,
2255            callback,
2256        )
2257    }
2258
2259    /// [`create_timer_on_clock`](Self::create_timer_on_clock) **in** a callback
2260    /// group — the clock axis on [`create_timer_in_group`](Self::create_timer_in_group)
2261    /// (phase-430 W4). The two axes are independent: the group decides which
2262    /// `SchedContext` the callback runs under, the clock decides what advances
2263    /// the timer towards its next activation.
2264    pub fn create_timer_on_clock_in_group<F>(
2265        &mut self,
2266        group: &CallbackGroup,
2267        period: crate::timer::TimerDuration,
2268        source: crate::executor::TimerClockSource,
2269        callback: F,
2270    ) -> Result<super::types::HandleId, NodeError>
2271    where
2272        F: FnMut() + 'static,
2273    {
2274        self.executor.register_timer_entry(
2275            Some(self.node_id),
2276            period,
2277            source,
2278            false,
2279            Some(group.name()),
2280            callback,
2281        )
2282    }
2283
2284    /// Create a repeating timer whose callback receives the activation's
2285    /// [`TimerCallInfo`](crate::TimerCallInfo) — issue 1974, the Rust
2286    /// counterpart of Jazzy's `rclcpp::TimerInfoCallbackType`.
2287    ///
2288    /// `info.expected_call_time` is the deadline the executor was serving and
2289    /// `info.actual_call_time` when the callback ran, both on `source`'s clock;
2290    /// `info.lateness_ns()` is the jitter a real-time node otherwise measures
2291    /// by hand. `autostart == false` creates the timer cancelled (rclcpp's
2292    /// `create_timer(..., autostart = false)`); `Executor::reset_timer` on the
2293    /// returned handle starts it.
2294    ///
2295    /// ```ignore
2296    /// node.create_timer_with_info(
2297    ///     TimerDuration::from_millis(10),
2298    ///     TimerClockSource::Steady,
2299    ///     true,
2300    ///     |info| worst = worst.max(info.lateness_ns()),
2301    /// )?;
2302    /// ```
2303    pub fn create_timer_with_info<F>(
2304        &mut self,
2305        period: crate::timer::TimerDuration,
2306        source: crate::executor::TimerClockSource,
2307        autostart: bool,
2308        callback: F,
2309    ) -> Result<super::types::HandleId, NodeError>
2310    where
2311        F: FnMut(&crate::TimerCallInfo) + 'static,
2312    {
2313        self.executor.register_timer_with_info_on(
2314            Some(self.node_id),
2315            period,
2316            source,
2317            autostart,
2318            None,
2319            callback,
2320        )
2321    }
2322
2323    /// Create a typed subscription **in** a callback group (phase 273, rclcpp shape).
2324    ///
2325    /// The subscription's callback is bound to the `SchedContext` associated
2326    /// with `group` in the `group_sched_table` for this node. If no entry was
2327    /// seeded, the node default applies.
2328    pub fn create_subscription_in_group<M, F>(
2329        &mut self,
2330        group: &CallbackGroup,
2331        topic: &str,
2332        callback: F,
2333    ) -> Result<super::types::HandleId, NodeError>
2334    where
2335        M: MessageForRmw + 'static,
2336        F: FnMut(&M) + 'static,
2337    {
2338        self.executor
2339            .register_subscription_buffered_on::<M, F, { crate::config::DEFAULT_RX_BUF_SIZE }>(
2340                self.node_id,
2341                topic,
2342                QoSProfile::default(),
2343                callback,
2344                Some(group.name()),
2345                None, // phase-403 W2: the configured default, unchanged
2346            )
2347    }
2348
2349    /// Create a typed publisher **in** a callback group (phase 273, rclcpp shape).
2350    ///
2351    /// Publishers do not have an executor-dispatched callback, so the group
2352    /// name has no scheduling effect today (publishers are explicitly driven by
2353    /// the user via `publish()`). The API is provided for symmetry and
2354    /// forward-compatibility (intra-process / loaned-message knobs may use
2355    /// it in the future).
2356    pub fn create_publisher_in_group<M: MessageForRmw>(
2357        &mut self,
2358        _group: &CallbackGroup,
2359        topic: &str,
2360    ) -> Result<EmbeddedPublisher<M>, NodeError> {
2361        // Publishers carry no executor callback slot; group is forward-compat.
2362        self.create_publisher::<M>(topic)
2363    }
2364
2365    /// RFC-0041 / Phase 239.1 — callback-based service client (rclcpp
2366    /// `async_send_request(req, cb)` analogue). The reply is delivered to
2367    /// `callback` at `spin_once` (no `Promise` poll). Returns a
2368    /// [`ServiceClientCallback`] send handle; dual-mode — the `Promise`-based
2369    /// `create_client` is unchanged.
2370    pub fn create_client_with_callback<Svc, F>(
2371        &mut self,
2372        service_name: &str,
2373        callback: F,
2374    ) -> Result<ServiceClientCallback<Svc>, NodeError>
2375    where
2376        Svc: RosService + 'static,
2377        Svc::Request: MessageForRmw,
2378        Svc::Reply: MessageForRmw,
2379        F: FnMut(&Svc::Reply) + 'static,
2380    {
2381        self.create_client_with_callback_sized::<
2382            Svc,
2383            F,
2384            { crate::config::DEFAULT_RX_BUF_SIZE },
2385            { crate::config::DEFAULT_RX_BUF_SIZE },
2386        >(service_name, callback)
2387    }
2388
2389    /// Callback-based service client with custom buffer sizes (Phase 239.1).
2390    pub fn create_client_with_callback_sized<Svc, F, const REQ_BUF: usize, const REPLY_BUF: usize>(
2391        &mut self,
2392        service_name: &str,
2393        callback: F,
2394    ) -> Result<ServiceClientCallback<Svc, REQ_BUF, REPLY_BUF>, NodeError>
2395    where
2396        Svc: RosService + 'static,
2397        Svc::Request: MessageForRmw,
2398        Svc::Reply: MessageForRmw,
2399        F: FnMut(&Svc::Reply) + 'static,
2400    {
2401        self.register_client_callback::<Svc, _, REQ_BUF, REPLY_BUF>(
2402            service_name,
2403            super::arena::ReplyOnly(callback),
2404        )
2405    }
2406
2407    /// [`create_client_with_callback`](Self::create_client_with_callback)
2408    /// whose closure also receives the reply's
2409    /// [`ServiceInfo`](nros_core::ServiceInfo) — issue 2148, the callback twin
2410    /// of [`Promise::take_with_info`](super::handles::Promise::take_with_info)
2411    /// and the client twin of
2412    /// [`create_service_with_info`](Self::create_service_with_info).
2413    ///
2414    /// rclrs's `(Response, ServiceInfo)` reply and upstream
2415    /// `rmw_take_response`'s `rmw_service_info_t`: `request_id` names the
2416    /// request this reply answers (this client's GID and the sequence number
2417    /// of its request), `source_timestamp` is when the SERVER sent the reply,
2418    /// on its clock, and `received_timestamp` is when this client received it.
2419    /// A field the backend cannot report is `None`, never a zero — see
2420    /// [`ServiceInfo`](nros_core::ServiceInfo).
2421    pub fn create_client_with_callback_with_info<Svc, F>(
2422        &mut self,
2423        service_name: &str,
2424        callback: F,
2425    ) -> Result<ServiceClientCallback<Svc>, NodeError>
2426    where
2427        Svc: RosService + 'static,
2428        Svc::Request: MessageForRmw,
2429        Svc::Reply: MessageForRmw,
2430        F: FnMut(&Svc::Reply, &nros_core::ServiceInfo) + 'static,
2431    {
2432        self.register_client_callback::<
2433            Svc,
2434            _,
2435            { crate::config::DEFAULT_RX_BUF_SIZE },
2436            { crate::config::DEFAULT_RX_BUF_SIZE },
2437        >(service_name, super::arena::ReplyWithInfo(callback))
2438    }
2439
2440    /// The one registration behind both callback-client shapes (issue 2148).
2441    fn register_client_callback<Svc, C, const REQ_BUF: usize, const REPLY_BUF: usize>(
2442        &mut self,
2443        service_name: &str,
2444        callback: C,
2445    ) -> Result<ServiceClientCallback<Svc, REQ_BUF, REPLY_BUF>, NodeError>
2446    where
2447        Svc: RosService + 'static,
2448        Svc::Request: MessageForRmw,
2449        Svc::Reply: MessageForRmw,
2450        C: super::arena::ClientReplyCallback<Svc::Reply> + 'static,
2451    {
2452        register_type::<Svc::Request>()?;
2453        register_type::<Svc::Reply>()?;
2454        let (_id, hdr) = self
2455            .executor
2456            .register_service_client_callback::<Svc, C, REPLY_BUF>(
2457                Some(self.node_id),
2458                service_name,
2459                Svc::SERVICE_NAME,
2460                Svc::SERVICE_HASH,
2461                QoSProfile::services_default(),
2462                callback,
2463            )?;
2464        Ok(ServiceClientCallback::new(hdr))
2465    }
2466
2467    /// RFC-0041 / Phase 239.2 — callback-based action client (rclcpp
2468    /// `SendGoalOptions{goal_response_callback, feedback_callback,
2469    /// result_callback}` analogue). Goal-response / feedback / result are
2470    /// delivered to the closures at `spin_once`. Returns an
2471    /// [`ActionClientCallback`] send handle (`send_goal` / `get_result`);
2472    /// dual-mode — the `Promise`-based `create_action_client`
2473    /// is unchanged.
2474    #[allow(clippy::type_complexity)]
2475    pub fn create_action_client_with_callbacks<A, GRespF, FbF, ResF>(
2476        &mut self,
2477        action_name: &str,
2478        on_goal_response: GRespF,
2479        on_feedback: FbF,
2480        on_result: ResF,
2481    ) -> Result<ActionClientCallback<A>, NodeError>
2482    where
2483        A: RosAction + 'static,
2484        A::Goal: MessageForRmw,
2485        A::Result: MessageForRmw,
2486        A::Feedback: MessageForRmw,
2487        GRespF: FnMut(&nros_core::GoalUuid, bool) + 'static,
2488        FbF: FnMut(&nros_core::GoalUuid, &A::Feedback) + 'static,
2489        ResF: FnMut(&nros_core::GoalUuid, nros_core::GoalStatusCode, &A::Result) + 'static,
2490    {
2491        self.create_action_client_with_callbacks_sized::<
2492            A,
2493            GRespF,
2494            FbF,
2495            ResF,
2496            { crate::config::DEFAULT_RX_BUF_SIZE },
2497            { crate::config::DEFAULT_RX_BUF_SIZE },
2498            { crate::config::DEFAULT_RX_BUF_SIZE },
2499        >(action_name, on_goal_response, on_feedback, on_result)
2500    }
2501
2502    /// Callback-based action client with custom buffer sizes (Phase 239.2).
2503    #[allow(clippy::type_complexity)]
2504    pub fn create_action_client_with_callbacks_sized<
2505        A,
2506        GRespF,
2507        FbF,
2508        ResF,
2509        const GOAL_BUF: usize,
2510        const RESULT_BUF: usize,
2511        const FEEDBACK_BUF: usize,
2512    >(
2513        &mut self,
2514        action_name: &str,
2515        on_goal_response: GRespF,
2516        on_feedback: FbF,
2517        on_result: ResF,
2518    ) -> Result<ActionClientCallback<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, NodeError>
2519    where
2520        A: RosAction + 'static,
2521        A::Goal: MessageForRmw,
2522        A::Result: MessageForRmw,
2523        A::Feedback: MessageForRmw,
2524        GRespF: FnMut(&nros_core::GoalUuid, bool) + 'static,
2525        FbF: FnMut(&nros_core::GoalUuid, &A::Feedback) + 'static,
2526        ResF: FnMut(&nros_core::GoalUuid, nros_core::GoalStatusCode, &A::Result) + 'static,
2527    {
2528        register_type::<A::Goal>()?;
2529        register_type::<A::Result>()?;
2530        register_type::<A::Feedback>()?;
2531        let (_id, core) = self
2532            .executor
2533            .register_action_client_callback::<A, GRespF, FbF, ResF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>(
2534                Some(self.node_id),
2535                action_name,
2536                A::ACTION_NAME,
2537                A::ACTION_HASH,
2538                // Feedback is a stream → buffer a short QoS-depth history (Phase
2539                // 239.5). Goal-response / result are single-outstanding (gated).
2540                super::arena::DEFAULT_ACTION_FEEDBACK_DEPTH,
2541                on_goal_response,
2542                on_feedback,
2543                on_result,
2544            )?;
2545        Ok(ActionClientCallback::new(core))
2546    }
2547
2548    /// Convenient generic (type-erased) subscription — rclcpp `create_generic_*`.
2549    pub fn create_generic_subscription<F>(
2550        &mut self,
2551        topic: &str,
2552        type_name: &str,
2553        type_hash: &str,
2554        callback: F,
2555    ) -> Result<super::types::HandleId, NodeError>
2556    where
2557        F: FnMut(&[u8]) + 'static,
2558    {
2559        self.create_generic_subscription_with_qos(
2560            topic,
2561            type_name,
2562            type_hash,
2563            QoSProfile::default(),
2564            callback,
2565        )
2566    }
2567
2568    /// Issue 0306 — the same, with an explicit profile (see
2569    /// [`Self::create_generic_publisher_with_qos`] for why).
2570    pub fn create_generic_subscription_with_qos<F>(
2571        &mut self,
2572        topic: &str,
2573        type_name: &str,
2574        type_hash: &str,
2575        qos: QoSProfile,
2576        callback: F,
2577    ) -> Result<super::types::HandleId, NodeError>
2578    where
2579        F: FnMut(&[u8]) + 'static,
2580    {
2581        self.create_generic_subscription_with_qos_and_rx_hint(
2582            topic, type_name, type_hash, qos, None, callback,
2583        )
2584    }
2585
2586    /// Issue 2208 -- [`Self::create_generic_subscription_with_qos`] for a
2587    /// caller that knows the message type's bound although it registers
2588    /// type-erased (the declarative runtime, which recorded it at
2589    /// declaration). `rx_hint` reaches the backend as `rx_buffer_hint`, which
2590    /// sizes zenoh-pico's receive class and, with a declared arrival curve, the
2591    /// XRCE agent's pace (RFC-0074 "On XRCE"). `None` states nothing.
2592    pub fn create_generic_subscription_with_qos_and_rx_hint<F>(
2593        &mut self,
2594        topic: &str,
2595        type_name: &str,
2596        type_hash: &str,
2597        qos: QoSProfile,
2598        rx_hint: Option<usize>,
2599        callback: F,
2600    ) -> Result<super::types::HandleId, NodeError>
2601    where
2602        F: FnMut(&[u8]) + 'static,
2603    {
2604        self.executor
2605            .register_subscription_buffered_raw_on::<F, { crate::config::DEFAULT_RX_BUF_SIZE }>(
2606                self.node_id,
2607                topic,
2608                type_name,
2609                type_hash,
2610                qos,
2611                rx_hint,
2612                callback,
2613            )
2614    }
2615
2616    /// Phase 250 (Wave 2) — generic (type-erased) subscription that surfaces E2E
2617    /// [`IntegrityStatus`](nros_rmw::IntegrityStatus) (CRC + sequence gap/dup) to
2618    /// the callback (`FnMut(&[u8], &IntegrityStatus)`). The declarative-`Node`
2619    /// analog of the typed `.typed::<M>().safety()` builder: the validator lives
2620    /// in the `RmwSubscriber`, so the raw bytes + status arrive together without
2621    /// a typed `M`. Wired by the declarative runtime's `.safety()` opt-in.
2622    #[cfg(feature = "safety-e2e")]
2623    pub fn create_generic_subscription_with_integrity<F>(
2624        &mut self,
2625        topic: &str,
2626        type_name: &str,
2627        type_hash: &str,
2628        callback: F,
2629    ) -> Result<super::types::HandleId, NodeError>
2630    where
2631        F: FnMut(&[u8], &nros_rmw::IntegrityStatus) + 'static,
2632    {
2633        self.create_generic_subscription_with_integrity_and_rx_hint(
2634            topic, type_name, type_hash, None, callback,
2635        )
2636    }
2637
2638    /// Issue 2208 -- [`Self::create_generic_subscription_with_integrity`]
2639    /// with the message type's bound, as
2640    /// [`Self::create_generic_subscription_with_qos_and_rx_hint`].
2641    #[cfg(feature = "safety-e2e")]
2642    pub fn create_generic_subscription_with_integrity_and_rx_hint<F>(
2643        &mut self,
2644        topic: &str,
2645        type_name: &str,
2646        type_hash: &str,
2647        rx_hint: Option<usize>,
2648        callback: F,
2649    ) -> Result<super::types::HandleId, NodeError>
2650    where
2651        F: FnMut(&[u8], &nros_rmw::IntegrityStatus) + 'static,
2652    {
2653        self.executor
2654            .register_subscription_buffered_raw_safety_on::<F, { crate::config::DEFAULT_RX_BUF_SIZE }>(
2655                self.node_id,
2656                topic,
2657                type_name,
2658                type_hash,
2659                QoSProfile::default(),
2660                rx_hint,
2661                callback,
2662            )
2663    }
2664
2665    /// Convenient zero-copy subscription (Phase 229.6, issue 0007 / RFC-0033
2666    /// `view` mode).
2667    ///
2668    /// `B` is the code-generated VIEWABLE marker (e.g. `ImageViewable`, emitted
2669    /// alongside the `inline` `Image` for a `.msg` with a `view`-mode field).
2670    /// The marker is zero-sized and carries NO lifetime, which is the whole
2671    /// reason it exists: `B::View<'a>` does, and a generic parameter cannot be
2672    /// the lifetime-carrying type itself. That is also why phase-390 could not
2673    /// simply rename it to `{Msg}View` — the view struct already owns that
2674    /// name, and the marker points AT it.
2675    ///
2676    /// The callback receives `&B::View<'a>` — a message whose unbounded
2677    /// sequence/string fields point directly into the receive buffer (no
2678    /// `heapless::Vec` copy); valid only for the callback's duration.
2679    ///
2680    /// Uses `KEEP_LAST(1)` QoS → triple buffer, as view subscriptions require
2681    /// (a single well-defined slot for the callback). For an explicit deeper
2682    /// queue use the `inline` [`create_subscription`](Self::create_subscription);
2683    /// a view subscription registered with `KEEP_LAST(N>1)` is rejected.
2684    pub fn create_subscription_viewable<B, F>(
2685        &mut self,
2686        topic: &str,
2687        callback: F,
2688    ) -> Result<super::types::HandleId, NodeError>
2689    where
2690        B: nros_core::ViewableMessage + 'static,
2691        F: for<'a> FnMut(&B::View<'a>) + 'static,
2692    {
2693        self.executor
2694            .register_subscription_buffered_borrowed_on::<B, F, { crate::config::DEFAULT_RX_BUF_SIZE }>(
2695                self.node_id,
2696                topic,
2697                QoSProfile::default().keep_last(1),
2698                callback,
2699            )
2700    }
2701
2702    /// Service-server builder (the `clone` tier) — `node.service(name)`.
2703    /// Set `.qos()` (defaults to the services profile = RELIABLE+VOLATILE+
2704    /// KEEP_LAST(10)), then `.build::<Svc, _>(callback)` (Phase 193.2).
2705    pub fn service<'t>(&mut self, name: &'t str) -> CtxServiceBuilder<'_, 'e, 't, 's> {
2706        CtxServiceBuilder {
2707            ctx: self,
2708            name,
2709            qos: QoSProfile::services_default(),
2710        }
2711    }
2712
2713    /// Convenient service server (the `fork` tier — rclrs/rclcpp shape), default
2714    /// services QoS. Mirror of `create_subscription`.
2715    ///
2716    /// phase-515 R1 — `options` is rclrs's `impl IntoPrimitiveOptions`; a bare
2717    /// name keeps the services profile. ADOPT-BOUNDED (service): returns the
2718    /// entity's `HandleId`, not an owned rclrs `Service` — the entity lives in
2719    /// the executor's table.
2720    pub fn create_service<'o, Svc, F>(
2721        &mut self,
2722        options: impl crate::IntoPrimitiveOptions<'o>,
2723        callback: F,
2724    ) -> Result<super::types::HandleId, NodeError>
2725    where
2726        Svc: RosService + 'static,
2727        Svc::Request: crate::rmw_type_registry::MessageForRmw,
2728        Svc::Reply: crate::rmw_type_registry::MessageForRmw,
2729        F: FnMut(&Svc::Request) -> Svc::Reply + 'static,
2730    {
2731        let options = options.into_primitive_options();
2732        self.executor.register_service_sized_on::<
2733            Svc,
2734            F,
2735            { crate::config::DEFAULT_RX_BUF_SIZE },
2736            { crate::config::DEFAULT_RX_BUF_SIZE },
2737        >(
2738            self.node_id,
2739            options.name,
2740            options.resolve(QoSProfile::services_default()),
2741            callback,
2742        )
2743    }
2744
2745    /// [`create_service`](Self::create_service) whose callback also receives
2746    /// the request's [`ServiceInfo`](nros_core::ServiceInfo) — rclrs's
2747    /// `(request, ServiceInfo)` service callback (issue 1942).
2748    ///
2749    /// The info says who sent the request (client GID + the client's sequence
2750    /// number) and when. Which fields arrive depends on the backend; one it
2751    /// cannot report is `None`, never a zero — see
2752    /// [`ServiceInfo`](nros_core::ServiceInfo).
2753    pub fn create_service_with_info<Svc, F>(
2754        &mut self,
2755        name: &str,
2756        callback: F,
2757    ) -> Result<super::types::HandleId, NodeError>
2758    where
2759        Svc: RosService + 'static,
2760        Svc::Request: crate::rmw_type_registry::MessageForRmw,
2761        Svc::Reply: crate::rmw_type_registry::MessageForRmw,
2762        F: FnMut(&Svc::Request, &nros_core::ServiceInfo) -> Svc::Reply + 'static,
2763    {
2764        self.service(name).service_info().build::<Svc, F>(callback)
2765    }
2766
2767    // ====================================================================
2768    // Graph introspection — RFC-0036, phase-444.
2769    //
2770    // Upstream hangs the graph family off the NODE (`rclrs::Node::
2771    // get_node_names`, `rcl_get_node_names(node, ...)`). We open ONE transport
2772    // session per image and the Executor owns it, so the capability lives on
2773    // [`Executor`](super::spin::Executor) — see phase-381 W4. These are the
2774    // forwarders that let a ported node keep upstream's receiver and upstream's
2775    // verb, the same forwarders phase-417 stage 2b wrote for C++ `nros::Node`.
2776    //
2777    // They add no behaviour: each is a one-line delegation to the executor
2778    // this handle already borrows.
2779    //
2780    // THE CONTRACT EVERY ONE OF THEM KEEPS (RFC-0036 §"Static CONNECTION,
2781    // dynamic GRAPH"), stated once here and referenced from each:
2782    //
2783    //  * It reports what has been DISCOVERED, and NEVER BLOCKS. The backend
2784    //    keeps a standing view fed by `spin`, so a first call legitimately
2785    //    returns a partial graph and the view fills in over successive calls.
2786    //    Code waiting for a peer must poll, not call once and conclude.
2787    //  * An empty answer is "nobody seen yet", NEVER "nobody exists".
2788    //  * `Err(Transport(Unsupported))` — a backend that cannot tell you, such
2789    //    as XRCE — is a DIFFERENT answer from empty, and must not be collapsed
2790    //    into zero or into an empty enumeration.
2791    //
2792    // SHAPE DIVERGENCE from rclrs, and why: rclrs returns owned heap
2793    // containers (`Vec<NodeNameInfo>`, `TopicNamesAndTypes`). The graph has no
2794    // bound the caller can know, so a caller-sized buffer is an unanswerable
2795    // question and an owned container needs an allocator a 128 KiB target does
2796    // not have. We stream through a visitor instead, which costs one entry of
2797    // peak RAM. Every `&str` handed to a visitor is BORROWED for the duration
2798    // of that call — copy anything you keep. Returning `false` stops the walk.
2799    // ====================================================================
2800
2801    /// Every node on the graph, with its namespace — rclrs's
2802    /// `Node::get_node_names`.
2803    ///
2804    /// `visit(name, namespace)` once per node. For the enclave as well, use
2805    /// [`get_node_names_with_enclaves`](Self::get_node_names_with_enclaves):
2806    /// one backend slot answers both, exactly as `rmw_get_node_names` and
2807    /// `rmw_get_node_names_with_enclaves` are one query upstream.
2808    ///
2809    /// Reports what has been DISCOVERED and never blocks, so an empty
2810    /// enumeration means "nobody seen yet", not "nobody exists";
2811    /// `Err(Transport(Unsupported))` from a backend with no graph is a
2812    /// different answer from empty. See the module note above.
2813    pub fn get_node_names(
2814        &mut self,
2815        visit: &mut dyn FnMut(&str, &str) -> bool,
2816    ) -> Result<(), NodeError> {
2817        self.executor
2818            .get_node_names(&mut |name, ns, _enclave| visit(name, ns))
2819    }
2820
2821    /// Every node on the graph with its namespace AND its enclave — rclrs's
2822    /// `Node::get_node_names_with_enclaves`.
2823    ///
2824    /// `visit(name, namespace, enclave)`. `enclave` is `None` when the backend
2825    /// reports the node but not its enclave, which is a partial answer rather
2826    /// than an error — reporting the node without the enclave beats dropping
2827    /// it. Same discovery envelope as [`get_node_names`](Self::get_node_names).
2828    pub fn get_node_names_with_enclaves(
2829        &mut self,
2830        visit: &mut dyn FnMut(&str, &str, Option<&str>) -> bool,
2831    ) -> Result<(), NodeError> {
2832        self.executor.get_node_names(visit)
2833    }
2834
2835    /// Every topic on the graph, with the types on it — rclrs's
2836    /// `Node::get_topic_names_and_types`.
2837    ///
2838    /// `visit(topic_name, types)` once per distinct TOPIC: a topic carrying
2839    /// two types is one call with two entries, not two calls. `types` may
2840    /// legitimately be empty on a partially discovered graph. Same discovery
2841    /// envelope as [`get_node_names`](Self::get_node_names).
2842    pub fn get_topic_names_and_types(
2843        &mut self,
2844        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2845    ) -> Result<(), NodeError> {
2846        self.executor.get_topic_names_and_types(visit)
2847    }
2848
2849    /// Every service on the graph, with its types — rclrs's
2850    /// `Node::get_service_names_and_types`. As
2851    /// [`get_topic_names_and_types`](Self::get_topic_names_and_types), over
2852    /// servers and clients.
2853    pub fn get_service_names_and_types(
2854        &mut self,
2855        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2856    ) -> Result<(), NodeError> {
2857        self.executor.get_service_names_and_types(visit)
2858    }
2859
2860    /// How many publishers are visible on `topic_name` — rclrs's
2861    /// `Node::count_publishers`.
2862    ///
2863    /// **This is the GRAPH's count, not this node's.** The number of
2864    /// publishers this node itself DECLARED is
2865    /// [`StandaloneNode::publisher_count`](crate::StandaloneNode), a different
2866    /// question with a different answer.
2867    ///
2868    /// A count reflects what has been DISCOVERED, so it can be low right after
2869    /// startup and is never a proof of absence. See
2870    /// [`get_node_names`](Self::get_node_names).
2871    pub fn count_publishers(&mut self, topic_name: &str) -> Result<usize, NodeError> {
2872        self.executor.count_publishers(topic_name)
2873    }
2874
2875    /// How many subscriptions are visible on `topic_name` — rclrs's
2876    /// `Node::count_subscriptions`.
2877    ///
2878    /// **`subscriptions`, not `subscribers`** — this is rclrs's spelling, and
2879    /// the Rust surface takes its vocabulary from rclrs so a user porting Rust
2880    /// ROS 2 code types what they already know. The C surface says
2881    /// `subscribers` because rcl does, and the vtable slot says `subscribers`
2882    /// because upstream rmw does. Three layers, three upstreams, one word each
2883    /// — not drift.
2884    ///
2885    /// The graph's count, not this node's; see
2886    /// [`count_publishers`](Self::count_publishers) for that distinction and
2887    /// for the discovery caveats.
2888    pub fn count_subscriptions(&mut self, topic_name: &str) -> Result<usize, NodeError> {
2889        self.executor.count_subscribers(topic_name)
2890    }
2891
2892    /// How many service clients are visible on `service_name` — rclcpp's
2893    /// (Jazzy) `Node::count_clients`; rclrs has no counterpart. Issue 1978.
2894    ///
2895    /// The graph's count, not this node's; see
2896    /// [`count_publishers`](Self::count_publishers) for that distinction and
2897    /// for the discovery caveats.
2898    pub fn count_clients(&mut self, service_name: &str) -> Result<usize, NodeError> {
2899        self.executor.count_clients(service_name)
2900    }
2901
2902    /// How many service servers are visible on `service_name` — rclcpp's
2903    /// (Jazzy) `Node::count_services`. See [`count_clients`](Self::count_clients).
2904    pub fn count_services(&mut self, service_name: &str) -> Result<usize, NodeError> {
2905        self.executor.count_services(service_name)
2906    }
2907
2908    /// What one named node PUBLISHES, with the types — rclrs's
2909    /// `Node::get_publisher_names_and_types_by_node`.
2910    ///
2911    /// A node the graph has not discovered yields no visits, which is not an
2912    /// error — see [`get_node_names`](Self::get_node_names) for why an empty
2913    /// answer means "not seen yet".
2914    pub fn get_publisher_names_and_types_by_node(
2915        &mut self,
2916        node_name: &str,
2917        node_namespace: &str,
2918        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2919    ) -> Result<(), NodeError> {
2920        self.executor
2921            .get_publisher_names_and_types_by_node(node_name, node_namespace, visit)
2922    }
2923
2924    /// What one named node SUBSCRIBES to, with the types — rclrs's
2925    /// `Node::get_subscription_names_and_types_by_node`. See
2926    /// [`count_subscriptions`](Self::count_subscriptions) on the spelling.
2927    pub fn get_subscription_names_and_types_by_node(
2928        &mut self,
2929        node_name: &str,
2930        node_namespace: &str,
2931        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2932    ) -> Result<(), NodeError> {
2933        self.executor
2934            .get_subscription_names_and_types_by_node(node_name, node_namespace, visit)
2935    }
2936
2937    /// What services one named node SERVES, with the types — rclrs's
2938    /// `Node::get_service_names_and_types_by_node`.
2939    pub fn get_service_names_and_types_by_node(
2940        &mut self,
2941        node_name: &str,
2942        node_namespace: &str,
2943        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2944    ) -> Result<(), NodeError> {
2945        self.executor
2946            .get_service_names_and_types_by_node(node_name, node_namespace, visit)
2947    }
2948
2949    /// What services one named node CALLS, with the types — rclrs's
2950    /// `Node::get_client_names_and_types_by_node`.
2951    pub fn get_client_names_and_types_by_node(
2952        &mut self,
2953        node_name: &str,
2954        node_namespace: &str,
2955        visit: &mut dyn FnMut(&str, &[&str]) -> bool,
2956    ) -> Result<(), NodeError> {
2957        self.executor
2958            .get_client_names_and_types_by_node(node_name, node_namespace, visit)
2959    }
2960
2961    /// The PUBLISHERS on `topic_name`, one visit each — rclrs's
2962    /// `Node::get_publishers_info_by_topic`.
2963    ///
2964    /// The last two members of the graph family to reach upstream's receiver.
2965    /// `visit(info)` per discovered endpoint; every `&str` on the
2966    /// [`GraphEndpointInfo`](nros_rmw::GraphEndpointInfo) is BORROWED for the
2967    /// duration of that call, so copy anything you keep, and returning `false`
2968    /// stops the walk.
2969    ///
2970    /// ADOPT-BOUNDED: rclrs returns an owned `Vec<TopicEndpointInfo>`; here the
2971    /// answer is STREAMED to `visit`, because the graph has no bound a caller could
2972    /// size a buffer to and there is no allocator for a `Vec`. It reports what has
2973    /// been DISCOVERED and never blocks, so an empty walk means "nobody seen yet";
2974    /// a backend with no graph answers `Err(Transport(Unsupported))`, never an
2975    /// empty `Ok`.
2976    ///
2977    /// It carries NO QoS, and that absence is the point: the GRANTED profile is
2978    /// what answers "why is nothing arriving", no backend can read a remote's
2979    /// back yet, and reporting the remote's DECLARED profile instead would be
2980    /// the confident wrong answer the field exists to avoid.
2981    pub fn get_publishers_info_by_topic(
2982        &mut self,
2983        topic_name: &str,
2984        visit: &mut dyn FnMut(&nros_rmw::GraphEndpointInfo<'_>) -> bool,
2985    ) -> Result<(), NodeError> {
2986        self.executor
2987            .get_publishers_info_by_topic(topic_name, visit)
2988    }
2989
2990    /// The SUBSCRIPTIONS on `topic_name`, one visit each — rclrs's
2991    /// `Node::get_subscriptions_info_by_topic`. See
2992    /// [`get_publishers_info_by_topic`](Self::get_publishers_info_by_topic).
2993    ///
2994    /// Same envelope: STREAMED to `visit` rather than returned as a `Vec`, every
2995    /// `&str` borrowed for the visit, discovered-only and non-blocking, and
2996    /// `Err(Transport(Unsupported))` from a backend with no graph.
2997    pub fn get_subscriptions_info_by_topic(
2998        &mut self,
2999        topic_name: &str,
3000        visit: &mut dyn FnMut(&nros_rmw::GraphEndpointInfo<'_>) -> bool,
3001    ) -> Result<(), NodeError> {
3002        self.executor
3003            .get_subscriptions_info_by_topic(topic_name, visit)
3004    }
3005
3006    // ------------------------------------------------------------------
3007    // phase-483 W2 — the constructors that only `NodeHandle` had. `Node`
3008    // is the ONE node type, so it carries them too; each forwards through
3009    // `Executor::with_node_try`, which opens a `NodeHandle` on this node's
3010    // session for the duration of the call. The polled subscription and
3011    // service constructors are `create_polling_*`: rclrs's
3012    // `create_subscription` / `create_service` take a callback, and this type
3013    // already has those under exactly those names.
3014    // ------------------------------------------------------------------
3015
3016    /// [`create_publisher`](Self::create_publisher) with an explicit profile (phase-483 W2).
3017    ///
3018    /// Through [`NodeHandle::create_publisher_with_qos`], so a publisher made
3019    /// here gets what one made on the handle `Executor::create_node` used to
3020    /// return: the node's remap rules (launch, then `--ros-args`), the plan's
3021    /// QoS overrides and the declared-QoS check. `Executor::create_publisher_on`
3022    /// takes a name its caller already resolved, which is right for the C and
3023    /// component roads and wrong for a user's source name.
3024    pub fn create_publisher_with_qos<M: MessageForRmw>(
3025        &mut self,
3026        topic_name: &str,
3027        qos: QoSProfile,
3028    ) -> Result<EmbeddedPublisher<M>, NodeError> {
3029        self.executor.with_node_try(self.node_id, |h| {
3030            h.create_publisher_with_qos::<M>(topic_name, qos)
3031        })
3032    }
3033
3034    /// [`NodeHandle::create_publisher_raw`] on this node (phase-483 W2).
3035    pub fn create_publisher_raw(
3036        &mut self,
3037        topic_name: &str,
3038        type_name: &str,
3039        type_hash: &str,
3040    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
3041        self.executor.with_node_try(self.node_id, |h| {
3042            h.create_publisher_raw(topic_name, type_name, type_hash)
3043        })
3044    }
3045
3046    /// [`NodeHandle::create_publisher_raw_with_qos`] on this node (phase-483 W2).
3047    pub fn create_publisher_raw_with_qos(
3048        &mut self,
3049        topic_name: &str,
3050        type_name: &str,
3051        type_hash: &str,
3052        qos: QoSProfile,
3053    ) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
3054        self.executor.with_node_try(self.node_id, |h| {
3055            h.create_publisher_raw_with_qos(topic_name, type_name, type_hash, qos)
3056        })
3057    }
3058
3059    /// The POLLED form of [`NodeHandle::create_subscription`] (phase-483 W2): the caller
3060    /// takes from it, where rclrs's `create_subscription` takes a callback.
3061    pub fn create_polling_subscription<M: MessageForRmw>(
3062        &mut self,
3063        topic_name: &str,
3064    ) -> Result<Subscription<M>, NodeError> {
3065        self.executor
3066            .with_node_try(self.node_id, |h| h.create_subscription::<M>(topic_name))
3067    }
3068
3069    /// The POLLED form of [`NodeHandle::create_subscription_sized`] (phase-483 W2): the caller
3070    /// takes from it, where rclrs's `create_subscription` takes a callback.
3071    pub fn create_polling_subscription_sized<M: MessageForRmw, const RX_BUF: usize>(
3072        &mut self,
3073        topic_name: &str,
3074    ) -> Result<Subscription<M, RX_BUF>, NodeError> {
3075        self.executor.with_node_try(self.node_id, |h| {
3076            h.create_subscription_sized::<M, RX_BUF>(topic_name)
3077        })
3078    }
3079
3080    /// The POLLED form of [`NodeHandle::create_subscription_with_qos`] (phase-483 W2): the caller
3081    /// takes from it, where rclrs's `create_subscription` takes a callback.
3082    pub fn create_polling_subscription_with_qos<M: MessageForRmw, const RX_BUF: usize>(
3083        &mut self,
3084        topic_name: &str,
3085        qos: QoSProfile,
3086    ) -> Result<Subscription<M, RX_BUF>, NodeError> {
3087        self.executor.with_node_try(self.node_id, |h| {
3088            h.create_subscription_with_qos::<M, RX_BUF>(topic_name, qos)
3089        })
3090    }
3091
3092    /// The POLLED form of [`NodeHandle::create_subscription_raw`] (phase-483 W2): the caller
3093    /// takes from it, where rclrs's `create_subscription` takes a callback.
3094    pub fn create_polling_subscription_raw(
3095        &mut self,
3096        topic_name: &str,
3097        type_name: &str,
3098        type_hash: &str,
3099    ) -> Result<crate::executor::handles::RawSubscription, NodeError> {
3100        self.executor.with_node_try(self.node_id, |h| {
3101            h.create_subscription_raw(topic_name, type_name, type_hash)
3102        })
3103    }
3104
3105    /// The POLLED form of [`NodeHandle::create_subscription_raw_sized`] (phase-483 W2): the caller
3106    /// takes from it, where rclrs's `create_subscription` takes a callback.
3107    pub fn create_polling_subscription_raw_sized<const RX_BUF: usize>(
3108        &mut self,
3109        topic_name: &str,
3110        type_name: &str,
3111        type_hash: &str,
3112    ) -> Result<crate::executor::handles::RawSubscription<RX_BUF>, NodeError> {
3113        self.executor.with_node_try(self.node_id, |h| {
3114            h.create_subscription_raw_sized::<RX_BUF>(topic_name, type_name, type_hash)
3115        })
3116    }
3117
3118    /// The POLLED form of [`NodeHandle::create_service`] (phase-483 W2): the caller
3119    /// takes from it, where rclrs's `create_service` takes a callback.
3120    pub fn create_polling_service<Svc: RosService>(
3121        &mut self,
3122        service_name: &str,
3123    ) -> Result<EmbeddedServiceServer<Svc>, NodeError>
3124    where
3125        Svc::Request: MessageForRmw,
3126        Svc::Reply: MessageForRmw,
3127    {
3128        self.executor
3129            .with_node_try(self.node_id, |h| h.create_service::<Svc>(service_name))
3130    }
3131
3132    /// The POLLED form of [`NodeHandle::create_service_with_qos`] (phase-483 W2): the caller
3133    /// takes from it, where rclrs's `create_service` takes a callback.
3134    pub fn create_polling_service_with_qos<Svc: RosService>(
3135        &mut self,
3136        service_name: &str,
3137        qos: QoSProfile,
3138    ) -> Result<EmbeddedServiceServer<Svc>, NodeError>
3139    where
3140        Svc::Request: MessageForRmw,
3141        Svc::Reply: MessageForRmw,
3142    {
3143        self.executor.with_node_try(self.node_id, |h| {
3144            h.create_service_with_qos::<Svc>(service_name, qos)
3145        })
3146    }
3147
3148    /// The POLLED form of [`NodeHandle::create_service_sized`] (phase-483 W2): the caller
3149    /// takes from it, where rclrs's `create_service` takes a callback.
3150    pub fn create_polling_service_sized<
3151        Svc: RosService,
3152        const REQ_BUF: usize,
3153        const REPLY_BUF: usize,
3154    >(
3155        &mut self,
3156        service_name: &str,
3157        qos: QoSProfile,
3158    ) -> Result<EmbeddedServiceServer<Svc, REQ_BUF, REPLY_BUF>, NodeError>
3159    where
3160        Svc::Request: MessageForRmw,
3161        Svc::Reply: MessageForRmw,
3162    {
3163        self.executor.with_node_try(self.node_id, |h| {
3164            h.create_service_sized::<Svc, REQ_BUF, REPLY_BUF>(service_name, qos)
3165        })
3166    }
3167
3168    /// [`NodeHandle::create_client`] on this node (phase-483 W2).
3169    ///
3170    /// phase-515 R1 — `options` is rclrs's `impl IntoPrimitiveOptions`; a bare
3171    /// name keeps the services profile. ADOPT-BOUNDED (client): returns an
3172    /// `EmbeddedServiceClient`, the executor-table handle, where rclrs returns
3173    /// a reference-counted `Client`.
3174    pub fn create_client<'o, Svc: RosService>(
3175        &mut self,
3176        options: impl crate::IntoPrimitiveOptions<'o>,
3177    ) -> Result<EmbeddedServiceClient<Svc>, NodeError>
3178    where
3179        Svc::Request: MessageForRmw,
3180        Svc::Reply: MessageForRmw,
3181    {
3182        let options = options.into_primitive_options();
3183        let qos = options.resolve(QoSProfile::services_default());
3184        self.executor.with_node_try(self.node_id, |h| {
3185            h.create_client_with_qos::<Svc>(options.name, qos)
3186        })
3187    }
3188
3189    /// [`NodeHandle::create_client_with_qos`] on this node (phase-483 W2).
3190    pub fn create_client_with_qos<Svc: RosService>(
3191        &mut self,
3192        service_name: &str,
3193        qos: QoSProfile,
3194    ) -> Result<EmbeddedServiceClient<Svc>, NodeError>
3195    where
3196        Svc::Request: MessageForRmw,
3197        Svc::Reply: MessageForRmw,
3198    {
3199        self.executor.with_node_try(self.node_id, |h| {
3200            h.create_client_with_qos::<Svc>(service_name, qos)
3201        })
3202    }
3203
3204    /// [`NodeHandle::create_client_sized`] on this node (phase-483 W2).
3205    pub fn create_client_sized<Svc: RosService, const REQ_BUF: usize, const REPLY_BUF: usize>(
3206        &mut self,
3207        service_name: &str,
3208        qos: QoSProfile,
3209    ) -> Result<EmbeddedServiceClient<Svc, REQ_BUF, REPLY_BUF>, NodeError>
3210    where
3211        Svc::Request: MessageForRmw,
3212        Svc::Reply: MessageForRmw,
3213    {
3214        self.executor.with_node_try(self.node_id, |h| {
3215            h.create_client_sized::<Svc, REQ_BUF, REPLY_BUF>(service_name, qos)
3216        })
3217    }
3218
3219    /// The POLLED form of [`NodeHandle::create_service_raw`] (phase-483 W2): the caller
3220    /// takes from it, where rclrs's `create_service` takes a callback.
3221    pub fn create_polling_service_raw(
3222        &mut self,
3223        service_name: &str,
3224        type_name: &str,
3225        type_hash: &str,
3226    ) -> Result<crate::executor::handles::RawServiceServer, NodeError> {
3227        self.executor.with_node_try(self.node_id, |h| {
3228            h.create_service_raw(service_name, type_name, type_hash)
3229        })
3230    }
3231
3232    /// The POLLED form of [`NodeHandle::create_service_raw_sized`] (phase-483 W2): the caller
3233    /// takes from it, where rclrs's `create_service` takes a callback.
3234    pub fn create_polling_service_raw_sized<const REQ_BUF: usize, const RESP_BUF: usize>(
3235        &mut self,
3236        service_name: &str,
3237        type_name: &str,
3238        type_hash: &str,
3239    ) -> Result<crate::executor::handles::RawServiceServer<REQ_BUF, RESP_BUF>, NodeError> {
3240        self.executor.with_node_try(self.node_id, |h| {
3241            h.create_service_raw_sized::<REQ_BUF, RESP_BUF>(service_name, type_name, type_hash)
3242        })
3243    }
3244
3245    /// [`NodeHandle::create_client_raw`] on this node (phase-483 W2).
3246    pub fn create_client_raw(
3247        &mut self,
3248        service_name: &str,
3249        type_name: &str,
3250        type_hash: &str,
3251    ) -> Result<crate::executor::handles::RawServiceClient, NodeError> {
3252        self.executor.with_node_try(self.node_id, |h| {
3253            h.create_client_raw(service_name, type_name, type_hash)
3254        })
3255    }
3256
3257    /// [`NodeHandle::create_client_raw_sized`] on this node (phase-483 W2).
3258    pub fn create_client_raw_sized<const REQ_BUF: usize, const REPLY_BUF: usize>(
3259        &mut self,
3260        service_name: &str,
3261        type_name: &str,
3262        type_hash: &str,
3263    ) -> Result<crate::executor::handles::RawServiceClient<REQ_BUF, REPLY_BUF>, NodeError> {
3264        self.executor.with_node_try(self.node_id, |h| {
3265            h.create_client_raw_sized::<REQ_BUF, REPLY_BUF>(service_name, type_name, type_hash)
3266        })
3267    }
3268
3269    /// [`NodeHandle::create_action_server_raw`] on this node (phase-483 W2).
3270    pub fn create_action_server_raw(
3271        &mut self,
3272        action_name: &str,
3273        type_name: &str,
3274        type_hash: &str,
3275    ) -> Result<
3276        super::action_core::ActionServerCore<
3277            { crate::config::DEFAULT_RX_BUF_SIZE },
3278            { crate::config::DEFAULT_RX_BUF_SIZE },
3279            { crate::config::DEFAULT_RX_BUF_SIZE },
3280            4,
3281        >,
3282        NodeError,
3283    > {
3284        self.executor.with_node_try(self.node_id, |h| {
3285            h.create_action_server_raw(action_name, type_name, type_hash)
3286        })
3287    }
3288
3289    /// [`NodeHandle::create_action_server_raw_sized`] on this node (phase-483 W2).
3290    pub fn create_action_server_raw_sized<
3291        const GOAL_BUF: usize,
3292        const RESULT_BUF: usize,
3293        const FEEDBACK_BUF: usize,
3294        const MAX_GOALS: usize,
3295    >(
3296        &mut self,
3297        action_name: &str,
3298        type_name: &str,
3299        type_hash: &str,
3300    ) -> Result<
3301        super::action_core::ActionServerCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
3302        NodeError,
3303    > {
3304        self.executor.with_node_try(self.node_id, |h| {
3305            h.create_action_server_raw_sized::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>(
3306                action_name,
3307                type_name,
3308                type_hash,
3309            )
3310        })
3311    }
3312
3313    /// [`NodeHandle::create_action_client_raw`] on this node (phase-483 W2).
3314    pub fn create_action_client_raw(
3315        &mut self,
3316        action_name: &str,
3317        type_name: &str,
3318        type_hash: &str,
3319    ) -> Result<
3320        super::action_core::ActionClientCore<
3321            { crate::config::DEFAULT_RX_BUF_SIZE },
3322            { crate::config::DEFAULT_RX_BUF_SIZE },
3323            { crate::config::DEFAULT_RX_BUF_SIZE },
3324        >,
3325        NodeError,
3326    > {
3327        self.executor.with_node_try(self.node_id, |h| {
3328            h.create_action_client_raw(action_name, type_name, type_hash)
3329        })
3330    }
3331
3332    /// [`NodeHandle::create_action_client_raw_sized`] on this node (phase-483 W2).
3333    pub fn create_action_client_raw_sized<
3334        const GOAL_BUF: usize,
3335        const RESULT_BUF: usize,
3336        const FEEDBACK_BUF: usize,
3337    >(
3338        &mut self,
3339        action_name: &str,
3340        type_name: &str,
3341        type_hash: &str,
3342    ) -> Result<super::action_core::ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, NodeError>
3343    {
3344        self.executor.with_node_try(self.node_id, |h| {
3345            h.create_action_client_raw_sized::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>(
3346                action_name,
3347                type_name,
3348                type_hash,
3349            )
3350        })
3351    }
3352
3353    /// [`NodeHandle::create_action_server`] on this node (phase-483 W2).
3354    pub fn create_action_server<A: RosAction>(
3355        &mut self,
3356        action_name: &str,
3357    ) -> Result<ActionServer<A>, NodeError>
3358    where
3359        A::Goal: MessageForRmw,
3360        A::Result: MessageForRmw,
3361        A::Feedback: MessageForRmw,
3362        A::SendGoalRequest: MessageForRmw,
3363        A::SendGoalResponse: MessageForRmw,
3364        A::GetResultRequest: MessageForRmw,
3365        A::GetResultResponse: MessageForRmw,
3366        A::FeedbackMessage: MessageForRmw,
3367    {
3368        self.executor
3369            .with_node_try(self.node_id, |h| h.create_action_server::<A>(action_name))
3370    }
3371
3372    /// [`NodeHandle::create_action_server_sized`] on this node (phase-483 W2).
3373    pub fn create_action_server_sized<
3374        A: RosAction,
3375        const GOAL_BUF: usize,
3376        const RESULT_BUF: usize,
3377        const FEEDBACK_BUF: usize,
3378        const MAX_GOALS: usize,
3379    >(
3380        &mut self,
3381        action_name: &str,
3382    ) -> Result<ActionServer<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>, NodeError>
3383    where
3384        A::Goal: MessageForRmw,
3385        A::Result: MessageForRmw,
3386        A::Feedback: MessageForRmw,
3387        A::SendGoalRequest: MessageForRmw,
3388        A::SendGoalResponse: MessageForRmw,
3389        A::GetResultRequest: MessageForRmw,
3390        A::GetResultResponse: MessageForRmw,
3391        A::FeedbackMessage: MessageForRmw,
3392    {
3393        self.executor.with_node_try(self.node_id, |h| {
3394            h.create_action_server_sized::<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>(
3395                action_name,
3396            )
3397        })
3398    }
3399
3400    /// [`NodeHandle::create_action_client`] on this node (phase-483 W2).
3401    pub fn create_action_client<A: RosAction>(
3402        &mut self,
3403        action_name: &str,
3404    ) -> Result<ActionClient<A>, NodeError>
3405    where
3406        A::Goal: MessageForRmw,
3407        A::Result: MessageForRmw,
3408        A::Feedback: MessageForRmw,
3409        A::SendGoalRequest: MessageForRmw,
3410        A::SendGoalResponse: MessageForRmw,
3411        A::GetResultRequest: MessageForRmw,
3412        A::GetResultResponse: MessageForRmw,
3413        A::FeedbackMessage: MessageForRmw,
3414    {
3415        self.executor
3416            .with_node_try(self.node_id, |h| h.create_action_client::<A>(action_name))
3417    }
3418
3419    /// [`NodeHandle::create_action_client_sized`] on this node (phase-483 W2).
3420    pub fn create_action_client_sized<
3421        A: RosAction,
3422        const GOAL_BUF: usize,
3423        const RESULT_BUF: usize,
3424        const FEEDBACK_BUF: usize,
3425    >(
3426        &mut self,
3427        action_name: &str,
3428    ) -> Result<ActionClient<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, NodeError>
3429    where
3430        A::Goal: MessageForRmw,
3431        A::Result: MessageForRmw,
3432        A::Feedback: MessageForRmw,
3433        A::SendGoalRequest: MessageForRmw,
3434        A::SendGoalResponse: MessageForRmw,
3435        A::GetResultRequest: MessageForRmw,
3436        A::GetResultResponse: MessageForRmw,
3437        A::FeedbackMessage: MessageForRmw,
3438    {
3439        self.executor.with_node_try(self.node_id, |h| {
3440            h.create_action_client_sized::<A, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>(action_name)
3441        })
3442    }
3443}
3444
3445/// Service-server builder on a [`Node`] — `node.service(name)`.
3446pub struct CtxServiceBuilder<'c, 'e, 't, 's> {
3447    ctx: &'c mut Node<'e, 's>,
3448    name: &'t str,
3449    qos: QoSProfile,
3450}
3451
3452impl<'c, 'e, 't, 's> CtxServiceBuilder<'c, 'e, 't, 's> {
3453    /// Service QoS (applies to both the request + reply endpoints). Defaults to
3454    /// `QoSProfile::services_default()`.
3455    pub fn qos(mut self, qos: QoSProfile) -> Self {
3456        self.qos = qos;
3457        self
3458    }
3459
3460    /// Surface the request's [`ServiceInfo`](nros_core::ServiceInfo) to the
3461    /// callback — `FnMut(&Request, &ServiceInfo) -> Reply` (issue 1942). The
3462    /// service-side twin of the subscription builder's `.message_info()`.
3463    pub fn service_info(self) -> CtxServiceInfoBuilder<'c, 'e, 't, 's> {
3464        CtxServiceInfoBuilder {
3465            ctx: self.ctx,
3466            name: self.name,
3467            qos: self.qos,
3468        }
3469    }
3470
3471    pub fn build<Svc, F>(self, callback: F) -> Result<super::types::HandleId, NodeError>
3472    where
3473        Svc: RosService + 'static,
3474        Svc::Request: crate::rmw_type_registry::MessageForRmw,
3475        Svc::Reply: crate::rmw_type_registry::MessageForRmw,
3476        F: FnMut(&Svc::Request) -> Svc::Reply + 'static,
3477    {
3478        self.ctx.executor.register_service_sized_on::<
3479            Svc,
3480            F,
3481            { crate::config::DEFAULT_RX_BUF_SIZE },
3482            { crate::config::DEFAULT_RX_BUF_SIZE },
3483        >(self.ctx.node_id, self.name, self.qos, callback)
3484    }
3485}
3486
3487/// Service-server builder with [`ServiceInfo`](nros_core::ServiceInfo)
3488/// surfaced (`node.service(name).service_info()`). Callback is
3489/// `FnMut(&Svc::Request, &ServiceInfo) -> Svc::Reply`.
3490pub struct CtxServiceInfoBuilder<'c, 'e, 't, 's> {
3491    ctx: &'c mut Node<'e, 's>,
3492    name: &'t str,
3493    qos: QoSProfile,
3494}
3495
3496impl<'c, 'e, 't, 's> CtxServiceInfoBuilder<'c, 'e, 't, 's> {
3497    /// Service QoS (applies to both the request + reply endpoints). Defaults to
3498    /// `QoSProfile::services_default()`.
3499    pub fn qos(mut self, qos: QoSProfile) -> Self {
3500        self.qos = qos;
3501        self
3502    }
3503
3504    pub fn build<Svc, F>(self, callback: F) -> Result<super::types::HandleId, NodeError>
3505    where
3506        Svc: RosService + 'static,
3507        Svc::Request: crate::rmw_type_registry::MessageForRmw,
3508        Svc::Reply: crate::rmw_type_registry::MessageForRmw,
3509        F: FnMut(&Svc::Request, &nros_core::ServiceInfo) -> Svc::Reply + 'static,
3510    {
3511        self.ctx.executor.register_service_with_info_sized_on::<
3512            Svc,
3513            F,
3514            { crate::config::DEFAULT_RX_BUF_SIZE },
3515            { crate::config::DEFAULT_RX_BUF_SIZE },
3516        >(self.ctx.node_id, self.name, self.qos, callback)
3517    }
3518}
3519
3520/// Publisher builder on a [`Node`] — `node.publisher(topic)`.
3521pub struct CtxPublisherBuilder<'c, 'e, 't, 's> {
3522    ctx: &'c mut Node<'e, 's>,
3523    topic: &'t str,
3524    qos: QoSProfile,
3525}
3526
3527impl<'c, 'e, 't, 's> CtxPublisherBuilder<'c, 'e, 't, 's> {
3528    pub fn qos(mut self, qos: QoSProfile) -> Self {
3529        self.qos = qos;
3530        self
3531    }
3532
3533    /// Typed publisher for a ROS message `M`.
3534    pub fn typed<M: MessageForRmw>(self) -> CtxTypedPublisherBuilder<'c, 'e, 't, 's, M> {
3535        CtxTypedPublisherBuilder {
3536            ctx: self.ctx,
3537            topic: self.topic,
3538            qos: self.qos,
3539            _phantom: PhantomData,
3540        }
3541    }
3542
3543    /// Generic (type-erased) publisher.
3544    pub fn generic(
3545        self,
3546        type_name: &'t str,
3547        type_hash: &'t str,
3548    ) -> CtxGenericPublisherBuilder<'c, 'e, 't, 's> {
3549        CtxGenericPublisherBuilder {
3550            ctx: self.ctx,
3551            topic: self.topic,
3552            type_name,
3553            type_hash,
3554            qos: self.qos,
3555        }
3556    }
3557}
3558
3559/// Typed publisher builder on a `Node` (`.typed::<M>()`).
3560pub struct CtxTypedPublisherBuilder<'c, 'e, 't, 's, M> {
3561    ctx: &'c mut Node<'e, 's>,
3562    topic: &'t str,
3563    qos: QoSProfile,
3564    _phantom: PhantomData<M>,
3565}
3566
3567impl<'c, 'e, 't, 's, M: MessageForRmw> CtxTypedPublisherBuilder<'c, 'e, 't, 's, M> {
3568    pub fn qos(mut self, qos: QoSProfile) -> Self {
3569        self.qos = qos;
3570        self
3571    }
3572
3573    pub fn build(self) -> Result<EmbeddedPublisher<M>, NodeError> {
3574        self.ctx
3575            .executor
3576            .create_publisher_on::<M>(self.ctx.node_id, self.topic, self.qos)
3577    }
3578}
3579
3580/// Generic publisher builder on a `Node` (`.generic(type, hash)`).
3581pub struct CtxGenericPublisherBuilder<'c, 'e, 't, 's> {
3582    ctx: &'c mut Node<'e, 's>,
3583    topic: &'t str,
3584    type_name: &'t str,
3585    type_hash: &'t str,
3586    qos: QoSProfile,
3587}
3588
3589impl<'c, 'e, 't, 's> CtxGenericPublisherBuilder<'c, 'e, 't, 's> {
3590    pub fn qos(mut self, qos: QoSProfile) -> Self {
3591        self.qos = qos;
3592        self
3593    }
3594
3595    pub fn build(self) -> Result<crate::executor::handles::EmbeddedRawPublisher, NodeError> {
3596        self.ctx.executor.create_publisher_raw_on(
3597            self.ctx.node_id,
3598            self.topic,
3599            self.type_name,
3600            self.type_hash,
3601            self.qos,
3602        )
3603    }
3604}
3605
3606/// Subscription builder — `node.subscription(topic)`.
3607pub struct SubscriptionBuilder<'c, 'e, 't, 's> {
3608    ctx: &'c mut Node<'e, 's>,
3609    topic: &'t str,
3610    qos: QoSProfile,
3611}
3612
3613impl<'c, 'e, 't, 's> SubscriptionBuilder<'c, 'e, 't, 's> {
3614    pub fn qos(mut self, qos: QoSProfile) -> Self {
3615        self.qos = qos;
3616        self
3617    }
3618
3619    /// Typed subscription for a ROS message `M`.
3620    pub fn typed<M: MessageForRmw + 'static>(self) -> TypedSubscriptionBuilder<'c, 'e, 't, 's, M> {
3621        TypedSubscriptionBuilder {
3622            ctx: self.ctx,
3623            topic: self.topic,
3624            qos: self.qos,
3625            sched: None,
3626            rx_explicit: false,
3627            _phantom: PhantomData,
3628        }
3629    }
3630
3631    /// Generic (type-erased) subscription — raw CDR bytes to the callback.
3632    pub fn generic(
3633        self,
3634        type_name: &'t str,
3635        type_hash: &'t str,
3636    ) -> GenericSubscriptionBuilder<'c, 'e, 't, 's> {
3637        GenericSubscriptionBuilder {
3638            ctx: self.ctx,
3639            topic: self.topic,
3640            type_name,
3641            type_hash,
3642            qos: self.qos,
3643            sched: None,
3644        }
3645    }
3646}
3647
3648/// Typed subscription builder (`.typed::<M>()`). `RX` is the staging-buffer
3649/// size, set via `.rx_buffer::<N>()` (defaults to `DEFAULT_RX_BUF_SIZE`).
3650pub struct TypedSubscriptionBuilder<
3651    'c,
3652    'e,
3653    't,
3654    's,
3655    M,
3656    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
3657> {
3658    ctx: &'c mut Node<'e, 's>,
3659    topic: &'t str,
3660    qos: QoSProfile,
3661    sched: Option<super::sched_context::SchedContextId>,
3662    /// phase-392 W3c -- did the CONSUMER name `RX`, or is it just the default?
3663    ///
3664    /// The two are the same const generic and mean opposite things now: an
3665    /// unnamed `RX` is a ceiling the derivation clamps to, a named one is the
3666    /// number the consumer asked for. `.rx_buffer::<N>()` is the only thing that
3667    /// sets this, so the flag cannot drift from "somebody wrote a number".
3668    rx_explicit: bool,
3669    _phantom: PhantomData<M>,
3670}
3671
3672impl<'c, 'e, 't, 's, M: MessageForRmw + 'static, const RX: usize>
3673    TypedSubscriptionBuilder<'c, 'e, 't, 's, M, RX>
3674{
3675    pub fn qos(mut self, qos: QoSProfile) -> Self {
3676        self.qos = qos;
3677        self
3678    }
3679
3680    /// Bind the subscription's callback to a scheduling context.
3681    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
3682        self.sched = Some(sc);
3683        self
3684    }
3685
3686    /// Set the staging-buffer size (const-generic).
3687    ///
3688    /// phase-392 W3c -- **this is the opt-out.** A subscription that does not
3689    /// call it is sized from `M`'s own serialized bound; calling it says "give
3690    /// me exactly `N` bytes per slot" and the derivation is skipped. Naming a
3691    /// number is the only way to spend more arena than the type needs, which is
3692    /// the direction that should require saying so.
3693    ///
3694    /// It stays the way to raise a buffer above the type's bound -- for a peer
3695    /// that pads, or a type whose `.msg` bound is looser than what it really
3696    /// sends -- and `.rx_buffer_from_type()` after it re-enters the derivation
3697    /// with `N` as the ceiling.
3698    pub fn rx_buffer<const N: usize>(self) -> TypedSubscriptionBuilder<'c, 'e, 't, 's, M, N> {
3699        TypedSubscriptionBuilder {
3700            ctx: self.ctx,
3701            topic: self.topic,
3702            qos: self.qos,
3703            sched: self.sched,
3704            rx_explicit: true,
3705            _phantom: PhantomData,
3706        }
3707    }
3708
3709    /// Phase 403 W2 -- size the receive buffer from `M`'s OWN serialized bound
3710    /// instead of the configured default.
3711    ///
3712    /// ```ignore
3713    /// node.subscription("/chatter")
3714    ///     .typed::<Int32>()
3715    ///     .rx_buffer_from_type()   // 12 bytes per slot, not 1024
3716    ///     .build(on_msg)?;
3717    /// ```
3718    ///
3719    /// **No longer the only way to get it (phase-392 W3c).** This was opt-in,
3720    /// and its own doc said making it the default "would move every image's
3721    /// arena occupancy at once, which is a decision, not a refactor". W3c is
3722    /// that decision: a plain `.typed::<M>().build()` now derives too, wherever
3723    /// the bound is reachable, and `.rx_buffer::<N>()` is the opt-out.
3724    ///
3725    /// Two jobs are left, and they are why this method stays:
3726    ///
3727    /// * **It REFUSES an unbounded `M` at build time.** The default path cannot
3728    ///   -- erroring there would make a bound mandatory for every message type
3729    ///   in every image at once -- so it keeps `RX` for a type with no bound.
3730    ///   Calling this says the bound must exist, and the `const` assert below
3731    ///   is the error that says so.
3732    /// * **It reaches the derivation where `MessageForRmw` carries no schema.**
3733    ///   Only a backend declaring `type-descriptors` makes the type-erased
3734    ///   default site able to read a bound at all (see
3735    ///   [`default_subscription_rx_bytes`](crate::rmw_type_registry::default_subscription_rx_bytes));
3736    ///   this method names `M: nros_serdes::schema::Message` itself, so it works
3737    ///   on every backend.
3738    ///
3739    /// **What it costs and what it buys.** The buffered path stores `depth <= 1
3740    /// ? 3 : depth+1` slots of this size in the executor arena, so a bounded
3741    /// type recovers `slots * (default - bound)` bytes per subscription. The
3742    /// number is the LARGER of the type's XCDR1 and XCDR2 bounds, because the
3743    /// stack writes XCDR1 but a peer may send XCDR2 (see
3744    /// [`subscription_rx_bytes`](crate::rmw_type_registry::subscription_rx_bytes)).
3745    ///
3746    /// **An unbounded `M` fails the BUILD.** Every message type is required to
3747    /// carry a bound -- stated in the `.msg` (`string<=64`) or capped in
3748    /// `nros-codegen.toml` -- so "this type has no bound" is a defect to report,
3749    /// not a case to absorb. The alternative, quietly keeping the configured
3750    /// default, is precisely the substitution phase 380 forbids: `None` means
3751    /// "no bound EXISTS", never "unknown", and a buffer sized from a fallback is
3752    /// the failure that rule was written to prevent. The C header already refuses
3753    /// the same thing the same way -- naming an unbounded type's
3754    /// `{PREFIX}_RX_MAX_SERIALIZED_SIZE` expands to a deliberate compile error
3755    /// carrying the member that costs the bound (`unbounded_token`) -- and this
3756    /// is that refusal on the Rust path. rustc names `M` in the instantiation
3757    /// note; the member comes from the codegen diagnostic for the same type.
3758    ///
3759    /// **The bound never GROWS the buffer.** A type larger than `RX` keeps `RX`;
3760    /// spending unbudgeted arena silently is the worse failure, and the
3761    /// too-small case already reports itself (`report_dropped_take`).
3762    ///
3763    /// Returns a builder without `.message_info()` / `.safety()` on purpose:
3764    /// those entries hold a real `[u8; RX]` array, so their size can only come
3765    /// from a const generic -- `nros::rx_buffer_for!(M)` at the call site -- and
3766    /// carrying the flag into them would silently drop it.
3767    pub fn rx_buffer_from_type(self) -> TypedSubBoundBuilder<'c, 'e, 't, 's, M, RX>
3768    where
3769        M: nros_serdes::schema::Message,
3770    {
3771        // The diagnostic lives HERE, at the opt-in, rather than in `build()`:
3772        // this is the call that asserts the type has a bound, so it is the call
3773        // the error should point at.
3774        const {
3775            assert!(
3776                crate::rmw_type_registry::subscription_rx_bytes::<M>(RX).is_some(),
3777                "this message type has NO maximum serialized size, so its receive \
3778                 buffer cannot be sized from it. Every message type must carry a \
3779                 bound: give the unbounded member one in the `.msg` \
3780                 (`string<=64`, `int32[<=8]`) or a `cap` in `nros-codegen.toml`. \
3781                 The generated C header for this type names the member that costs \
3782                 it the bound (`NROS_UNBOUNDED__<type>__field_<member>`)."
3783            )
3784        }
3785        TypedSubBoundBuilder {
3786            ctx: self.ctx,
3787            topic: self.topic,
3788            qos: self.qos,
3789            sched: self.sched,
3790            _phantom: PhantomData,
3791        }
3792    }
3793
3794    /// Surface per-message [`MessageInfo`](nros_core::MessageInfo) (seq,
3795    /// publisher GID, timestamps) to the callback — `FnMut(&M, Option<&MessageInfo>)`,
3796    /// the rclrs shape. Distinct from the generic builder's `.message_info()`
3797    /// (which yields a `RawMessageInfo` with the wire attachment).
3798    pub fn message_info(self) -> TypedSubInfoBuilder<'c, 'e, 't, 's, M, RX> {
3799        TypedSubInfoBuilder {
3800            ctx: self.ctx,
3801            topic: self.topic,
3802            qos: self.qos,
3803            sched: self.sched,
3804            rx_explicit: self.rx_explicit,
3805            _phantom: PhantomData,
3806        }
3807    }
3808
3809    /// Surface E2E-safety validation (CRC + sequence gap/duplicate) to the
3810    /// callback — `FnMut(&M, &IntegrityStatus)`.
3811    #[cfg(feature = "safety-e2e")]
3812    pub fn safety(self) -> TypedSubSafetyBuilder<'c, 'e, 't, 's, M, RX> {
3813        TypedSubSafetyBuilder {
3814            ctx: self.ctx,
3815            topic: self.topic,
3816            qos: self.qos,
3817            sched: self.sched,
3818            rx_explicit: self.rx_explicit,
3819            _phantom: PhantomData,
3820        }
3821    }
3822
3823    pub fn build<F: FnMut(&M) + 'static>(
3824        self,
3825        callback: F,
3826    ) -> Result<super::types::HandleId, NodeError> {
3827        let handle = self
3828            .ctx
3829            .executor
3830            .register_subscription_buffered_on::<M, F, RX>(
3831                self.ctx.node_id,
3832                self.topic,
3833                self.qos,
3834                callback,
3835                None, // group threaded via create_subscription_in_group; builder uses sched override
3836                // phase-392 W3c -- `None` asks the choke point to DERIVE from
3837                // `M`; `Some(RX)` is the consumer's own number, and only
3838                // `.rx_buffer::<N>()` sets that flag.
3839                if self.rx_explicit { Some(RX) } else { None },
3840            )?;
3841        if let Some(sc) = self.sched {
3842            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
3843        }
3844        Ok(handle)
3845    }
3846}
3847
3848/// Phase 403 W2 -- a typed subscription whose buffer is sized by `M`'s own
3849/// serialized bound (`.typed::<M>().rx_buffer_from_type()`).
3850///
3851/// Terminal by construction: `.message_info()` and `.safety()` are absent
3852/// because their entries store `[u8; RX]` and cannot take a runtime size. `RX`
3853/// survives as the CEILING -- an unbounded `M`, or one whose bound exceeds `RX`,
3854/// gets `RX` unchanged.
3855pub struct TypedSubBoundBuilder<
3856    'c,
3857    'e,
3858    't,
3859    's,
3860    M,
3861    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
3862> {
3863    ctx: &'c mut Node<'e, 's>,
3864    topic: &'t str,
3865    qos: QoSProfile,
3866    sched: Option<super::sched_context::SchedContextId>,
3867    _phantom: PhantomData<M>,
3868}
3869
3870impl<'c, 'e, 't, 's, M: MessageForRmw + nros_serdes::schema::Message + 'static, const RX: usize>
3871    TypedSubBoundBuilder<'c, 'e, 't, 's, M, RX>
3872{
3873    pub fn qos(mut self, qos: QoSProfile) -> Self {
3874        self.qos = qos;
3875        self
3876    }
3877
3878    /// Bind the subscription's callback to a scheduling context.
3879    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
3880        self.sched = Some(sc);
3881        self
3882    }
3883
3884    pub fn build<F: FnMut(&M) + 'static>(
3885        self,
3886        callback: F,
3887    ) -> Result<super::types::HandleId, NodeError> {
3888        // Unreachable: `rx_buffer_from_type` is the only constructor of this
3889        // builder and its `const` assert already refused an unbounded `M`.
3890        let rx_bytes = crate::rmw_type_registry::subscription_rx_bytes::<M>(RX)
3891            .expect("rx_buffer_from_type asserts the bound exists at build time");
3892        let handle = self
3893            .ctx
3894            .executor
3895            .register_subscription_buffered_on::<M, F, RX>(
3896                self.ctx.node_id,
3897                self.topic,
3898                self.qos,
3899                callback,
3900                None,
3901                Some(rx_bytes),
3902            )?;
3903        if let Some(sc) = self.sched {
3904            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
3905        }
3906        Ok(handle)
3907    }
3908}
3909
3910/// Typed subscription builder with `MessageInfo` (`.typed::<M>().message_info()`).
3911/// Callback is `FnMut(&M, Option<&MessageInfo>)`.
3912pub struct TypedSubInfoBuilder<
3913    'c,
3914    'e,
3915    't,
3916    's,
3917    M,
3918    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
3919> {
3920    ctx: &'c mut Node<'e, 's>,
3921    topic: &'t str,
3922    qos: QoSProfile,
3923    sched: Option<super::sched_context::SchedContextId>,
3924    /// Issue 1860 -- did the consumer NAME `RX` (`.rx_buffer::<N>()`), or is it
3925    /// the default the type's bound is clamped to? As `TypedSubscriptionBuilder`.
3926    rx_explicit: bool,
3927    _phantom: PhantomData<M>,
3928}
3929
3930impl<'c, 'e, 't, 's, M: MessageForRmw + 'static, const RX: usize>
3931    TypedSubInfoBuilder<'c, 'e, 't, 's, M, RX>
3932{
3933    pub fn qos(mut self, qos: QoSProfile) -> Self {
3934        self.qos = qos;
3935        self
3936    }
3937
3938    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
3939        self.sched = Some(sc);
3940        self
3941    }
3942
3943    pub fn rx_buffer<const N: usize>(self) -> TypedSubInfoBuilder<'c, 'e, 't, 's, M, N> {
3944        TypedSubInfoBuilder {
3945            ctx: self.ctx,
3946            topic: self.topic,
3947            qos: self.qos,
3948            sched: self.sched,
3949            rx_explicit: true,
3950            _phantom: PhantomData,
3951        }
3952    }
3953
3954    pub fn build<F: FnMut(&M, Option<&nros_core::MessageInfo>) + 'static>(
3955        self,
3956        callback: F,
3957    ) -> Result<super::types::HandleId, NodeError> {
3958        let handle = self
3959            .ctx
3960            .executor
3961            .register_subscription_with_info_sized_inner::<M, F, RX>(
3962                Some(self.ctx.node_id),
3963                self.topic,
3964                self.qos,
3965                callback,
3966                if self.rx_explicit { Some(RX) } else { None },
3967            )?;
3968        if let Some(sc) = self.sched {
3969            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
3970        }
3971        Ok(handle)
3972    }
3973}
3974
3975/// Typed subscription builder with E2E-safety validation
3976/// (`.typed::<M>().safety()`). Callback is `FnMut(&M, &IntegrityStatus)`.
3977#[cfg(feature = "safety-e2e")]
3978pub struct TypedSubSafetyBuilder<
3979    'c,
3980    'e,
3981    't,
3982    's,
3983    M,
3984    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
3985> {
3986    ctx: &'c mut Node<'e, 's>,
3987    topic: &'t str,
3988    qos: QoSProfile,
3989    sched: Option<super::sched_context::SchedContextId>,
3990    /// Issue 1860 -- did the consumer NAME `RX` (`.rx_buffer::<N>()`), or is it
3991    /// the default the type's bound is clamped to? As `TypedSubscriptionBuilder`.
3992    rx_explicit: bool,
3993    _phantom: PhantomData<M>,
3994}
3995
3996#[cfg(feature = "safety-e2e")]
3997impl<'c, 'e, 't, 's, M: MessageForRmw + 'static, const RX: usize>
3998    TypedSubSafetyBuilder<'c, 'e, 't, 's, M, RX>
3999{
4000    pub fn qos(mut self, qos: QoSProfile) -> Self {
4001        self.qos = qos;
4002        self
4003    }
4004
4005    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
4006        self.sched = Some(sc);
4007        self
4008    }
4009
4010    pub fn rx_buffer<const N: usize>(self) -> TypedSubSafetyBuilder<'c, 'e, 't, 's, M, N> {
4011        TypedSubSafetyBuilder {
4012            ctx: self.ctx,
4013            topic: self.topic,
4014            qos: self.qos,
4015            sched: self.sched,
4016            rx_explicit: true,
4017            _phantom: PhantomData,
4018        }
4019    }
4020
4021    pub fn build<F: FnMut(&M, &nros_rmw::IntegrityStatus) + 'static>(
4022        self,
4023        callback: F,
4024    ) -> Result<super::types::HandleId, NodeError> {
4025        let handle = self
4026            .ctx
4027            .executor
4028            .register_subscription_with_safety_sized_inner::<M, F, RX>(
4029                Some(self.ctx.node_id),
4030                self.topic,
4031                self.qos,
4032                callback,
4033                if self.rx_explicit { Some(RX) } else { None },
4034            )?;
4035        if let Some(sc) = self.sched {
4036            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
4037        }
4038        Ok(handle)
4039    }
4040}
4041
4042/// Generic (type-erased) subscription builder (`.generic(type, hash)`).
4043pub struct GenericSubscriptionBuilder<
4044    'c,
4045    'e,
4046    't,
4047    's,
4048    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
4049> {
4050    ctx: &'c mut Node<'e, 's>,
4051    topic: &'t str,
4052    type_name: &'t str,
4053    type_hash: &'t str,
4054    qos: QoSProfile,
4055    sched: Option<super::sched_context::SchedContextId>,
4056}
4057
4058impl<'c, 'e, 't, 's, const RX: usize> GenericSubscriptionBuilder<'c, 'e, 't, 's, RX> {
4059    pub fn qos(mut self, qos: QoSProfile) -> Self {
4060        self.qos = qos;
4061        self
4062    }
4063
4064    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
4065        self.sched = Some(sc);
4066        self
4067    }
4068
4069    pub fn rx_buffer<const N: usize>(self) -> GenericSubscriptionBuilder<'c, 'e, 't, 's, N> {
4070        GenericSubscriptionBuilder {
4071            ctx: self.ctx,
4072            topic: self.topic,
4073            type_name: self.type_name,
4074            type_hash: self.type_hash,
4075            qos: self.qos,
4076            sched: self.sched,
4077        }
4078    }
4079
4080    /// Surface the sample's wire attachment + metadata to the callback
4081    /// (`FnMut(&[u8], &RawMessageInfo)`). The cross-RMW bridge reads the
4082    /// bridge_origin tag from `info.attachment()` for echo suppression.
4083    pub fn message_info(self) -> GenericSubInfoBuilder<'c, 'e, 't, 's, RX> {
4084        GenericSubInfoBuilder {
4085            ctx: self.ctx,
4086            topic: self.topic,
4087            type_name: self.type_name,
4088            type_hash: self.type_hash,
4089            qos: self.qos,
4090            sched: self.sched,
4091        }
4092    }
4093
4094    pub fn build<F: FnMut(&[u8]) + 'static>(
4095        self,
4096        callback: F,
4097    ) -> Result<super::types::HandleId, NodeError> {
4098        let handle = self
4099            .ctx
4100            .executor
4101            .register_subscription_buffered_raw_on::<F, RX>(
4102                self.ctx.node_id,
4103                self.topic,
4104                self.type_name,
4105                self.type_hash,
4106                self.qos,
4107                None,
4108                callback,
4109            )?;
4110        if let Some(sc) = self.sched {
4111            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
4112        }
4113        Ok(handle)
4114    }
4115}
4116
4117/// Generic subscription builder with `MessageInfo` surfaced
4118/// (`.message_info()`). Callback is `FnMut(&[u8], &RawMessageInfo)`.
4119pub struct GenericSubInfoBuilder<
4120    'c,
4121    'e,
4122    't,
4123    's,
4124    const RX: usize = { crate::config::DEFAULT_RX_BUF_SIZE },
4125> {
4126    ctx: &'c mut Node<'e, 's>,
4127    topic: &'t str,
4128    type_name: &'t str,
4129    type_hash: &'t str,
4130    qos: QoSProfile,
4131    sched: Option<super::sched_context::SchedContextId>,
4132}
4133
4134impl<'c, 'e, 't, 's, const RX: usize> GenericSubInfoBuilder<'c, 'e, 't, 's, RX> {
4135    pub fn qos(mut self, qos: QoSProfile) -> Self {
4136        self.qos = qos;
4137        self
4138    }
4139
4140    pub fn sched_context(mut self, sc: super::sched_context::SchedContextId) -> Self {
4141        self.sched = Some(sc);
4142        self
4143    }
4144
4145    pub fn rx_buffer<const N: usize>(self) -> GenericSubInfoBuilder<'c, 'e, 't, 's, N> {
4146        GenericSubInfoBuilder {
4147            ctx: self.ctx,
4148            topic: self.topic,
4149            type_name: self.type_name,
4150            type_hash: self.type_hash,
4151            qos: self.qos,
4152            sched: self.sched,
4153        }
4154    }
4155
4156    pub fn build<F: FnMut(&[u8], &nros_core::RawMessageInfo) + 'static>(
4157        self,
4158        callback: F,
4159    ) -> Result<super::types::HandleId, NodeError> {
4160        let handle = self
4161            .ctx
4162            .executor
4163            .register_subscription_buffered_raw_info_on::<F, RX>(
4164                self.ctx.node_id,
4165                self.topic,
4166                self.type_name,
4167                self.type_hash,
4168                self.qos,
4169                callback,
4170            )?;
4171        if let Some(sc) = self.sched {
4172            self.ctx.executor.bind_handle_to_sched_context(handle, sc)?;
4173        }
4174        Ok(handle)
4175    }
4176}
4177
4178// `not(feature = "rmw-cffi")` — these tests use the `mock` backend
4179// (`crate::mock`, itself `cfg(all(test, not(rmw-cffi)))`); a workspace test
4180// build that unifies `rmw-cffi` on swaps `ConcreteSession` to the cffi session
4181// and drops `mock`, so the module must drop with it (matches the
4182// `mock_integration` gate in lifecycle_services.rs).
4183#[cfg(all(test, feature = "alloc", not(feature = "rmw-cffi")))]
4184mod builder_tests {
4185    use super::*;
4186    use crate::{executor::Executor, mock::MockSession};
4187    use nros_core::{CdrReader, CdrWriter, DeserError, Deserialize, SerError, Serialize};
4188
4189    struct TestMsg;
4190    impl RosMessage for TestMsg {
4191        const TYPE_NAME: &'static str = "test/msg/TestMsg";
4192        const TYPE_HASH: &'static str = "test_hash";
4193    }
4194    impl Serialize for TestMsg {
4195        fn serialize(&self, _w: &mut CdrWriter) -> Result<(), SerError> {
4196            Ok(())
4197        }
4198    }
4199    impl Deserialize for TestMsg {
4200        fn deserialize(_r: &mut CdrReader) -> Result<Self, DeserError> {
4201            Ok(Self)
4202        }
4203    }
4204    // Phase 212.K.7.6.b — minimal single-field `Message` impl so
4205    // `TypedPublisherBuilder::build` resolves under the cyclonedds-tightened
4206    // bound AND the runtime register call succeeds. `DescriptorBuilder`
4207    // rejects empty `FIELDS` with `BuildError::EmptySchema`; pretend
4208    // there's one byte so the bridge stub returns a non-NULL pointer.
4209    // phase-380 W4 — was `#[cfg(rmw_needs_type_descriptors)]`: the schema is
4210    // now required by `MessageForRmw` on EVERY backend, because that is where
4211    // a subscription's build-time size bound comes from.
4212    impl nros_serdes::schema::Message for TestMsg {
4213        const TYPE_NAME: &'static str = "test/msg/TestMsg";
4214        const FIELDS: &'static [nros_serdes::schema::Field] = &[nros_serdes::schema::Field {
4215            name: "data",
4216            ty: nros_serdes::schema::FieldType::Uint8,
4217            offset: 0,
4218        }];
4219    }
4220
4221    fn s(v: &str) -> heapless::String<64> {
4222        heapless::String::try_from(v).unwrap()
4223    }
4224
4225    #[test]
4226    fn publisher_builder_typed_and_generic() {
4227        let mut session = MockSession::new();
4228        let mut node = NodeHandle::new(
4229            s("n"),
4230            s("/"),
4231            &mut session,
4232            crate::executor::spin::RemapView {
4233                launch: &[],
4234                runtime: &[],
4235            },
4236            0,
4237        );
4238
4239        // typed: node.publisher(t).typed::<M>().qos(..).build()
4240        let _typed = node
4241            .publisher("/chatter")
4242            .typed::<TestMsg>()
4243            .qos(QoSProfile::default().keep_last(5))
4244            .build()
4245            .expect("typed publisher builds");
4246
4247        // generic: node.publisher(t).qos(..).generic(type, hash).build()
4248        let _generic = node
4249            .publisher("/chatter")
4250            .qos(QoSProfile::default())
4251            .generic("std_msgs/msg/Int32", "hash")
4252            .build()
4253            .expect("generic publisher builds");
4254    }
4255
4256    // The handle `Executor::create_node` returns resolves through the same two
4257    // tiers. Asserted on the name the BACKEND was handed (`MockPublisher`
4258    // records it), not on a helper's return value.
4259
4260    fn argv_rule(from: &'static str, to: &'static str) -> crate::ros_args::RemapArg<'static> {
4261        crate::ros_args::RemapArg {
4262            node: None,
4263            from,
4264            to,
4265        }
4266    }
4267
4268    #[test]
4269    fn create_node_handle_applies_an_argv_remap() {
4270        let mut exec: Executor = Executor::from_session(MockSession::new());
4271        crate::ros_args::install_argv_remaps(&mut exec, [argv_rule("chatter", "/from_argv")])
4272            .unwrap();
4273        let mut node = exec.create_node("talker").expect("node");
4274        let p = node
4275            .create_publisher::<TestMsg>("chatter")
4276            .expect("publisher");
4277        assert_eq!(p.handle.topic_name(), "/from_argv");
4278    }
4279
4280    #[test]
4281    fn create_node_handle_prefers_a_launch_rule_over_argv() {
4282        let mut exec: Executor = Executor::from_session(MockSession::new());
4283        crate::ros_args::install_argv_remaps(&mut exec, [argv_rule("chatter", "/from_argv")])
4284            .unwrap();
4285        exec.declare_remap("talker", "/", "chatter", "/from_launch")
4286            .unwrap();
4287        let mut node = exec.create_node("talker").expect("node");
4288        let p = node
4289            .create_publisher::<TestMsg>("chatter")
4290            .expect("publisher");
4291        assert_eq!(p.handle.topic_name(), "/from_launch");
4292    }
4293
4294    /// Issue 1971 -- the handle reads the flash launch table too, above argv.
4295    #[test]
4296    fn create_node_handle_prefers_a_flash_launch_rule_over_argv() {
4297        static LAUNCH: [crate::executor::LaunchRemap; 1] = [crate::executor::LaunchRemap::new(
4298            c"talker",
4299            c"/",
4300            c"chatter",
4301            c"/from_flash",
4302        )];
4303        let mut exec: Executor = Executor::from_session(MockSession::new());
4304        crate::ros_args::install_argv_remaps(&mut exec, [argv_rule("chatter", "/from_argv")])
4305            .unwrap();
4306        exec.set_launch_remaps(&LAUNCH);
4307        let mut node = exec.create_node("talker").expect("node");
4308        let p = node
4309            .create_publisher::<TestMsg>("chatter")
4310            .expect("publisher");
4311        assert_eq!(p.handle.topic_name(), "/from_flash");
4312    }
4313
4314    #[test]
4315    fn create_node_handle_passes_an_unmatched_name_through_unchanged() {
4316        // No rule matches, so the handle hands the backend exactly what it was
4317        // given — NOT the expanded `/talker_ns/chatter`. This handle has never
4318        // expanded names, and a remap that does not fire must not start.
4319        let mut exec: Executor = Executor::from_session(MockSession::new());
4320        crate::ros_args::install_argv_remaps(&mut exec, [argv_rule("other", "/x")]).unwrap();
4321        let mut node = exec.create_node("talker").expect("node");
4322        let p = node
4323            .create_publisher::<TestMsg>("chatter")
4324            .expect("publisher");
4325        assert_eq!(p.handle.topic_name(), "chatter");
4326    }
4327
4328    #[test]
4329    fn subscription_builder_and_convenient() {
4330        let mut exec: Executor = Executor::from_session(MockSession::new());
4331        let id = exec.node_builder("n").build().expect("node");
4332
4333        // builder: typed
4334        let _h = exec
4335            .node_mut(id)
4336            .subscription("/chatter")
4337            .typed::<TestMsg>()
4338            .qos(QoSProfile::default().keep_last(5))
4339            .build(|_m: &TestMsg| {})
4340            .expect("typed subscription builds");
4341
4342        // builder: generic (raw bytes)
4343        let _g = exec
4344            .node_mut(id)
4345            .subscription("/raw")
4346            .generic("std_msgs/msg/Int32", "hash")
4347            .build(|_b: &[u8]| {})
4348            .expect("generic subscription builds");
4349
4350        // builder: sized + sched-context (slice 3 knobs)
4351        let sc = exec.default_sched_context_id();
4352        let _s = exec
4353            .node_mut(id)
4354            .subscription("/sized")
4355            .typed::<TestMsg>()
4356            .rx_buffer::<64>()
4357            .sched_context(sc)
4358            .build(|_m: &TestMsg| {})
4359            .expect("sized + sched subscription builds");
4360
4361        // convenient (fork tier) — one node-ctx at a time, re-acquired
4362        let _c = exec
4363            .node_mut(id)
4364            .create_subscription::<TestMsg, _>("/conv", |_m: &TestMsg| {})
4365            .expect("convenient typed subscription builds");
4366    }
4367
4368    #[test]
4369    fn generic_message_info_builder() {
4370        // slice 3b — the bridge echo path: generic sub whose callback
4371        // receives the wire attachment via RawMessageInfo.
4372        let mut exec: Executor = Executor::from_session(MockSession::new());
4373        let id = exec.node_builder("n").build().expect("node");
4374
4375        let _i = exec
4376            .node_mut(id)
4377            .subscription("/info")
4378            .generic("std_msgs/msg/Int32", "hash")
4379            .message_info()
4380            .rx_buffer::<256>()
4381            .build(|_payload: &[u8], info: &nros_core::RawMessageInfo| {
4382                let _ = info.attachment();
4383            })
4384            .expect("generic + message_info subscription builds");
4385    }
4386
4387    #[test]
4388    fn typed_message_info_builder() {
4389        // M2.a — typed .message_info() (rclrs shape FnMut(&M, Option<&MessageInfo>)),
4390        // replacing register_subscription_with_info.
4391        let mut exec: Executor = Executor::from_session(MockSession::new());
4392        let id = exec.node_builder("n").build().expect("node");
4393        let _h = exec
4394            .node_mut(id)
4395            .subscription("/chatter")
4396            .typed::<TestMsg>()
4397            .qos(QoSProfile::default().keep_last(5))
4398            .message_info()
4399            .build(|_m: &TestMsg, _info: Option<&nros_core::MessageInfo>| {})
4400            .expect("typed + message_info subscription builds");
4401    }
4402
4403    #[cfg(feature = "safety-e2e")]
4404    #[test]
4405    fn typed_safety_builder() {
4406        // M2.a — typed .safety(), replacing register_subscription_with_safety.
4407        let mut exec: Executor = Executor::from_session(MockSession::new());
4408        let id = exec.node_builder("n").build().expect("node");
4409        let _h = exec
4410            .node_mut(id)
4411            .subscription("/chatter")
4412            .typed::<TestMsg>()
4413            .safety()
4414            .build(|_m: &TestMsg, _status: &nros_rmw::IntegrityStatus| {})
4415            .expect("typed + safety subscription builds");
4416    }
4417
4418    #[test]
4419    fn generator_emitted_chain_compiles() {
4420        // Locks the exact builder chain the orchestration generator emits
4421        // for a subscriber (replaces register_subscription_raw_with_qos_sized_on).
4422        let mut exec: Executor = Executor::from_session(MockSession::new());
4423        let id = exec.node_builder("n").build().expect("node");
4424        let _h = exec
4425            .node_mut(id)
4426            .subscription("/topic")
4427            .generic("std_msgs/msg/Int32", "hash")
4428            .qos(QoSProfile::default().keep_last(1))
4429            .rx_buffer::<1024>()
4430            .build(|_data: &[u8]| {})
4431            .expect("generator-shape subscription builds");
4432    }
4433
4434    #[test]
4435    fn nodectx_publisher_and_bridge_shape() {
4436        // Node publisher symmetry + the bridge two-ctx borrow pattern:
4437        // build the dest publisher on one Node (dropped), then register
4438        // the source subscription on another — the owned publisher outlives.
4439        let mut exec: Executor = Executor::from_session(MockSession::new());
4440        let id = exec.node_builder("n").build().expect("node");
4441
4442        // convenient + builder publisher on Node
4443        let _p = exec
4444            .node_mut(id)
4445            .create_publisher::<TestMsg>("/p")
4446            .expect("ctx convenient publisher");
4447        let dest_pub = exec
4448            .node_mut(id)
4449            .publisher("/fwd")
4450            .generic("std_msgs/msg/Int32", "hash")
4451            .build()
4452            .expect("ctx generic publisher builds"); // Node dropped here
4453
4454        // re-borrow exec for the source sub; closure owns dest_pub
4455        let _s = exec
4456            .node_mut(id)
4457            .subscription("/src")
4458            .generic("std_msgs/msg/Int32", "hash")
4459            .message_info()
4460            .build(move |payload: &[u8], _info: &nros_core::RawMessageInfo| {
4461                let _ = dest_pub.publish_serialized(payload);
4462            })
4463            .expect("bridge-shape source subscription builds");
4464    }
4465}