Skip to content
Nomicon

Resources addressing

Every resource (method, property, stream, sink or trait) is reached by its path: an array of indices from the API root, each a UNib32 (4 bits for 0..=7). [0, 1, 2] takes 2 bytes, its length included. As a user you never write these indices: they are assigned in declaration order, and generated code uses the names.

#[ww_api_root]
trait ApiRoot {
    fn turn_on();               // [0]
    fn turn_off();              // [1]
    ww_impl!(motor: Motor);     // [2, ..]
    ww_impl!(port[]: Port);     // [3, <index>, ..]
}

A nested trait adds one index for its own items, an array one more for the selected index; with several arrays on the way, each puts its index at its place in the path, and the server's handler gets them all as its index chain.

A path only means something for one API and version, so it needs the host to know the device's API. When that is not possible or not wanted (calling a reusable trait on any device, or on many devices at once), a request names the trait instead.

Global trait addressing (FullVersion)

A request can call a reusable trait without knowing where it sits: put a device to sleep through its power management trait, or read the firmware version of any device with ww_firmware_info. Instead of an absolute path, it carries the trait's crate name and version (FullVersion). That takes 8 to 16 bytes or more, depending on the crate name, where a path takes a few: costly on a constrained link, or when many calls share one packet.

The request carries the trait's crate name and version (FullVersion), the trait's name in that crate (a crate can define several, ww_dfu has five) and the path from the trait's root. The server finds the trait wherever it sits in its API tree and continues from there. It answers ErrorKind::TraitNotFound when it doesn't implement the trait, only implements an incompatible version of its crate (major and, before 1.0, minor have to match), or only implements it under a resource array (ww_impl!(port[]: ww_gpio::Bank): a global path can't select an index). When a trait is implemented several times, the first one in the tree (depth first, in resource id order) answers.

Global paths don't depend on the user API or its version, which makes them the way to talk to a device whose API the host doesn't know: a host built against a new API version can still reboot a device running an old one into its updater (ww_dfu::BootControl) or read its info traits. On the host, connect with traits_only and call through a trait client:

// any device, whatever its API and version: the user API check of link setup is skipped
let device = DynClient::traits_only(|c| c.usb().serial_eq(serial)).connect().await?;
// a client generated with `client = "trait_client"` for ww_dfu::BootControl, see tests/cross_version
let mut boot = BootControl::new(device.global_attachment(ww_dfu::BOOT_CONTROL_FULL_GID, "BootControl"));
boot.reboot(BootTarget::Updater).call().await?;

A traits_only connection refuses absolute paths (Error::AbsolutePathInTraitsOnly): without the device's API they could mean anything. ww dfu reboot in the CLI does exactly this.

Global trait addressing (CompactVersion)

CompactVersion is the short form: the trait's id in the ww_global registry and its crate's version numbers, all UNib32. A ww_user_info::UserInfo read takes 6 bytes this way and 24 with FullVersion.

Every public trait in ww_stdlib has an id, one per trait, keyed crate::Trait, in the same number space as the few crates that have one (ww_log = 0, wire_weaver_usb_link = 512, ww_client_server = 513). Small numbers are shorter on the wire, so the ranges go by encoded size:

Ids Size For
0-7 1 nibble ww_log (crate) and ww_log::LogDefmt only, 2-7 kept free
8-63 1 byte the device contract and common IO: firmware and board info, identity, counters, firmware update, clock, indication, GPIO, UART, I2C, SPI, CAN, ADC, PWM
64-511 1.5 byte all other reusable traits
512+ 2 bytes link and protocol crates

Ids are assigned by hand in ww_global/wire_weaver_gid.json and nowhere else: code never carries the number. The codegen looks a trait's id up by its crate and trait name (<TRAIT>_COMPACT_GID, ApiLevel::compact_id in the API model), so moving a trait to another module, reordering traits or adding a sibling trait changes nothing. A rename or a move to another crate keeps the id (the old name goes into aliases), ids are never reused (a removed trait is marked retired), and assigned_ids.lock keeps every id ever assigned, checked by a test. ww api check reports a trait whose id changed or disappeared as a breaking change.

A server serves a GlobalCompact path like a GlobalFull one: it finds the trait by its id wherever it sits in the API tree, checks that the requested version is protocol compatible with its own (same major, and same minor before 1.0), and answers ErrorKind::TraitNotFound otherwise, also for an id it doesn't know, a crate's id or a trait under a resource array. Serving it costs 64 bytes of flash and about 170 instructions more than an absolute path on a Cortex-M3 (GlobalFull: about 790), see examples_mcu/mcu_qemu/src/bin/global_paths.rs.

The host prefers the compact form: a global call to a trait the registry knows goes out with its id, whether the client asked for GlobalFull or not. A device built before the trait had an id answers TraitNotFound; the client then sends the same request again with GlobalFull and keeps using it for that trait for the rest of the connection. The tap decoder, ww --monitor=api and ww explain name compact calls through the registry and decode them from the trait's snapshot.

API report

TODO: export a file describing all levels of API with IDs (DOC-5).