Skip to content
Basic

Getting started with APIs

A WireWeaver API is a Rust trait describing what a device offers: methods to call, properties to read and write, streams of data from the device, sinks of data into it, and nested traits grouping them. From that one trait, ww_codegen! generates the device's server (request decoding and dispatch to your handlers, no_std and no allocation) and the host's client (a typed function per item, async, blocking or as a promise for GUIs). Data crossing the wire is any ShrinkWrap type.

#[ww_api_root]
pub trait BlinkyApi {
    fn led_on();
    fn led_off();
}

The project shape

An API lives in its own small crate, shared by the firmware and the host:

Crate What it holds In this repo
blinky_api the trait and its data types, no_std examples/blinky_api
firmware implements the handlers, runs the server examples_mcu/rp2/src/bin/usb_blinky.rs
blinky the generated client, used by apps and scripts examples/blinky

The API crate's name and version identify the API on the wire, see Folder structure. To start a new project, copy the wire_weaver_template repository.

A first API, end to end

1. The API crate (examples/blinky_api/src/lib.rs):

#![no_std]

use wire_weaver::prelude::*;

#[ww_api_root]
pub trait BlinkyApi {
    fn led_on();
    fn led_off();
}

#[ww_api_root] marks the top-level trait of a device (#[ww_trait] the others). It generates nothing by itself: ww_codegen! reads the trait from its source file.

2. The device (from examples_mcu/rp2/src/bin/usb_blinky.rs) implements one handler per method:

struct ServerState {
    led: Output<'static>,
}

impl ServerState {
    async fn led_on(&mut self, _cx: &mut Context<'_, impl EventOut>) -> RpcResult<()> {
        self.led.set_high();
        Ready(())
    }

    async fn led_off(&mut self, _cx: &mut Context<'_, impl EventOut>) -> RpcResult<()> {
        self.led.set_low();
        Ready(())
    }
}

mod server_impl {
    wire_weaver::ww_codegen!(
        blinky_api :: BlinkyApi for super::ServerState,
        server = true, no_alloc = true, use_async = true,
    );
}

ww api scaffold blinky_api (CLI) prints the struct, a stub for every handler and the ww_codegen! call, so you never write handler signatures by hand. The generated server is sans-IO: a transport (USB, RTT, UDP, WebSocket, see Transport protocols) feeds it requests; the example uses wire_weaver_usb_embassy.

3. The host (examples/blinky) generates a client and calls the device:

pub struct Blinky {
    cmd: Commander,
}

impl WwClient for Blinky {
    fn default_config() -> ClientConfig {
        ClientConfig::new().usb_vid_pid(0xc0de, 0xcafe)
    }
    fn from_cmd(cmd: Commander) -> Self {
        Self { cmd }
    }
}

mod api_client {
    wire_weaver::ww_codegen!(blinky_api :: BlinkyApi for crate::Blinky, client = "std_client");
}

// examples/blinky/examples/simple.rs
let device = Blinky::new().connect().await?;
device.led_on().call().await?;
device.led_off().call().await?;

.call().await is the async form; .blocking_call() and .call_promise(..) do the same without async. With no host code at all, ww introspect and Python talk to the device from the API it describes itself.

What an API can have

use ww_si::{Fixed, Point, Range, origins::Celsius, prefix::{Centi, Milli}};
use ww_si::units::{Ampere, Kelvin, Meter, Per, Ratio, Second};

#[ww_api_root]
trait MyDevice {
    /// Methods: any number of arguments, any return type, Result for errors.
    fn set_brightness(value: Fixed<Ratio, Centi, u8, Range<0, 100>>);
    fn temperature() -> Point<Celsius, Fixed<Kelvin, Centi, i16>>;
    fn read() -> Result<u16, SensorError<'i>>;

    /// Properties: values the host reads, writes or observes.
    property!(rw speed: Fixed<Per<Meter, Second>, Milli, i32>);
    property!(ro+observe current: Fixed<Ampere, Milli, i32>);

    /// Streams out of the device, sinks into it.
    stream!(button: ButtonEvent);
    sink!(uart_tx: [u8]);

    /// Traits: groups of items, here an array of GPIO banks from the standard library.
    ww_impl!(gpio[]: ww_gpio::Bank);
}
Item Page
fn Methods: handlers, async or blocking servers, deferred answers, the client
property!, const Properties: get_set handlers or a plain field, reads, writes, consts
stream!, sink! Streams: plain, counted and reliable streams, sinks
ww_impl! Traits: grouping, reusable traits from crates, attachments
Result<T, E> Errors: error enums, common errors, CallError on the client

Everything else builds on these: items can be arrays (gpio[]), optional for devices that lack them, observed without polling, and many can be answered in one multi request. The standard library has ready-made traits for GPIO, I2C, UART, firmware updates, logs and more.

The API and its types can evolve: newer hosts work with older devices and the other way round, as long as the evolution rules are followed.

Numbers are typed

Almost every number in an API (an argument, a return value, a property, a stream item, a field of a data type) is typed, not a bare u32 or f32: a physical quantity has its unit in its type, an exact_units::Fixed (re-exported by ww_si); a count is a counted unit; an identifier is a newtype; a position or sequence number is Index/IndexFrom1/Seq. timeout_ms: u16 becomes timeout: Millis<u16>, the same two bytes on the wire and the same code on the device, and ww, Python and the dynamic client show 100 ms, take q!(0.1 s) and refuse what doesn't fit. A bare integer or float stays only with a reason: a raw protocol field or a truly dimensionless scale.

Instead of Write
timeout_ms: u16, uptime_s: u32 timeout: Millis<u16>, uptime: Fixed<Second, One, u32>
buffer_kib: u16 buffer: Fixed<Byte, Kibi, u16>
battery: u8 (percent) battery: Fixed<Ratio, Centi, u8, Range<0, 100>>
temp: i16 (°C × 100) temp: Point<Celsius, Fixed<Kelvin, Centi, i16>>
item_count: u32 items: Fixed<Item, One, u32>
scale_id: u8 scale: ScaleId (pub struct ScaleId(u8);)
channel: u8 (1 to 4) channel: IndexFrom1<Item, u8, Range<1, 4>>
frame_seq: u16 frame: Seq<Frame, u16>

Choose the number type as the smallest that holds the largest value at the device's real resolution (integers unless the value has no fixed resolution), and give bounds whenever the device can't take every value: they are checked on every read from the wire. A value whose resolution each device picks is a ww_si::Quantity. Positions and sequence numbers are on Indices, temperatures and other readings against a zero on Points. An instant against a named epoch (Unix, GPS) has no type yet (STD-65): until then it is a Micros<u64> with the epoch in its doc comment. ww api check points at plain integers whose names end in a unit (_ms, _us, _hz, _bytes, ...).