Device contract¶
Six ww_stdlib crates that every device implements, whatever it does. Together they answer what a host, a tester's
bug report or a production line asks of any unit: what firmware is this and where is its ELF, what board, which
unit, what has it counted, what did it log, and why did it last reset. One host tool, ww info
and ww report, asks the same questions of every device, because all of them implement the
same global traits.
| Question | Crate | Trait | What ww shows |
|---|---|---|---|
| What firmware, which build? | ww_firmware_info |
FirmwareInfo |
name, version, git, build time, profile, build id |
| Why did it last reset? | ww_firmware_info |
LastReset |
cause, boot count, fault registers, stack, panic message |
| Which board? | ww_board_info |
BoardInfo |
name, revision and where it came from, variant, BOM, serial, optional list of reworks |
| Which unit? | ww_uid |
Uid |
chip id, MAC, ..., the first is the primary id |
| What did the user call it? | ww_user_info |
UserInfo |
label and notes, whether they survive a power cycle |
| What has it counted? | ww_counters |
Counters |
the cnt counters, decoded with the ELF |
| What did it log? | ww_log |
LogDefmt |
the stored defmt log, decoded with the ELF |
Where to put them¶
Implement each trait once, at any depth of the API tree. The conventional place is one Device subtree of the API
root:
#[ww_trait]
pub trait Device {
ww_impl!(firmware: ww_firmware_info::FirmwareInfo);
ww_impl!(last_reset: ww_firmware_info::LastReset);
ww_impl!(board: ww_board_info::BoardInfo);
ww_impl!(uid: ww_uid::Uid);
ww_impl!(label: ww_user_info::UserInfo);
ww_impl!(counters: ww_counters::Counters);
ww_impl!(log: ww_log::LogDefmt);
}
#[ww_api_root]
pub trait MyDevice {
ww_impl!(device: Device);
// ... what the device does
}
Where they sit doesn't matter to ww: its calls carry a global trait path (crate, version, trait name) that the
device resolves itself (addressing), so ww info works with a device whatever its own API and
whatever version of it, also one flashed years ago. A device that doesn't implement a trait, or implements an
incompatible version of it, is reported as such for that part; the rest is still shown.
Every item of the first four crates is a const: the device serves it from a value, without a handler
(const items), and the host reads it once per connection.
Linking a firmware to its ELF: the build id¶
Three of the traits need the firmware's ELF on the host: defmt logs (the format strings are in the ELF, not on the
wire), cnt counters (names, types and units are in non-loaded sections of the ELF), and a crash record (addresses
to name). The ELF is found by the firmware's GNU build id: the linker hashes the linked output, so it changes with
every byte of code, and the device has it in flash while the ELF has it in a note.
Three lines make a firmware report it:
-
The linker writes the note. In
.cargo/config.toml(or the build'sRUSTFLAGS): -
memory.xkeeps the note in flash and names its bounds: -
The firmware reads it where the traits ask for it,
ww_firmware_info::build_id()(featurebuild-id). The same value goes intoFirmwareInfo::BUILD_ID,DefmtInfo::build_idandCountersInfo::build_id, so each trait is usable alone.build.rsof the firmware callsww_firmware_info::build::emit()for the rest ofFirmwareInfo::FIRMWARE(seeww_firmware_info).
Without the note the build id is empty. ww then can't tell which ELF belongs to the device: it uses only an ELF
passed with --elf, and says it couldn't check it.
Where ww looks for the ELF¶
For each build id the device reports, in this order:
--elf PATH: used, and refused if its build id differs from the device's (--forceto use it anyway).- The ELF directory:
<build id>.elfin each directory ofWW_ELF_PATH, then~/.cache/ww/elf. - The ELFs of the cargo project in the current directory (
target/), the one with that build id.
Nothing is found by guessing: when no ELF has the device's build id, the parts that need one say so and name the
places searched. --force takes the newest binary under target/ instead, with a warning.
The ELF directory is filled with ww elf add:
ww elf add target/thumbv8m.main-none-eabihf/release/usb_io_fw # one ELF, stored as <build id>.elf
ww elf add usb_io_fw-0.5.0-elf.tar.zst # a whole release
ww elf id usb_io_fw # its build id, and whether it is in the directory
ww elf add and cnt elf add are the same code (cnt_core::elf), they fill one directory.
The release archive¶
A firmware release ships one archive of all its ELFs, not loose files: <firmware>-<version>-elf.tar.zst, with
every board variant and profile of the release. A tester who got a bug report from a unit runs ww elf add on the
archive of the version printed in the report, and ww report finds the right ELF by build id from then on.
# at release time, in the firmware's repo: every ELF of the release, and a plain list of build ids to read
for elf in release/*.elf; do echo "$(ww elf id $elf) $(basename $elf)"; done > index.txt
tar --zstd -cf usb_io_fw-0.5.0-elf.tar.zst index.txt release/*.elf
ww elf add reads each ELF's build id from the ELF itself, so a file in the archive that is not an ELF (the index,
notes) is ignored and an ELF without a build id is listed as skipped. The version a unit runs is in its report
(FirmwareInfo::FIRMWARE), which says which archive to fetch.
ww info¶
$ ww info --serial 4B333720
API usb_io_api@0.5.0
Firmware b200_usb_io 0.5.0 (a1b2c3d-dirty, built 2026-10-03 18:20), release
Build id 28a4e144e867138da2cccccd14e3e6baa2c78fbc
Board rePort rev B (strap 2), BOM std, serial RP-0042
Ids chip e6613871235b1c2d (internal, primary)
Label "bench 2", notes "DUT of the CAN test" (kept over power cycles)
Last reset Panic, boot 3 since cleared, ran 2.5 s
message: index out of bounds
The same data as JSON with --format json (each part is {"status": "value" | "not_implemented" | "error", ...}).
It needs no ELF and no user API.
ww report¶
ww report is ww info plus the counters, the stored log and the last reset's addresses named with the ELF, in one
file a tester sends along with a bug report:
It has the sections of the output above, then:
- Last reset: the fault's
pcandlrand the stack words named by function (fn+0x1c) from the ELF of the firmware that crashed (the record carries its build id: it may be older than an update since). - Counters: every
cntcounter of RAM and backup with its value and unit, decoded with the ELF. - Log: what the device's log store holds, from the first message since boot, decoded with
defmt; gaps are shown where the device dropped messages. Reading it does not empty the store.
Each part that needs an ELF it couldn't get says why and the rest of the report is still complete, so a tester
without the archive still sends something useful. --elf, --force and --format json behave as for
ww counters.
The log is a stream, which is opened on the trait's absolute path in the device's API (the host reads the device's
introspection data for it, it needs no API of its own): that is the one part where ww report is more than global
trait calls.
On the host (Rust)¶
ww_report is the library behind both commands:
let device = DynClient::from_config(config).connect().await?;
let contract = ww_report::clients::Contract::new(&device); // keep it: dropping it disconnects the device
let report = ww_report::report::collect_report(&contract, &ReportOptions::default()).await;
println!("{}", ww_report::report::render_report(&report));
tests/device_report runs it against a mock device implementing every trait of the contract.