ww_client_server¶
ww_client_server is the wire protocol every generated server and client speaks underneath a device's own API -
"an API for an API", or a meta-API: a Request/Event pair that addresses a resource (by path or by trait),
carries a method's serialized arguments or return value as opaque bytes, and leaves what those bytes mean to the
API-specific codegen built on top. It is no_std by default (std is the crate default; disable it on firmware
with default-features = false) and is mostly consumed by ww_codegen!-generated code rather than directly,
though some types (StreamCursor, SinkCredit, the max_len helpers) are meant for user code too. Add it with:
Request and Event¶
Request<'i>-seq: UVlq32Backfill(request id, 0 = no answer expected),path_kind: PathKind<'i>,kind: RequestKind<'i>.Request::set_seq/peek_seqfill in or readseqin an already-serialized buffer without touching the rest, since a client picks the id only right before sending.PathKind<'i>- how a resource is addressed, one of three ways:Absolute { path }(a plain index path, smallest on the wire),GlobalCompact { gid: CompactVersion, path_from_trait }(the trait's ww_global id, everyww_stdlibtrait has one),GlobalFull { gid: FullVersion, trait_name, path_from_trait }(any crates.io trait, by crate name and version - biggest, since it carries the name as a string). A server honors a global path wherever the matching trait sits in its API tree (API-20), after checking its own version is protocol-compatible (FullVersion::is_protocol_compatible) - what makes a global path work across API versions, e.g. for firmware update or device info calls.RequestKind<'i>- the operation:Call { args }/MultiCall(same method over an array or several methods at once),Read/MultiRead,Write { data }/MultiWrite,StreamSideband { sideband },Introspect(send theww_self::ApiBundleof the addressed resource),StreamOpen { resume, args }(open a counted or reliable stream, optionally resuming aStreamCursor),Observe { cfg }(how a trait level's properties are sent, see below and Observing properties). Arguments and written values travel asTailBytes<'i>- already ShrinkWrap-serialized by the generated code on either end, opaque to this crate.MultiIndex<'i>/MultiArgs<'i>- which resources a multi-request covers (Range,List, orMask32) and whether every one gets the same serialized arguments (MultiArgs::Same) or each its own (MultiArgs::Different) - see Multi requests for the full request/reply shape.Event<'i>-seq: UVlq32(echoes the request's, 0 for unsolicited stream/property updates),result: Result<EventKind<'i>, Error<'i>>.EventKind<'i>-Value { data }(a call's or read's return value),Written,StreamData { path, data }(plain stream/property update),StreamSideband { path, sideband },StreamItem { path, head: StreamHead, data }(one event of a counted/reliable stream, see below),PropertyUpdate { path, t, missed, data }(observed properties of one trait level, see below).Error<'i>/ErrorKind<'i>-err_seq(the site in the generated server that raised it, numbered the same in every server: seeerr_seqand where an error came from) plus a kind: path/argument/property deserialization failures,Unimplemented(an optional item the device leaves out),TraitNotFound(a global path to a trait the device doesn't implement in a compatible version),UserBytes/UserStr(an API's own error, forwarded verbatim), stream session errors, and more.
A Request is as small as 3 bytes (seq ≤ 127, an empty Absolute path - a root call with no arguments) once
set_seq has filled in the real seq; a GlobalCompact call adds about 2 bytes for its CompactVersion when
every number in it is ≤ 7.
Building an Event/Error while writing its payload¶
Generated server code often doesn't know yet which EventKind/ErrorKind variant it's building until the
payload is already (partly) written - a call might succeed or fail partway through serializing its answer.
EventBuilder, EventKindBuilder and ErrorBuilder (builder module) use BufWriter::save_state/
restore_state to reserve space for a result flag / discriminant up front and patch it in afterwards, the same
discriminant-patching trick
used elsewhere in the wire format - the crate's own tests check the result is byte-for-byte identical to building
the value up front and calling .to_ww_bytes().
Counted and reliable streams¶
StreamHead- per-event head of anEventKind::StreamItem:pos/units(where the event's units sit in the stream, up toMAX_POS= 2⁵⁷-1),lost(units the device dropped right before it),t: Option<Second>(device time),ext: Option<HeadExt>(rare markers, evolvable).end()/after_previous()/lost_units()are small helpers over these positions. See stream protocol for how a host turns a sequence of heads into "what did I just lose".StreamCursor-session/pos: a point in a stream session, kept by the host and passed back inRequestKind::StreamOpento resume after a reconnect.StreamOpened<'i>- the answer toStreamOpen: the (possibly new)session,oldestposition the device still has,live_from(where the live run starts),resume_from(where backfill starts,Nonewithout one), andresult(the stream function's own serialized return value).
Observing properties¶
The observe module (API-38) is the protocol for sending a trait level's properties at the rates a host asks for,
any subset of them in one event. A level is one trait (the root API or a ww_impl! trait), addressed by its path;
its properties are named by their resource index within the level, as in the introspection data. Only properties
declared with +observe can be observed. See Observing properties for the device/host APIs
built on this protocol and the full wire-format picture; this section is the raw types.
ObserveRequest<'i>- body ofRequestKind::Observe:entries(anObserveEntry { index, mode }per property),patch(true: properties not listed keep their setting;false: they areOff) andbudget(bytes per second the level's updates may take on the link,Noneleaves it to the device).ObserveMode-Off,OnChange { min_us }(on a change, at most everymin_us),Periodic { every_us },OnChangeOrBeat { min_us, max_us }(on a change, and at least everymax_us). Periods are in microseconds. Evolvable: a mode from a newer host reads asUnknown(_)and is refused.ObserveGranted<'i>- the answer, in anEventKind::Value: every property observed now with its mode as clamped by the device, therefusedentries with anObserveRefusal(NoSuchIndex,NotObservable,ModeNotSupported,Duplicate), and the grantedbudget.EventKind::PropertyUpdate { path, t, missed, data }- one level's update: device timet,missed(sends that were due but didn't fit the budget since the previous update) anddata, one slot per resource index with the properties sent this time present.ser_property_update_eventwrites it with the data in place. Withouttandmissed, the event costs 4 bytes plus its data for a level one deep.
Old hosts never send Observe, so they never receive a PropertyUpdate. A device without observe support answers
Observe with OperationNotSupported.
Credit sinks¶
A #[credit] sink!(..) (credit sinks) uses the messages of reliable streams
the other way round: the host opens its session with RequestKind::StreamOpen { resume: Some(cursor) } (answered
with stream::StreamOpened, whose capacity is the sink's buffer), writes with RequestKind::SinkWrite { pos, data }
(one unit) or SinkWriteUnits { units, pos, data } (build both with RequestKind::sink_write(units, pos, data), read
them with as_sink_write()), and the device answers with StreamSideband::Ack, Resend and Query. A read of the
sink returns its stream::StreamStatus. The state machines, SinkCredit on the device and SinkTx on the host, are
in ww_stream::sink (stream protocol).
Worst-case message sizes (max_len module)¶
call_request/call_request_compact/value_event compute the upper bound in bytes of a Request/Event for a
given resource path and argument/return MaxSize (SW-19) - None when something in them is unbounded. This is
what backs the generated <METHOD>_REQUEST_MAX_LEN/<METHOD>_RESPONSE_MAX_LEN consts (API-18), used to size
fixed buffers ahead of time.
On the wire¶
PathKind, RequestKind and EventKind are all final_structure with a ww_repr = nib discriminant - one
nibble to say what kind of request or event this is, no reverse-length for the enum itself since it's always the
outermost or last-written thing. StreamSideband and ErrorKind similarly pick discriminant widths (nib,
u8) sized to how many variants they have and how often each is used. FULL_VERSION/COMPACT_VERSION (the
protocol's own version, built with wire_weaver::full_version!() the same way every #[ww_trait]
is) are exchanged and checked during link setup with a remote device, independently of any user API's own
version.