Skip to main content

nros_platform/board/
entry.rs

1//! [`BoardEntry`] — Phase 212.N.1.
2//!
3//! The single boot-driver trait every Entry pkg `main.rs` invokes:
4//!
5//! ```ignore
6//! fn main() {
7//!     let _ = <MyBoard as BoardEntry>::run(|runtime| {
8//!         run_plan(runtime)         // codegen-emitted (212.N.4)
9//!     });
10//! }
11//! ```
12//!
13//! `run` owns the full lifecycle:
14//!
15//! 1. [`super::BoardInit::init_hardware`]
16//! 2. device bring-up — link layer to L2, then the carrier / DHCP gate if the
17//!    board has an IP stack. This is the `run` BODY's job, usually delegated to
18//!    a family helper (`nros_board_freertos::run_entry`, …). There is no mixin
19//!    trait for it: phase-206 W4 / issue 1067 removed ~~`TransportBringup`~~ and
20//!    ~~`NetworkWait`~~, which had zero and zero production callers respectively,
21//!    and whose "skipped if not implemented" order was not expressible in Rust.
22//! 4. Open executor, build `RuntimeCtx`, invoke `setup(runtime)`.
23//! 5. Spin executor to completion (or termination signal).
24//! 6. [`super::BoardExit::exit_success`] / `exit_failure`.
25//!
26//! The exact body lives in the family driver crates (212.N.2); the
27//! trait here pins the signature so codegen + user Entry pkg can
28//! call it without knowing the family.
29
30use super::runtime::RuntimeCtx;
31
32/// Deploy-metadata overlay threaded from `nros::main!()` into the board's
33/// boot config (issue #48 cause 1).
34///
35/// The `nros::main!()` macro reads the Entry pkg's
36/// `[package.metadata.nros.deploy.<board>]` block at expansion time and bakes
37/// the present keys here. Each field is `None` when the deploy block omitted
38/// it, so the board overlays only the supplied values onto its own
39/// `Config::default()` (the firmware's compiled-in default stays the source of
40/// truth for everything the deploy block does not name).
41///
42/// Boards whose `BoardEntry::run` ignores network/locator config (POSIX hosts,
43/// RTIC MCUs that take their transport elsewhere) inherit the default
44/// [`BoardEntry::run_with_deploy`] body, which drops the overlay and calls
45/// [`BoardEntry::run`] — so adding a *network* field here never touches those
46/// boards. The exception is [`node_name`](DeployOverlay::node_name): hosted
47/// boards override `run_with_deploy` to apply it to the boot config (issue #98),
48/// since the ROS graph node name is a launch identity, not a network knob.
49#[derive(Clone, Copy, Default, Debug)]
50pub struct DeployOverlay {
51    /// `locator = "tcp/10.0.2.2:7451"` — the zenoh/RMW endpoint the firmware
52    /// dials. `None` → keep the board default.
53    pub locator: Option<&'static str>,
54    /// The image's resolved network (RFC-0103 D10f, phase-497 W2): the
55    /// `NROS_IMAGE_NET` text `nros sync` rendered from `[image.<id>.net.<if>]`,
56    /// `[network.<name>]` and the board's `[board.net]`, baked verbatim.
57    /// `None` → the image states no interface; keep the board default. Decoded
58    /// by `nros_board_common::image_net` — this crate carries it, it does not
59    /// read it.
60    ///
61    /// It replaced three fields (`ip`, `gateway`, `netmask`) that `nros::main!`
62    /// lowered from `system.toml` itself: the macro has no board catalog, so it
63    /// could not take a board's default network or derive a MAC, and one fact
64    /// would have resolved two ways.
65    pub net: Option<&'static str>,
66    /// `domain_id = 0` — ROS 2 domain. `None` → keep the board default.
67    pub domain_id: Option<u32>,
68    /// `[image.<id>] transport = "serial"` — the LINK kind the image rides
69    /// (RFC-0086 D2). `None` → the board's default link. Read by
70    /// [`BoardEntry::setup_transport`] (phase-244.D1), which may install a
71    /// board custom transport for it BEFORE the linked RMW registers.
72    ///
73    /// Typed, not a string (issue 1601): phase-445 W6 moved the leaf key from
74    /// the RMW name (`"xrce"`) to the link kind (`"serial"`), and the one
75    /// board reading it kept comparing against `"xrce"` — a comparison that
76    /// could no longer be true and compiled anyway. A [`LinkKind`] cannot be
77    /// compared against an RMW name.
78    pub transport: Option<LinkKind>,
79    /// `rmw = "xrce"` — the RMW backend the image declares (`[image.<id>]
80    /// rmw` > `[image_defaults] rmw` > `[system] rmw`), verbatim. `None` → not
81    /// declared (a Form 2 entry, or a leaf with no `system.toml`).
82    ///
83    /// The BACKEND choice, which [`transport`](Self::transport) is not: an
84    /// XRCE image over a UART states both, and a board that registers a
85    /// backend keys on this field (issue 1601).
86    pub rmw: Option<&'static str>,
87    /// The ROS graph node name for the primary session, baked from the launch
88    /// file's single `<node name=…>` / `system.toml` `[[component]].name` (issue
89    /// #98). `None` → the board default (`from_env()`'s `"node"`). Only set by
90    /// `nros::main!` when the launch declares exactly one node — multiple nodes
91    /// share one primary session, so naming it after one component would be
92    /// wrong (per-node naming is the deferred multi-node piece). Applied to the
93    /// boot `ExecutorConfig` by the board, so unlike `locator` this IS honored on
94    /// hosted boards (locator stays env-driven; node name is a launch identity).
95    pub node_name: Option<&'static str>,
96    /// Issue #101 / RFC-0045 — the patchable baked boot-config static
97    /// (`.nros_boot_config`), emitted by `nros::main!` for embedded targets and
98    /// read by the board to resolve node_name/locator/domain. `None` on hosted /
99    /// when the macro emits no static.
100    pub boot_config: Option<&'static nros_platform_api::BakedBootConfig>,
101}
102
103impl DeployOverlay {
104    /// Does the image declare RMW `rmw` riding link `link`? Both halves are
105    /// required: the backend is `rmw`, the link is its locator's (RFC-0109
106    /// D6), and neither stands in for the other (issue 1601).
107    pub fn selects(&self, rmw: &str, link: LinkKind) -> bool {
108        self.rmw == Some(rmw) && self.transport == Some(link)
109    }
110}
111
112/// The link kind an image rides — its locator's scheme (RFC-0109 D6).
113///
114/// The same words as `nros_orchestration_ir::leaf_system::TRANSPORT_KINDS`,
115/// which `nros::main!` maps onto these variants: a word the macro maps to a
116/// variant missing here fails the image's compile. It names a LINK, never an
117/// RMW — `"xrce"` is not one (issue 1601).
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum LinkKind {
120    /// A byte-stream link (UART, USB-CDC, …).
121    Serial,
122    /// TCP over the board's IP stack.
123    Tcp,
124    /// UDP over the board's IP stack.
125    Udp,
126    /// A transport the board supplies itself (`custom://…`) — on the
127    /// reference boards, XRCE framed over a UART.
128    Custom,
129}
130
131impl LinkKind {
132    /// Every link kind, in `TRANSPORT_KINDS` order.
133    pub const ALL: [LinkKind; 4] = [
134        LinkKind::Serial,
135        LinkKind::Tcp,
136        LinkKind::Udp,
137        LinkKind::Custom,
138    ];
139
140    /// The `system.toml` spelling.
141    pub const fn name(self) -> &'static str {
142        match self {
143            LinkKind::Serial => "serial",
144            LinkKind::Tcp => "tcp",
145            LinkKind::Udp => "udp",
146            LinkKind::Custom => "custom",
147        }
148    }
149
150    /// Parse the `system.toml` spelling; `None` for anything that is not a
151    /// link kind — an RMW name included.
152    pub fn from_name(name: &str) -> Option<Self> {
153        Self::ALL.into_iter().find(|k| k.name() == name)
154    }
155}
156
157/// Per-board boot driver.
158///
159/// Implementations live in the family driver crates
160/// (`nros-board-linux`, `nros-board-freertos`, …). Per-board crates
161/// (`nros-board-mps2-an385-freertos`, …) plug the family.
162pub trait BoardEntry: super::Board {
163    /// Drive the full boot → user-closure → exit flow.
164    ///
165    /// `setup` receives a `&mut RuntimeCtx` with overlay knobs projected
166    /// from the launch file at BUILD time (there is no CLI parse; see
167    /// [`super::RuntimeCtx`]). Returning `Err` from `setup` makes
168    /// `run` route to [`super::BoardExit::exit_failure`]; `Ok`
169    /// proceeds to executor spin + clean exit.
170    ///
171    /// **Returns `Result`, not `!`.** The legacy
172    /// `nros-board-common::board_init::BoardEntry::run` diverged;
173    /// 212.N keeps the option to return so unit tests can drive it
174    /// in a hosted process without `exit()` killing the test
175    /// harness. Production boards still call `exit_*` from inside
176    /// `run`'s body after spin returns.
177    fn run<F, E>(setup: F) -> Result<(), E>
178    where
179        F: FnOnce(&mut RuntimeCtx<'_>) -> Result<(), E>,
180        E: core::fmt::Debug;
181
182    /// Boot like [`run`](Self::run) but apply a deploy-metadata overlay to the
183    /// board's boot config first (issue #48 cause 1).
184    ///
185    /// The default body **ignores** `deploy` and forwards to
186    /// [`run`](Self::run); boards that compile a network/locator config (the
187    /// FreeRTOS / bare-metal firmware boards) override it to overlay the
188    /// supplied fields onto their `Config::default()`. `nros::main!()` calls
189    /// this (not `run`) for `target_os = "none"` OwnedSpin targets so the
190    /// `[package.metadata.nros.deploy.<board>]` block stops being inert.
191    fn run_with_deploy<F, E>(_deploy: &DeployOverlay, setup: F) -> Result<(), E>
192    where
193        F: FnOnce(&mut RuntimeCtx<'_>) -> Result<(), E>,
194        E: core::fmt::Debug,
195    {
196        Self::run(setup)
197    }
198
199    /// phase-271 (issue #110) — boot like [`run_with_deploy`](Self::run_with_deploy)
200    /// but size the executor's callback table + arena to the entry's OWN declared
201    /// topology (`max_cbs` / `max_sched_contexts`, from the entry's
202    /// `[package.metadata.nros.entry]`), instead of the workspace-global
203    /// `NROS_EXECUTOR_MAX_CBS` build const.
204    ///
205    /// Sizes are plain `usize`s (not `nros::ExecutorSizing`) because
206    /// `nros-platform` sits below `nros`; the hosted board converts them. A
207    /// `max_sched_contexts` of `0` means "use the build default". The **default
208    /// body IGNORES the sizing** and forwards to
209    /// [`run_with_deploy`](Self::run_with_deploy), so every board except the
210    /// hosted (Linux) one — which opens via `Executor::open` and could grow its
211    /// arena — is byte-identical; `nros-board-linux` overrides this to
212    /// `Executor::open_sized`. `nros::main!()` emits this (instead of
213    /// `run_with_deploy`) only when the entry declares `max_callbacks`.
214    fn run_with_deploy_sized<F, E>(
215        deploy: &DeployOverlay,
216        _max_cbs: usize,
217        _max_sched_contexts: usize,
218        setup: F,
219    ) -> Result<(), E>
220    where
221        F: FnOnce(&mut RuntimeCtx<'_>) -> Result<(), E>,
222        E: core::fmt::Debug,
223    {
224        Self::run_with_deploy(deploy, setup)
225    }
226
227    /// **Custom-transport install seam.** Install a board-specific transport
228    /// selected by `deploy.rmw` + `deploy.transport`, BEFORE the linked RMW registers
229    /// (phase-244.D1).
230    ///
231    /// `nros::main!()` always emits a `setup_transport` call (gated on
232    /// `target_os = "none"`) immediately before `__register_linked_rmw()`,
233    /// so that the vtable is in place before the XRCE backend registers —
234    /// the ordering `set_custom_transport_ops` requires.
235    ///
236    /// **This method is intentionally kept** — it is not dead code. The
237    /// **default no-op** is correct for every board whose transport is
238    /// registered automatically (Zenoh, native sockets, etc.). The only
239    /// current override is **`nros-board-mps2-an385`** with the
240    /// `xrce-transport` feature, which installs an XRCE-over-UART vtable
241    /// when the image declares `rmw = "xrce"` over `transport = "serial"`
242    /// ([`DeployOverlay::selects`]). Future boards that need to
243    /// pre-register a custom transport vtable should override this method in
244    /// the same pattern.
245    ///
246    /// Failures are the board's to handle (it owns `exit_failure`).
247    fn setup_transport(_deploy: &DeployOverlay) {}
248}
249
250#[cfg(test)]
251mod link_kind_tests {
252    use super::{DeployOverlay, LinkKind};
253
254    #[test]
255    fn link_kinds_round_trip_and_an_rmw_name_is_not_one() {
256        for k in LinkKind::ALL {
257            assert_eq!(LinkKind::from_name(k.name()), Some(k));
258        }
259        for rmw in ["xrce", "zenoh", "cyclonedds", "uorb"] {
260            assert_eq!(
261                LinkKind::from_name(rmw),
262                None,
263                "`{rmw}` is an RMW, not a link"
264            );
265        }
266    }
267
268    /// Issue 1601: the XRCE-over-UART image declares `rmw = "xrce"` and
269    /// `transport = "serial"`; the board's predicate must answer yes to that
270    /// pair and no when either half is something else.
271    #[test]
272    fn selects_needs_both_the_rmw_and_the_link() {
273        let xrce_uart = DeployOverlay {
274            rmw: Some("xrce"),
275            transport: Some(LinkKind::Serial),
276            ..Default::default()
277        };
278        assert!(xrce_uart.selects("xrce", LinkKind::Serial));
279        assert!(!xrce_uart.selects("zenoh", LinkKind::Serial));
280        assert!(!xrce_uart.selects("xrce", LinkKind::Udp));
281        let zenoh_serial = DeployOverlay {
282            rmw: Some("zenoh"),
283            transport: Some(LinkKind::Serial),
284            ..Default::default()
285        };
286        assert!(!zenoh_serial.selects("xrce", LinkKind::Serial));
287        assert!(!DeployOverlay::default().selects("xrce", LinkKind::Serial));
288    }
289}