Expand description
phase-412 – the boot self-report, for boards with no reachable log sink. A fixed RAM record the image writes about itself, for targets where no log sink reaches a human.
§Why this exists
phase-412 derived six pool counts and the executor arena for the mr-canhubk344 island, and then could not tell whether the derived image was correct. The only available signal was the ROS graph’s node count, and one unchanged configuration produced 4, 0, 0, 4, 4 across five runs. Every other channel was already disqualified for that board:
- The console is on
lpuart0, which is not wired on the MR-CANHUBK344.lpuart2is the zenoh serial transport and cannot carry a second protocol. nros_logtherefore reaches nothing. Both arena diagnostics (issue 0900) go through it, so the two messages written specifically to explain an arena failure are invisible on the one board that needed them.- SEGGER RTT was tried and could not discriminate: a working image and a derived image both emitted only the Zephyr banner, and a deliberate positive control produced nothing at all.
- Semihosting halts the core until a probe answers and FAULTS with no probe attached, so an image carrying it cannot run standalone.
What all of those share is that they are STREAMS: they need the board to still be running, and they need somebody attached at the moment the interesting thing happens. The failure this campaign is trying to see is the opposite shape. An under-sized arena halts DURING entity creation, before the first spin, so issue 0900’s advisory never prints – the failure cannot report itself through any stream.
So this is not a stream. It is a fixed-size record in RAM that the image keeps up to date as it boots, read out AFTERWARDS by halting the core and dumping memory. It survives the halt because it does not depend on anything still running, and a PARTIAL record is the useful case rather than a lost one: the last stage reached and the allocation that did not fit are exactly what names the knob to change.
§Reading it
The record is a #[no_mangle] static, so it has a symbol in the ELF and a
debugger can find it without the address being wired in anywhere:
pyocd commander -t s32k344 -c "halt" -c "savemem <addr> <len> report.bin"
python3 scripts/read-boot-report.py <elf> report.binMAGIC distinguishes a written record from uninitialised RAM, and
BootReport::struct_size lets a reader refuse a layout it does not know
rather than decode it wrongly.
§Cost, and why it is opt-in
Enabled by setting NROS_BOOT_REPORT=1 at build time (Zephyr:
CONFIG_NROS_BOOT_REPORT=y), which makes nros-node’s build script emit
cfg(nros_boot_report). With the cfg absent every function here is an empty
#[inline(always)] body and the static does not exist, so an image that
does not opt in is byte-identical to one built before this module – the
same rule issue 0900’s arena knob and phase-403’s rx_buffer_from_type()
both keep.
Enabled, it costs BootReport::struct_size bytes of .bss – 132, the
same on every target because every field is a u32 – and a handful of
relaxed atomic stores on paths that run once per entity at registration.
The 132 is not a detail: it is the LENGTH an operator types into savemem,
and this sentence said 60 for as long as the record had fifteen fields. A
short dump decodes – read-boot-report.py needs 33 * 4 bytes and a
100-byte one is refused, but a reader who trusts the prose over the tool
spends the refusal looking at the wrong thing. Ask the tool instead:
read-boot-report.py --addr-only <elf> prints the address AND the length,
from the ELF’s own symbol size.
§Adding a field (phase-460 W5, for W7 and after)
The record is meant to grow, and the order it grows in is the whole
contract: read-boot-report.py decodes POSITIONALLY. So a new field is
APPENDED – after the last field, never inserted between two – and the
four edits are one commit:
- the field on
BootReport, its zero inBootReport::new, the same name in the same position onSnapshot, and its load insnapshot; VERSIONbumped, so an older decoder REFUSES the record instead of reading the new word as one it knows;FIELDSinscripts/read-boot-report.py, same name, same position, andKNOWN_VERSIONto match;- the field count in
the_record_is_thirty_three_packed_u32sbelow.
check-boot-report-layout fails on 1 without 3, and the Rust test fails if
the compiler laid the record out with padding. Appending is what keeps a
dump taken from an older image decodable by eye: every word before the new
one is still where it was, and the version says which words exist.
Enums§
- Alloc
Arena - Which allocator refused the allocation
BootReport::failed_alloc_sizenames. - Knob
Source - Which rung of the knob ladder decided a knob the record carries.
- Stage
- How far boot got. Monotonic, and the single most useful field: an arena failure halts during entity creation, so the stage that was NOT reached names the phase to look at.
Constants§
- MAGIC
"NRSR"– nano-ros self report. Written LAST, so a reader that finds it knows every field before it is already valid.- VERSION
- Layout version. Bump on any field change; a reader refuses what it does not know rather than decoding a record it would misread.
Functions§
- checkpoint
- init
- note_
alloc - note_
alloc_ failed - note_
arena_ capacity - note_
cpp_ init_ ret - note_
domain - note_
error - note_
heap - note_
heap_ alloc_ failed - note_
sample_ dropped_ too_ small - Always 0 without the record, and the CALLER must not care.
- nros_
boot_ report_ note_ heap - The C entry point exists in BOTH builds, unlike every other stub here.
- nros_
boot_ report_ note_ heap_ alloc_ failed - The C entry point exists in BOTH builds, for the reason below.
- nros_
boot_ report_ note_ sample_ dropped_ too_ small - The C entry point exists in BOTH builds, for the reason above.