WireWeaver features and roadmap¶
This file is the single source of truth for what WireWeaver supports, what is being worked on and what is planned, for humans and AI agents alike. It replaces the external task tracker.
How to use this file¶
- Status of each item:
- β done
- π§ in progress or partially done (the note says what is missing)
- π planned
- π‘ idea, not committed to
- β blocked (the note says on what)
- π probably done or obsolete, needs a check before closing
- π§ͺ done and tested on the host, the check on hardware is still open (the note says what to check)
- Target is the release an item is planned for:
v0.5is the next release (the workspace is at 0.5.0, crates.io has 0.4.0).v0.5.xis the next compatible release after it:v0.5.1if nothing turns out to be breaking, otherwise the next minor. No tag means not scheduled.grep 'v0.5' FEATURES.mdgives the release scope. - IDs (
SW-3,CLT-12) are stable: never renumber or reuse one. New items take the next free number of their area, even when they are inserted in the middle of it. Use the ID in commit messages, CHANGELOG entries and codeTODOs (// TODO(SRV-4): ...). When talking to people, name an ID with a short slug from its title (SW-3 string-and-vec), never bare. - Items are grouped by area. Each area lists what works first (β ), then open items ordered by target.
- When finishing work, update the item in the same change: mark it β , move it up to the done items of its area and add a pointer (docs page, crate, test or example). Don't delete done items. Move items that turn out to be obsolete to Dropped and superseded with a one-line reason.
- This file is also rendered as the docs site's "Feature tracker" page (
docs/features.mdincludes it), so indent nested lists and paragraphs inside an item by 4 spaces, as Python-Markdown requires. The "Features" page (docs/features_today.md) is generated from it and fromtests/features_page/entries.md, which has a short line, a reading level and a docs link per item: an item that becomes done or in progress gets a line there, then runjust features-page;just testfails until both are done. - New ideas go into the matching area as π‘. Small code-level gaps stay as
TODOcomments in the source; only those that limit users or block a feature get an item here.
Wire format (shrink_wrap, SW)¶
- β
SW-1 Structs, plain enums and data enums, discriminant type selection with
ww_repr. - β
SW-2
OptionandResult, also insideVec, as one-bit flags. - β
SW-3
StringandVec<T>, with special handling ofVec<u8>. - β
SW-4 Fixed size arrays, tuples and the unit type
(). - β
SW-5 Bit-aligned integers: nibbles and
u1..u32(un);un8/un16are forwarded toun32for smaller flash. - β
SW-6 Variable length numbers:
UNib32,UVlq32, andUVlq32Backfillfor values known only after the rest is serialized. - β
SW-7 Borrowed and owned types:
SizedandUnsizedtraits,RefStr/RefVec, borrowed types by default andOwnedvariants generated automatically. - β
SW-8 Evolvable by default: every struct and enum is
Unsized;final_structure,self_describingandsizedare opt-ins. - β
SW-9
TailBytes/TailBytesOwned: bytes that take up the rest of the buffer, written without a size; only valid as the last item. Generated API code puts them in place of byte slices, e.g. for return values and property values. - β
SW-10
EitherAnyVec: zero-copy, no-alloc array ofEitherelements of any types, with the L/R flags packed in groups of 8 instead of padding each element. Packs e.g. a list ofResults tightly; used for multi-read and multi-call results (only multi-read exists so far). - β
SW-55
OptionAnyVec: zero-copy, no-alloc array ofOptionslots of any types (numbers, structs, lists, strings, a nestedOptionAnyVecfor a child level), only the present values written, in slot order without padding between them. A presence header up front in the smallest of four modes (all, dense bits, two-level bits, index list), self-delimiting so that readers with fewer or more slots than the writer read it (slots are only appended); a malformed header is an error. Writer takes the present set up front (SetBitsover a bitset, any increasing iterator),OptionAnyVecWriter/ in-placeOptionAnyVecBuilder. For a trait level's properties in one update (API-38). Seedocs/serdes/special_types.md#optionanyvec-sparse-slots-of-any-types,docs/serdes/showcase.md. - β
SW-68 Word-wide bit writer and reader, the same bytes:
BitWriterwith a 32-bit accumulator flushed a word at a time,BitSink::put_blockfor packed blocks,write_un*/write_bool/UNib32and the reader'sread_un*/UNib32through one 4-byte window and one bounds check per call. The FOC observe poll on an STM32H725 75.5 k cycles instead of 99.9 k atopt-level = 3(121.5 k against 172.2 k at"z"), a FOC level batch from the store 36.8 k instead of 58.9 k; reads of UN numbers andUNib32take 0.4-0.6x the instructions on a Cortex-M4F; about 1.5 KB more flash at"z". Seedocs/articles/series.md#the-bit-writer-and-reader-a-word-at-a-time,examples_mcu/mcu_qemu/src/bit_io.rs(qemu-bit-io,qemu-m4f-bit-io,stm32h725ig-bit-io-cycles). - β
SW-64
Serieswritten straight from aSeriesStore, column by column, the same bytes as the item walk:SeriesStore::columnar/write_store(leaves,Option, arrays, tuples, derived structs and enums, by hand forNumericValue) andseries::write_store_series, used byww_stream's batchers on their own. Needs one variant per enum andOptionover the rows (a prefix change falls back to the walk). One FOC level batch (21 xSeries<T>+ clock, n = 32,DeltaPacked): 74.9 k instructions on a Cortex-M3 instead of 143.5 k, 100 k cycles on an STM32H725 instead of 312 k (60 k against 125 k atopt-level = 3). Seedocs/serdes/series.md#from-the-store-column-by-column,shrink_wrap/shrink_wrap/src/series/store_write_tests.rs,examples_mcu/mcu_qemu(qemu-store-columns). - β
SW-63 Fast and small
Seriesencoding on MCUs, the same bytes: about 6x fewer instructions per item on a Cortex-M3/M4F (Series<Timed<Ampere>>, n = 32: 341 with a fixed codec, 969 withSMALLEST; 25 200 cycles per batch of 32 on an STM32H725), and codec policies fixed at compile time (SeriesPolicy,FixedCodecs,DeltaPacked, theno_stddefault ofww_stream's batcher) that link only their own codecs: 15 KB of encoder flash on a Cortex-M3 instead of 91 KB (25 KB with a run-timeCodecPolicy). Seedocs/serdes/series.md#cpu-and-flash-on-an-mcu,examples_mcu/mcu_qemu,examples_mcu/qemu_icount. - β
SW-62
SeriesStoreand series size bounds: room forNitems of aseriestype as a struct of arrays (empty_store,set,get), derived for structs (field stores) and enums (discriminants and every data variant's fields), by hand forww_numeric::NumericValue(a variant byte and one payload as wide as the widest variant enabled):Timed<Ampere>takes 12 bytes per item with number types up to 32 bits, against 24 as items.SeriesColumns::MAX_ITEM_BITS/MAX_FIXED_BITSandSeries::max_bytes(n): the worst case of a batch under any codec policy,Nonefor strings and opaque fields.series(no_store)leaves the derived store out. Piece 1 of API-41. Seedocs/serdes/series.md#collecting-items-on-a-device-seriesstore,shrink_wrap/shrink_wrap/src/series/store_tests.rs,ww_stdlib/ww_si/tests/store.rs. - β
SW-61
RefVecBuilder<T>: a list written in place, element by element (push,extend,push_withfor a nested builder or an element written by hand),finishwrites the count; same bytes asRefVec::Slice, as the whole buffer, a struct field or an element of another list. A failed push undoes its element,with_max_lenforMaxLen<RefVec<..>, N>,RefVecBuilderOwnedforBufWriterOwned. A byte list also hasextend_from_sliceandwrite_bytes_with(a byte blob written by other means), which replaceRefVecU8Builder. Seedocs/serdes/special_types.md#refvecbuilder-lists-written-in-place,shrink_wrap/shrink_wrap/tests/ref_vec_builder.rs. - β
SW-59 Tuple and unit structs in the derive:
struct Celsius(f32);,struct Pair<'i>(u8, &'i str);,struct Marker;in every layout and size assumption, same bytes as the struct with the fields named; positional names_0,_1, ... in consts, C names and errors.#[flag]needs named fields. The API model has them as unnamed fields, dynamic clients and Python (a tuple, a single field its value, a unitNone) read and write them. A unit struct is evolvable (it records its length inside a parent); withsized,final_structure,self_describingorfixed_layoutit is a compile error, as it could never change. Seedocs/types.md,shrink_wrap/shrink_wrap/tests/tuple_structs.rs,tests/dynamic. - β
SW-58
validate = pathderive directive: a function checking every struct value just read, so a derived type (and the API model it gives to dynamic clients) can still reject out-of-range data; used byww_date_time::Year. - β
SW-57
ArrayString<N>/ArrayVec<T, N>: owned, no-alloc string/list types backed by a fixed-size array, same bytes on the wire as&str/StringandVec<T>, for an owned no-alloc field (avalue_on_changedfield of a string/list property in ano_allocserver, a settings value);Error::MaxLenExceededrather than a cut value, for serializing, deserializing,push_str/pushandTryFrom<&str>alike.docs/serdes/special_types.md#arraystring-arrayvec-owned-values-without-an-allocator. - β
SW-11 Alloc writer
BufWriterOwned,SerializeShrinkWrapfor&[u8],Stringas UFS. - β
SW-12 In-place builders used by the server codegen (event and error builders), and the manual
patch-the-discriminant-later pattern (
docs/serdes/showcase.md). - β
SW-13 Binary format documented in
docs/serdes/wire_format.md, worked tricks indocs/serdes/showcase.md. - β
SW-14 Usable standalone as a dense data storage format (
examples/minimal_shrink_wrap,examples/compare_wire_formats). - β
SW-34 Smaller code on small CPUs:
BufReader/BufWriterreads and writes that keep an error instead of returning it (read_u8_latch,write_u32_latch, ..,latched()), used by#[derive_shrink_wrap]for plain fields with one check per type;Errorwithout payloads; one shared integer read and write; no division inUNib32. 19 to 20 % less code for a derived request/reply codec on RV32IC and Cortex-M0, same wire format. Numbers, where the rest goes and what is left to do:docs/serdes/code_size.md(measured by TEST-17). - β
SW-36 No length FIFO code for types without lengths:
SerializeShrinkWrap::PUSHES_LEN, computed by#[derive_shrink_wrap]from the fields (defaulttrueunlessSized), letsto_ww_bytesandBufWriter::finish_for::<T>()end withfinish_no_lens()at compile time, so the FIFO encoder is not linked: about 510 bytes less per such encoder on RV32IC (a struct of plain fields 1476 β 964). For types with lengthsencode_len_fifo/write_nibare smaller (vec_ser1378 β 1142). Same wire format (shrink_wrap/tests/pushes_len.rs,docs/serdes/code_size.mdsteps 4 and 5). - β
SW-37
RefVecof plain numbers without the decode pass: a list ofu8..u128,i8..i128,f32orf64finds its end with one bounds check instead of reading every element (4096 bytes: 10 Β΅s β 6 ns on a host), same result, errors and reader state as the pass, same wire format (shrink_wrap/tests/ref_vec_skip.rs,just fuzz-shrink-wrap-ref-vec). Costs 50 to 140 bytes of code in a decoder with such a list. OtherSizedtypes keep the pass on purpose (stride is notsize_bits, elements are validated there):docs/serdes/code_size.md. - β
SW-38 Latched reads of
U1..U64/I2..I64fields: read like the integers of SW-34, with one error check per type (through a hiddenDeserializeShrinkWrap::des_shrink_wrap_latch); a bit-packed struct decodes in 40 bytes less on RV32IC (886 β 846), 88 on RV32I. Same wire format, results and errors as reading field by field (shrink_wrap/tests/latched_reads.rs,just fuzz-shrink-wrap-latched).Nibble,u64and varints are not latched: one such field costs more than it saves (docs/serdes/code_size.md). - β
SW-41 First error in types that mix latched and other fields: an error kept by a
_latchread stops the reader (reads with aResultreturn it, no read moves), so a derived decoder with lists or nested types next to plain fields returns the first error again, as before SW-34. 20 bytes on a request/reply decoder (RV32IC), four forms measured (docs/serdes/code_size.md,shrink_wrap/tests/latched_errors.rs). - β
SW-43 C99 runtime (first step of API-26):
shrink_wrap/c/shrink_wrap.{h,c}, theBufReader/BufWriterof the Rust crate for C: bits, nibbles,un8..un64, LE integers, floats,UNib32,UVlq32/UVlq64, bytes and strings as borrowed views,split, lengths at the back andfinish,Unsizedvalues. Nomalloc, the first error kept as in SW-41 (a failing call moves nothing), octets through accessors so 16-bitchar(TI C2000) works. Checked against the Rust crate with golden vectors and a proptest cross-check in both directions, also on truncated and corrupted input (shrink_wrap/c/tests, part ofjust test;just lintbuilds the C with-std=c99 -pedantic -Werror). How to use it:docs/c.md.TailSize, the series types and the_latch/finish_no_lenscode size tricks are not ported. - β
SW-44 Fixed-layout fast path:
#[derive_shrink_wrap]sees when a type's encoding is a fixed little-endian byte layout (asizedorfinal_structurestruct, asizedorself_describingenum with au8/u16/u32ww_repr; fields onlyu8..u64,i8..i64,f32,f64and arrays of them) and generates an offset const per field,ENCODED_LEN, andfrom_fixed/to_fixedthat load and store at those offsets on the slice, noBufReader/BufWriter, nothing generic. Thefixed_layoutdirective makes it a requirement (and allows nested fixed-layout types): abool,Option, list, string, varint or bit-packed field is a compile error naming the field. Same bytes asser_shrink_wrap(shrink_wrap/tests/fixed_layout.rs, 2000 random values per type).FixedLayout::write_cprints the matching C reader (offset defines andstatic inlineloads) from the same derive,fixed_layout::write_c_headera whole header: the first generated C of API-26. Code size on Cortex-M0 for a header of eight integers: decode 484 β 276 bytes, encode 888 β 462 (docs/serdes/code_size.md). First user: chunky_mcu_updater's plan format (P2625 PROTO-3); it serves the busgen idea of P2401 #9oct-1433 for types without dynamic fields. Open: arrays of nested types, a C writer, 16-bitchartargets in the C reader. - β
SW-45 Bit fields and a variable last field in the fixed-layout fast path:
bool,U1..U32,I2..I32andNibbleget compile-time bit offsets (most significant bit first, aNibbleon a 4-bit boundary, a byte field after them starting a fresh byte, asBufWriterpacks them) and are read and written with shifts and masks, in Rust and in the C reader (sw_fixed_bits,sw_fixed_sbits).Option<T>,Result<T, E>and enums of unequal variants are allowed as the last field (one rule for all variable-length fields); their flags are bit fields, also where#[flag]moves them.MIN_LENandfixed_len()next toENCODED_LEN(the longest, the buffer to have ready). A nested type cannot share a byte with bits of its parent: a compile error says so. Every refusal says why and what to use instead. Byte-equal toser_shrink_wrapon mixed bit runs, a run at the end, options, results, relocated flags and enum variants with both (shrink_wrap/tests/fixed_layout.rs); the generated C is built and run with every warning an error (just lint-c). A bit-packed sample on Cortex-M0: decode 652 β 276 bytes, encode 1034 β 560. - β
SW-46 Fixed head, sequential tail (
head_tail): an opt-in wire format of its own for types with lists, strings and options anywhere. The head holds every fixed-width field at a constant offset (numbers, arrays, bit fields as in SW-45) plus au16count per list or string, the flag bit of eachOption/Resultand the head of each nested type; the tail holds the variable parts in field order. An enum is its tag, then its variant's head and tail. Generated by#[derive_shrink_wrap(head_tail)]: offset consts,HEAD_LEN,from_head_tail/to_head_tail/head_tail_len, theHeadTailtrait (also implemented for every fixed-layout type) and a C reader (head readers plus a<name>_tail()walker,shrink_wrap::head_tail::write_c_header), built and run byjust lint-c. Non-generic, size first: noBufReader/BufWriter, no length FIFO, no reversed lengths; the tail is a cursor (head_tail::Tail) that takes lengths it already knows. Lists arehead_tail::List<'i, T>(items numbers or fixed-layout types of one length, read when iterated). No sw serdes and no owned version: one type, one format. Tests: exact bytes, round trips, every truncation, 200 000 random buffers, UTF-8, counts over u16 (shrink_wrap/tests/head_tail.rs). busgen's request and reply enums (size benchbusgen_ht): 4626 bytes on Cortex-M0 and 6004 on RV32IC, against 4408 / 5762 hand-written and 6652 / 8210 derived (P2401 #9oct-1433). Open:head_tailtypes as ww API arguments (they have no sw serdes), lists of variable-length items, a C writer,u8counts for short lists (MaxLen). - β
SW-52
head_tailviews and variant writers:Type::view(buf)gives aTypeView<'_>without making the value: the lengths checked and the tail walked once, the head's fields read by accessors at their offsets when called (the same code asfrom_head_tail, one field at a time), lists, strings, options and nested views kept from the walk; an enum's view is an enum of its variants' views (a tuple variant of one nested type holds its view). Every enum variant getsEnum::write_<variant>(buf, fields..): the bytes ofto_head_tailfor that variant without making the value (the samewrite_htcode).HeadTail::View/view_ht,head_tail::view. Tests: values back from every accessor, writers byte-equal toto_head_tail, every truncation, 200 000 random buffers agreeing withfrom_head_tail. Size benchbusgen_view: 4988 bytes on RV32IC and 3894 on Cortex-M0, 13 % and 12 % below the hand-written codec; busgen's firmware 5608 / 5090 bytes (UART / I2C image, fpga_tools BUS-12). View types are alwayspub(theViewof a public trait). Open: lazy views of nested fixed-layout types (read when the view is made), a writer for structs. - β
SW-53 Numbers through
fixed_layout::Le, out of line on RISC-V: generated fixed-layout andhead_tailcode reads and writes multi-byte numbers withLe::ld/Le::ston the bytes at the field (fixed_layout::at), one function per type on RISC-V (no unaligned loads), inline elsewhere (on Cortex-M0 the byte loads are smaller than the calls, measured). Under LTOldpasses its pointer throughblack_box: argument promotion would otherwise move the load into every caller. Bit-field bytes cleared once per head.busgen_hton RV32IC 6004 β 5658; a type read or written in one place grows (fixed_ser498 β 612, docs/serdes/code_size.md). - β
SW-54
#[cfg]onhead_tailenum variants: passed to everything generated for the variant (consts, arms, view, writer, C text); a variant compiled out keeps its tag and reads as an unknown one. busgen's firmware builds only its bitstream family's requests with it. - β
SW-39 Byte-wise
memcpy, measured, advice for very small targets: a firmware's ownmemcpy(and__aeabi_memcpyon Arm) of 22 bytes instead of the word-wise one ofcompiler_builtins: 324 bytes less on RV32IC, 304 on Cortex-M0, for every image that moves a value, at the price of speed and of replacingmemcpyfor the whole firmware. Code and downsides indocs/serdes/code_size.md("Build settings"), measured withjust size-bench --fw-features byte_memcpy. Nothing inshrink_wrapdepends on it. - β
SW-40 Bit reads stay out of the lengths in the last byte:
read_un8..read_un64(andU1..U64,I2..I64) no longer read into the length nibble the last byte shares with the forward data, and a length is no longer read from bits the forward side has read; both areOutOfBounds..errors. Malformed input only, every message the writers produce decodes as before. Checked against a bit-level model in every reader state of 1 to 3 byte buffers (buf_reader.rsbounds_every_state_every_read) and by round trips of random messages (shrink_wrap/tests/rev_nibble_bounds.rs). - β
SW-35 Exact
ElementSizeof enums: asized/self_describingenum reports the discriminant plus one variant (ElementSize::one_of) instead of the sum of all variants without the discriminant;sizedwith variants of different sizes is a compile error; user types inside arrays, tuples, ranges and options are counted. Same wire format (shrink_wrap/tests/enum_size.rs,docs/serdes/size_assumptions.md). Unblocks readingRefVec<Sized T>in place. - β
SW-33
UVlq64: byte-based variable lengthu64, same big endian VLQ encoding asUVlq32, 1 to 10 bytes,uvlq64field type in#[derive_shrink_wrap(..)],BufReader::read_uvlq64/BufWriter/BufWriterOwned::write_uvlq64(shrink_wrap/src/vlq64.rs,docs/types.md,docs/serdes/showcase.md). NoUVlq64Backfill(not asked for). - β
SW-19 Bounded lists and strings
MaxLen<T, N>: aRefVec,Vec,&strorStringwith at mostNelements, same bytes asT(a bound can be added, raised or removed without a new wire form), checked on serialize and deserialize (Error::MaxLenExceeded, never truncated). Works in derived types and in every API position (arguments, returns, properties, stream and sink items), visible in introspection (ww_self::Type::MaxLen), honoured by the dynamic client and Python; raising a bound is compatible, lowering or adding one breaking (ww api check).SerializeShrinkWrap::MAX_SIZE: Option<MaxSize>, the worst-case size of a type whose every part is bounded, feeds API-18.docs/types.md,docs/evolution/rules.md,shrink_wrap/tests/max_len.rs,tests/bounded,examples_mcu/mcu_qemu(qemu-bounded). - β
SW-15 Handle end of buffer when reading evolved fields: a field with
#[default = ..]reads as its default only when it starts at the end of the data and runs out of it (Error::is_read_eob); a truncated or malformed value is an error. The flag of an evolvedOption/Resultfalls back with it,#[default]works on any type and its value is used (anOptionwas alwaysNone). Same in theww_selfdynamic decoder. Padding bits of the old data's last byte read as 0, so bit-sized fields default to their zero value. Decided: no implicit defaults forVec/String/Optionfields without#[default], the attribute is what marks a field as added later (and whatww api checklooks for).docs/serdes/derive.md#evolving-types,shrink_wrap/tests/evolution.rs. - β
SW-16
FutureVersionenum variant: an enum ending with#[future_version] Unknown(u32)reads an unknown discriminant into it instead of failing withEnumFutureVersionOrMalformedData, and writes it back; anUnsizedenum skips the newer variant's fields with its size slot,sized/self_describingones only survive new unit variants. No wire number of its own, no change toELEMENT_SIZE, left out of the API model. Theww_selfdynamic decoder reads an unknown variant of anUnsizedenum as<unknown N>. Added by default since SW-47.docs/serdes/derive.md#enum-variants-from-a-newer-version,shrink_wrap/tests/evolution.rs. - β
SW-48 Bit-sized fields and variant payloads:
U1..U64/I2..I64(u7,ub10,i2.. in the API model) as struct fields, enum variant payloads and#[ww_trait]method arguments, returns and properties, packed bit after bit with a sub-byte discriminant:enum I2cAddress { Seven(U7), Ten(U10) }withww_repr = u1, self_describingtakes 8 or 11 bits instead of 16 or 24 withu8/u16. Server and client codegen (it panicked on such a method argument),I16(was missing),From/TryFromto and from the backing integer. The dynamic client and Python already handled them, now tested byte for byte against derived types; the C runtime has the bit reads. Same wire format (shrink_wrap/tests/sub_byte.rs,tests/dynamic,tests/methods,docs/serdes/showcase.md#bit-sized-fields-and-variant-payloads-a-7-or-10-bit-i2c-address). - β
SW-47
#[future_version]variant by default: the derive addsUnknown(u32)to everyUnsizedenum with a free discriminant;no_future_versionopts out (framework enums whose readers must fail:ww_selfmodel,MultiIndex),future_version = Namerenames it,final_structure/self_describing/sizedenums only get it when written. A Rust API change of every such enum (exhaustive matches need the arm), not of the wire or the API model.docs/serdes/derive.md#enum-variants-from-a-newer-version,shrink_wrap/tests/evolution.rs. - π SW-66 128-bit numbers in the C runtime β
shrink_wrap/creads and writes up to 64-bit numbers;u128/i128(twou64halves in C) are missing. Conformance casesint.u128.max,int.i128.min. - π SW-67
no_future_versionand#[series(opaque)]have no conformance case β found by the coverage guard (TEST-25): no case turns off theUnknown(u32)catch-all (SW-47) and then feeds an out-of-range discriminant (needs hand-written reject bytes, no valid Rust value produces them), and noSeries<T>(SW-60) case has a field marked#[series(opaque)], so its own column form is untested. - π SW-17 Const / magic type Β·
v0.5β a simple type with a generic parameter that serializes a constant value or byte string and checks it on deserialization, failing on a mismatch. For example themagicfield inww_self(a plainu32today). - β
SW-18 Size-of-enclosing-type field
TailSize<N>: a field the derive macro fills in with the size of the rest of the value once it is serialized (aUVlq32right-justified inNbytes, 1 to 5), in any position, nested types each with their own. Readers bound the value by it (a longer buffer stops at the right byte, a truncated one errors, newer trailing fields are skipped) or skip it without parsing. Fields before it must beSizedorSelfDescribing, checked at compile time.docs/serdes/special_types.md, showcase. - β
SW-60
Series<T>: columnar compressed sequences of any derive type β a list ofT(structs, enums with data,Option, nested) written column by column, one column per leaf field: numbers as a numeric column whose predictor (raw, delta, delta of delta) and coder (SW-31 prefix code, bit-packed blocks of 8, constant, plain) the writer picks per column, floats plain or SW-32 XOR, enum tags, bools,Optionpresence and strings as runs (a value and one entry per change); a column restarts where an earlier enum field (a ww_si prefix) changes. 3 to 8 times smaller than a plainVec<T>of ADC samples, self-contained per batch: the column codecs (numeric, 128-bit, float, runs, codec choice by counting bits or a fixedCodecPolicy, no alloc, malformed input an error), theSeries<'i, T>/SeriesOwned<T>types and theSeriesColumnstrait with impls for the primitives,Option, tuples, arrays, strings and bytes (one-pass validating skim, parallel item iteration, evolvable structs both ways, the design's examples and size tables as golden bytes), fuzz targetsshrink-wrap-series-columnsandshrink-wrap-series-type; the opt-inseriesdirective of#[derive_shrink_wrap]for structs (also generic, DER-14) and enums,#[series(opaque)]fields, compile errors naming the field for what v1 doesn't take, onww_siquantities andww_numeric::NumericValue(shrink_wrap/tests/series_derive.rs,ww_stdlib/ww_si/tests/series.rs); docs (docs/serdes/series.md, the layout indocs/serdes/wire_format.md#seriest, worked bytes indocs/serdes/showcase.md#compressed-columns-seriest).Series<T>is usable today in any property, method or stream item; a dynamic (non-generated) decoder and batching a stream or property intoSeries<Timed<T>>automatically build on it as their own items (CLT-29, API-41), not blocking this one. - π SW-42
MAX_SIZEon owned types Β·v0.5.xβSerializeShrinkWrapOwnedhas no worst-case size yet, only the borrowed trait has (SW-19); host code sizing a buffer for an owned value uses the borrowed type's. - π SW-20 Sub-type numbers Β·
v0.5.xβ numbers restricted to a range, like10..=25, or a set with gaps, like0..=8, 12, 16; checked on serialization and deserialization, possibly using fewer bits on the wire (docs/types.md, "Subtypes"). - π SW-21 Multi-dimensional arrays (2D, 3D).
- π§ SW-22 Fuzz
shrink_wrapβfuzz/has oneshrink_wraptarget,shrink-wrap-tail-size(TailSizetypes round-tripped, random bytes into their readers);shrink-wrap-ref-vec(SW-37) came with its feature,shrink-wrap-option-any-vecwith SW-55 and DER-13 (sparse structs),shrink-wrap-series-columnsandshrink-wrap-series-typewith SW-60,shrink-wrap-nestingwith the nesting depth limit (recursive types at random limits). Round-trip every built-in type and the derive's features. - π SW-23 Tuple enum variants evolution β check that tuple variants evolve as
docs/evolution/rules.mdsays (fields can be appended) and add a test. - π‘ SW-65 Aligned plain
Seriescolumns β numbers byte-aligned and little-endian at their width (numeric escape11 11, float codec0010, anAlignedPlainpolicy), copied from aSeriesStore: 34 k cycles per FOC level batch on an STM32H725 atopt-level = 3against 60 k for column-wiseDeltaPacked(SW-64), for 2.3x the bytes. Built and measured on branchs/18oct2026-memcpy-fast-sw65, not landed; may come back after observe cells and au32payload inNumericValueStore. - π‘ SW-24 ZigZag encoding for signed variable length numbers β consider ZigZag (as in protobuf) so that small
negative values stay short, e.g. signed counterparts of
UNib32/UVlq32. - π‘ SW-25 In-place builder for server handlers Β·
v0.5.xβ a new step-by-step / partial builder (e.g. for aRefVec) might be useful with the newContextin server handlers, to write replies and events straight into the buffer. It might not work out. - π‘ SW-26 Map support β
HashMap/BTreeMap, probably as aVecof(K, V). - π‘ SW-27 In-place editing of serialized data.
- π‘ SW-28 Compression wrappers β the time-series part is done as
Series<T>(SW-60, with the SW-31 delta and SW-32 XOR codes); in-place dictionary compression asCompression<T>(strings?) stays an idea. - π‘ SW-29 Flatten support β apply a type's layout across struct/enum boundaries.
- π‘ SW-30
derive_shrink_wrapon a whole module at once. For definitions in separate files with own syntax, see standalone definition files. - π‘ SW-56 Tri-state slots in
OptionAnyVecβ absent / value / error per slot, e.g. a property update that says why one value is missing. The header has room for it: mode nibbles4..=15are reserved (a reader fails on them today), so 2-valued vecs don't pay for it. - β
DER-1
#[derive_shrink_wrap(..)]attribute with clear syntax, no hidden magic and good error messages (docs/serdes/derive.md); replaced#[derive(ShrinkWrap)]. - β
DER-2 Discriminant checks: discriminants must fit
ww_repr, implicit ones are numbered like in Rust. - β
DER-3
#[default = ..]for evolved fields and#[flag]relocation. - β
DER-4
TailBytesmust be last: rejected anywhere but as the last field of a struct or enum variant, and nested inOption,Vec, tuples or arrays (check_tail_bytes_position), likeUVlq32Backfillmust be first. - β
DER-10 Generic arguments on user field types:
Foo<u8>,Wrapper<'i, T>are emitted as written,Wrapper<'i, T>maps toWrapperOwned<T>in the owned variant (docs/serdes/derive.md, type mapping; testshrink_wrap/tests/generic_external.rs). - β
DER-14 Generics on derived types: structs and enums with type parameters (
struct Timed<T> { t: Second, value: T },enum Reading<'i, T>), const parameters of any type (struct Buf<const N: usize> { data: [u8; N] },MaxLen<&'i str, N>,const STRICT: bool), defaults and where clauses, also with a lifetime (Tagged<'i, T>/TaggedOwned<T>); each impl bounds the type parameters by its trait (serdes, owned serdes,SeriesColumns) on top of the bounds and where clause as written, the size ofTandNis part of the type's.series,display,validateanddiscriminantstake generics,fixed_layout,head_tailandsparsereject them (docs/serdes/derive_reference.md#generics; testsshrink_wrap/tests/generic_items.rs,series_derive.rs;ww_si::Timed<T>). In APIs and the API model: API-43. - β
DER-15
PhantomDatafields:PhantomData<T>is zero bits (Sized { size_bits: 0 }, like()) borrowed and owned, and aSeriesColumnswith no columns, for anyT; the derive leaves a type parameter used only insidePhantomData<..>without the serdes /SeriesColumnsbounds of DER-14, sostruct Quantity<U> { value: i32, _unit: PhantomData<U> }works with a bare unit marker and writes the same bytes as without the field (docs/serdes/derive_reference.md#generics; testshrink_wrap/tests/phantom_data.rs). Forww_siunits. - β
DER-17
#[default = ..]fields default in serde too: a type derivingserde::Deserialize(throughderive(..)or acfg_attr) gets#[serde(default = ..)]on each#[default = ..]field of a struct or struct variant, so RON or JSON written before the field was added still reads; for savedww_selfAPI snapshots when a later model field isn't anOption(EVO-13) (docs/serdes/derive.md#evolving-types; testshrink_wrap/tests/serde_default.rs). - β
DER-19
#[wire(W)]field attribute: a field keeps a plain Rust integer (i8,u16,u64, ..) and is written as a narrower or variable-length wire type (#[wire(I4)] i8,#[wire(UNib32)] u64,#[wire(U12)] u16): checked on write (Error::SubtypeOutOfRange, never a cut value), widened on read, same bytes as a field of type W (runtime traitshrink_wrap::WireAs). Struct and enum fields,#[default = ..],sized/final_structure,#[cfg_attr(pred, wire(W))],seriescolumns (series::Wired<W, T>, a W column). The API model records W (the walker reads it, also insidecfg_attr), so the dynamic client, Python and C need nothing new (docs/serdes/derive_reference.md#wirew-field-attribute; testsshrink_wrap/tests/wire_as.rs, conformancewire.*). Forww_siexact units (prj/P2401-wireweaver/design/ww_si_in.md). - π‘ DER-21
#[wire(W)]beyond plain fields β insideOption, arrays and lists (Option<#[wire] u8>has no syntax yet: a wrapper type or#[wire(Option<U4>)]), and infixed_layout/head_tailtypes, which reject the attribute with a compile error naming DER-21 today. - β
DER-18
transparentderive directive: a struct with one field that isn't zero bits (PhantomData,()) takes that field's size class (ELEMENT_SIZE,PUSHES_LEN,MAX_SIZE), series columns, store and cells, and writes exactly its bytes in every position, borrowed and owned, generic, withvalidateanddisplay; compile errors name the type for zero or two real fields and for the directives it excludes. Recorded in the API model (ItemStructExtra::transparent, read by the walker,ValueOwned's decoder andSeriescolumns, so thewwCLI andwire_weaver_pyread it too). Forww_si::Fixed(STD-55), where afinal_structurewrapper wasUnsizedFinalStructureeven around au64and couldn't sit in asizedparent. Also fixed: a generic parameter namedP,I,SorCclashing with the derive's own in series code (PhantomData<(U, P)>included), and#[repr(..)]of a derived struct being dropped (docs/serdes/derive_reference.md#transparent; testsshrink_wrap/tests/transparent.rs,phantom_data.rs, conformance casestransparent.*). The evolution checker compares wrappers by meaning: EVO-15. - β
DER-20 Unit-typed fields cost what plain fields cost: a
transparentwrapper (DER-18, e.g.exact_units::Fixed) inside a derived struct is written and read on exactly the path of its inner plain field: the latchedwrite_*_latch/des_shrink_wrap_latchcalls with one error check per type (SW-34, SW-38), not the wrapper's ownser_shrink_wrapwith a checked write per field. Hiddenser_shrink_wrap_latch(borrowed and owned) withBufWriter::write_field, used by the derive for every user-type field; a wrapper withvalidate(Fixed's bounds) keeps a value out of range as the latched error (BufReader::keep_error). TEST-27 had measured +2 to 15 instructions and +4 to 42 bytes per struct encode/decode on M0, M4F and M33 before it; same wire format (docs/serdes/derive_reference.md#transparent,docs/serdes/code_size.md#transparent-fields; testshrink_wrap/tests/transparent.rsfields_are_latched_like_the_plain_field, fuzzshrink_wrap_latched). - β
DER-11 Field type from a macro_rules
$T:tyfragment: accepted like the type written directly, instead of rejected as an unsupportedsyn::Type::Group(testshrink_wrap/tests/macro_ty_fragment.rs). - β
DER-12
Displayfrom doc comments: adisplaydirective writescore::fmt::Displayandcore::error::Error(borrowed and owned) from the first doc line of each variant/struct,{field}filled in,#[display("..")]to override; a variant with neither is a compile error. Error enums need no hand-writtenDisplayon hosts (docs/serdes/derive_reference.md; testshrink_wrap/tests/display.rs). - β
DER-13 Sparse structs:
#[derive_shrink_wrap(sparse, ..)]on a struct ofOption<T>fields (borrowed and owned) encodes it as anOptionAnyVec(SW-55), one slot per field in declaration order or at#[slot = N](increasing, gaps allowed, a present value in a gap isError::OptionAnyVecUnknownSlot); a nested sparse struct is one slot. Fields are only appended. A non-Optionfield or a combination withfinal_structure/sized/self_describing/fixed_layout/head_tailis a compile error. For the per trait level<Level>Updateof SRV-18 (docs/serdes/layouts.md,docs/evolution/rules.md; testshrink_wrap/tests/sparse.rs, fuzzshrink-wrap-option-any-vec).ww_selfcarries the flag (ItemStruct::sparse,Field::slot, TOOL-11), so a dynamic decoder (ValueOwned, thewwCLI,wire_weaver_py) reads a sparse struct correctly. - π DER-5
#[since = "x.y.z"]on fields β generate correct evolution code from it (TODOinshrink_wrap_derive/src/lib.rs). - π DER-6 Check that
TailBytesends the outer type too β a struct ending inTailBytesused as a non-last field of an outersized/final_structuretype ("last in the firstUnsizedtype on the way up") is not visible to the derive; needs a marker on the type, e.g. an associated const, and a compile-time assertion in the outer type. - π DER-7 Prefix enum discriminants Β·
v0.5.x. - π‘ DER-16 Where clauses naming the borrowed lifetime, for owned types β a generic type bounded on its
lifetime (
enum Batched<'i, T> where Timed<T>: SeriesColumns<'i>) can't be derived withowned: the owned side has no'iand would need the bound asfor<'i> SeriesColumns<'i>.ww_client_server::observe::Batched/BatchedOwned(API-39) are written by hand for that. - β
DER-8
#[cfg(..)]on enum variants β a variant keeps its discriminant in every configuration, a build without it readsError::EnumVariantDisabled(docs/serdes/derive_reference.md). First user:ww_numeric::NumericValue, one cargo feature per number type (32 to 8 bytes in RAM with 32-bit types only). Not yet:#[cfg]on fields (it would change the wire format, should be a compile error),ww_sienums. - β
DER-23 A field written as a user type:
#[wire(W)](DER-19) with anyW: WireAs<T>, not only integers: a field keeps a handle in Rust and writes the value it stands for (a one-byte handle in Rust, a fullMagnitudeon the wire;exact_units::Prefix's families use the integer form,Pow10(#[wire(i8)] Pow10Exp)). In aSeriesthe field is an opaque run ofW's bytes (series::WiredOpaque, the model marks it series-opaque) while store and cells keep the handle. Testsshrink_wrap/tests/wire_as_user.rs. - β
DER-24 An enum's tag in a niche:
nicheleaves the#[repr]the derive gives an enum (u32forunib32) out, so Rust keeps the tag in a field's spare values (exact_units::Prefixwith its families andUnknown(u16)is 4 B, not 8). The same bytes,discriminant()amatch. Whethernichebecomes the default for every enum is open (Roman). Testsshrink_wrap/tests/niche.rs. -
β DER-25 Narrow tag-only catch-all for
nicheenums β#[future_version] Unknown(u16)(oru8) on aself_describing/nicheenum (DER-24) such asPrefix: the catch-all holds the tag only, so it doesn't widen the enum the way SW-47'sUnknown(u32)would (Prefix4 B instead of 8). The rule it enforces (Roman, 28 Oct 2026): every variant added after the enum's first release is tag-only, its numbers packed into theUNib32tag, never bytes after it, so an older reader yieldsUnknown(tag), skips the self-describing value and decodes the rest of the message. The derive (a known discriminant beyond the field is a compile error, a read one fails as without the catch-all, series tag store of the field's type; also ascfg_attr(feature = .., future_version)), the width in the API model (FutureVersion::Variant::bits,Noneforu32so older models keep their bytes), the dynamic client and the evolution checker (an appended discriminant beyond it is breaking),exact_units::Prefix, and the conformance casestdlib.si.quantity.unknown_scale(derive, dynamic, Python; C gap API-26). -
β API-1 Methods, properties, streams and sinks, nested traits (
docs/api/overview.md). - β
API-2 Arrays of resources with
ValidIndicesand a built-in array size getter returning a range or list. - β
API-3 Global traits from
ww_stdlib(impl Trait) and trait attachments (examples/all_gpio). - β
API-4 Results: methods return
RpcResult(Ready,Defer,Unimplemented), properties returnGetResult/SetResult, with user errors and property errors. - β
API-5 Request
Contextin every handler: seq, medium, event out, deferred replies. - β
API-6 Event,
EventKindand error builders. - β
API-7
matchon index slices, the base of multi-read support. - β
API-8
build.rscodegen viagen_server/gen_client, as an alternative to the proc macro. - β
API-9 Full name chains in generated names (fixed the
event_sidebandcollision). - β API-10 Type paths from Cargo: trait source path, external types in API crates as full paths.
- β API-11 Sync and async servers, async / blocking / promise clients from the same trait.
- β
API-28 Credit-based flow control for sinks β
#[credit] sink!(..): the device grants credit in bytes of items (the serialized value of each write, never split; the grant of anAcksince API-34), so a receive buffer holds as many items as fit whatever their sizes; the host waits when out of credit (async, blocking,try_send), writes carry the byte position of their first byte so a lossy medium resumes with go-back-N fromhave(Resend,Query),flush()waits for the final ack. No deadlock: generated<SINK>_MAX_ITEM_BYTES(fromMAX_SIZE,MaxLenbounds included) andSinkCredit::sized_for()check at compile time that the buffer holds the largest item and cap the re-grant threshold, the host fails an item bigger than the buffer withStreamError::ItemTooBig. Device state inww_stream::sink::SinkCredit(API-34), dynamic and Python clients too. Used byww_dfuStageBulk::plan. For bulk transfers (chunky OTA plans over LTE-M/LoRa/UDP), UART bridges, CAN tx. See also CLT-18 backpressure (docs/api/credit_sinks.md,tests/credit_sink). - β
API-42 Credit-sink positions without a session limit Β·
v0.5β host (SinkTx) and device (SinkCredit) count positions inu64, the wire carries them modulo 2^28 (ww_stream::sink::POS_MODULUS, at most 4 bytes as aUVlq32), the receiver extends them from the position it expects (serial number arithmetic). The buffer is at mostMAX_WINDOW(64 MiB, a quarter of the modulus), checked at compile time bysized_for, and the host never has more in flight; one fixed modulus on every link, nothing negotiated at the open. Tested over a seeded lossy link (loss, duplicates, reordering, reconnects) with sessions starting near the wrap and far past 4 GiB (docs/api/stream_protocol.md#positions-wrap-at-256-mib). - β
API-43 Generic types in APIs Β·
v0.5β generic structs and enums (DER-14,Timed<Ampere>,Window<4>,Reading<i16>,Tagged<'i, T>) as method arguments and return values, properties, stream items and fields. The API model (ww_self0.3) has generics: each definition once with its type and const parameters (Type::Param,Length::Param), each use aType::Instancewith its arguments (types, integer/bool values, a const parameter passed on, defaults filled in by codegen); generic definitions are in crate snapshots, soww_si::Timedis left out of introspection data like any known type. Generated code uses the types as written. Hosts check untrusted introspection data and instantiate it (ww_self::generics::prepare: indices, argument count and kinds, no cycles through generic definitions, nesting depth, number and size of instances bounded; dynamic decoding bounded by a value budget), so the dynamic client,wwand Python see concrete types; the evolution check compares parameters by position and kind (docs/types.md#generic-types-in-an-api,docs/evolution/rules.md#generic-types; teststests/generics,ww_selfunit tests,fuzztargetintrospection). Breaks the introspection format: hosts and devices must be built with the sameww_self. Generic traits (#[ww_trait] trait T<X>) are not supported. - β
API-44 Self-referential types in APIs Β·
v0.5β types that contain themselves throughRefBoxor a list (enum Expr<'i> { Neg(RefBox<'i, Expr<'i>>), Lit(i32) },struct Tree<'i> { children: RefVec<'i, Tree<'i>> }), directly or through other types, as method arguments and return values, properties, stream items and fields. Codegen puts each definition into the API model once, referring to its own index; a cycle through a generic definition is an error. Hosts check untrusted introspection data (ww_self::generics::validate: cycles found with Tarjan's algorithm, only throughBox/Vec, never through a generic definition, depth bounded by the sum of a cycle's definitions), and every walk over types stops at a definition it is already in (layout, compatibility check, names, definitions, default values: the first enum variant that doesn't contain the enum); values decode as deep as the reader allows (MAX_DECODE_DEPTH, ~90 levels on a 2 MiB stack in a debug build), Python objects are depth bounded (docs/types.md#self-referential-types-refbox; teststests/recursive,ww_self/wire_weaver_core/wire_weaver_clientunit tests,fuzztargetintrospection).ww_si::SIExpris one (STD-37). - β
API-45 Type aliases in APIs Β·
v0.5βpub type Volt = Quantity<VoltUnit>;and generic aliases (type Reading<T> = Timed<Quantity<T>>, const parameters too) in every API position, found like structs and enums (lib.rs, modules,useimports, globs, other crates); the right-hand side is converted in the alias' crate with each parameter replaced by its argument. The API model has no alias definitions (what an alias names is used directly, ww_si generic D2 a); alias definitions inww_selfare an idea for later.PhantomDatafields (DER-15) are left out of a struct's definition, the parameter stays in its generics (ww_self::generics::prepareaccepts that). Teststests/generics(property, method argument, insideTimed, aPhantomDatamarker, dynamic client),wire_weaver_coreunit tests,ww api tree(docs/types.md#type-aliases).use .. as ..renames are followed (API-46). - β
API-47 Resend and resume from inside a multi-unit event Β·
v0.5β an event of several units (a block, a batch, API-41) may be asked for again from a position inside it: aResendthere, or a resume from a cursor user code confirmed in the middle of it (AckMode::Explicit). The device sends the event that holds the position whole (StreamStore::readreturns it for every store), the host'sReceivertakes it withVerdict::Accept { skip }(also for ranges it had before a resume),PositionedStreamgives the new units. Fixed with it:Resendno longer counts as an acknowledgement on the device (the store kept less than an explicit host had confirmed), acknowledgements and resends are valid up to the highest position sent (a lateResendon a reordering link stalled a backfill), the store is released when the backfill run ends innext_event. Testsww_streamtests/sim.rs(batches of 1..64 units, a resend from every position inside, a resume cursor inside a batch, chaos with acks inside batches),wire_weaver_clientstream_e2e_tests(docs/api/stream_protocol.md#positions-inside-an-event). - β
API-46 Codegen names types by their module path Β·
v0.5β each struct and enum records the path other crates name it by (ItemStruct::path/ItemEnum::pathinww_self,Noneat the crate root): the shortest one throughpub mods andpub usere-exports (by name, renamed, glob); a type re-exported under another name is named that way, a type no other crate can reach is an error naming it. Its identity in its crate is path and name (units::Voltand a rootVoltare two definitions): generated code names types by it (a type in apub modneeds no root re-export, the ww_si 0.2 layout of a markerunits::Voltnext to the aliasVoltworks), snapshots take#[derive_shrink_wrap]types and unit marker structs frompub mods too, andSkippedFullVersion::type_name, signature resolution,inline_skippedand the definition cache look definitions up by it. Part of signatures, so introspection data and API hashes changed (snapshots saved again). The walker followsuse .. as ..imports, and converts the arguments ofa::b::Ty<Arg>where they are written. Teststests/module_paths(pub module, lifetime two modules deep, rename, root re-export, re-export under another name, two equalPoints,Quantity<units::Volt>next to a rootVolt),tests/generics(ww_si layout as property, method argument, insideTimed),wire_weaver_coreunit tests (a snapshot withunits::Voltand a rootVoltsaved through RON and put back in an API) (docs/types.md#types-in-modules). - β
API-31 Stream functions β
#[stream(T, counted)] fn name(args) -> Result<A, E>and#[counted]/#[reliable] stream!(name: T): every stream (plain ones too) is opened by calling it (RequestKind::StreamOpen), with typed arguments and result; the device's handler named like the stream decides,Okstarts a session,close_<name>closes it. Counted and reliable streams keep their state in whatstream_<name>()returns (ww_stream::tx::Counted/Reliable), resume by cursor (name_resume(cursor)), answer a read with theirStreamStatus(name_status()), send withstream_data_ser().<name>_send(counted) or push into the store with<name>_push(reliable), which the generatedpoll_streamssends by priority between requests and resends on the host'sResend;ww api scaffold, introspection (ApiItemKind::StreamFn), compatibility check and API diff know them. Includes API-15. Not yet: reliable streams in resource arrays (poll_streamsrefuses them), the open arguments inStreamStatus, stream functions in the dynamic client and Python (CLT-23), streams that detach by themselves when the link goes down (a resume re-attaches) (docs/api/stream_reliability.md,tests/streams_reliable). - β
API-16 Optional items β
#[optional]/#[optional(group)]on a trait's methods, properties and streams; a server leaves them out withww_codegen!(.., omit = ["i2c.pullups"])oromit_if(<cfg>) = [..](the user crate's features), pays no dispatch arm and writes no handler (about 100 bytes of flash per item on Cortex-M). Only optional items can be left out (compile error otherwise). A call answersUnimplementedfrom the level's fallback arm (id < ITEM_COUNT), the device's model lists the omitted items (ww_selfomitted,root_omitted), so the generated and dynamic clients and Python fail locally withNotImplementedByDevice;ww api scaffold --omit, CLI tree and Python show them (docs/api/optional_items.md,tests/traits). Whole traits: API-54. Not yet: the GUI (wire_weaver_tool). - β
API-54 Optional traits β
#[optional]/#[optional(group)]on aww_impl!(a single trait or an array), left out withomit/omit_iflike API-16 items: no dispatch below it and none of its handlers (cfg-gated withomit_if), its consts out of (or cfg-gated in) the consts trait, its streams not sent bypoll_streams(one peromit_ifcombination, same signature), the device's model marks it left out so clients fail every call below it locally;ww api scaffoldandww api treeshow it,ww api checkclassifies#[optional]changes (docs/api/optional_items.md#optional-traits,docs/evolution/rules.md#optional-items,tests/traits,tests/streams_batched). Not yet:+observeproperties of a trait left out withomit_ifaren't observed in either build. - β
API-32 Per-method error lists β
#[errors(Common(InUse), Nack, ..)]on a trait method, a stream function (the open'sResulterror) or above aproperty!with an error type (its reads and writes): checked against the error enum when the trait is parsed (a compile error on a wrong name), in the API model (ww_selfMethod,StreamFnandPropertyerrors), generated client and rustdoc# Errorswith each variant's doc line,ww api tree, Pythonhelp(),ww api check(compatible); the generated and dynamic clients warn when a device returns a variant outside the list, tests fail (tests_common). Nothing on the device (docs/api/methods.md#error-lists,tests/methods,tests/properties,tests/streams_reliable). Not yet: the generated client's property reads (their errors are raw bytes, so only the dynamic client checks them); the GUI (wire_weaver_tool). - π API-12 Calculate max path depth in server codegen Β·
v0.5βMAX_DEPTHis hardcoded to 16 inwire_weaver_core/src/codegen/api_server.rs. - π API-13 Error system, remove
unwraps from codegen Β·v0.5β report errors with spans instead of panicking. - π API-56 care-marker Care marker on switching writes β a typed attribute (the
ww_self::Metaof SRV-34) on a method or property whose call or write switches power or outputs (a rail, a relay, a motor enable), carried in introspection: GUIs ask to confirm,ww call/ww setwant--yes, the hub can log who did it. Wire compatible, nothing on the device. - π API-57 device-metadata Device-level metadata β typed attributes on the API root or in a stdlib trait for what
a device is: product, board revision, connectors (with the resources behind each, so a label like "J4" points at
pins and buses), supported clock sync (
ww_timebasehost sync and its sources); in introspection next to SRV-34's per-itemMeta, shown byww info, the hub's registry and GUIs. Design first: which part is fixed in the API (connectors, sync support) and which is per unit at run time (ww_board_info, serial, revision). - π‘ API-33 Narrow error types per method β generated per-method error enums with the crate enum's
discriminants, as server handler return types only (
narrow_errors = true), for a compile-time contract on the device. Explored and not recommended for clients: friction in every driver crate. - β
API-35 Stream functions with a borrowed error type Β·
v0.5β a stream function whoseResulterror has a lifetime (#[stream(I2cRecord<'i>, counted)] fn records(..) -> Result<(), I2cError<'i>>, every IO family error since it holdsCommonError<'i>) compiles on the server with the handler signatureww api scaffoldprints (RpcResult<Result<(), I2cError<'_>>>, an error borrowed from the server), for plain, counted and reliable streams, sync and async servers: the generated dispatch takes theOkvalue out of the result before it opens the stream's state (tests/streams_borrowed_error). AnOkvalue that borrows from the server still can't. - β
API-36 Const items served from a value Β·
v0.5β aconst NAME: T;of a#[ww_trait]is served without aget_<name>handler: the server struct implements the generated<RootTrait>Constswithconst NAME: T = ..;(a Rust const) or, picked withconst_model = "NAME=fn",fn name(&self) -> Tfor a value fixed at boot or link time (OTP, chip id, build id); names carry the trait item chain prefix (BOARD_BOARD/board_board).ww api scaffold --const-modelprints the impl. Clients read a const once per connection, then answer from a cache (generated, dynamic and Python clients, promises too);ww introspectshows const values (tests/consts,docs/api/properties.md).ww_firmware_info,ww_board_info,ww_uidandww_user_infoservers use it. - β
API-38 Observe requests Β·
v0.5.xβRequestKind::Observeon a trait level: per propertyOff,OnChange,PeriodicorOnChangeOrBeatand a byte budget, the device answers with what it granted; updates arrive as oneEventKind::PropertyUpdateper level carrying any subset of its properties (SW-55). Only+observeproperties can be observed. Protocolww_client_server::observe(docs/std_library/ww_client_server.md), device side SRV-18, host side CLT-28 (docs/api/observing.md,tests/observe). - π‘ API-37 Const values in the introspection blob β a codegen option putting the values of
constitems into the device's introspection data, soww infogets them with the link setup and no extra reads. Costs flash per device, so not a default. - β
API-41 Batched streams and properties on
Seriesβ a stream or property opts into batching (+batched/batched, no size in the API: the device owns the batch size through its own config, default 32, the host picks latency) and sendsSeries<Timed<T>>(SW-60) batches instead of one item per event (device-sideBatcher, reliable streams storing encoded batches so a resend costs no re-encoding); the same mechanism under API-39's batched property samples and a batched observe slot. Designed, with motor-demo numbers backing it (integer storage over floats, default batch 32, autoranging cost cheap); the batch store and size bounds are in (SW-62). In: the device side,ww_stream::batch(Batcherwith two stores handed between a producer, also an interrupt, and the event loop through one atomic word, sealing on size, age and budget, encoding in the event loop;Outboxfor observe;Batchedreliable and counted streams), and batched streams end to end:batched/batched(untimed)in#[stream(..)]and#[batched]onstream!for plain, counted and reliable streams,StreamOpen { max_age_us }, generated<name>_push(O(1), interrupt-safe) andpoll_streamsencoding sealed batches, typed clients giving items one by one with their own time or whole batches (recv_batch),tests/streams_batched; the dynamic client giving the same (DynPositionedStream/DynStream, no generated code,ApiItemKind::StreamFn::batch_item_ty),ww stream <path> [--batches]and Python'srecv_batch()(numpy columns,ww_self::SeriesColumn::from_items) (piece 6, no-arg streams only); batched observe is API-39. Docs:docs/api/batching.md(how-to, sealing, budgets, the motor demo's numbers). Motor demo (motor_simrepo): everyFocproperty andControl's 13 measured values are+observe+batched. - π§ͺ API-55 Compressed counted streams Β·
v0.5β a counted stream's events carry their serialized item as one LZ4 block that copies from the items before it (history kept across events, SRV-27ww_lz4), so a byte stream of text (a UART at 6 Mbaud of shell or TUI output, 3-5x) fits a slower link. Negotiated at open:StreamOpen { codecs }is the host's offer,StreamOpened { codec }the device's answer (StreamCodec::Lz4 { history }), so an older host gets the events as they are; an item that doesn't shrink goes as a block of literals.HeadExt::codec_restartmarks where the history starts over: at open and resume, after an event the device couldn't send, and every 32 KiB, so a link loss heals; the host drops what it can't decode until then and reports it as a link gap (StreamStats::lost_codec,codec_bytes_in/_out). The API doesn't change: the device opts in per stream with its state,ww_stream::codec::Compressed(featurelz4) instead ofCounted, throughww_stream::codec::CountedState;ClientConfig::stream_compression(false)turns the host's offer off. Typed and dynamic clients and Python decode alike (docs/api/stream_compression.md; testsww_streamtests::codec,wire_weaver_clientcompressed_stream_decodes_and_heals,tests/io_uartrx_compressed). To check on hardware: Donguru's UART rx at 6 Mbaud (typec_dongle FW-42). Not yet: a restart the host asks for, batched and reliable streams, sinks (host -> device).
Server / device side (SRV)¶
- β
API-39 Batched property samples (observe every sample) β a
ro+observe+batchedproperty observed inObserveMode::Samples { every_us, max_age_us }sends every sample the device takes as aSeries<T>in itsBatched<T>slot of the level's update, their times shared by the level (API-49) (docs/api/observing.md#every-sample-batched,tests/observe_batched). In: parsing and the model (ApiItemKind::Property::batched), the mode validated and clamped (ObserveLimits::samples_max_age_us), generated<Level>Sampler::sample(t, ..)(O(1), interrupt-safe afterObserveBatches::split()), double-buffered stores and a level outbox encoded inpoll_propertieswith several updates per call,missedfor lost samples and dropped batches, typed observers per sample, the dynamic client,ww observe --samplesand Python (recv()samples,recv_batch()columns). Motor demo (motor_simrepo) on it, measured through the real generated code:docs/api/batching.md, andsample()itself in an interrupt on an STM32H725 next to observe cells (API-50,docs/api/batching.md#observe-cells-measured). - β
API-49 One time column per observe level (shared clock) β the samples a
sample(t, ..)call takes for a level's+batchedproperties share one time column per update:EventKind::PropertyUpdate::sample_times(Series<Second>) next tot, theBatched::Samplesslots untimed,Batching::SharedClockin introspection. A level keeps one batcher of rows (time and every value), itsSamplesentries share the finestevery_usand the shortestmax_age_us. Against a time column per property (21 FOC properties, n = 32,DeltaPacked): -36 % bytes, -52 % encode cycles on an STM32H725, -46 % instructions on Cortex-M3/M4F (docs/api/observing.md#which-rates-fit-an-mcu,tests/observe_batched). - β
API-60 Types from crates that aren't API crates Β·
v0.5β a crate withoutwire_weaveror#[ww_trait](exact_units, STD-61) is walked and snapshotted like an API crate; generated code names its types through a crate that re-exports it (ww_si::exact_units::..,CratePathsfromload_dep_with_paths) when the generating crate doesn't depend on it; the walker followsuse dep as name;anduse dep::module;for leading path segments. Withshrink_wrapoptional (#[cfg_attr(feature = "shrink_wrap", derive_shrink_wrap(..))]) the walker readscfg_attrs with the features Cargo builds the crate with (dependency entries,[workspace.dependencies],dep/feature, defaults, unified across dependents), snapshots with all features; a field's or variant'scfg_attrof the derive's own attributes (default,wire, ..) is read as holding by the derive and the walker, a compile error where it doesn't; other field and variant attributes pass through to the generated type. Teststests/standalone(server and client without a dependency onstandalone_units),wire_weaver_coreunit tests,shrink_wrap/tests/cfg_attr.rsandfield_attributes.rs, conformancecfg_attr.*(docs/types.md#types-from-crates-that-arent-api-crates). - β
API-14 API crates with git and crates.io dependencies Β·
v0.5β the crate walker (#[ww_api_root],#[ww_trait],ww_codegen!,ww api) follows{ git = .. }and"version"/{ version = .. }dependencies (also through[workspace.dependencies]andpackage = ".."renames), not only path ones: the exact package from the nearestCargo.lock(the entry in the dependent's own list, so two versions of a crate are told apart), its source where Cargo fetched it (CARGO_HOME'sgit/checkouts/<repo>-*/<rev>/, the member found by package name anywhere in the repository;registry/src/*/<name>-<version>/), clear errors namingcargo fetchwhen the lock or the source is missing. In a crate Cargo fetched itself (a published API crate's own#[ww_trait], docs.rs), crates.io dependencies are the newest matching version on disk, as the lock in use is out of reach. Testswire_weaver_corecrate_walker::tests(a realcargo fetchof a local git repository),docs/api/traits.md. - β
API-50 Observe cells: sampling a level from an interrupt in place β
ww_codegen!(.., observe_cells = "root_foc")puts a level with+batchedproperties on a ring of atomics instead of<Level>Sampler::sample: aconst fn<Level>Cells<R>(astatic, or anArc), one<Level>Writerwith a setter per property (relaxed stores where the value is computed) andcommit(t)(the level's gate and grid, release fences, no lock), its reader inObserveBatches::new/init;poll_propertiescopies the rows into one staging batch and sends them as the sampler's (same wire; a row the writer laps ismissed, never sent mixed). Opt-in per level, the default stays. Built onshrink_wrap::series::SeriesCells(series(cells)derive,ww_siand numbers have it). On an STM32H725 a row of the motor demo's 21 FOC values takes 354 cycles atopt-level = "z"against 412 throughsample()(328 against 419 at 3), 44-52 B of stack against 108-116 B, the same RAM.ww api scaffold --observe-cells. Writer and reader on two threads tested under miri (just miri). (docs/api/observing.md#cells-sampling-from-an-interrupt,tests/observe_batched,examples_mcufoc_observe.) - β
API-52 A cheaper observe poll β
poll_propertiesof a level with+batchedproperties spends its cycles on the codec, not on bookkeeping, with the same bytes: a batch whose worst case doesn't fit the outbox's free room is written into it straight away and measured only when it didn't fit, a measure counts each column from the store (shrink_wrap::series::count_store_series) instead of rebuilding it into the scratch, the outbox keeps its batch count and offsets (LevelBatch::column_after), and a cells level copies only the time and the observed columns out of its ring (a column observed mid-batch is copied for the staged rows afterwards, seqlock-checked). On an STM32H725 the poll that encodes a 32-row batch of the 21 FOC properties takes 100 k cycles instead of 191 k atopt-level = 3and 169 k instead of 513 k at"z"(cells 107 k / 180 k), so all 21 at 25 kHz are 17 % of the core with the rows instead of 30 % (27 % instead of 77 % at"z"); a cells drain of 3 of 21 observed columns 4.4 k instead of 9.2 k. The high rates moved little (3 values at 400 kHz 74 % of the H725 instead of 78 %, 1 MHz 181 %, 2 values at 2 MHz 313 %): their polls are the codec and each column's framing now (docs/articles/series.md#high-rates-a-few-values-at-400-khz-to-2-mhz,examples_mcufoc_observe,high_rate,ww_streamcells_tests). - β
API-21
no_alloc = falseservers β servers generated forstdwhose handlers take and give owned types (String,Vec<T>,FooOwned): method arguments and returns, property values and write errors, sink items, stream function results, stream items, deferred returns; written into the reply buffer with the bytes ano_allocserver sends (shrink_wrapBufWriter::ser_owned/write_owned,OwnedSer). Themethods,propertiesandstreamssuites run against such servers with ano_allocone as the byte reference (tests_common::Twin);ww api scaffold --allocprints the owned signatures. Prerequisite of the hub API (SRV-28) (docs/transport/hub.md#servers-on-std-no_alloc-false). - π API-17 Methods arguments to a user struct Β·
v0.5.x. - π§ API-18 Size analysis Β·
v0.5.xβ maximum buffer usage in the worst case, auto-calculated buffer sizes for events, arguments and outputs. Done:MAX_SIZEof types (SW-19) and per-method<METHOD>_REQUEST_MAX_LEN/<METHOD>_RESPONSE_MAX_LENconsts inno_allocservers, checked at compile time against a buffer intests/boundedandexamples_mcu/mcu_qemu. Missing: consts for properties, streams and sinks, the largest of all of them for a whole API (to sizeww_devicebuffers such asUdpBuffers<MAX_MESSAGE_LEN>automatically), paths of nested traits counted exactly (now the worst case of the full path), the same consts on the client. - π API-19 Change dispatcher match order to process common requests faster Β·
v0.5.x. - π§ API-20 Trait paths on the server side Β·
v0.5β servers servePathKind::GlobalFullrequests (crate, version and trait name) andPathKind::GlobalCompactones (the trait'sww_globalid) wherever the trait sits in the tree,ErrorKind::TraitNotFoundotherwise (tests/cross_version,docs/api/addressing.md). Every publicww_stdlibtrait has an id (ww_global/wire_weaver_gid.json, looked up by crate and trait name, guarded bytests/global_idsandassigned_ids.lock), the API model carries it (ApiLevel::compact_id), the client sends compact paths and falls back toGlobalFullfor devices built before an id existed, the tap decoder andww explainname compact calls; 64 B of flash on a Cortex-M3 (examples_mcu/mcu_qemuqemu-global-paths). Missing: traits under a resource array. - β API-22 Implement the same trait several times β enum selector Β·
v0.5.xβ stalled. At least print a proper error when a trait is implemented twice (tests/traits_api). - β API-23 Index chain array β named arguments Β·
v0.5.xβ stalled. Generating ad-hoc structs with named index fields might be a good ergonomic: users see the names through IDE lookup instead of guessing array indices. - π API-29 Bounds of resource array indices Β·
v0.5.xβstream!(adc[8]: u16)ormotor[1..=4], checked by the server before a handler is called and visible in introspection (docs/api/arrays.md). Lengths of lists and strings inside a resource's type are bounded withMaxLen(SW-19). With a bound, observed properties (SRV-18) can get a table per array element: until then a trait level in a resource array can't be observed (STD-31ww_gpiopin level waits for it). - π§ API-30 Counted and reliable streams Β·
v0.5β counted streams put a head in every event (flags and a length-coded 64-bitposin units the device names when it sends, the event'sunits,lost, optional device timet), so drops on the device, link losses and duplicates are reported exactly and item types carry noseq/lost/tfields (IO family D4, D5). Reliable streams addAck/Resend/Query(go-back-N, grants in bytes), keep unacknowledged events in a store, resume by cursor after a reconnect with the latest values first and the backlog beside them; a send scheduler with priorities between streams; typedcntcounters (docs/api/stream_reliability.md). Done, the protocol core: wire types inww_client_server::stream(StreamHeadwith its hand-laid encoding, byte-verified;StreamCursor,StreamOpened,EventKind::StreamItem,RequestKind::StreamOpen, the sideband), theww_streamcrate (Counted,Reliable,Receiver,RamRing,Scheduler,StreamCnt; sans-IO,no_std, tested over a seeded lossy link with random disconnects),ww_self::ApiItemKind::StreamFn, codegen (API-31), the host runtime (CLT-23). Missing: credit sinks on it (API-34). The persistent store is done (SRV-15). - π§ͺ API-34 Credit sinks on the stream protocol Β·
v0.5βAck/Query/Resendshared with reliable streams, sessions and resume for credit sinks, so a sink (UART tx, a DFU plan) continues after a reconnect. Positions count units the host names per item (1 by default, the payload's bytes for a byte sink), grants count bytes; positions stay as API-42 has them (u64in state, modulo 2^28 on the wire), the host keeps at mostMAX_WINDOWbytes and units in flight; streams keep their fullUVlq64positions (their gaps are unbounded). The host names the session inStreamOpen's cursor, so a repeated or late duplicate open (UDP) no longer resets a running session. Done: the protocol core,ww_stream::sink(SinkCrediton the device,SinkTxon the host),RequestKind::SinkWrite/SinkWriteUnits, and the switch-over: generated server code (open byStreamOpen, a read returnsStreamStatus), the hostSink(send_units,open_session, a device that lost the session is reported), dynamic client and Python,ww_dfu, every test crate; the oldCredit/CreditQuery/Resend,CreditWriteandww_client_server::creditare gone,ResendFromisResend. Resume in the host API:Sink::cursor(), sinks indisconnect().keep_streams()/Commander::resume_streams()(the session goes on from where the device is, nothing repeated or lost), the generated<sink>_resume(cursor)after a host restart,StreamError::SinkSessionLostfor a device that lost the session; PythonSink.cursor/resume/ww.SessionLost. Tests:wire_weaver_clientevent_loop/sink_e2e_tests.rs(reconnect in the middle of a transfer with lost writes, a rebooted device, a restarted host, a late duplicate open),tests/credit_sink, the 2C-APP-BULK DFU scenario. Seedocs/api/credit_sinks.md#resume. To check on hardware: TEST-26. - π‘ API-53 Sink functions β
#[sink(T, credit)] fn uart_tx(config: TxConfig) -> Result<A, E>, the counterpart of stream functions (API-31): a sink opened with a configuration and a typed result, a new session exactly when the configuration changes. The wire has it (StreamOpen { args }); codegen and the host API are missing. - π‘ API-24 Guarded calls for critical resources Β·
v0.5.xβ for safety-critical actions, such as starting a motor or switching high power, a request that happens to match predefined bytes must not be enough. Add an extra check, e.g. a CRC over the resource identity and arguments (a "strict mode" per resource), or a request/response challenge before the call is executed. - π§ API-26 Generate C Β·
v0.5.x, and an FFI example. Generated C comes from theww_selfmodel, not from the derive's AST: a generator run after the build, from an introspection bundle (e.g.ww gen c), writes.h/.cfor an API's types on top of the C runtime (SW-43 c-runtime), later a device-side dispatcher, introspection as a const blob andww_framer. None of that is there yet. The C header textfixed_layout(SW-44, SW-45) andhead_tail(SW-46) print from the derive today moves to that generator over time. Other languages are generated the same way, from the model. The conformance matrix (TEST-23) is what keeps the generated code byte-equal to the derive. - π‘ API-27 Standalone definition files with custom syntax β APIs and types in their own files, with a friendlier syntax: nested levels, no macro calls, SI units. Tried twice already, approach with care:
- at the very beginning of this repo, a YAML based language;
- later, a big effort to support a custom syntax, which turned into a project of its own and took a lot of resources away from the actual work.
Plain Rust with #[ww_trait] and #[derive_shrink_wrap] is the current answer; revisit only with a concrete
benefit that outweighs the cost of a parser, tooling and IDE support.
- π‘ API-40 value_on_changed strings without an allocator β a no_alloc server's field for a
MaxLen<&'i str, N> property is &'static str today, which a write can't fill; generate an
shrink_wrap::ArrayString<N> field (SW-57) instead, and ArrayVec<T, N> for lists, so settings like a device label
can be value_on_changed too (STD-32).
- π‘ API-51 Stream codecs across items β any stream or observed property could send each item as a diff against
the one before, generic over T, from the derive per leaf: integers as delta or delta of delta, floats XOR coded,
enums, units, strings and bool as one "same" bit or the new value, a whole item "unchanged" bit; byte items
(UART or CAN frames) XOR'd with the previous one or through a small no_std LZ window (LZ4 across a
counted stream's events is API-55). Declared in the API
(#[stream(T, codec = auto)]), in the introspection data so dynamic clients and Python decode it; keyframes on
open, after a gap and every N items. It is how a string or a frame, which a Series batch can't take (no size
bound), would get cheaper per item. Prior art: Gorilla, Sprintz, Chimp, ALP.
- π API-58 #[read_only] methods Β· v0.5.x β a typed attribute in ww_self::Meta (next to SRV-34's claim)
for methods without effects, carried in introspection: the hub lets peers with only the observe right call them
(SRV-37), GUIs may call them freely. The API author's promise, not something the hub can check. Design: hub
federation (SRV-33).
- β
SRV-1 no_std, no-alloc server, IO-free generated code.
- β
SRV-2 Sans-IO DeviceLink, async Server with wait()/handle() for a user-owned event loop, and
blocking::Server.
- β
SRV-3 Fully split TX and RX flows for every transport (fixed the half-duplex interlock), framing without IO.
- β
SRV-4 User handlers called straight from the RX loop, transport glue extracted into ww_device.
- β
SRV-5 Device descriptors: serial, WireWeaver version, API id, version, hash and user label in the USB interface
string.
- β
SRV-6 Media: USB (embassy), RTT, WebSocket and UDP (both also on embassy-net).
- β
SRV-7 UDP device medium β ww_device::udp, one frame per datagram over any datagram socket, one host at a
time, another host takes over by starting link setup (docs/transport/udp.md).
- β
SRV-15 Stream stores and backfill β ww_stream::store: what a reliable stream (API-30) keeps until the
host acknowledges it: RamRing (API-30) and LogStore, a power-cut safe log on a block device (feature
log-store; SdMedium on an embedded-sdmmc BlockDevice with sdmmc, NorMedium on an
embedded-storage-async NorFlash with nor-flash) that keeps session, positions and clock epochs across reboots;
drop-newest or drop-oldest when full, counted exactly. Tested with a power cut at every write
(ww_stream/src/tests/log_store.rs); docs/api/stream_stores.md.
- β
SRV-16 Persistent RAM store β ww_stream::store::PersistentRamRing (feature persistent-ram): a reliable
stream's store in .uninit RAM that survives a soft reset (fault β reset), for the logs before a crash (STD-26).
Header slots and records checked by CRC at boot, so garbage after a power-on is never taken for data; continues
session and positions like LogStore, a new clock epoch per boot. Tested with a fault after every byte written
(ww_stream/src/tests/persistent_ram.rs); docs/api/stream_stores.md.
- β
SRV-18 Observed properties on the device β servers of APIs with +observe properties answer Observe
requests (API-38) and send PropertyUpdates from the generated poll_properties: ObserveState with one table
per trait level sized at compile time, a sparse <Trait>Update per level (DER-13), mark_changed_<name>() (called
for value_on_changed writes), periodic properties sampled through their getter. A token bucket per level at the
granted budget, earliest deadline first, at most one update per level per call, the rest slips and counts as
missed; properties have a reserved share of the link (ObserveLimits::total_budget) next to stored streams.
Observe is untrusted input: validated in full before anything changes, clamped, bounded work, refused per
entry. Levels under a resource array answer OperationNotSupported until their bound is known (API-29), +observe
properties in an array or #[optional] aren't observable yet. ww_stream::observe,
docs/api/observing.md#the-device-side; tests tests/observe (fake clock, real host event loop),
ww_stream/src/observe/tests.rs.
- β
SRV-20 deferred-nested-levels Deferred return helpers on nested levels β <prefix>_<method>_send_return,
_send_return_blocking and _ser_return_event are generated for deferred methods on nested trait levels too, named
like the handler (plug_vbus_cycle_send_return); method_model patterns match the prefixed name. Test
tests/methods (deferred_on_nested_level), docs/api/methods.md#deferred.
- β
SRV-21 handler-model Sync handlers in an async server β ww_codegen!(.., handler_model =
"device.settings.*|meter.reading=async, _=sync"): per item (regex over its dotted path, first match, _ the
default, use_async without one) its handlers are async fn or plain fn; the dispatcher and poll_properties
call sync ones without .await, a level whose items are all sync gets a plain fn dispatcher. A handler of the
other kind is a compile error (wire_weaver::handler checks), so is a pattern that matches nothing.
ww api scaffold --handler-model. Donguru: -20.2 KiB flash at z, -12.6 KiB at 3. docs/api/handlers.md; test
tests/observe (a mixed server).
- β
SRV-23 observe-table-unsized One observe table code for every level β ww_stream::observe::ObserveTable<S: ?Sized = [Slot]>: a level stores ObserveTable<[Slot; N]>, the methods (apply, plan, commit, ...) are compiled once, on ObserveTable<[Slot]>; LevelDesc is slices, the plan and the selection keep their per-slot state in caller storage (Due, Mark), and levels without observable properties answer Observe through one shared path (refuse_all). Donguru: β13.3 KiB flash at opt-level = 3, β9.4 KiB at z. ww_stream/src/observe.rs, docs/api/observing.md#the-device-side.
- β
SRV-25 event-driven-observe No fixed observe tick β a device sleeps until something observed is due
instead of polling every millisecond. ObserveState::next_wake(now_us) (ObserveTable::next_wake per level: the
periodic grid, last + min of a changed property, last + max of a beat, a held level's bucket refill, a batch
sealed or as old as allowed; None when nothing comes due without a change); a generated ObserveSignal any task
or interrupt marks a property changed through (mark_changed_<name>()) or wakes the server task with, no lost
wake between a change and the sleep (ww_stream::wake); with_wake on samplers and cells writers;
ww_device::Server::wait_until(at); ww api scaffold prints the signal. Tests: ww_stream (polling only at
next_wake sends what polling every Β΅s does), tests/observe (fake clock: polls only on the grid, a change from
another thread sent at once), tests/observe_batched. docs/api/observing.md#the-server-loop.
- β
SRV-24 shared-observe-loop One observe loop for every level β the algorithm of poll_properties (plan, overhead, selection, measuring, writing, commit, missed counts, batches of samples) is ww_stream::observe's, sync: plan_level, select, write, ser_update. The generated poll_properties is one loop over the levels with one send site; per property it gives only a size and a write arm (through &mut dyn FnMut) and awaits its async getters (SRV-21) itself, reading an unbounded async value once. ObserveState::level / oldest_batch / remove_oldest_batch reach a level by its number. Donguru, on top of SRV-21 and SRV-23: β5.8 KiB flash at opt-level = 3, β5.0 KiB at z. ww_stream/src/observe.rs, docs/api/observing.md#the-device-side; tests tests/observe, tests/observe_batched, ww_stream/src/observe/tests.rs. A value that can't be had at write time is dropped alone (counted missed), the rest of the update goes; tested on Donguru B153A (plug with cc unimplemented).
- β
SRV-22 error-seq-per-item Error sites numbered the same in every server β err_seq of an Error is no
longer one counter across the generated server (705 numbers on Donguru) but fixed per site
(ww_client_server::err_seq): item steps in step order and the same for every item (0 kind not taken, 1 decode,
2 handler, 3 encode, ..), level sites (40..), request sites (48..), deferred return (32..). With the build, the
request path and the error kind it still pinpoints one site, and identical error arms are shared code: Donguru
-1.5 KiB flash at z, -1.7 KiB at 3. Error::site(), the host prints Unimplemented at item site 2 (handler).
docs/api/errors.md#where-on-the-device-an-error-came-from; tests tests/methods, tests/traits.
- β
SRV-26 Device-side engine for I2C and SPI slaves β ww_bus_slave, no_std, sans-IO, no alloc: I2cSlave and
SpiSlave do what STD-35's API promises, so a firmware only wires its peripheral's interrupts to them. I2C: the
register map (pointer bytes, auto-increment, wrap at len, page wrap, read_only), host updates staged until the
stop, the answer queue with Fill, Stretch { max } (deadline, resume, ReadWaiting) and Nack
(acks_address), unsent for a TX FIFO loaded ahead. SPI: queue answers (idle, repeat_last, underrun) and
SpiRegisters frames (address, rw bit, dummy bytes before read data), the answer picked at the first miso()
(FIFO preload, rewind); 8-bit words only. Events built as I2cSlaveEvent / SpiSlaveFrame. const fn new for a
static; 58-100 instructions per byte on a Cortex-M3 (examples_mcu/mcu_qemu bin qemu-bus-slave).
docs/api/bus_slave.md, tests ww_bus_slave/tests. First firmware user: usb_io_fw IF-5 (rePort, RP2350B).
- β
SRV-27 ww-lz4-encoder Streaming LZ4 encoder for byte streams β ww_lz4, no_std, no alloc: each chunk of a
stream (UART RX, a log) becomes one standard LZ4 block whose matches reach back into earlier chunks, so 512 B chunks
of shell text go 3.2-5.1x and 64 B chunks 2.4-3.3x (captured boot log, ls/rg colour output, top), about 2x
without history. Encoder<HASH, BUF> with a const fn new(keep) for a static: a hash table of u16 positions
(2 B per entry), input(want) gives the free tail of the history buffer (slides first, keep bytes survive) so a
chunk is written in place, compress(len, out) writes the block, reset() starts the history over, HISTORY,
max_block_len(n). A chunk that doesn't shrink goes out as a literal-only block, and after 1 KiB of those the
encoder only retries every 4 KiB (with_backoff). Feature ram puts the hot code (1.3 KiB) in .data.ww_lz4.
Host side Decoder (feature std, lz4_flex with the last 64 KiB as dictionary). Same ratios as the lz4s bench of
P2534 (within 0.1 %); tests ww_lz4/tests (round trips against lz4_flex, random chunking, untrusted blocks) and
just test-lz4-c (C liblz4 decodes the blocks). Device and host user: API-55.
- β
SRV-34 per-resource-claims Per-resource claims through the hub Β· v0.5.x β typed metadata attributes,
carried in introspection as data (ww_self::Meta on ApiItem and ApiLevel), checked when the API is loaded:
#[claim(exclusive | shared | observe)] on a ww_impl! or trait definition (the root is always claimable),
#[shares(pins = "PA9, PA10", peripherals = "USART1", index = N)] (pin ranges PA0-PA15) so overlapping
resources conflict, #[label("CAN1, DB-9 on the front")] (one per array element) on any item;
ApiItemOwned::claim_mode / label, ww api tree, rustdoc notes, ww api check reports changes as compatible;
stdlib IO traits claimable (ww_can_bus, ww_uart, ww_i2c, ww_spi, ww_gpio::Bank, ww_dfu,
ww_pin_diag). The hub's claims group (claim(key, path, purpose, ttl_s) -> Result<u32, ClaimError {
UnknownDevice, ApiUnknown, BadPath, NotClaimable, InUse, Overlap }>, release, claims(key), Claimed /
Released events): calls and writes of other client instances at or under a claimed path are answered by the hub
with Common(InUse { by }) in the item's own error type (an error event naming the holder without one), without
reaching the device; reads, streams and observe stay open (taken too for observe); a claim belongs to the
client instance (CLT-41) as its principal and drops with its last connection or at the ttl; overlap by path and by
#[shares], nesting within one instance. docs/api/metadata.md, docs/transport/hub.md (with how hub and
device-side claims combine); tests tests/traits (meta_in_introspection), wire_weaver_core (meta_*),
ww_hub (claims), tests/hub (claim*). Later attributes: API-56, API-57.
- π§ͺ SRV-19 ECC-safe STM32 flash for stores β mcu/ww_ecc_flash: EccSafeFlash wraps any NorFlash (embassy-stm32's
Flash) and reads it one ECC word at a time; a word with an uncorrectable ECC error (power cut while programming
or erasing) reads as zeros instead of an NMI (STM32G0, FLASH_ECCR.ECCD, through on_nmi() or feature
nmi-handler) or a bus error (STM32H7, DBECCERR, ignored under FAULTMASK + BFHFNMIGN), so sequential-storage
skips it. Never MultiwriteNorFlash. Blocking and async. docs/api/ecc_flash.md. Strict mode for bootloaders
and image checks: EccSafeFlash::new_strict β EccStrictFlash, whose read fails with Error::Ecc { offset }
(is_ecc()) instead of zeros; chunky's feature ww-ecc-flash implements chunky::ecc::EccError for it (chunky
ECC-2), for the B135 bootloader and app. Both modes host-tested on a simulated flash (just test-ecc-flash).
On hardware: a G0B1 (B153A) store survives 3000 resets in the middle of its programs and erases (TEST-21), but a
reset never tears a G0 flash word, so the zeros path itself still waits for a real power cut (TEST-21); H7 is
built, and examples_mcu/ecc_flash_h7 makes real ECC errors by programming a scratch flash word twice to check
both modes (not run on a board yet).
- β SRV-8 One crate per role for firmware and host Β· v0.5 β blocked on generated code using absolute paths.
Agreed direction, instead of putting everything into wire_weaver behind features or splitting by
std/no_std:
- wire_weaver stays lean (shrink_wrap, macros, ww_version, result types) since every API crate and
ww_stdlib crate depends on it;
- ww_device re-exports wire_weaver, ww_framer, ww_link and gets features usb-embassy, rtt, udp,
ws, defmt, embassy-time, std;
- wire_weaver_client re-exports wire_weaver with transport features.
Requires: codegen emitting paths through the role crate (or a crate = ".." argument / proc-macro-crate;
WIRE_WEAVER_REEXPORTS in the crate walker is partly there), and wire_weaver_usb_embassy building in the
root workspace or merging into ww_device. Separately consider making wire_weaver's std default off.
- π SRV-9 Handle USB suspend/resume Β·
v0.5β run until suspend, wait for resume, loop (mcu/wire_weaver_usb_embassy). Later (v0.5.x): detect the host going to sleep and keep the connection, informing the device. - π SRV-10 Collect error codes into an ELF section for easier error decoding Β·
v0.5.x. - π SRV-11 Link statistics in
ww_device(TODOinlink.rs). - π‘ SRV-12 std server on Tower Β·
v0.5. - π SRV-13 ww hub: one device, many programs Β·
v0.5.xβ a per-user hub that starts itself on first use and exits when idle (sccache / ssh ControlMaster style, no system service, Linux, macOS and Windows), holds the devices (USB, RTT, later UDP) and serves every client over a user-only local socket (TR-16); it is itself a wire_weaver device with its own API (ww_hub_api). Today a second program gets "busy". Pieces: SRV-28 process and API, SRV-29 device multiplexing, SRV-30 version skew, SRV-31 clock sync, SRV-32 exclusive locks, SRV-33 federation (SRV-36..40), SRV-34 per-resource claims, TR-16, CLT-38..41, CLT-43, CLT-44, TOOL-17, TOOL-18; prerequisite API-21 (done). Later API groups: logs to several viewers, shared captures (TR-14'sClientMap), labels and settings, fault notifications, published (virtual) devices. - π§ͺ SRV-28 Hub process and its API Β·
v0.5.xβ cratesww_hub_api(Hub:info,handover,shutdown,keep;Registry:devices,clients,take,events) andww_hub(docs/transport/hub.md): one hub per user (hub.lock), the socket with the preamble picking the hub API or a device, the API served with generatedno_alloc = falsecode onww_device::Serverper connection, devices enumerated without opening plus the CLT-38 holders, plug / unplug / holder / client events, idle exit,keep,shutdown;ww hub serve,ww_hub::main_if_hub(), the typedww_hub::client::HubClient. Starting it detached is CLT-39's,handoverSRV-30's;takecame with SRV-29. Tested on Linux; check open on macOS and Windows (compiles for both). - π§ͺ SRV-29 Hub device multiplexing Β·
v0.5.xβ device tunnels inww_hub(docs/transport/hub.md#device-tunnels-srv-29,ClientConfig::hub_device): one client event loop per open device (Command::Forward/ForwardSink), aww_device::DeviceLinkper client tunnel answering like the device, seq remap, stream opens counted per path and fanned out, observe settings merged per level, introspection from cache, bounded per-client queues (stream data dropped and counted, replies never), devices opened on demand and closed after a linger (30 s), lock file modehub,registry().take. The hub reads only seq, path and kind, never a user API. E2E intests/hub; measured against direct USB on three boards (tests/hub/examples/hub_loopback.rs: throughput as direct, 16 B round trips over USB FS 10-25 % fewer on an idle PC, 3-13 % under load, from the two extra process wakes). Tested on Linux; check on macOS and Windows open. Limits: sinks and the sideband of a shared stream are the first client's, streams on trait paths are not counted per client. - π§ͺ SRV-42 Device flushes by priority, also deferred replies Β·
v0.5.xβ the device answers by the request's priority (CLT-50):Context::priority()andReplyTo::prioritycarry it to handlers, a reply to an urgent request is flushed at once fromServer::handle, and a deferred one (generated<method>_send_return(out, reply_to, ..), sent throughServer::sink()from outsidehandle) throughMessageSink::send_reply; default-priority replies, immediate or deferred, wait for the accumulation window like everything else.Sink::flush(async and blocking) lets firmware send what waits now. Before, a deferred reply always waited for the window and firmware had no way to flush it. Tests:ww_deviceasync_server::replies_flushed_by_priority,wire_weaver_clientflush_e2e_tests. On hardware: part of CLT-50's re-measure. - π‘ SRV-41 Fewer wakes on the hub path β the tunnel's cost is two extra process wakes per message (client β
hub β client): 16 B round trips over USB FS are 15 % (donguru) and 23 % (b200) fewer than direct on an idle PC
(median of 7 rounds), 3.5 % and 12.6 % under load; the hub's own USB round trip equals direct (480 Β΅s against
510-600 Β΅s), throughput and max-size transfers are as direct. Ideas if it matters: busy-poll the socket for a short
window after a request (spends a core), or a shared-memory ring between client and hub with a futex / event for
the wake. Kept as is for now (person, 22 Oct 2026); measure with
tests/hub/examples/hub_loopback.rsfirst. - π§ͺ SRV-30 Hub version skew and handover Β·
v0.5.xβ a hub serves every client whose link and API model versions its build can serve; a client that can start a newer hub asks the running one to hand over (only strictly newer replaces older, so it converges); a refusal names both versions and the fix. Built (docs/transport/hub.md#version-skew-and-handover-srv-30,docs/evolution/rules.md#the-ww-hub-and-version-skew): preamble version 2 with a skippable trailer carrying the client'sww_linkandww_client_serverversions (version 1 still read: versions unknown; a client asks a v1-only hub again in v1), the hub's window (ww_hub/src/window.rs: SemVer-compatible with its build, plus earlier majors it keeps after a break, none yet) and status 4 withError::HubRefusednaming both sides and the fix;Hub::handover(socket)(strictly newer only,Event::Handover, links closed withDisconnectReason::HubHandover,Exit::Handover),ww_hub::Bound::take_over(own socket, thenhub.lock, rename into place on Unix, the pipe anew on Windows), andhub::ensure_runningreplacing an older running hub with the program's own (main_if_hub,WW_HUB_HANDOVER, underhub.start.lock). Tested on Linux intests/hub_reconnect(versions.rs,handover.rswith clients connected,replace.rsacross processes); check on macOS and Windows open (compiles for both). - β
SRV-31 Clock sync in the hub Β·
v0.5.xβ the hub steers every open device servingww_timebase::Clockwithhost_sync(found in its introspection data at any path) onto the PC's UTC withclock_sync::ClockSyncon its own connection, and keeps it open for a sync linger (30 min,WW_HUB_SYNC_LINGER,forever) so boards stay steered between short-lived commands;WW_HUB_SYNC=off/sync().set_enabledper device or all.ww_hub_api::ClockSyncatsync:devices(SyncEntry: phase, state, source, the steerer the device follows and one it only notes, rate, shortest RTT, residual viashared_now, error, slew, samples),linger, thestatsstream. Rate cache per device serial in the state dir (clock_sync::RateCache, atomic writes, shared with direct-mode programs): a known device is steered after its first reading. Steer points say who steers (ww_timebase::Steerer, STD-49 follow-up); the device follows one and notes the others (embedded_bedrockbedrock_sync).ww syncshows the hub's steering with a hub, steers directly without one, STEERED BY column.docs/transport/hub.md#clock-sync-in-the-hub-srv-31,docs/std_library/ww_timebase.md#one-steerer-at-a-time; E2E intests/hub(clock_sync),tests/io_timebase. Paused (SyncPhase::Paused, readings go on, no steer points) while a program holds the whole device (SRV-32); programs holding a device directly steer on their own connection by default (CLT-46). Checked on omarchy-m1 (22 Oct 2026, Donguru and B135 through the hub,ww syncas its view):Locked1.4-2.6 s after the hub opened them with no cached rate, 0.4 s with one, also after a hub restart and after 60 s closed in holdover; a loop of shortww infocalls goes through the hub from its third call and the board stays steered without clients for the sync linger, then closes; residual against UTC B135 -5..-16 Β΅s (Β±55 Β΅s, RTT 120 Β΅s), Donguru within Β±1.2 ms (RTT 1.4-1.6 ms). Two steerers on one board (follow one, note the other, take over) checked on Donguru and B135 bybedrock_sync's side. - π§ͺ SRV-32 Device-wide exclusive and lending Β·
v0.5.xβ exclusive access to a whole device (DFU, settings writes) is a claim on its root path (SRV-34): other programs' calls and writes anywhere on it refused naming the holder, andHub::steering_pausedfor the hub's clock steering (SRV-31 skips the device while it holds);wire_weaver_client::hub::Exclusivetakes it when a hub holds the device (Noneotherwise: the program holds it directly), andww dfu reboot | flash | confirm | revertdo,flashacross the device's reset. Lending:Claims::lend(a root claim marked lent) closes the device (tunnels getDisconnect(Other(DISCONNECT_LENT))), refuses tunnels withStatus::Lentand others' claims naming it until released, then opens it again if the hub held it;Lent/Returnedevents; a crashed borrower or a ttl returns it.ww hub lend DEVICE -- CMD(Ctrl+C reaches the command, its exit code is ww's),hub::Lend.docs/transport/hub.md; testsww_hub(claims),tests/hub(exclusive*,lend*). Lending checked on omarchy-m1 (22 Oct 2026):ww hub lend 204B319B5431 -- sh -c '...; probe-rs reset --probe <J-Link> ...'on Donguru whileww syncwatched it through the hub: the hub paused its steering and closed the device 7 ms after the ask, the command ran, the device was back open in the hub 3 ms after the return (1.4 s for the whole lend), andww sync's connection came back and the board wasLockedagain 1.8 s later. Open:ww dfu flashthrough the hub on hardware, as no free bench board has chunky A/B (Donguru and B135 run in place without it, rePort was busy; B135 could be provisioned with vb135'sjust provision-ab-dfu, which erases its settings: the person's call); noww settingscommand writes to a device yet (dumpreads a file), one that does takesExclusivethe same way. Clients reconnect after a lend with theLentreason (CLT-40), and the hub's steering pauses while the root is held (SRV-31,tests/huba_root_claim_pauses_steering). - π SRV-33 Hub federation between PCs Β·
v0.5.xβ remote devices through another PC's hub, asHOST/<key>, with who may connect decided by the hub that owns the device (security design reviewed before code). Steps: SRV-36 ssh (full access), SRV-37 principals and per-device rights, SRV-38 restricted ssh keys, SRV-39 the browser on localhost (TOOL-6), SRV-40 native hub-to-hub with pinned keys (Noise IK). No UDP, no transitive federation. - π SRV-36
ww hub attachover ssh Β·v0.5.xβww hub attach HOSTlogs in once in the terminal (an ssh ControlMaster), the local hub then opens onessh HOST ww hub stdiochannel per tunnel and lists the remote devices asHOST/<key>; ssh authenticates, the remote hub serves the channel as its own user. Byte-spliced tunnels (the remote hub multiplexes), host names and remote keys validated,ww hub detach. - π SRV-37 Hub principals and per-device rights Β·
v0.5.xβ who is connected, from what the OS, ssh or a key says (never the client's own metadata), and what it may do per device:observe,control,dfu, from glob rules in~/.config/ww/hub_peers.toml; the hub answers refused requests withForbiddenitself, filters the registry, keeps hub API methods local unless classified, and owns claims per principal. - π SRV-38 Restricted ssh keys for the hub Β·
v0.5.xβauthorized_keyslines withrestrict,command="ww hub stdio --peer NAME"give a key only the rights of[peer.NAME];ww hub allow --sshprints the line. - π SRV-39 The hub for a browser on localhost Β·
v0.5.xβ an HTTP + WebSocket listener on loopback only that serves the TOOL-6 web GUI:HostandOriginchecked, a single-use launch token fromww guiin the URL fragment exchanged for a per-tab session token, the peer's user checked on Linux and Windows. - π‘ SRV-40 Native hub-to-hub with pinned keys β for PCs without ssh: TCP with Noise IK (
snow, pure Rust), a static key per PC (ww hub key), peers allowlisted by key and source address with SRV-37 rules. Built when a box without ssh needs it. - π‘ SRV-14 FPGA server Β·
v0.5.x. - π SRV-44 Device serves waiting requests by priority β requests that arrived in one frame are handled in
order; take the highest priority (CLT-50) first, so its reply leaves first.
TODO(SRV-44)inww_deviceserver.rsandblocking.rs. - π SRV-45 Hub queues by priority across clients β the hub (SRV-29) forwards each client's requests in the order they came and flushes urgent ones at once (CLT-50); with several busy clients on one device, an urgent request of one could still wait behind another's batch in the device link's queue. Sort what waits there by priority (with CLT-52).
Client / host side (CLT)¶
- β
CLT-1
stdclient with async, blocking and promise flavors, prepared call objects, noroot()needed. - β
CLT-2 Generic sans-IO event loop over a
Transport, independent TX and RX flows. - β CLT-3 USB transport on nusb: endpoint addresses from descriptors, bulk with lower priority, vec recycling.
- β CLT-4 RTT transport over a debug probe, control block address from an ELF file.
- β CLT-5 WebSocket, UDP and in-process transports.
- β
CLT-6
DynClientand dynamic resources, for APIs known only at runtime. - β CLT-7 Introspection cache keyed by API hash with and without docs, with a download timeout.
- β CLT-8 Compatibility check on connect: each resource is checked against device introspection when the API hash differs.
- β
CLT-9 Connection selectors, clear errors when no device is found,
disconnect()releases the transport. - β
CLT-24 One
?per call β a method returningResult<T, E>givesResult<T, CallError<E>>(Device(E),Unimplemented { resource }orClient(Error)) fromcall,blocking_calland promises, a property with a user error type the same fromwriteandwrite_promise, instead ofResult<Result<T, E>, Error>and??;Error::is_unimplemented()for items without an error type (docs/api/methods.md#client-side,tests/methods). - β
CLT-25
ww infoandww reportΒ·v0.5βww infoshows firmware, board, ids, label and last reset of any device by global trait path, whatever its API version;ww reportadds the counters, the storeddefmtlog and the crash's addresses named with the ELF, found by build id, as one file a tester sends;ww elf add <elf|archive>fills the ELF directory from an ELF or a release's<firmware>-<version>-elf.tar.zst(shared withcnt elf add,cnt_core::elf). Library crateww_report(Contract,collect_info,collect_report),docs/std_library/device_contract.md,tests/device_report. The stored log is a stream and streams open on the absolute path only (CLT-23), soww reportreads the device's introspection data for the log's path;ww infoneeds none. Not yet: fetching the release archive by itself. - π CLT-30 Link setup gives up too early on slow RTT probes β the client sends
GetDeviceInfo5 times 50 ms apart (LINK_SETUP_RETRIES/LINK_SETUP_RETRY_INTERVALinevent_loop/core/tx.rs), 250 ms for every transport. Through a J-Link on omarchy-m1 (aarch64, probe-rs, each RTT write ~17 ms) most connects fail with "LinkSetup was not received from device after several retries" while the device is fine; with 40 retries they mostly work. Needs a per-transport budget (RTT: seconds) or one taken from the connect timeout. Found 16 Oct 2026. - β
CLT-28 Observing properties on the host Β·
v0.5.xβ generated std clients haveobserve(cfg)per trait level with+observeproperties (anObserverof the level's<Trait>Update) andobserve_<name>(mode)per property, fanned out from the level: one device subscription per level covering all its observers (merged modes, biggest budget), sent again when an observer comes, changes (configure) or goes,Offafter the last one;<Trait>Observetyped settings and what was granted,<Trait>Latestlatest-value cache with receive times; subscriptions sent again byresume_streams()after a reconnect;Error::Unsupportedfor a device whose API model predates API-38 (wire_weaver_client::observe,docs/api/observing.md#the-host-side,tests/observe,observe_e2e_tests). Not yet: levels under a resource array. - β
CLT-29
Series<T>in introspection and dynamic clients Β·v0.5.xβww_self::Type::Series(andField::series_opaquefor#[series(opaque)]fields) in the API model, emitted by codegen; a dynamic decoder inww_selfreads aSeriescolumn by column from the type tree alone (ValueOwned::des_shrink_wrap_dyngives the items,des_series_columns_dynthe columns), with the typed decoder's rules (restarts after enum andOptiontags, evolvable structs both ways, declared defaults) and the decode budget bounding work and memory (a constant column costs no bytes);wwshows the items, Python decodes the items or numpy columns (decode_columns, masked where an item is in another variant orNone). Dyn equals typed on the design's examples, on generated and on fuzzed series (fuzz targetseries-dynamic,tests/series,wire_weaver_py/tests/test_offline.py,docs/serdes/series.md#introspection-and-dynamic-clients). Writing too:ValueOwned::ser_shrink_wrap_dyn(andser_series_dynwith aCodecPolicy) writes aSeriesfrom its items, the typed writer's bytes for the same policy (codec choice, restarts, struct heads, generics), so a dynamic client and Python writeSeriesarguments and properties. - β
CLT-31 Quantities in dynamic clients,
wwand Python Β·v0.5.xβ aww_si::Quantity<U>(ww_si 0.2 and later) is recognized from introspection data alone: a concrete definition records the generic one and arguments it was made from (ww_self::ItemStruct::instance_of, host-derived, rejected from devices, left out of signatures), the unit comes fromww_si::units::by_name, thePer/Timestree, or a marker of the API's own crate by name and docs.wire_weaver_client::quantity(DynQuantity:3300 mV,{:#}3.3 V,to_si_f64,to_value;DynUnit),ww introspectconst values andww observeprint with symbols, Pythonww.Quantity(value,prefix,exponent,storage,symbol,unit,dim,docs,to_si(); writes take aQuantityor a number in the unit) (docs/std_library/ww_si.md#in-ww-python-and-dynamic-clients,tests/quantities,wire_weaver_py/tests). Types are still named by their definitions (Timed<Quantity<Ampere>>), alias names need ww_self type aliases. - β
CLT-30 Link setup budget per transport Β·
v0.5.xβ the client re-sentGetDeviceInfo5 times 50 ms apart (250 ms for every transport), so connecting through a debug probe (~17 ms per RTT write through a J-Link) mostly failed withLinkSetup was not received. Now a timeout per transport (USB, UDP, WebSocket 1 s, in-process 250 ms, RTT 10 s, one place:event_loop::transport::link_setup_timeout), retries back off (first after 1/20 of it, doubling up to ¼),ClientConfig::link_setup_timeout()andww --link-setup-timeout-ms/WW_LINK_SETUP_TIMEOUT_MS/link_setup_timeout_msin ww.toml override it (wire_weaver_client/src/event_loop/transport.rs,event_loop/core.rstests). On a J-Link + STM32G0 over RTT the old 250 ms was not what failed in the test: connects succeeded with both budgets once TR-13 was fixed. - π§ CLT-57 Local trait: one call site, local or remote β
ww_codegen!(crate :: Api, local_trait = "Name")emits the API as an async Rust trait (fnreturningimpl Future + Send, owned types, every result aResultwith the client's error types) and aNameDyncompanion with boxed futures for trait objects (Box/Arc<dyn NameDyn>implement the trait);impl_local_trait = ".."implements it on the std client, a plain struct implements it withasync fns, so a generic call site runs against a direct local implementation (no server, no serialization) or a remote one. Methods, property reads and writes and plain streams (ItemStream<T>) of the root level are done (examples/local_trait,tests/methodslocal_trait_on_the_client, Local trait). Missing, and how they could map:- Nested trait levels and resource arrays: one trait per level, reached by
fn plug(&self) -> impl Plug/fn unit(&self, index: u32) -> impl Unit; the client's level structs borrow it (UnitClient<'_>), so the level trait needs a lifetime or owned level handles. - Observing (
+observeproperties), sinks, counted, reliable and batched streams: today only the client has them. Observing could be anItemStreamof updates; sinks afuturesSink; counted and reliable streams need cursors in the trait (resume(cursor)), which a local implementation would have to keep. - Errors: the trait names
wire_weaver_client::ErrorandCallError, so the API crate depends onwire_weaver_client(behind a feature, but with its transports). They belong in a small crate (or inwire_weaverunderstd) that API crates can name without the client runtime. - The caller's identity (SRV-46 peer-identity): a local call has no peer, a server handler gets it in
Context. Forwarding it through the trait needs an argument or a task-local, not decided. - Server side: handlers stay inherent methods with
ContextandRpcResult, which don't fit the trait (deferred replies,Unimplemented, stream sends,&mut self, no_std/no_alloc borrowed types). Async alloc servers can forward each handler to a local implementation (the example does); a generated forwarding server (server = true, forward_to = "field") would remove that boilerplate for std servers. no_std/no_alloc servers keep their handlers: the trait's owned types and boxed futures don't fit firmware. - Naming: the
#[ww_trait]name is taken by the doc-only trait rustdoc shows; withstdthe doc trait could become this trait, soimpl CounterApi for LocalCounterreads as the API itself.
- Nested trait levels and resource arrays: one trait per level, reached by
- π§ CLT-10 Trait calls that work on any device Β·
v0.5β serial, user label, a CLI that works with all devices.ww listwith filters and USB interface string metadata are done, and global trait calls on any device whatever its API version:ClientConfig::traits_only(),DynClient::global_attachment()(served through API-20,tests/cross_version),ww dfu reboot,ww counters(STD-25) andww info/ww report(CLT-25). Missing: CLI commands for the otherww_stdlibtraits (GPIO, UART, ...). - π CLT-11 Typed property handles Β·
v0.5βRwProperty/RoProperty/WoPropertyso that.prop_name().read(),.prepare_write(1).blocking(),.asynch().awaitwork. - π CLT-12 Stream receive timeout Β·
v0.5β and a beat timeout between updates. - π CLT-13 Typed attachments with
TraitMarkerΒ·v0.5β trait attachments exist (Attachment,examples/all_gpio); check whether a typed marker is still wanted. - π‘ CLT-14
no_stdclient (client = "raw") β documented as not working inClientModel::Raw, its test is commented out intests/traits. Its generated code serializes arguments into a fixed 128 byte scratch and then calls.to_vec(), which isn'tno_std; it should write into a caller-provided buffer instead. The std clients (std_client,trait_client) already serialize withto_ww_bytes_owned()and have no size limit. - β
CLT-15 Multi read/write/call Β·
v0.5.xβ servers withmulti_req = truedispatchMultiRead,MultiCallandMultiWrite(SameorDifferentargs), each index answered on its own (Either<Error, value>), and answer malformed multi requests with errors instead of panicking (tests/multi_req). Client side: genericMultiRead/MultiCall/MultiWritetuple traits (any arity 2..=8, any depth, not just root 2-tuples),read_array/call_array/write_arrayfor homogeneous requests across an array's indices (List/Range/Mask32,SameorDifferentargs chosen automatically), and aDynResourceequivalent for the dynamic client (docs/api/multi_requests.md,tests/properties,tests/methods,tests/dynamic). Deferred methods inside a multi request are refused withOperationNotSupported. - π CLT-32 Generic definitions from another revision by the evolution rules Β·
v0.5.xβ a definition a device leaves out that differs from the host's snapshot (an unpublished crate changed without a bump) is found in the definition cache and compared by the evolution rules, but only when it is not generic: for an instance of a generic definition (ww_si::Quantity<U>,Timed<T>)check_instancescompares identity and signature only, so a docs-only change reads as incompatible. Compare the two generic definitions field by field with the arguments substituted. Found by STD-38 (wire_weaver_clientcompat::tests::other_revisionnow use the markerunits::Volt). - β
CLT-34 stm32-rom-dfu: STM32 system bootloader (ROM DFU) as an updater β
stm32_dfu_host(ww_stm32_dfu), a pure-Rust DfuSe host on nusb in a crate of its own (library first, to move to its own repo likepicoboot_host): lists0483:DF11, parses the alt setting's memory layout string (@Internal Flash /0x08000000/08*128Kg), erases only the sectors an image touches, writes in flash words (32 B, padded with0xFF), reads back and compares, leaves DFU by jumping to an address (the start of flash by default). ELF (PT_LOADby physical address) and binary at an address.dfu-core/dfu-nusbevaluated first and not used: they own the download loop, have no upload (no read-back verify), no sans-IO boundary for a simulated device and no say over the seconds a 128 KB erase takes.follow::stm32_dfu_update()/stm32_dfu_update_with()(wire_weaver_client, featurestm32_dfu, the_withone needs neither it norusb) wait for the bootloader under the app's serial number, flash, jump and wait for the app, tested end to end onSimStm32Dfu(tests/follow_reboot);ww dfu stm32 list/info/flashwith a progress bar,--wait,--follow. Facts: AN2606 Rev 70 Β§59: H72x ROM DFU on PA11/PA12 USB FS, HSI48+CRS (no HSE), BL ID 0x93, start 0x1FF09800; ES0491 2.2.5: USB DFU is dead under RDP level 1. On a real H725 (B135 rev B, 21 Oct 2026): the ROM serial isrom_serial(UID 000F0009 34315102 34363338, serial 344533413431), the version byte at 0x1FF1E7FE is 0x93 (V9.3, AN2606 Table 134: only V9.1/V9.2 lock PWR_CR3,flashsays "unplug and replug" for them), "get commands" answers 0x00 first (the command's code, no version), 220 KB erased, written and verified in 7.7 s. "Leave" jumps to the firmware with the ROM's clocks and USB still on: the firmware must reset first (B135 does). Missing: option bytes / OTP alt settings are listed but not written. - π CLT-16 Verify the hash of a cached API bundle Β·
v0.5.x(wire_weaver_client/src/local_registry.rs). - π CLT-17 Reliable requests over unreliable media Β·
v0.5.xβ a second, non-increasing seq number to guard against a lost reply for a request that was executed, when executing twice is not OK. Relevant for UDP. - π CLT-18 Backpressure β commands already go through a bounded channel (
ClientConfig::cmd_queue_size), so user code should block or yield when the queue is full; add tests for it. A request sent while every request id is in flight waits for one. Flow control towards the device for sinks: API-28 credit-sinks. - π CLT-19 Connection priority in the client config (
TODOinconfig.rs). - π CLT-20 Bound check of generated byte sink writes Β·
v0.5.xβ asink!(x: MaxLen<RefVec<'i, u8>, N>)write from a generated client is sent as raw bytes without a length check, the device rejects a longer one (ArgsDesFailed); the dynamic client and Python already check before sending. - π§ CLT-21 Follow a device across a reboot into other firmware β for DFU (
ww_dfuBootControl::reboot(Updater)): reconnect to the same device by serial number, whatever answers there, with a timeout. Done for USB:wire_weaver_client::follow(Follow,Bus,Found), and the RP2350 / RP2040 boot ROM as the updater:ww_picoboot(PICOBOOT host on nusb,SimRp2350for tests),follow::rp2_update(),ww dfu rp, tested end to end on a simulated device (tests/follow_reboot,docs/transport/follow_reboot.md). The PICOBOOT host moved to its own repo, picoboot_host (evaluated the crates.iopicoboot0.3.0 crate first: no sans-IO boundary to putSimRp2350behind, noImage/UF2 handling, no orchestrated flash+verify, nochip_id/GET_INFOβ adopting it would have meant donating most ofww_picobootupstream anyway); WireWeaver depends on it asww_picoboot(a path dependency on../picoboot_hostwhile its ELF support, IMG-4, is unlanded; a git dependency on github.com/vhrdtech/picoboot_host once it lands, until it is on crates.io).wire_weaver_client'spicobootfeature (off by default) gatesfollow::rp2_update()and theww_picobootre-export, so a host app that only talks WireWeaver over USB doesn't pull in PICOBOOT;ww dfu rpturns it on, andfollow::rp2_update_with()(whattests/follow_rebootruns against a simulated device) needs neither it norusb.ww dfu rp flashtakes a UF2, an ELF (detected by magic bytes, taken by itsPT_LOADsegments' flash physical address β noobjcopy/elf2uf2needed) or a raw binary. Missing: a run against a Pico 2 / rePort; RTT (another control block: a second ELF or a RAM scan) and UDP / WebSocket (the port may change); asking for the reboot from the CLI needs CLT-10. - π CLT-22 One
Sink::sendfor borrowed and owned items β owned types (FooOwned) implement onlySerializeShrinkWrapOwned, so they go throughsend_owned; onesendfor both (a sink item trait, or the derive implementing both), then dropsend_owned. - π§ CLT-23 Stream positions, gaps and resume on the host Β·
v0.5βCommander::prepare_stream_open/prepare_stream_resume(what generated code calls,PreparedStreamOpen,StreamOpenResult),PositionedStreamwithStreamEvent::Item/Gap/Resumed/BackfillDone/Disconnected, live and backfill runs andsplit(),StreamStats,AckMode::OnReceive/Explicit, cursors; every stream's queue is bounded (ClientConfig::stream_queue_bytes, plain streams reportRawStreamEvent::Dropped); afterdisconnect().keep_streams()streams wait andCommander::resume_streams()resumes them;DynPositionedStreamand the PythonPositionedStream(docs/api/stream_reliability.md,docs/python.md). Tested end to end against a hand-written device: S4 host stuck, S6 host restart, resume after a reconnect (wire_weaver_client/src/event_loop/stream_e2e_tests.rs). Generated clients open streams through it (tests/streams_reliable, API-31); the dynamic client and Python pass the stream function's arguments and return its result. Missing: counted and reliable streams on trait paths (events carry the absolute path), the reconnect itself (EX-10), lossy-medium scenarios (TEST-19). - β
CLT-47 ww-plugins Β·
v0.5.xβww NAME ARGS..runsww-NAME ARGS..from PATH, ascargodoes: the device selection given before NAME reaches the plugin as theWW_*variables, its exit code isww's, an unknown NAME says there is noww-NAMEon PATH.ww dfu flash ARGS..runsww-chunky flash ARGS..(chunky_mcu_updater'sww_chunky; the error without it names the install step), which cut the repository cycle:wire_weaver_clino longer depends on chunky_mcu_updater, socargo install --git https://github.com/vhrdtech/wire_weaver wire_weaver_cli --lockedworks on any PC. The completion lists plugins on PATH and asks them for the words after their name (ww dfu flash --<TAB>gives the flags ofww-chunky flash).wire_weaver_cliis a library too:wire_weaver_cli::pluginis the plugin API (DeviceArgs,Selection,take_exclusive,complete_if_requested,escape), so a plugin has the connection options, the config file and the completion ofww.just check-clean-clonebuildswwfrom a fresh clone. - π§ CLT-33
ww dfu flash: updates of a chunky device βww dfu flash <ELF|sealed image|bin>,status,confirm,revert(ww_chunky/src/commands.rsin chunky_mcu_updater;ww dfu status | confirm | revertalso run inwire_weaver_cli/src/cmd/dfu.rs) on the host librarychunky_ww_hostof chunky_mcu_updater (NET-2), now theww-chunkyplugin of chunky_mcu_updater thatww dfu flashruns (CLT-47): seals an ELF or a binary for the device (product and seal scheme fromNodeInfo, which carriesproduct_idandsealfor it, STD-12), plans a delta, sends the steps pipelined up toStageLimits::in_flight(PreparedCall::sendβPendingCall: requests leave in send order, replies awaited later), a progress bar, reboots into the trial, follows the device by serial number and waits for its confirm or revert.reboot --into updateron an A/B app that takes updates itself says it goes to the boot ROM (ww dfu stm32, CLT-34). Tested end to end in chunky_mcu_updater (chunky_ww_host/tests/e2e.rs: simulated USB HS, delta, full image, lost reply, re-plan, revert). Open: on hardware (B135, HW-5 there), the version from the ELF, a CLI-level test. - β
CLT-41 client-metadata Β·
v0.5.xβ who connects:ww_link::ClientMeta(program, app version, label, kindCli|Gui|Notebook|Service|Hub|Test, pid, user, host, purpose, random instance id) in an appended#[default = None]field ofLinkSetup(wire-compatible, conformance caseslink_setup.*).ClientConfig::client_meta(ClientMeta)overrides; defaults are found by the library (executable name, pid, login and host name, kindCliwhen the parent process is a shell; no new C dependencies:/proc,libcon macOS,windows-sysToolhelp on Windows),no_client_meta()sends nothing, a device that can't receive it gets a shortened form or none.DeviceLink::peer_meta()on the device;wire_weaver_pysetsNotebook. Docs:docs/transport/hub.md. Tested on Linux; the macOS and Windows parent-process detection compiles by inspection only (no such target here). Used by the hub (SRV-13) and the holder lock files (CLT-38). - π§ͺ CLT-50 Request priority Β·
v0.5.xβ every request carries a 4-bit priority (ww_client_server::Request'sprioritynibble,wire_weaver::Priority: 0..=15,NORMAL= 4 by default,URGENT= 8, wire change EVO-14). Default traffic follows one plain rule on both ends: a frame goes out when it is full or when the device's accumulation window is over, never later (CLT-53), also for a request or reply alone. An urgent request (8 and up) is flushed at once by the host, and its reply, immediate or deferred (SRV-42), by the device. API:.priority(p)/.urgent()onPreparedCall,PreparedRead,PreparedWriteandPreparedStreamOpen,Commander::set_default_priority,ClientConfig::default_priority; a multi request (CLT-15) goes at the highest priority of its items. Through the hub (SRV-29) the nibble stays as the client sent it and the hub's link to the device flushes an urgent request at once too (tests/huburgent_call_through_the_hub_goes_at_once). Replaces the lone request/reply fast paths (CLT-37, TR-12's reply flush), which made latency depend on what else happened to be queued. Docs:docs/transport/ww_framer.md#latency-and-priority; testswire_weaver_clientflush_e2e_tests,core::tests::urgent_request_flushes_at_once,ww_deviceasync_server::replies_flushed_by_priority. On hardware: re-measure the rePort B200A round trips (scratch fixes: immediate call 2.69 β 0.54 ms p50, deferredi2c_read5.3 β 2.3 ms). - π§ͺ CLT-53 An accumulation window never costs more than itself Β·
v0.5.xβ the host waited for the window withtokio::time::sleep_until, which rounds up to whole milliseconds: a 1 ms window (rePort B200A,UsbTimings::fs_higher_speed) cost ~2.2 ms per request. The tx task now waits on a fine timer (event_loop/fine_timer.rs: one thread per process for deadlines under 2 ms away, tokio's timer for the rest), within tens of Β΅s of the deadline. On hardware: part of CLT-50's re-measure. - β
CLT-38 Who holds a device Β·
v0.5.xβwire_weaver_client::holders(key_for,holder_of,list), always on, not gated by a feature: before claiming a USB interface or RTT probe,claim()takes an exclusivestd::fs::File::try_lockon<runtime dir>/ww/holders/<key>.lock($XDG_RUNTIME_DIR/wwon Linux,$TMPDIR/wwon macOS,%LOCALAPPDATA%\ww\runon Windows, created 0700 and refused if not owned by us) and writeskey = valuelines (pid, program, exe,mode = directfor now, since, ww version; unknown keys ignored by the reader, so later fields β CLT-41's client metadata β add without a format change). A crash releases the OS lock immediately, even with stale content left on disk:holder_of/listonly ever trust a lock file while its lock is actually held. Device keysusb:<vid>:<pid>:<serial>[:<iface>],rtt:<probe serial>:<chip>. A busy error from another WireWeaver holder names it (usb/connect.rs'swith_claim_hint,rtt/event_loop.rs'swith_claim_hint): "held by dg, pid 4711, direct, since 14:02:03 UTC"; without a lock file (a non-ww program: probe-rs CLI, minicom, dfu-util), Linux scans/proc/*/fdfor the device node and names the process, elsewhere it just says "another program". Tested with a real second holder (a child process, killed to simulate a crash) and a released claim's stale content never reported as held. - β
CLT-36 A call on a link that went down fails at once β after the device resets or the link goes (its event
loop exits), a call fails with
Error::DisconnecteduntilCommander::reconnectconnects again, instead of waiting in the command channel the residual keeps: a keeper task holds the channel between event loops and answers every command that comes in (prepared_connection.rs'skeep). Tested intests/methods(call_while_disconnected_fails_at_once). - π§ CLT-39 Hub routing in
connectΒ·v0.5.xβ direct by default, the hub when it pays off: a device the hub holds is reached through it, and repeated short connections (N opens within T seconds, shell-launched calls weighted) move a device into the hub, so a loop ofwwcalls doesn't re-open it every time.HubModeAuto,Prefer,Required,Direct(WW_HUB); hub found or started (start lock, readiness, newest hub executable:WW_HUB_EXE, the app itself,ww); user code doesn't change. Built (featurehubofwire_weaver_client, on inww): the mode (ClientConfig::hub,WW_HUB), the open counter in<runtime>/ww/opens/<key>(3 in 60 s,ClientConfig::hub_opens,WW_HUB_OPENS; kindClicounts 1, others ¼), the registry asked whether the hub holds the device, the tunnel opened before the event loop starts (so a refusal falls back to direct inAutoandPrefer),hub::ensure_running(start lock, stale socket removal, the newest ofWW_HUB_EXE/ the app itself viaww_hub::main_if_hub()/wwnext to the exe / onPATHby--hub-version, started detached, readiness by polling the socket with the preamble for 2 s), lock files with the holder's client metadata, busy errors that say how to get a hub,ww hub take(hub::take). Seedocs/transport/hub.md#reaching-devices-through-the-hub-clt-39. Tested on Linux (tests/hub_routing: two programs starting a hub at once get one, stale socket, no executable; unit tests for the modes). Missing: the open counter counts USB only (RTT andipc_exclusivelocal sockets go through the hub when it holds them, their holder hands them over (CLT-43) or inPrefer/Required; the rest connect directly); a routed USB connection through the SRV-29 tunnels is only tested on hardware (P2401#21oct-2103e, the routing picks USB devices only, which tests can't fake); a check on macOS and Windows (compiles for both). - π§ͺ CLT-40 Reconnect after a hub restart Β·
v0.5.xβ the client reconnects by itself, re-opens its streams and observe subscriptions and tells the app (Reconnected); requests in flight fail, never replayed. Built (docs/transport/hub.md#reconnect-after-a-hub-restart-clt-40): a connection through the hub that loses its link (hub exited, crashed, killed, handed over, device lent) is reconnected by the keeper of its command channel (CLT-36) with backoff, through a hub again (one started if none runs), and resumes counted and reliable streams, observed levels and sinks (resume_streams); calls fail withDisconnectedmeanwhile;ConnectionEvent/GapReasonfromCommander::connection_events();ClientConfig::hub_reconnect(give_up)(60 s, lent time not counted, no hub started while lent). Tested on Linux intests/hub_reconnect(restart.rs;kill.rs:SIGKILLtwice under load, back in ~90 ms, stream resumed, nothing sent twice); check on macOS and Windows open. Plain streams end with the link, as on an unplug. - π§ͺ CLT-46 Clock steering in every direct connection Β·
v0.5.xβ a program that holds a device directly steers its clock onto the PC's UTC without code of its own, as the hub does for the devices it holds (SRV-31): on by default,ClientConfig::clock_sync(false)orWW_CLOCK_SYNC=offto turn it off,clock_sync_optionsfor the pace;Phase::ThroughHub(no steering) while the connection goes through a hub, again after a reconnect that reaches it directly. The code ofww_clock_syncmoved intowire_weaver_client::clock_sync(featureclock_sync, default;ww_clock_syncre-exports it),Clockwithhost_syncfound by introspection (serves_host_sync,now()tried without introspection data), rate cache by the device's serial and the connection'sClientMetaas steerer, as in SRV-31. The task holds a weakCommanderfor one round of calls at a time, so the connection closes with the program's lastCommander.Commander::clock_sync()(ClockSteering: phase, status,set_paused);ww dfupauses it while it holds the device directly,ww syncand the hub turn it off (they steer with their own sync).docs/std_library/ww_timebase.md#steering-in-every-direct-connection; teststests/io_timebase(a_direct_connection_steers_by_itself,own_steering_never_keeps_the_connection_open),tests/hub(ThroughHub). Hardware check open: a board (rePort, Donguru on omarchy-m1) steered by a directwwcommand or app,ww syncwith a hub showing it followed, no second steerer with the hub. - π§ͺ CLT-43 Polite handover to the hub Β·
v0.5.xβ a direct holder that allows it (by default command-line tools, notebooks and tests, not GUIs or services) hands its device to the hub when another program wants it, and goes on through the hub after a short gap; one that doesn't is named in the other's error. Built (docs/transport/hub.md#polite-handover-from-a-direct-holder-clt-43):ClientConfig::allow_handover(default by client kind, only where the holder can reconnect through the hub); a direct holder listens on<runtime>/ww/holders/<key>.sock(user-only, peer checked, named in its lock file ashandover = <path>) for the frozenWWHOrequest (ww_local_socket::HandoverRequest/HandoverAnswer, TLV trailer with the client metadata); its event loop lets go of the device (HandedToHub, streams kept), the claim is shared by both link halves soReleasedcomes only once the medium is closed, and it reconnects through the hub on CLT-40's path (GapReason::HandedToHub,Reconnected).connect()(USB, RTT,ClientConfig::ipc_exclusivelocal sockets) andww hub takeask a direct holder before opening when a hub runs or can start; a refusal fails with the holder's name and kind. Release and reopen on every OS. Tested on Linux between two processes on a simulated device (tests/hub_reconnectpolite_handover.rs: aCliholder hands over with a counted stream resumed, gap 4.6 ms median; aGuiholder refuses and is named). Real USB (rePort, omarchy-m1,WW_LIVE_HANDOVER=off): link gap 9.7 ms median, the calls in flight and in the gap failing (104-2068 per run of 20 calls in flight). Open: an RTT probe's gap; a check on macOS and Windows (compiles by inspection only). - π§ͺ CLT-44 Live link handover on Linux Β·
v0.5.xβ the hub adopts a direct holder's live link (link, requests in flight, streams and subscriptions), so the device never sees a reconnect and the holder goes on through the hub without a gap; elsewhere CLT-43's release and reopen. Built (docs/transport/hub.md#live-link-handover-clt-44): the holder pauses (OUT drained, IN cancelled and reaped into the framer), sends its residual (wire_weaver_client::adopt::HandoverOwned: the link residual, the same on every medium, and a medium residual only the taking medium decodes) toww_hub_api::Adopt::prepare, releases the interface keeping the device open (no bus traffic at alternate setting 0, toggles kept),commithas the hub open the device itself and go on with the link (PreparedConnection::adopt), the holder's tunnel attaches by adoption id (TAG_ADOPT) and its event loop puts the tunnel in its one transport slot (a relink from any medium to any other) keeping seqs and waiters. No fd passing: nusb 0.2 can't give one out, and a shared open file's claim goes with the holder'sInterface. Needs the samewire_weaver_clientminor in the hub. Rollback when the hub dies before it holds the device, cleanup afterHubConfig::adopt_attachwhen the holder dies. Real USB on Linux (nusb) and the simulated USB device (usb_sim). Tested between processes onusb_sim(tests/hub_reconnectlive_handover.rs: handed over in 8.5 ms median with 21 requests in flight, nothing lost or doubled, the device's link up once; hub or holder killed in the commit, interface grabbed in the gap, a prepared adoption whose connection went, answers waiting in the device). On a real board (rePort, omarchy-m1,live_bench): 20/20 handovers in 16.7 ms median with 20-21 requests in flight, no failed call, no connection event, no link setup or control transfer from the hub (its trace); CLT-43 on the same board: 104-2068 failed calls per run.WW_LIVE_HANDOVER=offfalls back to CLT-43. Open: a usbmon trace of one handover showing nothing at all on the bus (root on omarchy-m1:sudo modprobe usbmon,sudo cat /sys/kernel/debug/usb/usbmon/1u > /tmp/usbmon-1u.txtwhilelive_benchhands over the rePort on bus 1; noCi/Cocontrol transfers in the window). - β
CLT-45 Lock file names one per device key β
holdersused to map every character outside[A-Za-z0-9._-]to_, so two keys could share a lock file (...:a:band...:a_b) and a USB device with a crafted serial could make another one look held. Now lower-case letters, digits and-stay and every other byte is_xx, one file per key also on a case-insensitive filesystem; long names are the key's SHA-256 (the handover socket's too, was FNV), and the key inside the file is checked on read. - π CLT-48 Fallible methods in multi calls β
MultiCall(CLT-15) only takes infallible methods, so a method returningResult(ww_i2c::I2c::i2c_read, any method with#[errors]) can't be batched into one round trip; found on the rePort B200A bench run (report-i2c-master, 27 Oct 2026), where a write + read burst to a USB-C PD controller wants exactly that. Needs each index's answer to carry the method's ownResultnext to the per-indexError. - β
CLT-48 Fallible methods in multi calls Β·
v0.5.xβ a method returningResult<T, E>(any with#[errors], e.g.ww_i2c::I2c::i2c_read/i2c_write) goes into aMultiCalltuple (CLT-15) and intocall_array, mixed with methods withoutResult; each position answers what its singlecall()would,Result<T, CallError<E>>(Devicechecked against the#[errors]list,Unimplemented,Clientfor the index's own framework error), methods withoutResultstillResult<T, ErrorOwned>. No device change: servers already put the method'sResultinto the index's slot. Generated client calls with arguments take&selfnow (was&mut self), so two of them fit into one tuple. Found on the rePort B200A bench run (report-i2c-master, 27 Oct 2026): a write + read burst to a USB-C PD controller in one USB round trip. Host-tested (tests/methods: 2- and 3-tuples,call_arrayasync and blocking, Ok, deviceErr,UnimplementedandBadIndexmixed),docs/api/multi_requests.md#methods-returning-result. - β
CLT-49 Same method twice in a multi call Β·
v0.5.xβ aMultiCall/MultiRead/MultiWritetuple naming the same resource more than once (twoww_i2c::I2c::i2c_writein one burst) failed on the client with "resources are identical", though the server already handlesMultiIndex::List([id, id]). The client now sends the repeated id, answers come back in tuple order.call_arrayand friends still refuse an accessor that builds the same path for every index. Host-tested (tests/methodsmulti_call_same_method_twice: 3-tuple at the root, 2-tuple in an array item,SameandDifferentargs, Ok and deviceErrmixed, infallible too),docs/api/multi_requests.md#several-resources-at-the-same-level. - β
CLT-54
exact_units::Fixedin dynamic clients,wwand Python Β·v0.5β aFixed<U, P, T, B>(STD-55) read from introspection data alone (quantity::fixed_of: unit, scale as aDynPrefixwith the binary and a crate's own scales,Storage { base, bounds }), shown as the typedFixedshows itself (1730000000000000 Β΅sexactly from au64,4 KiB),ww introspectwith[ms, 1..=5000]after the type; written exactly into the stored scale and type fromq!(5 ms)or another quantity of the dimension (not whole, overflow or out of bounds: an error naming the type), a plain number as the stored one;Index/IndexFrom1/Seq(STD-58) shown#3 (Sample),seq 41 (Frame)and checked (bounds, anIndexFrom1's 0) in the dynamic codec. Pythonww.Quantityfor aFixed(value,prefix,storage,fixed,bounds,to_si()), writes take aQuantityor the stored number;ww.requantizeand Csw_requantize.cbit for bit withexact_units.tests/quantities(property, method argument and return, stream item, counts throughin_process),tests/customary(the MCOFixedfield),wire_weaver_py/tests/test_offline.py, conformancestdlib.si.*green in every consumer (C's model-built cases wait on API-26). A GUI's slider limits come fromStorage::bounds; the widget is TOOL-6's. In clients. - π CLT-52 Host sends waiting requests by priority β today priority (CLT-50) only decides when a frame is
flushed; requests waiting for a free seq or for the transport go out in the order they were made. Sort that queue
by priority (the event loop's, before the framer), so an urgent request overtakes a long queue of default ones.
TODO(CLT-52)inevent_loop/core/tx.rs.
Transports and protocols (TR)¶
- β
TR-1
ww_framer: packs many small messages into one packet or splits a big one across packets; U2Head with 1 byte overhead, CRC, recovery from missed frames, fuzzed. - β
TR-2
ww_link: link setup, device info, API version exchange. - β
TR-3 USB, RTT, WebSocket, UDP and in-process (messages, or simulated USB: real USB framing
and packet sizes,
in_process::usb_device). - β
TR-4 USB CDC-NCM: WebSocket over a USB network adapter, with a DHCP server so the host gets an address by
itself, ping and a hello page over HTTP (
examples_mcu/rp2,ww_ws_ncm); UDP the same way (ww_udp_ncm). - β
TR-14 Link tap Β·
v0.5β what crosses a link, recorded at the transport edge (docs/transport/tap.md):ww_tap(no_stdTap/NoTap; records are streams of theLinkTaptrait: session, link events, frames and messages each way, client map), taken by everywire_weaver_clienttransport and event loop (ClientConfig::tap,tap::default_recorder();TraceEvent/RegisterTraceran adapter over it, the iceoryx2 USB tracer removed) and byww_device'sFramedTx/FramedRx(with_tap; withNoTapa G0 firmware is the same size).ww_framer::pieces(frame)sharesFramedRx's head code, no callbacks in the framer.Recorderbatches into pooled 64 KiBww_tapeblocks (STD-42) flushed when full or afterflush_time(10 ms), no allocation in steady state, losses counted inLinkEvent::Lost.wire_weaver_client::tap::Decoder: frame β message linkage, cross-check against the message records, framer and link statistics, request/reply pairing by seq feeding the TOOL-9 explain code. E2E tests over simulated USB (tap/e2e_tests.rs,wire_weaver_clicmd/tap/tests.rs): shared packets, split replies, a lost packet, stats, captures replayed identically. Open: the tap of a ww server's clients (ClientMaprecords are defined, nothing writes them yet). - β
TR-15 Frame prefix and link setup token Β·
v0.5β stale frames after a reconnect are removed at the cause and each medium gets the checks it needs (docs/transport/ww_link.md#setup-token,docs/transport/ww_framer.md#frame-prefix): the host puts a random 64-bit token into link setup, the device echoes it and starts its reply in a fresh framer session, the host drops everything before it (previous_session_leftovers_are_dropped).ww_framer::prefix(FramePrefix,NopPrefix,SeqPrefix: 4-bit frame counter, 2-bit epoch; lost, duplicate, late and stale frames reported and counted), checksums over prefix and head, fuzz targetframer-lossy.ww_link::Framingper medium: USB without a checksum and without a prefix (SeqPrefixas a diagnostic,wire_weaver_usb_embassyfeatureseq-prefix, told by the interface protocol), UDP withSeqPrefixand a CRC-32 (ww_block's) on every message.ww stats/ww monitorshow the prefix. Wire change of the link (docs/evolution/crate_layers.md). - π‘ TR-5 USB CDC-NCM extras β test on Windows, macOS, iOS and Android; serve files from an SD card over HTTP; mass storage with a README.
- π TR-6 CAN Β·
v0.5.xβ server and client, using CANopen (docs/transport/overview.md). - π TR-7 UART Β·
v0.5.x. - π‘ TR-8 IP / WireGuard Β·
v0.5.x. - π‘ TR-9 EtherCAT.
- β
TR-13 RTT server panicked on the first request β since TR-12 the async
Server::handlechecked for a waiting request withembassy_futures::poll_once(rx.wait_message()), a no-op waker; the RTT medium waits on anembassy_time::Timerwhile no data is there, and embassy-time panics on a waker not from its executor ("Found waker not created by the Embassy executor"). Every RTT device reset on its first request, the host saw noLinkSetup(examples_mcu/nucleo_g0b1reww_rttand an embedded_bedrock template firmware on Donguru, 16 Oct 2026). Fixed: the check polls once with the calling task's waker (ww_deviceserver.rspoll_once_here); checked on Donguru over RTT through a J-Link. USB was not affected (no timer in its read). - π§ͺ TR-16 Local socket transport Β·
v0.5.xβ client featurelocal_socket: a device served by another process on the same PC (ClientConfig::ipc_path), over a Unix socket (Linux, macOS) or a named pipe (Windows) throughinterprocess(crateww_local_socket): user-only (0700 runtime dir, 0600 socket, owner-only pipe DACL, the peer's user checked on both ends), the frozenWWHBpreamble that picks the hub or a device, thenlen | kind | payloadmessages. Device sidelocal_socket::device::device_medium()forww_device::Server(docs/transport/hub.md). Tested on Linux, end to end with the device e2e suite; check open on macOS and Windows (compiles for both). - β
TR-17 USB VID:PID Β·
v0.5.xβwire_weaver_usb_embassy::usb_init'sconfig_mutclosure couldn't setConfig::vendor_id/product_id(pub(crate)in embassy-usb 0.6), so every device shared the0xc0de:0xcafetest ID; its doc wrongly said to set them there.UsbIds { vid, pid }(Default=0xc0de:0xcafe) plus additiveusb_init_with_ids()/usb_init_with_endpoints_with_ids()let a caller pass its own assigned VID:PID;usb_init()/usb_init_with_endpoints()keep working unchanged, forwardingUsbIds::default(). Needed by embedded_bedrock's board presets ([usb] vid/pid,gen-board-hw). - π‘ TR-10 One-shot connect β no link setup at all: the host sends the API hash and its requests in one go; when
the hash matches, the device runs them right away, else it answers with its hash and drops them and the normal setup
follows. With message aggregation both arrive in one packet or frame, so a request over LoRa or LTE-M costs no extra
round trip (introspection skipped by hash, CLT-7). Open: replayed and duplicate packets (CLT-17), which requests may
run unannounced, how the hash is versioned. Needed by
ww_dfuupdates over slow links. - π‘ TR-11 MQTT / store-and-forward transport β devices offline most of the time pick up a retained message on wake (an update plan, config), replies queued on the broker. Look at MQTT-SN and LwM2M first.
- π§ TR-12 USB throughput on small MCUs β measured with
ww usb-loopbackon Donguru (STM32G0B1, Cortex-M0+ 64 MHz, USB FS behind the board's HS hub, 15 Oct 2026). Done: U2Head encodes and decodes whole bytes in u32 instead of bit by bit (ww_framerframed.rs, was ~15% of the device's CPU per packet); the async device server flushed replies at once when no other request waited (ww_deviceServer::handle), removed again for request priority (CLT-50, SRV-42: a reply goes out at once only when its request is urgent).WireWeaverClass::with_endpoints()lets firmware allocate the endpoints, e.g. double-buffered bulk ones (Donguru, embassy-rs/embassy#7275: raw bulk 500 -> 950 kB/s OUT on the G0);usb_init_with_endpoints()offers the same choice to callers of the single-functionusb_init()convenience, who can't reach into its privateUsbBuffers. Next: CRC through a pluggable checksum so MCUs can use their CRC unit (~13% of the CPU per packet on the M0+). The lone request and reply flushes once planned here are superseded by request priority (CLT-50).
Introspection (ww_self, INT)¶
- β
INT-1 API model AST with syn-style
Visit/VisitMut, sent by devices as introspection data. - β INT-2 Bundles recording crate versions, signatures of skipped traits and types.
- β
INT-3 Relocated
#[flag]fields in introspection data and dynamic serialization. - β
INT-4 Bit-aligned integers in
ww_self:U1..U64,I2..I64and nibbles are in the model (UB/IB,Nibble) and the dynamic client and Python read and write them, also as struct fields and enum payloads (done with SW-48); checked byte for byte by the conformance matrix (TEST-23,bits.*,struct.bits,enum.sub_byte.*). - π INT-5 Documentation pass on
ww_selfΒ·v0.5β doc comments are missing (TODOinww_self/src/lib.rs). - π INT-6
sized/final_structureonww_selftypes Β·v0.5β where a type is unlikely to evolve, to make introspection data smaller. Can't be undone later, so decide per type. - π INT-9
TailBytesandTailSizein the API model β a user type with aTailBytes(SW-9) orTailSize<N>(SW-18) field can't be in an API: the crate walker doesn't find the types (Type TailSize not found,Module shrink_wrap not foundforshrink_wrap::tail_bytes::TailBytes), andww_self::Typehas no form for them, so the dynamic client and Python can't read or write them. Conformance casestail_bytes,struct.tail_size. - π INT-10
#[default]values in the API model β the crate walker records only that a field has a default (Field::defaultis a placeholderBool(false), aTODOinwire_weaver_core'sget_default_attr), so the dynamic client and Python read a field missing from older data as its type's default:#[default = 7] timeout: u16reads as 0. Convert the default expression (literals,None,Some(..), unit variants) to a value and use it in the dynamic decoder; mind thatField::defaultis part of API signatures. Conformance caseevolution.new_reads_old. - π INT-11
InstanceandParamhave no conformance case β found by the coverage guard (TEST-25):ww_self::generics::prepare(API-43) turns everyType::Instanceinto anOutOfLinereference to a monomorphized struct/enum before any client sees a bundle (a device's own bundle is prepared too), andType::Paramonly remains inside the now-unreferenced generic definition, so neither is reachable from any property's resolved type. Testing them needs a conformance mode over the introspection bundle's own wire encoding, not over app-level property values as the corpus does now. - π‘ INT-7 Compress
ww_selfstrings with a pre-shared dictionary β partly addressed by leaving snapshot-known traits and types out of introspection data. - π‘ INT-8 Model enums that tolerate newer variants β the
ww_selfenums (Type,ApiItemKind,Fields, ...) andww_numeric::NumericAnyTypeareno_future_version(SW-47): a host reading a newer model fails as a whole. WithUnknownhandled in every walker (visitor, signature, inline, values, compat, CLI, Python), an older host could show the rest of a newer device's API and mark the unknown parts. - π‘ INT-12 Bounds as a type of the API model β
Type::Bounded { ty, min, max }(likeMaxLen), checked by every consumer (the generic decoder, C, Python), shared with API-29's index bounds. Until then bounds are const arguments ofww_simarkers (STD-55) that onlyww_si-aware clients check; a newTypevariant needs a model version step. - β
INT-13 Unit definitions in the API model Β·
v0.5β every unit, scale and origin marker an API uses carries its definition inww_self(ItemStructExtra::marker, aMarkerDef: symbol,Dim, exactMagnitude; an origin's unit, reference, exact offset, modulus and step), named by crate and marker (D16b), so clients know a user crate's units (unit!,scale!,origin!) without a built-in table.ww_self::units::unit_ofgives a canonical sorted unit expression (F#-style:Per<Meter, Second>equalsTimes<Per<Ratio, Second>, Meter>) with exactfactor_to,scale_ofandorigin_ofthe factor of a scale and the definition of an origin. The walker and snapshots fill it, appended with#[default = None]so plain structs keep their bytes and signatures and older clients skip it. Conformancemodel.marker_def.*/model.item_struct_extra.*through derive, dynamic client and Python (C: API-26). Firmware: no code cost (markers stay zero-size), introspection bytes only for the crate's own markers (Rpm22 B, an origin ~40 B; TEST-27). Levels (STD-67), kinds (STD-68) and sentinels (STD-66) append theirMarkerDefvariants when their markers exist. Units in the API model.
Evolution, versioning and compatibility (EVO)¶
- β
EVO-1 Evolution rules in
docs/evolution/rules.md: crate layers and version bumping. - β
EVO-2 SHA signatures of APIs and types, hashing only the major version (or
0.minor), version independent. - β EVO-3 Crate version recorded in bundles.
- β
EVO-4 Global traits referenced by hash, global type registry (
ww_global): one id percrate::Traitfor every publicww_stdlibtrait, by hand inwire_weaver_gid.json, renames keep the id throughaliases, ids never reused (retired),assigned_ids.lockappend-only and checked;ww api checkflags a changed id (API-20). - β
EVO-5 API snapshots of
ww_globalandww_stdlib(wire_weaver_snapshots,ww api save), left out of introspection data and put back by the client. - β
EVO-6 Evolution checker and report:
ww api check/ww api diff(docs/evolution/checker_tool.md). - β
EVO-9 Skipped definitions from another revision Β·
v0.5.xβ codegen keeps every definition it leaves out of a device's introspection data in~/.wire_weaver/definitions/by signature; a host that doesn't find a skipped definition under its signature in its snapshots (a device built from another revision of an unpublished crate) takes it from there and compares it by the evolution rules, with a warning instead of an error. Not found anywhere: an error with both signatures and the fix. Rust client,wwand Python (docs/std_library/ww_self.md,wire_weaver_clientcompat::tests::other_revision).#[cfg]enum variants don't change signatures (tests/cfg_variants). - β
EVO-11 Evolution check at commit time β the checker is its own light crate,
wire_weaver_evolution(parses the API crate from source, no client, tokio or probe-rs), which an API crate runs from its owntests/evolution.rs(Check::new(env!("CARGO_MANIFEST_DIR")).strict(true).assert()), so its lock file pins the checker andcargo test, CI and a pre-commit hook check the same way. Strict mode fails a crate without snapshots, naming theww api saveto run; every breaking change names the rule it breaks with its docs section, and a failed check the version to bump to.ww api check --strictruns the same library. Template setup (test, just recipes, CI step, pre-commit hook) indocs/evolution/checker_tool.md#checking-at-every-commit;examples/blinky_apidoes it. - β
EVO-12 Current version's snapshot required β
Check::require_current(true)(andww api check --require-current) fails when the crate's current version has no snapshot or the source differs from it, and checks the bump from the previous version's snapshot to it, so a version can't ship without its snapshot (thenucleo_g0b1re_template0.1.5 shipped API 0.2.0 without one). The snapshot is saved from the API crate's own test (WW_API_SAVE=1 cargo test --test evolution,Check::save), with the checker the lock file pins;ww api saverefreshes the current version's snapshot too, never an older one (docs/evolution/checker_tool.md#checking-at-every-commit,wire_weaver_evolution/tests/check.rs). - β
EVO-10 Checker knows the
Unknown(u32)catch-all βww_self::ItemEnum::future_versionrecords an enum's#[future_version]catch-all (SW-47), set by codegen; the evolution checker (ww api check), the client's runtime compat check and the dynamic decoder use it: a variant appended to an enum with the catch-all is compatible, an inserted or reordered one is reported as moved, and without the catch-all a new variant stays breaking. Snapshots saved before EVO-10 count as without it until saved again, and compare clean otherwise (EVO-13) (docs/evolution/rules.md#unsized-types,wire_weaver_evolutiontestsenum_variant_appended_to_the_catch_all). Found withww_timebase::SyncState::Conflict. - β
EVO-13 Upgrading wire_weaver never forces an API bump or re-save β a field of the API model that a snapshot
file has in no instance (written by an older wire_weaver:
ItemEnum::future_versionbefore EVO-10) is not recorded, and the checker compares both sides without it and says so, instead of the unchanged source counting as changed andrequire_currentfailing (G0'snucleo_g0b1re_api0.4.0 after EVO-10). Read from the file itself, no list of fields orww_selfversions: the next model field gets it too, also a non-Optionone, which DER-17 makes readable (docs/evolution/checker_tool.md#the-current-versions-snapshot,wire_weaver_evolution/tests/check.rswith G0's old-format snapshots as a fixture). - β
EVO-14 Priority nibble in
RequestΒ·v0.5.xβ wire break ofww_client_server'sRequest: apriority: Nibbleafterseq(CLT-50). A host and a device must both have it; nothing with the old layout was released. Conformance cases ofRequestcarry the new bytes with"wire_change": "EVO-14 / ...". - β
EVO-16
ww_si::Dimwith angle, information and counted bases Β·v0.5β the wire change ofDimthat STD-60 brings, inSIExpr/AnyQuantityand in theww_sisnapshot: no longersizedbutfinal_structure, nine 4-bit exponents (rad and bit appended to the seven SI ones), a root bit and four optional counted bases (crate and marker name); a non-canonicalDimis refused on decode. Volt isd2 1f 00 00 00 00.ScalegainsExact(anExactScale, a degree is exactly Ο/180) in the same break. Conformance casesstdlib.si.dim.*(tests/conformance/corpus.json), ww_si. - β
EVO-15 Transparent wrappers in the checker Β·
v0.5βTand atransparentwrapper ofT(DER-18,ww_si::Fixed) are compatible, reported as the same bytes (u64 -> Fixed<Second, Micro, u64, Unbounded>, same bytes); a change of aFixed's unit or prefix is breaking although the bytes stay (a reader takes Β΅s for ms,Rule::Meaning): any type parameter no field uses is a marker compared by name; a bound narrowed or widened is breaking for the side it refuses (Rule::Bound, compared by value);QuantityβFixedis breaking with a hint that it is the intended move. A skippedww_sidefinition is read from the embedded snapshot, also on prepared bundles (the client at connect time) (docs/evolution/rules.md#transparent-wrappers-units-and-scales,#bounds;wire_weaver_evolutionteststests::transparent,tests::ww_si_fixed). - π‘ EVO-7 Git dirty flag β not implemented, although the old tracker had it as done: neither
ww_versionnor codegen record git state. Uncommitted API changes are still caught, since they change the API's SHA signature. Firmware git info (sha, dirty, branch, tag) can be exposed throughww_firmware_info'sbedrock_build_infostream. Decide whether the link or version also needs it. - π EVO-8 Snapshots of crates.io crates β depends on "Load API crates from crates.io" above.
- β
EVO-17 Units compared by meaning Β·
v0.5β markers with a definition in the model (INT-13) are compared by it: the unit and scale of aFixedmultiplied into one step (dimension and exact factor), so another spelling of the same step is compatible (MilliβPow10<-3>,Volt, MilliβMilliVolt, One, listed assame value), and the same bytes in a different scale are breaking with the factor in the finding (lbfΒ·s vs NΒ·s, the Mars Climate Orbiter, with STD-72'sPoundForce:1 on the old version is 4.448222), also at connect time; another dimension is breaking. A unit, scale or origin of the crate's own redefined under the same name is breaking, a new symbol is not. Origins by dimension, reference, zero and period (Celsius= an own origin at 273.15 K,Angle<Degree>=Angle<Radian>).SaturatingisUnbounded's values,Wrappingon one side only is breaking (Rule::Meaning). A unit by convention (STD-72 provenance) with its value updated is listed asconventional value updated, defined β conventional is breaking. Kinds of quantity are compared (STD-68: another kind of one dimension is breaking, a unit gaining a kind under its name iskind of its own added); levels and sentinels get their checks with STD-67 and STD-66 (rules written down); until then a marker kind the checker doesn't know is the same only when its definition is (docs/evolution/units_by_meaning.md;wire_weaver_evolutionteststests::by_meaning). - β
TOOL-1
wwCLI:listwith filters,introspectas a resource tree (--raw-as-sent,--types), dynamic shell completions (docs/cli.md). - β
TOOL-2
ww api save/check/difffor API bundles and snapshots. - β
TOOL-3
ww api scaffoldgenerates server handler stubs and the matchingww_codegen!call. - β
TOOL-4 Python module talking to any device, API built from introspection, dynamic values serdes, RTT built in
(
wire_weaver_py,docs/python.md). - β
TOOL-5 Runtime literals: values to bytes at runtime (
eval, dynamic values). - β
TOOL-10
ww usb-loopbacklink test Β·v0.5β round trips with lost and corrupted counts, host β device and device β host speed, on the sans-IO event loop (wire_weaver_client/src/event_loop/core/loopback.rs, windowed so a slow device holds the host back; e2e test againstww_device::Serverindevice_e2e_tests.rs), with a summary table and tuning options. Works with any device, the link layer answers. Checked on a full-speed board (Donguru, 3 hubs): about 155 round trips/s, 375 kB/s host β device, 317 kB/s device β host with 1 KiB messages. - β
TOOL-11 Dynamic observe β decodes a level's
PropertyUpdatefrom introspection alone (CLT-28):DynResource::observe()/observe_blocking()(observable_properties()lists what it can ask for), reusing the CLT-28 event-loop routing and subscription sharing through a schema-lessDynUpdate/DynConfigpair;ww observe <path> [--rate MS]prints each update's values, rate and link bytes/s (wire_weaver_client::DynObserver,wire_weaver_cli/src/cmd/observe.rs,tests/observe);wire_weaver_py'sNode.observe(cfg)iterates the same updates as dicts.ww_selfcarries thesparseflag a dynamic decoder needs (DER-13). - β
TOOL-12
ww settings dumpΒ·v0.5β host decoder of aww_settings::Storeflash dump, no device needed:ww_settings::dump::analyze(pages, every item slot valid/cut/garbage/erased, the current record of every key cross-checked against a realStoreover the same bytes throughfake_flash::FakeFlash) anddecode_groupfor a caller that knows the concreteSettingsGrouptype;ww settings dump FILE --word-size --page-size [--base] [--format text|json]inwire_weaver_cli(docs/cli_reference.md#ww-settings). - β
TOOL-14
ww monitor,ww stats,ww capture,ww explain FILEΒ·v0.5β the link tap (TR-14) in the CLI (docs/cli_reference.md#ww-monitor): live decoded print at the frame, link or API layer (--bytes,--bitsfor the TOOL-9 bit map of every message), framer and link statistics every few seconds or of a capture, captures with the device's API bundle in them, read back byww explain FILEexactly as monitored live. Blocks come from aFeed(wire_weaver_cli/src/cmd/tap.rs): the CLI's own connection today, a ww server's tap of all its clients later. Global--monitor[=frame|link|api](--monitor-bytes,--monitor-bits) and--capture FILEon every command that connects print (to stderr) or save that command's own connection, with the sameMonitor,LiveFeedandTapeWriter; the tap decoder orders a block's records by time so a reply is paired with its request (docs/cli_reference.md#ww-monitor). - β
TOOL-18 The hub from Python and notebooks Β·
v0.5.xβ the wheel (wire_weaver_py) carrieswire_weaver_clientwith itshubfeature andww_hub;python -m wire_weaver.hubserves the hub, found bywire_weaver_client::hub::ensure_runningthe same way as a Rustww_hub::main_if_hub()program (wire_weaverregisterssys.executable,["-m", "wire_weaver.hub"], at import).connect()takeshub_device(straight to a device key through the hub) in addition to the usual filters, and followsWW_HUB/WW_HUB_OPENSthe same as a Rust client.wire_weaver.hub()connects to the hub's own API (ww_hub_api) through the dynamic client, starting the hub if none runs (devices, clients,for e in hub().registry.events: ...). Fixed on the way:connect_blocking()had noInterfaceKind::Ipcbranch, so every blocking connect through a local socket or the hub silently failed. Tested without hardware (tests/hub_pytest_devicehosts the hub and an in-process device in one binary): a kernel restart reconnects well under a second with the device still open and steered, two Python processes share one device through the hub's tunnel (wire_weaver_py/tests/test_hub.py).docs/transport/hub.md#python-and-notebooks-tool-18. - β
TOOL-19 Escape device strings in
wwoutput βUntrusted/escape(wire_weaver_cli/src/untrusted.rs) turn a C0/C1 control character, DEL or a Unicode bidi override/isolate in a device- or hub-supplied string into a visible\u{..}escape (\n/\r/\tas their short two-character form) before it reaches the terminal, so a device can no longer write terminal escape sequences through its USB product/manufacturer/serial strings, introspection docs/idents,UserStrtext, pin names or hub/holder program/label/user/host fields;--jsonis untouched (JSON's own escaping already covers it), and the CLI's own ANSI colour codes stay outside the escaped text.docs/cli_reference.md#untrusted-device-and-hub-strings. - β
TOOL-20
wwover RTT β every device command reaches a board on a debug probe (probe-rs,wire_weaver_client'srttfeature):--rtt CHIP,--probe SERIAL|VID:PID[:SERIAL],--elf ELF(control block address, no RAM scan),--probe-speed-khz, as env variables (WW_RTT, ...) andww.tomlkeys; USB filters next to it refused or left out by source,--apichecked once connected;ww list --probeswith who holds each;ww syncandww dfugo through the hub with the keyrtt:<probe serial>:<chip>; completion of chip names and live probe serials; a busy probe names its holder (lock file, or/procfor probe-rs/OpenOCD).docs/cli.md#through-a-debug-probe-rtt. Tests:wire_weaver_clidevice::tests,tests/rtt.rs; the RTT byte path itself inwire_weaver_client/src/event_loop/device_e2e_tests.rs. Hardware (omarchy-m1, 23 Oct 2026, Nucleo G0B1RE with nucleo_g0b1re_template 5bcbba0 through its ST-LINK V2-1):ww infoin 0.4 s with--elf, 1.7 s with the RAM scan;ww counters readandww syncreach the device and getTraitNotFound(the template serves neither yet);held by ww, pid ..andheld by probe-rs (pid ..);ww hub take rtt:..and twoww infoat once through the hub. Found and fixed on the way: a stale control block of the previous firmware won the RAM scan,ww hub taketimed out while the hub scanned. - β
TOOL-22 Hint for plain fields that carry a unit β
ww api checkand the walkerww api scaffolduses note a plain integer field, method argument or property whose name ends in a known unit suffix (_us,_ms,_ns,_s,_hz,_khz,_ppb,_ppm,_bytes,_bits,_kib) with theexact_units::Fixed<U, P, T>it could become instead (wire_weaver_core::transform::unit_hint,Scratch::unit_hints): a note, never an error, printed onww api checkand onww api scaffold's stderr, never affecting the verdict or the generated code. Silenced per item with#[no_unit_hint]: bare on a struct/enum field, bare (every argument) or naming arguments (#[no_unit_hint(a, b)]) on a trait method, bare on aproperty!/stream!/sink!/ww_impl!item.docs/evolution/checker_tool.md#unit-suffix-hints-tool-22. - π§ͺ TOOL-16
ww syncβ several devices kept on the host's UTC at once (STD-49, crateww_clock_sync): devices by serial or label parts (or every device the selection matches), a live table per device (state, followed source, rate in ppm, shortest round trip, the residual offset measured withshared_now, samples),--modewrites theww_indicationmode on all,--duration,--poll-ms/--steer-ms;qor Ctrl+C stops, saying who steers the devices after (the hub, or nobody: holdover). With a ww hub running it shows the hub's steering (SRV-31) and never steers itself; a STEERED BY column (checked through the hub on Donguru and B135, 22 Oct 2026). Convergence (21 Oct 2026): residual Β± half the check's round trip, the device's error bound and slew left, a line when each device and then all are within--within-us(locked by their word, residual by the host's), the phase spread between them; steered at 10 Hz until locked; quiet into a pipe. Hardware (omarchy-m1, 21 Oct 2026,ww syncwith Donguru B153A, rePort B200 and B135B): all three locked within 2 ms after 1.2 s from a fresh start and 2.4 s after 45 s in holdover; phase spread as ww sync estimates it 30 Β΅s to 1.0 ms apart (Β±1.5-2.5 ms, rePort's and Donguru's ~1.4-1.8 ms round trips); over 5 min Donguru left once on a -2.0 ms residual (Β±1.2 ms) and was back in 37 s. No logic analyser on this PC: the phase is not measured on the pins.docs/cli_reference.md#ww-sync. Hardware check open: Donguru and rePort on omarchy-m1. Skill tree signal and skill: tpm P2618#21oct-1707. - π§ͺ TOOL-15
ww pins test/ww pins listΒ·v0.5.xβ pin tests on any device servingww_pin_diag::PinDiag(STD-47) at any path, found by introspection (docs/cli_reference.md#ww-pins): pulls, shorts to GND, to the supply and between points, only what each point's caps allow, refusals shown,release_allat the end also on Ctrl-C and errors, exit 1 on a fault. Points by names,P<n>ids, ranges and globs on names and groups,--watchpoints only read; plan files withexpect, narrowerallow,[shorts] among/same_net,--saveintoexpect. Libraryww_pin_probe(probe::runover aPinstrait,mock::MockBoard), end to end intests/pin_diag. Hardware check open: on rePort once it servesPinDiag. Names sent as ids show as#<id>(STD-48). The live decoder gets the device's API as soon as the command has it (the recorder hands a bundle over at once, as the device sent it, snapshot-known traits put back by the decoder), so a live print and the capture's replay are the same text; SIGINT and SIGTERM end a flagged command orww monitor/capture/statswith the capture finished, synced and renamed;ww explainof a path-like WHAT that is no file says so without connecting. - π§ͺ TOOL-21
ww pins tuiΒ·v0.5.xβ every point live, laid out per connector of a board preset (docs/cli_reference.md#ww-pins-tui): level and what was set on each pad, keys to drive, toggle, pull, weak drive, read all, release, pause; every point released on quit, signals and errors. The preset is boardmap's board description read as it is ([[view.connector]],[aliases],[pins]roles, pads from its geometry JSON, nets joined across fitted solder bridges), no dependency on embedded_bedrock; a preset for ww alone lists nets per connector. Devices withww_gpiobanks and noPinDiagare shown through the banks (modes and pulls put back on release). Libraryww_pin_probe::board,live,bank; tests onMockBoardwith the NUCLEO-G0B1RE preset (wire_weaver_clicmd::pins_tui) and over banks end to end (tests/io_gpio). Hardware check open: the Nucleo G0B1RE once its firmware servesPinDiag. Skill tree signal and skill: tpm P2618#23oct-1334. - π§ TOOL-6 Dev GUI / web tool Β·
v0.5.xβwire_weaver_tool(egui, Trunk) shows the AST and generated code. Goals: a web tool like the pest playground, a dynamic UI for any API (MVP prototyped), widgets per type, mockup server UI. Seedocs/dev_tool.md. AFixedfield's widget shows its unit and takes its slider limits fromquantity::fixed_of(..).storage.bounds(CLT-54). - π‘ TOOL-7 Generated Python wheel for a specific API β nothing is generated for Python yet:
examples/blinky_pyis a hand-written pyo3 wrapper around the Rust client, andwire_weaver_pyis dynamic. Generating typed pyo3 bindings from an API crate might be useful (see theTODOindocs/api/folder_structure.md). - π§ TOOL-17
ww hubΒ·v0.5.xβww hub serve | status | stop | who | keep(docs/cli_reference.md#ww-hub,docs/transport/hub.md),Hub::status()and the location, label and API hash inRegistry::devices();whonames the hub, direct ww programs (lock files) and, on Linux, other programs (/proc) per device, with or without a hub;ww listasks a running hub through the registry (with aSTATEcolumn,--directto enumerate USB anyway); completions of the subcommands,keepdurations and device keys with help texts; colour off without a TTY or withNO_COLOR;lend DEVICE -- CMD(SRV-32). Open:ww listasks a running hub and doesn't start one; the skill tree entry (P2618#21oct-2209). - π TOOL-23 Doc-comment / sentinel / bounds hints for
ww api checkβ the bigger version of TOOL-22's name-suffix hint: read a doc comment that states a unit, scale, range or sentinel ("in Β΅s", "0..=65535 = 0..100 %", "127 = not available") and suggestBounds/niche forms too, not only the name suffix. Needs STD-64, STD-66 and INT-13 landed first; tested on the scout's repos. - π TOOL-13
Some(None)in Python values β Python (and the conformance corpus, which uses its form) writesNonefor bothNoneandSome(None)of anOption<Option<T>>, so the innerNonecan't be written or told apart when read. Needs a marker for a presentNone(e.g.ww.Some(None)). Conformance caseoption.option.some_none. - π TOOL-8
wwinspect / read / write all GPIOs on any device Β·v0.5.x. -
π§ TOOL-9 Buffer visualizer / "explain" encoding Β·
v0.5.xβ each bit traced to the value it belongs to. Done:ww_self::explain(the dynamic decoder records every value, length, discriminant, flag and padding with its front bits and back nibbles);ww_self::expr(featureexpr: values and calls written as Rust expressions,Config { mode: Mode::Fast, .. },motor.set_speed(1.5, Dir::Cw),motor.limit = 3, read by the type with errors naming the field) andwire_weaver_client::expr(the same withww_siquantities,q!(3300 mV));wire_weaver_client::envelope(a whole request or event: theww_client_serverenvelope field by field, the payload by its resource);ww explain WHAT HEX | --value EXPR | 'call(..)' [--request],ww explain request|event HEX. Captures and the live link:ww explain FILE/ww monitor --layer api --bits(TOOL-14), replies explained by their request's seq;Observerequests and their grants read as property names and modes,PropertyUpdates as the properties they carry with typed values (units as inww observe); calls through global traits (GlobalFullpaths:ww info,ww report, ..) are namedww_user_info::UserInfo.nameand decoded from the snapshots the host carries, also by a command that never introspects the device. Open:GlobalCompactpaths, multi requests;Seriescolumns and sparse-struct slot headers in detail; shell completion of types and paths; aww callon the same expressions; a GUI view (hex pane + tree, like Kaitai's Web IDE); Pythonexplain()and expression input. -
β STD-1
ww_client_serverβ the request/event model, pruned of sideband/subscribe/rate, variable length seq numbers. - β
STD-2
ww_version. - β
STD-3
ww_siandww_numeric. - β
STD-4
ww_date_time. - β
STD-5
ww_gpio. - β
STD-16 Common errors β
ww_error::CommonError(Unsupported,InvalidArgument,InUse,Busy,WrongState,NoPower,Refused,Timeout,Fault, bounded strings), re-exported aswire_weaver::prelude::CommonError, nested first in an error enum as#[common] Common(CommonError)(the derive addsFrom<CommonError>andHasCommon); recognised by tools by its type (ww_self::TypeOwned::is_common_error): PythonRemoteError.commonwith the doc line as the message,ww api tree -tmarks it. Display by hand until DER-12 takes over. Everyww_stdlibAPI crate with fallible methods uses it exceptww_dfu(itsErrorhas no lifetime and predates this; onlyUnsupported/Busywould map cleanly, left as a follow-up) andww_can_bus(not ready yet);ww_client_server'sErrorKindis a framework/link-level error, not an API error, so it is out of scope.docs/std_library/ww_error.md,tests/common_error. - β
STD-19
ww_pwmβPwm/PwmChanneloutputs (set_output,set_duty,stop,state),PwmCapture/PwmCaptureChannelmeasurements (frequency, duty, pulse width, edge count) over a plain stream with sidebandOpen/Close. Full rustdoc, mock server and in-process tests (tests/io_pwm), docs page (docs/std_library/ww_pwm.md) (STD-13). - β
STD-17
ww_timebaseβClock: device time asww_si::Second(full width vianow(), truncated elsewhere), sync master / slave (IRIG-B, PPS),sync_status, atime_mapstream mapping device time to shared time,set_time_of_dayfor a master.tests/io_timebase,docs/std_library/ww_timebase.md. - β
STD-49
ww_timebasehost sync, one UTC timescale β devices without a sync line follow the host's wall clock (UTC), and every source is ranked: a locked line (IRIG-B, PPS) > host > holdover > free.Clockgets the optionalhost_syncgroup (steer(SteerPoint) -> SyncSource: offset in UTC Β΅s, rate in ppb and the host's error estimate; a line-locked device only notes the point;shared_now();set_sources/sources: an explicit ordered list of allowed sources,Nonekeeps the fixed ranking),SyncSource(Host,Line),SyncStatus::source/error,TimeMapPoint::source(the seam of a switch, slewed, not stepped),SyncState::Conflict(two masters on a line),SyncStatus::slew_us;Acquiringuntil within the device's lock tolerance, thenLocked(docs/std_library/ww_timebase.md#states), the host steering at 10 Hz until then.configure_syncstays the line's role, the IRIG-B / PPS model is unchanged. Host side: crateww_clock_sync(Estimator: min-RTT samples ofnow(), least squares over a 30 s window against the monotonic clock;DeviceTime;HostClock;ClockSyncfor any process holding a connection), deterministic tests with simulated device clocks (HSI 1 % fast, a drifting crystal, USB FS jitter), end to end intests/io_timebase;docs/std_library/ww_timebase.md#host-sync-std-49. Device side: embedded_bedrockbedrock_sync. Steer points name their steerer and a device follows one at a time, with a per-device rate cache on the PC (SRV-31). Checked on omarchy-m1 (21-22 Oct 2026): Donguru, rePort and B135 steered byww sync,sync_statusLockedin 1.2-1.6 s, the residual measured withshared_nowB135 -5..-16 Β΅s (Β±55 Β΅s), Donguru within Β±1.2 ms (its 1.4-1.6 ms round trips). - β
STD-18
ww_adcβAdc,AdcStream(sidebandOpen/Close),AdcCapture. Fill-in done: full rustdoc,tests/io_adcmock server (one test per trait),docs/std_library/ww_adc.md, API snapshot (STD-13). - β
STD-20
ww_onewireβOneWire: addressed transactions with strong pull-up, reset, paged and bit-level (triplet) ROM search, the real Dallas/Maxim search algorithm. Full rustdoc, docs page (docs/std_library/ww_onewire.md), API snapshot, in-process test with a mock bus (tests/io_onewire). - β
STD-23
ww_si/ww_numericextended Β·v0.5β quantity groups and host helpers as cargo features,Secondas a device timestamp (wrap rule, hostUnwrapper), constructors, conversions and comparisons across prefixes and storage,UVlq32/UVlq64storage,Baudfor baud rates (STD-13),Ratio,TorqueandTimed<T>(EX-11).docs/std_library/ww_si.md,docs/std_library/ww_numeric.md. Zero-overhead typed quantities (Volt<u16, Milli>) are not part of it. - β
STD-38
ww_si0.2: one genericQuantity<U: Unit>Β·v0.5β every quantity isQuantity<U> { prefix, value, PhantomData<U> }with a zero-size marker per unit inww_si::unitsand the 0.1 names as aliases (KiloGramβGram);Dim,Scale,UnitExpr,Unit(own markers in API crates),Per/Times; constructors,quantize, conversions (to_si_f64through each unit's scale),with_prefix,same_repr, comparisons,Display/Debug/defmtwritten once; unit algebra checked by the compiler against a product table verified at compile time;units::by_name. Same bytes on the wire as 0.1.docs/std_library/ww_si.md,ww_stdlib/ww_si/tests; the 0.2.0 snapshot holdsQuantity, every marker andPer/Times, so devices leave them out of their introspection data. Other crates' call sites moved to the new constructors; the motor demo's own helpers (units.rs) andLoadunits are the follow-up piece;BaseUnitis replaced by STD-37 (run-time units). - β
STD-37
ww_siunits known only at run time Β·v0.5βSIExpr<'i>{Named { name, symbol, dim, scale },Mul,Div,Pow} (recursive throughRefBox, API-44) andAnyQuantity<'i> { prefix, value, unit }: the marker model on the wire,Dimas seven 4-bit exponents (sized),Scaletoo. A marker'sUnitExprmaps one to one onto anSIExpr(Unit::SI_EXPR,SIExpr::of,SIExprOwned::from; back==andknown()), shown, named and converted to SI by the same code as typed units, walks bounded byMAX_UNIT_DEPTH.ww_adc::ChannelInfo::unitis one;BaseUnit,SIExp,INib8P3are removed.docs/std_library/ww_si.md#units-known-only-at-run-time(byte layouts),ww_stdlib/ww_si/tests/dynamic.rs,tests/io_adc. Dynamic clients,wwand Python show anAnyQuantitylike the typed quantity of its unit (CLT-31'sDynQuantity, both rendered by the publicww_si::treewalk), a device's own unit with its dimension and scale; writes take any quantity whose unit has a dimension.tests/quantities(anAnyQuantityproperty through the in-process transport),wire_weaver_py/tests. - β
STD-43
ww_siwithout wide integers on small targets Β·v0.5β comparisons,with_prefix, the exact arithmetic and the integer conversions of quantities work onww_numeric::Integer(sign and a magnitude as wide as the widest enabled number type): with types up to 32 bits onlyu32code, noi64/i128and nof64without float types.to_i32_in/to_u32_in/to_u32_wrapping_in;to_i128_inonly with 64- or 128-bit types. 6 KiB less flash on Donguru (thumbv6m,opt-level = "z").docs/std_library/ww_si.md#conversions-and-comparisons,ww_stdlib/ww_si/tests/narrow.rsandww_stdlib/ww_numeric/tests/integer.rs(run with 32-bit types only too). - β
STD-61
exact_units: the compile-time unit core as a crate of its own Β·v0.5β a standaloneno_stdcrate, no dependencies without features (Roman, 27 Oct 2026):Dim(SI, rad, bit, counted bases, root flag), unit markers andUnit(withSqrt<U>),ExactScale(with a power of Ο),Fixed, bounds, constsqrt_f64;shrink_wrap(wire format,SIExpr, through#[wire(..)]fields the same bytes as before),stdanddefmtbehind features.ww_sire-exports it and keeps the runtime side (Quantity,AnyQuantity,Timed); units are namedexact_units::..in the model (a counted base costs 6 bytes more on the wire). Walked and snapshotted like an API crate. exact_units,exact_units/tests, CI legs without features and on thumbv6m. Moved to a repository of its own, romixlab/exact_units, with its history (28 Oct 2026): wire_weaver takes it by git, its snapshot is saved from that source intowire_weaver_snapshots/other_repos(ww api save --dep exact_units --snapshots-dir). - β
STD-55
exact_units::Fixed<U, P, T, B = Unbounded>: unit, scale and bounds fixed at compile time Β·v0.5β a value of unitUat scalePstored asT, on the wire exactlyT(DER-18transparent; no prefix, noNumericValuetag, so au64doesn't turn onww_numeric'su64for everyone); scale markers (every prefix name,Kibi..,Pow10<E>,Rational<N, D>), bounds (Unbounded,Range,RangeU64,RangeDec) checked on decode and construction; information and counting units, exactto,Quantity::to_fixed/from_fixed,core::time::Duration, checked / wrapping / saturating ops,Display(1500 Β΅s); in the API model as an instance of one generic definition withBexplicit, so introspection shows the unit. Same code as a plain field on Cortex-M (DER-20). exact_units, conformancestdlib.si.fixed.*,tests/quantities. - β
STD-72 Imperial and US customary units, complete Β·
v0.5β featurecustomary, opt-in (not inquantitiesorstd, free for an API without it; host clients turn it on): 166 markers inexact_units::units, each the exact rational of SI from its legal definition (1959 yard and pound, NIST SP 811 and Handbook 44, UK WMA 1985, g_n), the source cited per unit and checked against a sourced catalogue with SP 811's printed values. No bare ambiguous name (47 groups: US / imperial gallon, fl oz, pint, quart, bushel; international / US survey foot and mile; short / long / metric ton; avoirdupois / troy; Btu and calorie variants; horsepower variants; thou vs angular mil; inHg / mmHg conventional or at a temperature);PoundAvdpandSlugmass,PoundForce,Kip,Poundalforce; Β°F and Β°R are origins (STD-64). Measured units carryProvenance::Conventional, in the API model aMarkerDef::ConventionalUnit(defined units keep their bytes).Fixed::convertbetween units of one dimension, exact orNone, factor at compile time;DynQuantity::convertand PythonQuantity.convertthe same at run time. The Mars Climate Orbiter case (lbfΒ·s read as NΒ·s) intests/customary(typed, dynamic) and Python: converts exactly or is refused; theFixedfield too, with its unit in the dynamic client (CLT-54). Imperial and US customary, conformancestdlib.si.fixed.customary,stdlib.si.si_expr.pound_force(derive, dynamic, Python; C waits on API-26). - β
STD-58
Index,IndexFrom1andSeqΒ·v0.5βexact_units(featurecounting, re-exported byww_si):Index<Of, T, B>, a 0-based position in a table ofOf(index Β± amount = index, index β index = aFixed<Of, One, T>distance, index + index and index Β· x don't compile, checked forms for bounded ones);IndexFrom1<Of, T, B>, 1-based as people count, refuses 0 on construction and decode,to_index/from_index;Seq<Of, T>, a wrapping sequence number (next,wrapping_add,distanceas aWrappingamount, RFC 1982is_before/serial_cmpwithin half the range, noOrd, noSeq + Seq). Alltransparent, the bytes ofT; in the model instances ofIndex/IndexFrom1[Of, T, B]andSeq[Of, T], shown and checked by clients (CLT-54). Indices and sequence numbers, conformancestdlib.si.index.*,stdlib.si.index_from_1.*,stdlib.si.seq.*(derive, dynamic, Python; C waits on API-26). - β
STD-64 Points and origins Β·
v0.5βexact_units::Point<O, Fixed<..>>: a position against an origin, a zero-size marker declared withorigin!carrying its unit, an exactOffset(sign andMagnitude), its reference and an optional modulus. Point Β± vector = point, point β point = vector, point + point and point Β· x don't compile.origins:AbsoluteZero,Celsius(273.15),Rankine,Fahrenheit(45967/180 K, steps of 5/9 K),Vacuum,Gauge(reference: ambient pressure, converts only withagainstat run time),AboveStandardAtmosphere,Angle<Turn | Degree | Radian>(wrap every turn, the difference signed and shortest). Register formulas are originsaboveanother (LSM6DSraw/256 + 25 Β°C, J1939 β40 Β°C).to::<O2, P2, T2>()between origins of the same unit and reference, its factor and offset at compile time, exact orNone. On the wire a point is its number;OriginDefholds an origin as data for INT-13. Points and origins, conformancestdlib.si.point.*,stdlib.si.offset.*(derive, dynamic, Python; C waits on API-26). - β
STD-62 One exact magnitude for every scale Β·
v0.5βexact_units::Magnitude, a canonical product of prime powers (2 and 5 as powers of 10 and 2) with powers of Ο, ln 2 and ln 10, whole or half exponents: every scale (prefixes,Pow10<E>,Pow2<E>,Rational<N, D>withu64parameters factored at compile time,Kibi..,FullScale<V, BITS>,FullScaleMax<V, BITS>) is one, equal however it is written (Milli=Pow10<-3>=Rational<1, 1000>); closed under*,/, powers and roots, a product that needs more than 12 primes or leaves Β±63Β½ is a compile error.Fixed::tocomputes its factor at compile time: not a fraction of integers, or a multiply that doesn't fit 64 bits (storages up to 32 bits) / 128 bits, doesn't compile, never a rounding. Q formatsQ15,Q31,UQ16_16, binary anglesBam16,Bam32. ReplacesExactScaleinScale::Exact(104 bytes in memory, a decimal prefix 2 bytes on the wire). Exact magnitudes, conformancestdlib.si.magnitude.*(derive, dynamic, Python; C waits on API-26). - β
STD-63 Named rounding and overflow policies Β·
v0.5βto()stays exact orNone; lossy conversions name their rounding (Rounding: truncate, floor, ceil, nearest ties-to-even, nearest ties-away) and overflow policy (Overflow: checked, saturate, wrap):Fixed::to_rounded,to_saturating,to_wrapping,requantize, exact (the true quotient rounded once), Ο/ln factors through a 64-bit fixed point (Magnitude::fixed_point). Arithmetic's policy is in the bounds marker:Wrapping(Bam16,Bam32),Saturating(Q15,Q31,UQ16_16),Unboundedchecked; same bytes,Fixed::of/with_overflow. Bounds prove conversions infallible:into_rounded,Fromof aRange-bounded value for a whole factor, a compile error where the proof fails. Run-timerequantize/Requantizerfor dynamic clients. Rounding and overflow,exact_units/tests/rounding.rs, conformancerequantize.*(derive, dynamic, Pythonww.requantize, Csw_requantize()).Seqwrapping comes with STD-58. - β
STD-60 Angle, information and counted things as dimensions Β·
v0.5βDim<'i>has rad and bit as base dimensions and up to four counted bases known by crate and marker name (Item,Sample,Frame,Packet,Cycle,Block,Sector,Chunk,Char,Code, featurecounting, an API's own withww_si::counted!;Bit,Nibble,Byte,Halfword,Word,Doubleword, featureinformation) and a root flag for half exponents (V/βHz,Dim::sqrt): bytes don't convert into % or rad,AngularVelocityisn'tHertz,Per<Sample, Second> Γ Secondis aSample, bytes of a sample only throughPer<Byte, Sample>; checked at compile time (Unit::DIM) and at run time (SIExpr,wire_weaver_client::DynUnit, PythonQuantity.dim).Torqueis J/rad (written NΒ·m). Wire change EVO-16. ww_si dimensions,ww_stdlib/ww_si/tests/quantities.rs. - β
STD-83 Typed products and quotients of quantities Β·
v0.5β inexact_units(locked at 93a1607):a * bis aFixed<Times<U1, U2>, Product<P1, P2>, T>,a / baFixed<Per<U1, U2>, Quotient<P1, P2>, T>, one multiply or division ofT;prefix::Product/prefix::Quotientcarry the exact result scale as a type (Product<Milli, Milli>isMicro), overflow by the operands' policy,checked_/saturating_/wrapping_timesand_per,times_into/per_intorounded once into another scale and storage;Fixedof one unit, scale and storage compare across bounds. In the model asMarkerDef::Product/Quotient(appended, older readers getUnknown), a client computes the scale as rustc does; the evolution check compares them by meaning (test intests/quantities). - β
STD-24
ww_firmware_info/ww_board_info/ww_uid/ww_user_infodevice contract Β·v0.5β the info crates every device implements.FirmwareInfoconst itemsFIRMWARE(name, version, git, build time, profile),BUILD_ID(GNU build id, finds the ELF) and optionalBUILD_RECORD;build::emit()+firmware_summary!()forbuild.rs,build_id()reader;LastReset(ResetRecordwith aFaultper architecture behindcortex-m/riscv, stack words,boot_count,clear).BoardInfo::BOARDwithRevisionSource(Record,Strap,Assumed,Provisional) and optionalREWORKS(Rework { id, title, applied }, up to 16;ww infoprints them).Uid::IDS, first id is the primary one (USB serial).UserInfowith boundedname/ optionalnotes,UserInfoError,CAPABILITIESand the persistence rule. Docsdocs/std_library/ww_{firmware_info,board_info,uid,user_info}.md, teststests/{firmware_info,board_info,uid,user_info}. The const items are served byget_handlers until constant items come from the introspection data; properties cannot carry#[errors]yet. - π§ͺ STD-26
ww_logdefmt over reliable streams Β·v0.5βww_log_bare_metalrenamedww_log.LogDefmt { const DEFMT (encoding, build id); frames() }: a reliable stream whose unit is onedefmtframe, whole frames per item (up to 512 B), solostis the exact number of dropped messages and logs from before the host connects arrive by resuming the device's session.LogText { records(); rw level: LevelFilter }with boundedLogRecord,SeveritywithoutUser,LogErroronCommonError. Device side: new crateww_defmt_logger(global_logger!, lock-free ring with whole-frame commits, RTT tee that never blocks (whole frames or skipped and counted, whatever mode a probe set),pump()into the store, drops reported in place), ring stress-tested on the host with concurrent producers.tests/log_defmtdecodes withdefmt-decoderend to end;examples/defmt_log(host) andexamples_mcu/rp2'srp2_usb_log(USB and RTT at once, build id in flash).docs/std_library/ww_log.md. Tested 16 Oct 2026 on Donguru B153A (STM32G0B1, typec_dongle API-10, with aPersistentRamRing): logs over USB and RTT at once,lostexact under a flood of 1000 (227 + 773), logs from before connecting and from before a forced HardFault; the non-blocking RTT tee with probe-rs attached (it sets the channel to blocking): floods of 1000 go on at ~62 us a line instead of ~620 us, RTT shows what fits. Known gap: frames dropped at the end of a burst are reported (lost) only with the next frame the device logs, so a quiet device shows the loss late.rp2_usb_lognot run (no Pico 2 probe on the mesh).ww login the CLI is a later item. - β
STD-25
ww_countersoncntwords Β·v0.5β the device'scntcounters as raw buffer words, decoded on the host with the firmware's ELF found by its build id:Counters::INFO(format, used/capacity words of RAM and backup, build id),read(buffer, offset)pages ofUVlq32,reset, optionalwatchstream (not served by anything yet);read_words/reset_wordsservecntbuffers, featurehosthasWwSource(acnt_core::Source).ww counters read|watch|tui|reset|infoshows thecnt_viewviews (CLT-10). In thecntrepo:cnt::ram_used_words(), word sources and ELF lookup by build id incnt_core,cnt_view, and the probe path checks the build id too. Docsdocs/std_library/ww_counters.md, teststests/counters;cnt_core/cnt_view0.5 from crates.io. - β
STD-27
reset-record-ageΒ·v0.5βww_firmware_info::ResetRecord::boots_since_crash: boots since the crash data was saved (0 = the last reset was the crash), so a record kept across later normal resets shows how old it is.bedrock_crashcounts it (Crash::boots_since) and the firmware template fills it. - β
STD-28
ww_indicationlightweight rework β cut to one required item:mode: Mode(AllOff/OnlyEssential/Normal).brightness: Brightness(5 levels, one setting for all indicators),identify(duration)(blink everything to find the unit on a bench, errors onCommonError) andNAMES(indicator names, API-36) are each their own#[optional]item. Droppedalert_brightness/AlertBrightness,animation_enable,test_mode/TestModeandHAS_NOT_CONTROLLABLE. Docsdocs/std_library/ww_indication.md, teststests/indication(mock servers with onlymode, and with every optional item). - π§ͺ STD-50
ww_indicationanimations in phase β a device with a shared clock (STD-49 or a sync line) aligns its periodic animations to period boundaries of shared time (UTC, WSPR-style:t mod P), so devices showing the same animation are in phase with no epoch passed around, across PCs to NTP precision. A rule in the trait's docs, no API item (an explicit epoch was dropped: every server would have had to implement or omit it).docs/std_library/ww_indication.md#animations-in-phase-std-50; device side embedded_bedrockbedrock_ledwithbedrock_sync. Hardware check open: Donguru and rePort breathing in phase. - β
STD-29
ww_can_buson the stdlib idioms β from simple bridges to bus debuggers.Canis one controller: six required items (capabilities(),configure(CanConfig)answering what was set,config(), the observablestatus, the countedrxstream, thetxcredit sink) and#[optional]set_mode(incl.ConfigandSleep),recover,tx_abort,tx_events(marker, measured queue time),bus_errors,protocol_status,bus_load,detect_timing,set_filters,set_layout,rx_buffer,tx_buffer(once or cyclic), transceiver (standby / silent / sleep, wiring faults), terminator,test_pins.CanGroup(interface[]: Can, enable / disable, optional clock source and frequency, performance) for controllers sharing clock, power and message RAM;CanTimeTriggered(TTCAN);CanXlbehind thexlfeature. Errors onCommonError+OutOfFilters,OutOfMemory,BusOff;defmtno longer pullsstd. Docsdocs/std_library/ww_can_bus.md, teststests/io_can(full and minimal node),tests/io_can_group(group, TTCAN, XL). - β
STD-42
ww_tapeΒ·v0.5β tape blocks, the generic unit of track records for live feeds and log files (ww_tape,docs/transport/tap.md#tape-blocks): a 32 B header, varint records (track, time delta, length, item), CRC-32C, never padded; describe blocks with the producer,TrackDefs (stable key, API, resource path, sticky) and API bundles;TapeWriterwrites them with database-style durability (.partialthen rename, file and directory synced, aSyncPolicy, a failed write or sync fatal, never retried),FileBlocksreads the longest valid prefix (or skips damage,Recovery::Resync); tests cut and flip every byte, fuzz targettape-reader. Blocks of a later version or an unknown kind are skipped by length,Describeis evolvable. Open: splitting into several files by size or time, preallocation (zeros at the end already read as the end). Nothing in it knows a producer: the link tap (TR-14) writes it; it is also eventfull's own file format (CORE-19). - π§ͺ STD-32
ww_settingssettings store Β·v0.5β a device's settings in its own NOR flash:#[derive(Settings)]on a plain struct (#[setting(slot = N, default = ..)],#[settings(retired(..))]), stored as a record of the fields not at their default, each a byte blob in anOptionAnyVec, so firmware versions skip and keep each other's slots;Settings<G, N>with write-on-change, a debounce and a durablepersist, factory reset per group (Key); served throughvalue_on_changedproperties. Its ownStoreover anembedded-storage-asyncNorFlash(per-key seq in every item, page headers with crc, current records copied off a page before it is erased, loads never write,maintain()to erase ahead): power-cut safe at every word written and byte erased in any erase order, whichsequential-storage8 isn't. Docsdocs/api/settings.md(pages, erase stalls on G0 / F4 / H7, big-sector options) anddocs/evolution/rules.md#settings-stored-on-a-device, testsww_settings/tests(just test-power-cut). Hardware check open: real power cuts on a G0B1 (TEST-21), dump decoder (TOOL-12), demo (EX). - π§ͺ STD-35 Slave data paths in
ww_i2candww_spiΒ·v0.5.xβ an#[optional(slave)]group inI2cand inSpi: opening the countedslave_events/slave_framesstream with a config starts slave mode (closing stops it) and reports every transfer (bytes both ways, register pointer or address, fill bytes, stretch time); the answers come from a register map the host fills and reads back (slave_map_write/slave_map_read, applied between two transfers) or from theslave_answerscredit sink, one answer per read or frame. A host that is too slow: I2CI2cSlaveUnderrun(Fill,Stretch { max }withReadWaitingsent at once,Nackwhile empty), SPI the idle byte or the last answer again, flaggedunderrun. Limits of clock stretching (SMBus 25 ms, masters without it) in the crate docs anddocs/std_library/ww_i2c.md#slave-mode.I2cMode::Slave,SpiRole::Slaveand theslavecapability flags are gone. API, snapshots, docs, the device-side engine (SRV-26,ww_bus_slave) and an end-to-end test of both slaves done:tests/io_i2c/tests/io_spidriveI2cSlave/SpiSlavebyte by byte as a simulated master (test-onlyslave_bus/slave_pumpmethods, a fake clock) through the real host and device event loops: register maps with page wrap and host updates between transactions, the answer queues, stretching to the deadline and past it,Nack,repeat_last, cleared answers and dropped events. Open: a check on real silicon, with the first user, the rePort (usb_io_fw IF-5, v1.0 launch, Roman 16 Oct 2026). - π§ͺ STD-52
ww_i2cmaster errors and limits Β·v0.5β what a good I2C master knows reaches the host (found by the rePort I2C driver, P2424 i2c-master-driver):I2cError::Nack { op, at }(which operation, and the address or which data byte, counted asI2cRecord::nack_at),ClockStretchwithI2cMode::Master'sstretch_timeout(set and answered byi2c_configure, range in the capabilities),BusStuck { scl_low, sda_low }(no silent recovery:i2c_wiggle_clockon request, at the bus speed up to 100 kHz), and the limits inI2cCapabilities(speeds,max_ops,max_write_read_len,zero_length_write,stretch_timeoutrange; grouped by topic, sizes allu16). Transactions are stated as a repeated start before every operation (LinuxI2C_RDWR), not embedded-hal merging;i2c_configureanswers the highest SCL not above the request. Breaking on the wire and in Rust (nothing released or deployed, user 27 Oct 2026): callers in usb_io_fw and typec_dongle adapt. Docsdocs/std_library/ww_i2c.md#transactions, tests inww_i2c(round trips) andtests/io_i2c. Open: the check on real silicon with the rePort driver (usb_io_fw IF-2, RP2350 DW_apb_i2c). - π§ͺ STD-76
ww_i2c_hlembedded-hal I2C overww_i2cΒ·v0.5β a stock embedded-hal 1.0 I2C driver, blocking or async, runs unchanged on the host against a remoteww_i2c::I2c(for P2534 fusb302-sink, a PD sink driven from the PC):I2cMastermerges contiguous same-direction operations (a repeated start before everyww_i2coperation, STD-52), so one embedded-hal transaction is one call and one round trip; refuses, never splits, what the device'sI2cCapabilitiesdon't take;Nack/Arbitration/Bus/Overrunmapped toErrorKind; request priority as a handle setting (urgent(), CLT-50). Docsdocs/std_library/ww_i2c_hl.md, teststests/io_i2c(tmp1x2async,eeprom24xblocking, unchanged). Open: the check on real silicon with the FUSB302 over the rePort's I2C (P2534). - π§ STD-6
ww_gpio_hlβ usable, some APIs are still missing. Blocking, async and promise flavors; the blocking pins implementembedded_hal::digital(OutputPin,StatefulOutputPin,InputPin), the async onesembedded_hal_async::digital::Waitwithout a race (stream opened, then level read, then edge awaited) and keep the edge's device time (last_edge_time), tested intests/io_gpio(docs/std_library/ww_gpio.md#embedded-hal-ww_gpio_hl). - π STD-7 Interrupt left enabled in
ww_gpio_hlΒ·v0.5β the falling edge helper documents that it leaves interrupts enabled; decide whether that is the intended fix or a bug. - π§ STD-8 Prototypes, to be completed later:
ww_i2c,ww_uart,ww_memory(wasww_ll: one word type per trait, optional watches, errors onCommonError, docs page), Known gaps areTODOs in their sources (e.g. SMBus and 7-bit addresses inww_i2c;ww_can_busis done in STD-29).ww_log(wasww_log_bare_metal) left for STD-26. - π‘ STD-9 Stubs, for later:
ww_spi(ww_board_info,ww_firmware_info,ww_user_infoandww_uidmoved to STD-24). - π§ STD-12
ww_dfufirmware update API β traitsUpdateNode,BootControl,Inventory,Stage,StageBulk: each node (app, bootloader, recovery app) implements what it needs; A/B live, in place from a stored plan, in place live; step by step or bulk. Carries the octet records of the chunky protocol spec (P2625). End to end testtests/dfu. Guide with the eight update scenarios (2C-APP-STEPSwap Shuttle ..1L-UPD-BULKPit-stop Freight), each tested end to end over USB (simulated), WebSocket or UDP with reboots and power cuts, its exchange recorded as a golden transcript the docs include (docs/std_library/ww_dfu.md,tests/dfu_scenarios,just dfu-transcripts).StageBulk::planis a credit sink (API-28 credit-sinks). Every variable-size field is bounded (SW-19:MAX_PIECE_LEN,MAX_HEADER_LEN,MAX_CHUNKS_PAGE,MAX_PRODUCT_LEN), so every method has a known largest request and reply.Profile::AB(SlotSwitch::{Copy, Remap})/InPlace. Next: a chunky implementation (chunky NET-1). - π§ STD-13 IO trait family Β·
v0.5βww_gpio,ww_uart(optionalUart::monitor),ww_i2c(+I2cWatch,I2cMonitor, 10-bit addresses),ww_spi(optionalSpi::monitor),ww_adc(STD-18),ww_pwm(STD-19),ww_onewire(STD-20),ww_timebase(STD-17),ww_logic_analyzer/ww_signal_generator(STD-21, STD-22);ww_sivalues and timestamps (STD-23): frequenciesHertz, baud ratesBaud. Family overview indocs/std_library/io_family.md; every crate filled in with full rustdoc, a mock-server test pairtests/io_<x>and a docs pagedocs/std_library/ww_<x>.md;examples_mcuGPIO and UART boards follow. A bus or bank is configured with oneconfigure(XConfig)answering what was actually set, the IO level inside it (io_level: Option<Volt>, capabilitiesio_levels/io_level_min/io_level_max). Every crate's error enum starts with#[common] Common(CommonError)(STD-16) and keeps only what is specific (Nack,NoPresence,NoSuchChannel, ...), methods list their variants with#[errors(..)](API-32). Streams with a configuration are counted stream functions (API-31:AdcStream::samples,LogicStream::data,Pin::event,PwmCaptureChannel::capture,Spi::monitor,I2cMonitor::records,Uart::monitor),Uart::rxandI2cWatch::changesare counted: positions, device losses and the time of the first unit in the event head; UART chunks end on line idle (UartConfig::idle_chars, one back-to-back burst each) and carry the time of their last byte when the device measures it (RxChunk::end). Clients take one?per call (CLT-24CallError),Unsizedenums read newer variants asUnknown(u32)(SW-47). Review round 1 (15 Oct 2026) done. Open:*_hlcrates are STD-14. - π§ STD-21
ww_logic_analyzerΒ·v0.5.xβLogicAnalyzer(triggered capture into RAM),LogicStream;Rawand per-channelTransitionsencodings (edges as aSeries<u32>), staged pattern triggers and protocol stages (UART byte, I2C address, SPI first byte). Skeleton written, docs pagedocs/std_library/ww_logic_analyzer.md; mock-server test pair still open (STD-13). - π§ STD-22
ww_signal_generatorΒ·v0.5.xβPatternGenerator(digital, replays analyzer captures through a credit sink),WaveformGenerator(DAC channels). Skeleton written, docs pagedocs/std_library/ww_signal_generator.md; mock-server test pair still open (STD-13). - β STD-31 Observable
ww_gpiopin level Β·v0.5.xβproperty!(ro+observe level: Level)onPinat one of its reserved indices 4..=6, so a host sees input changes without opening theeventstream of every pin (slow inputs, a dashboard). Blocked on API-29 array-index-bounds:Pinsits inBank::pin[](and banks usually in an array too), and levels under a resource array can't be observed until their bound is known (SRV-18). - π§ STD-34
ww_recorderΒ·v0.5.xβ control a recorder over WireWeaver:Recorder(status, card facts, two-step prepare/format with a token, recordings, markers,sync, settings with the flush thresholds, counters, optionalrefresh_usb) andTrack[](info, enable, durability, compression, counters). For the SD card log store of P2628 (repologring), usable by any recorder. API crate, snapshot and docs pagedocs/std_library/ww_recorder.mddone, the scaffold compiles; the first server is the logring's (rePort integration). - π§ STD-39
ww_blockΒ·v0.5.xβ a block device over WireWeaver (SD card, eMMC, flash):BlockDevicewithgeometry(block size and count, blocks per call, erase unit, erased value), freereadand device-sidecrc(CRC-32,ww_block::crc32),claim/releaseof a write lease that stops the card's own user,writewith a CRC anderaseunder it, and the optionalbulkgroup: thewrite_runscredit sink acknowledged on the countedwrittenstream, and the countedread_runsstream. Who may claim is the firmware's choice per link (Common(Refused)). Tier T1 of P2628 (logring's PCLinkMedium, S2 prepare on the PC). API crate, snapshot, docs pagedocs/std_library/ww_block.mdand the round triptests/block_sddone, the scaffold compiles; the first server is the logring's.crc32/crc32_updateare table-driven, table size (none,crc-table-256,crc-table-slice16) picked by cargo feature. - π§ STD-40
ww_sdΒ·v0.5.xβ raw SD card access over WireWeaver:SdCardwithcapabilities,state, thedetectstream, rawregisters(CID, CSD, OCR, RCA, SCR, SD Status),reinitwith a per-stepInitReport, andcommand: one raw command with an optional data phase, answering response bytes,CardStatusbits, theOutcomeand timing. Commands areFree, need theww_blocklease, or areRestricted(CMD42, CMD27) unless the firmware builds withraw-sd-unrestricted(ww_sd::check_access). Tier T0 of P2628. API crate, snapshot, docs pagedocs/std_library/ww_sd.mdand the round triptests/block_sddone, the scaffold compiles; a device needs a raw-command hook in its SD driver (not checked yet for embassy-stm32'sSdmmc). The docs page says how a device in SPI mode fills the answers ("In SPI mode", from rePort'ssd_protoengine); rePort's simulator servesSdCardon a simulated card in SPI mode (usb_io_fw IF-8). - π‘ STD-33
ww_settingssingle-sector mode β a store in one sector with a RAM copy, for a part with exactly one sector to spare; a power cut during its erase loses every group (the window is the whole erase, 1 to 4 s on a 128 KiB sector, seedocs/api/settings.md#parts-with-big-sectors). Only if a device needs it. - π‘ STD-10 Device state machine trait like CANopen.
- π‘ STD-11 Register description registry β WireWeaver for describing device registers.
- π‘ STD-41 Frame splitters for byte streams β a no_std, sans-IO splitter (bytes in, frames out with their
time and status: CRC good or bad, truncated, overrun), the same code on the device and the host. Kernels: idle
gap (today's
RxChunk), delimiter (\n,\r\n, any byte), fixed length, length field, sync byte + length + checksum, COBS, SLIP, Modbus RTU, NMEA; picked by the host when it opens the stream (Uart::frames(cfg)), no new firmware. Frames of a bounded length are what a batched stream can take, where a raw byte stream can't (no size bound); with API-51, each frame XOR'd with the last one of the same key (Modbus address, NMEA sentence, CAN ID). - π‘ STD-44 SPI slave data-ready line β a GPIO the slave asserts while an answer is queued (or the register map
changed), as most SPI slave protocols with a computed answer have (ESP32 handshake, nRF IRQ); the master reads only
when it is set, so the answer queue needs no polling. Part of
SpiSlaveConfig, the pin named by the board. - π‘ STD-45 Live registers in a slave's register map β address ranges of the I2C or SPI register map whose reads the host answers (I2C stretching meanwhile) or that change on read (clear-on-read status, FIFO data registers), so a chip with a few such registers can be emulated without moving it all to the answer queue.
- π‘ STD-46 More I2C slave addressing β a second address or an address mask (one emulator answering for several chips, a 24C16's block bits), general call, SMBus PEC and SMBALERT#/ARA. RP2350's controller has one address only, so this is per-device capability.
- π‘ STD-51 Automatic sync master election β boards that share a two-way link (CAN, Ethernet, USB through one
host) choose the master of their sync line themselves, PTP-BMCA style: the best source wins (a GPS or a
host-steered board with a good error bound), the lowest serial breaks a tie. Not over IRIG-B or PPS: they are
one-way, so today the master is set explicitly with
configure_sync(STD-49,docs/std_library/ww_timebase.md#two-pcs-two-boards-one-line). - π§ STD-47
ww_pin_diagpin tests Β·v0.5.xβPinDiag { const POINTS; set(point, PointState); read(point); release_all() }: raw access to a board's digital points (MCU pins, shift register and expander outputs) for bring-up, verification and production tests; the firmware keeps its board's rules and answersNotCapable,Common(InUse)orRefused { rule }. Done: crate, docs pagedocs/std_library/ww_pin_diag.md, test pairtests/pin_diag(mock board: short, external pull-up, input-only pin, interlocked outputs, held pin), snapshot; the host tests and plan files are TOOL-15 (ww pins test). Missing: a device implementing it (rePort). - π§ STD-48
ww_internedstrings as ids Β·v0.5.xβInterned { Id(UNib32), Text(&str) }for names and reasons; ids resolved on the host from the firmware's ELF by build id. Done: the type,Display(#17for an unresolved id),ww pinsfinds the ELF by build id (ww_pin_probe::device::Names). Missing: an accessor fordefmt::Str's index (upstream,Table::get_str), reading the ids from the ELF's table with it. - β
STD-59 Runtime
ww_siimprovements Β·v0.5βQuantityonexact_units' units, one unit system.Prefixwithout a table (Roman, 28 Oct 2026): named SI and binary prefixes (Kibi..Tebi), the familiesPow10(i8),Pow2(i8)andFullScale(bits)(32768Β·1/65535, a Q format,Pow10<-4>), andUnknown(u16)for scale kinds of a newer version (DER-25): 4 B in RAM on host, M0, M4F and M33,Volt12 B as before; named prefixes keep their one nibble on the wire, a family takes one byte more. Other scales (a BAM's Ο/2ΒΉβ΅) go toAnyQuantity, which holds anyFixedexactly.Quantity<U, B = Unbounded>with bounds checked on construction and decode,Bexplicit in the model;Quantity::try_from_fixedand Fixed/Quantity parity proptests; conformance cases (derive, dynamic, Python; C gap API-26); docs (ww_si). The client-side bounds check is CLT-54. - β
STD-84
Minute,Hour,Day,Weekand the Unix epoch βexact_units8584aad:Secs<T>,Minutes<T>,Hours<T>,Days<T>(Fixed<Second|Minute|Hour|Day, One, T>) andorigins::UnixEpochwithUnixSecs<T>/UnixMillis<T>/UnixMicros<T>/UnixNanos<T>(Tdefaults tou64), aPoint<UnixEpoch, Secs<T>|..>for POSIX time (time_t, journald's__REALTIME_TIMESTAMP).ww_sire-exportsSecs,Minutes,Hours,Daysnext toMicros,Millis,Nanos;units::{Minute, Hour, Day, Week}andorigins::{UnixEpoch, UnixSecs, ..}reachww_si::..through its existingunits/originsre-exports (featuretime). Docs:exact_units, points. - β
STD-85
Count,Percent/Permille, binary byte sizes,Hz, and countedLine/Row/Record/Entryβexact_unitsd28ec86:Count<Of, T>(Fixed<Of, One, T>, a count ofOfs, unconditional),Percent<T>andPermille<T>(Fixed<Ratio, Centi|Milli, T>, featuredimensionless),Kibibytes<T>/Mebibytes<T>/Gibibytes<T>besideBytes<T>(featureinformation) andHz<T>(Fixed<Hertz, One, T>, featuretime): aliases cat_stack's APIs had declared for themselves, now one set for every crate.units::{Line, Row, Record, Entry}(featurecounting), counted bases besideItem/Sample/Frame/Packet.ww_sire-exports all of them; they reachww_si::..through its existingunitsre-export. Docs:exact_units,ww_si, indices. - π STD-53
Voltwith au16value fails to decode on a device βI2cConfig { io_level: Some(Volt::milli(3300u16)), .. }from the host getsArgsDesFailedon the rePort B200A firmware, the same level as au32value works (report-i2c-master bench run, 27 Oct 2026). Find where theww_sivalue storage / numeric type differs between the host's writer and the no_std reader, fix it there, and add the case to the conformance corpus. - π STD-54
ww countersagainst the rePort βww countersfails against the B200A: the firmware sends no build id, and itscntformat is v0 while the tool reads v2 (report-i2c-master bench run, 27 Oct 2026). Either side may be behind (STD-25): bring the firmware'scntand build id up to what the tool needs, or make the tool say which format it got and what is missing instead of failing. - π STD-56 Plain fields with a unit in their name become
Fixedβshared_us: u64,rate_ppb: i32,ring_capacity_bytes: u64,queue_len: u32and about 80 more acrossww_stdlib,ww_link,ww_stream,ww_client_serverand the host crates: same bytes, a Rust-API break per crate. On hold until STD-55 is built. - π STD-65 Instants and clocks with named epochs Β·
v0.5βInstant<Clock, ..>: device-monotonic, Unix, TAI, GPS (TAI β 19 s), UTC through a versioned leap-second table on hosts; GPS week rollover as aSeqmodulo 1024. - π STD-66 Sentinels next to bounds Β·
v0.5β named codes in aFixed's raw value: single codes or code ranges (J1939 not-available / error ranges per 1, 2 and 4 bytes, SunSpec not-implemented 0x8000, Matter null, HCI RSSI 127, reserved ranges) decode to an enum (Value,NotAvailable,Error,NotImplemented, a user meaning),Optiononly as the one-code shorthand. Stored as the rawT(size ofT, no extra byte),get()gives the enum, and encoding it gives back the same code, so the bytes don't change. Niche codes andBoundsare checked disjoint and every code to fitTat compile time. Clipped / rail codes are a value with a saturation mark (at least / at most), not an absent value. Published in the model and honoured by the dynamic client, Python and C, with conformance cases. - π§ STD-67 Levels: dB, dBm, dBV, dBFS, dBc, Np, LUFS, RSSI Β·
v0.5β first piece done: a gain's dB by its kind (STD-68),Fixed::to_db/from_dbwith 20Β·lg for ratios of root-power quantities (V/V, A/A, Pa/Pa) and 10Β·lg for powers (W/W, J/J), a plainRatiorefused at compile time;to_db/from_dbinf64andf32, withoutstdthrough thelibmfeature;db!()works the stored value out exactly at compile time. Missing: the rest below.Level<Kind, Ref>on the point algebra: level β level = gain, level + gain = level, references named (1 mW, 1 V, 20 Β΅Pa, full scale with its sine or square convention). The log base comes from the level's unit (Np coherent on a log-ratio base, B, dB, octave, decade, shannon, hartley, nat), the form is power, root-power or plain with a sign (pH and absorbance are βlg), and the reference may be absent (a pure gain), a quantity or dimension one; gains compose (dB/km, Np/s β Hz). - π§ STD-68 Kinds that share a dimension Β·
v0.5β first piece done (gain kinds):exact_units::KindasUnit::KINDnext toDIM, checked byFixed::convertat compile time (rules after ISO 80000-1 / mp-units: a quotient of one dimension keeps what cancelled, V/V β A/A β W/W βRatio; a dimension-one factor keeps the other's kind; any other product plain, V/V Β· V = V);OhmresistanceandSiemensconductance(kind ".."on aunit!line), apart from V/A and A/V; named stepsas_ratio/as_gain,as_resistance/as_transimpedance,as_conductance/as_transconductance; in the model asMarkerDef::KindUnitandww_self::units::CanonicalUnit::kind, compared by the evolution check (EVO-17), conformance casemodel.marker_def.unit_kind. Missing: the rest below. relative humidity, duty cycle, state of charge, strain, volume fraction; Hz vs Bq vs rad/s; electrical vs mechanical angle through pole pairs; colour vs thermodynamic temperature; explicit casts between siblings. W / VA / var and their energies, Gy / Sv; a kind survives a product with plain units (varΒ·h stays reactive energy, never kWΒ·h). Kinds with arguments: gas volume and density at stated reference conditions (StandardVolume<Ref>: DIN 1343, ISO 13443, US SCF), with an opt-in exact ideal-gas bridge to mol. - π STD-69 More counted units Β·
v0.5β Symbol (baud = Symbol/s), Packet, Pulse, Event, Tick, Chunk, Particle, each a dimension of its own;AVOGADRObridgesParticleand mol exactly. - π STD-77 Runtime scale parts and reference values Β·
v0.5β what device standards do at run time, without a refactor later: a value whose exponent (decimal or binary) is a runtime field next to compile-time unit and scale (BLE GATT exponent in the format descriptor, USB HID unit exponent, a SunSpec*_SFscale-factor register), and a value in % or per-mille of another resource's reading ("% of a reference register"); the model names the field or resource the scale comes from, the dynamic client, Python and C apply it exactly. Exponents of 10 or 2 only: a free runtime factor (calibration, currency) is a runtime value, not a unit:ww_si::Quantity/AnyQuantityor the application (TEST-28's list). - π STD-70 Text unit codes in and out β UCUM, UNECE Rec 20, QUDT, SenML / RFC 8798, OpenMetrics: exact
imports, exports that say where they lose something. Later, in a separate optional crate
exact_units_interop(Roman, 27 Oct): this release builds the capability to represent them (STD-77, TEST-28), not the tables. - π STD-71 Device-standard units in and out β BLE GATT 0x2904 / GSS, USB HID, BACnet, OPC UA EUInformation, CAN
DBC / J1939, IEEE 1451 TEDS, SunSpec, IODD, with exact decimal factors and sentinel tables, from public sources
only. Later, in
exact_units_interoplike STD-70. - π STD-73 One table of unit symbols and spellings Β·
v0.5β display and parsing of every unit from one table, shared by Rust, si_dynamic,wwand Python, UCUM spelling where there is one; symbols are case-sensitive and a bare ambiguous one is refused (pH: picohenry or pH,B: bel or byte). - π STD-74 The runtime side catches up Β·
v0.5βQuantity,AnyQuantityandSIExprget the exact magnitude, the Β°C point / interval split, levels and sentinels, soww_sinever lags behindexact_units; theFixedβQuantityparity test (STD-59) covers kinds, points, levels and sentinels too: same result or same refusal. - π STD-75 Non-linear scales by name Β·
v0.5β AWG, mpg β L/100 km, Beaufort, Richter, shoe sizes: a named exact conversion function each, or out of scope with the reason written down. - π STD-78
serdefor units, three forms per field Β·v0.5β anexact_unitsfeatureserde(Roman, 28 Oct 2026), the form chosen per field with#[serde(with = ..)]adapters: a plain number (the unit stated in the schema), a human string"5 ms"(STD-73's table, exact) or a self-describing{ value, unit }. Deserializing accepts any compatible unit and converts exactly; it refuses a wrong dimension, an inexact conversion, a value out of bounds and an ambiguous name; sentinels map to their meaning. Tests per form, and a docs page "Storing units": a fixed unit per column is aFixedwith the unit in the schema and integers at the fixed scale (SQL aggregates stay right); a unit that varies per row is anAnyQuantityor a unit column;Quantityis a wire tool, normalized before storing; text headers likevbus [mV]. For configs, web APIs and queues, telemetry, logs and metrics, data files, CAD / EDA (nm in better_erc) and Python bindings, not firmware only. - π STD-79 JSON Schema with unit and range β a
schemarsfeature: each unit field's schema names its unit and bounds. Later, an optional feature like STD-78. - π STD-80 Unit values on the command line β a
clapvalue parser (--timeout 5ms) on STD-73's table, exact, refusing what STD-78 refuses. Later, optional feature. - π STD-81 Database columns with units β
sqlxanddieselcolumn types forFixed(integer at the fixed scale) andAnyQuantity, the unit in the schema or a unit column as "Storing units" (STD-78) says. Later, optional features. - π STD-82 Unit metadata in Arrow and Polars β a column's unit and scale as Arrow field metadata, read back exactly, Polars through it. Later, optional feature.
Examples (EX)¶
- β
EX-1 Host examples:
blinky(+ evolved API),all_gpio,uart,blinky_py(hand-written pyo3 wrapper). - β
EX-2 Serialization only:
minimal_shrink_wrap,compare_wire_formats. - β EX-3 MCU examples: nucleo_g0b1re, nucleo_h743zi2, usb_stm32h725ig, mcu_qemu, rp2 (USB, RTT, WebSocket and UDP over NCM).
- β
EX-4 NCM UDP example:
examples_mcu/rp2ww_udp_ncm, host sideblinky_udp. - β
EX-5 Project layout of paired
<name>_api/<name>crates (docs/api/folder_structure.md). - β
EX-11 Multi-property observe demo:
motor_demo(+motor_demo_api), a VESC-like FOC controller onmotor_simwith 52+observeproperties in three trait levels by rate tier (25 kHz, 1 kHz, 50 Hz), their valuesww_siquantities (I32at a fixed prefix,U32totals,F32settings), on simulated time faster or slower than real time, in-process and over UDP; themotor_demoCLI observes chosen sets at chosen rates and shows achieved rates, updates and bytes per second against the budget, missed sends and the cost as oneStreamDataper value (docs/examples/motor_demo.md). The code moved to the motor_sim repo (its DEMO-1) on 18 Oct 2026, so the demo can grow GUI dependencies there;tests/observecovers observe here. - β
EX-14 cnt counters in MCU examples:
cntdependency,-Tcnt.xandrunner = "cnt run --chip ..."on every probe-rs board (rp2, nucleo_g0b1re, nucleo_h743zi2, usb_stm32h725ig);unhandled_exceptionscounter inDefaultHandler.mcu_qemuhas no probe, so no counters. - β
EX-15
ww_settingsdemo:examples/settings_api(SettingsDemo, twovalue_on_changedproperties) +examples/settings(host, a simulated flash over the real client/server codepath,tests/reboot.rschecks values survive a reboot) + asettingsbin inexamples_mcu/nucleo_g0b1re(RTT, last 4 pages of bank 2) andexamples_mcu/nucleo_h743zi2(USB, last 2 sectors of bank 2), both behind asettingscargo feature.UserSettingslives insettings_apiso every implementation shares the same record shape. - π EX-6 Example without async Β·
v0.5βww_device::blocking::Serverexists, no example uses it. - π EX-7 Example with two media Β·
v0.5. - π EX-8 NCM WebSocket + UDP example Β·
v0.5β both media on one stack, sharing the backend. - π‘ EX-9 WebSocket over Ethernet example for some board Β·
v0.5.xβ thewsmedium already runs onembassy-net, so Ethernet needs no transport work, only an example. - π§ EX-10 Reconnect demo Β·
v0.5.xβ keep reconnecting with streams and UI feedback;disconnect_keep_streams. Done:Commander::reconnect()(re-selects the device with the stored config on any transport, restarts the loop on the kept command channel,resume_streams()). Missing:connect_keep_trying(retry until the device is back), UI feedback, the demo itself. - π EX-12 Hubris based example with sync code Β·
v0.5.x. - π EX-13 Advanced example / project template.
Documentation (DOC)¶
- β DOC-1 Docs site (zensical): wire format, derive macro, showcase, API, evolution rules, transports, CLI, Python, std library pages.
- β
DOC-7 Reading levels and a SerDes section by level: a Basic / Advanced / Nomicon badge on each page from
its front matter (
docs/levels.md,overrides/partials/level.html); the SerDes nav lists the Basic pages first, then Advanced and Nomicon;derive.mdsplit into derive, derive reference, size assumptions and layouts,shrink_wrap.mdinto an introduction, special types and wire format internals; the built-in types page rewritten against the code. A short "what works today" page (docs/features_today.md) is generated from this file (just features-page,tests/features_pagefails when it is stale); this file is the "Feature tracker", last in the nav. The other sections followed in DOC-8. - β
DOC-8 Reading levels for the rest of the site: level badges and a Basic-first nav (Advanced and Nomicon
sub-sections) for Home, API, Evolution, Transport, Examples and Dev tools; the standard library grouped by family
with an overview. Split by topic and level:
api/streams.md(+ counted streams, credit sinks, stream protocol),api/traits.md(+ trait attachments),evolution/rules.md(+ crate layers),transport/usb.md(+ internals),transport/ww_framer.md(+ wire format),ww_dfu.md(+ A/B, one slot, internals),ww_can_bus.md(+ reference),cli.md(+ command reference),python.md(+ advanced). New entry pages for API and transport. - β
DOC-9 Features page for newcomers:
docs/features_today.md("Features" in the nav) opens with what WireWeaver is, then per area a sentence on what it is for and its features grouped Basic / Advanced / Nomicon, each a short line with a link to its docs page, in-progress ones marked. Generated bytests/features_pagefromFEATURES.md(status) andtests/features_page/entries.md(area intros, level, line and link per item, several IDs per line allowed,# Omittedfor items left off on purpose); the test fails on a done or in-progress item without a line, a line for an item that isn't, and links to pages not indocs/. - π DOC-2 Run code in docs as tests Β·
v0.5β set up example tests from the.mdfiles. - π DOC-3 Link every feature to a docs page with an example Β·
v0.5β this file can serve as the index. - π DOC-4 README pictures and showcases Β·
v0.5β including a picture showing density compared to MessagePack and friends; fill theTODOs inREADME.md. - π DOC-5 Open docs
TODOs β export of all API levels with ids (api/addressing.md), splitting traits into multiple files (api/traits.md), interrupt vs bulk (transport/usb.md),Tβ struct containingT(evolution/rules.md). - π§ DOC-6 Docs for
ww_stdlibcrates and resource doc comments Β·v0.5.xβ done:#[ww_trait]leaves a Rust trait of the same name for rustdoc, with every resource and its doc comments (properties, streams, sinks and sub-traits aswire_weaver::api_docmarkers, seeapi/traits.md), checked with nightly--cfg docsrson a crates.io-shaped copy ofww_i2c. Missing: a docs.rs check of crates whose API uses another crate's types or traits (ww_uart,ww_gpio,ww_sitypes); the macro panicked on theirversion = ".."dependencies until API-14, try one on a crates.io-shaped copy. - π DOC-10 Units docs section Β·
v0.5β choosingFixed,Point,Level,QuantityorAnyQuantity, imperial / US customary with the ambiguity table, kinds, exactness and rounding rules, and what is out of scope on purpose; links STD-78's "Storing units" page.
Testing, CI and performance (TEST)¶
- β TEST-1 CI on GitHub Actions: fmt, sort, typos, clippy, rustdoc, nextest, doctests, semver-checks, MCU builds.
- β
TEST-2 Integration tests against the real host event loop over
in_process, one crate per API feature. - β TEST-3 Device side end-to-end tests with real framers on both ends.
- β TEST-4 Timeout tests for methods, properties and streams (async, blocking, promise).
- β TEST-5 Method tests: calls, unimplemented, deferred replies, deferred not answered.
- β TEST-6 Property tests: multi-req and single-req servers, user errors.
- β TEST-7 Stream tests: byte buffers, basic and user types, open/close, sideband both ways, sinks, arrays of streams.
- β
TEST-8 Framer fuzzing (
fuzz/). - β
TEST-23 Wire-format conformance matrix: one corpus of cases (
tests/conformance/corpus.json: a type, a value in the form Python uses, the golden bytes), run through every consumer of the wire format: the Rust derive (owned types, borrowed round trips), theww_selfdynamic client and Python (from the API model oftests/conformance_api), and C (the runtime for single values, the generatedfixed_layoutandhead_tailreaders compiled and run). 105 cases: numbers and varints, bit-sized fields and nibbles, relocated flags,Option/Result, enums with sub-byte, nibble and explicit discriminants, unknown and compiled-out variants, lists, strings,ArrayString/ArrayVec,MaxLenbounds both ways, nested, recursive, generic, sparse, sized and final structs,PhantomData,validate,TailBytes/TailSize, fixed layout,head_tail,Series, evolution both ways. A case a consumer can't express carries a gap mark naming the item that fixes it (shown indocs/serdes/conformance_matrix.md); a missing or stale mark fails. Part ofjust testand CI. It replaces an IR for the two layout engines (API-25 dropped). Found and fixed:fixed_layouttypes in APIs (the crate walker rejected them), unknown enum variants in Python; filed: INT-9, INT-10, TOOL-13, SW-66. Seedocs/serdes/conformance.md. - β
TEST-24 Frozen golden bytes guard: a case's
bytes,directionandbytes_fromdon't change, and a case doesn't disappear, once they are in the corpus at the merge base with main (git merge-base HEAD origin/master, falling back to localmaster) β a hand edit or a reset to"?"re-sealed silently before this,CONFORMANCE=updateon its own only refuses to overwrite a filled-in case."wire_change": "EVO-n / reason"on a case is the escape for a deliberate wire break (needs the matchingEVO-nitem and a**Breaking:**CHANGELOG entry), shown in the matrix report. Depends only on the repo's own git: without git, or without the corpus at the merge base (a shallow clone, or the corpus being new), the guard says so and passes rather than failing a check it can't run; CI'stestandcoveragejobs fetch full history so it actually runs there.tests/conformance/src/frozen.rs,tests/matrix.rs::frozen_bytes. Seedocs/serdes/conformance.md. - β
TEST-25 Coverage guard: every
ww_self::Typevariant reachable from some case's type (an exhaustive match, so a new variant fails to compile here until handled) and every wire-relevantshrink_wrap_derivedirective used by a type intests/conformance_apiis exercised by at least one case; a kind unlisted and unused fails, a kind listed (TYPE_GAPS/DIRECTIVE_GAPS) but now used fails too. Found two variants a prepared bundle never carries (filed INT-11) and two directives with no case yet (filed SW-67).tests/conformance/src/coverage.rs,tests/matrix.rs::coverage. Seedocs/serdes/conformance.md. - β
TEST-17
shrink_wrapcode size bench:just size-benchbuilds one bare firmware image per test case (plain struct, enum, list, bit fields,Option, decode or encode only, the request/reply codec of a small softcore firmware and the same by hand) for RV32I, RV32IC, Cortex-M0 and Cortex-M4F/M7 and prints.text+.rodata, or the largest symbols (shrink_wrap/size_bench, results indocs/serdes/code_size.md). - β
TEST-26 Credit sink resume on hardware Β·
v0.5β Donguru's header UART tx credit sink (API-34) on omarchy-m1 (USART5, 921 600 Bd, jumper IO7-IO8,donguru-core/examples/uart_resumein typec_dongle): 1 MB of numbered lines, every byte on the line exactly once and in order (0 mismatches) in a plain run (14.3 s, 70 kB/s), with 4 link drops (disconnect().keep_streams(), 200 ms down,Commander::reconnect()10-22 ms incl. resume, first flush after it 0 ms; 15.3 s) and with a restarted host process continuing from the saved cursor (resume 2.6 ms, device was 544 B past the cursor). Found on the way: a 1 s flush stall after a resume (fixed), and a lost wrapped write in the firmware's pipes. - π TEST-28 Units coverage against the standards Β·
v0.5β a dev-only test (not shipped) walks the public unit lists of UCUM, UNECE Rec 20, BLE GATT units, USB HID, the BACnet engineering-units enum and IODD as facts and shows each unit representable exactly inexact_units, or lists it on a short written "cannot, because" list for review. A capability entersexact_unitsonly as a pattern a class of units or a standard's structure uses, never for one odd value: non-linear or empirical scales (AWG, Beaufort, Richter, Rockwell, octane, API gravity, shoe sizes), ill-defined codes (GATT "decibel" without a reference, month / year durations, unqualified kcal) and data-defined scales go on the list, at most as named functions or "unknown, reported"; a free runtime factor (calibration, currency) is listed as "runtime value, not a unit", carried byww_si::Quantity/AnyQuantityor the application. - π TEST-29 Units zero-cost gate Β·
v0.5β the ELF comparison of TEST-27 (just units-elf) becomes a gate injust test/ CI: every plain / unit-typed pair (range check, struct encode and decode, Series encode and decode) compiles to identical code on every core and firmware profile measured (M0, M4F, M33;opt-levelzand3), and a pair that differs fails with the per-symbol diff; aQuantitywith a named prefix that grows in RAM fails too. - β
TEST-27 Units firmware budget Β·
v0.5β whatww_si::Fixedcosts on an MCU against the plain fields it replaces, one harness any later piece (Point, rounding conversions,Level) re-runs withjust units-budget:examples_mcu/mcu_qemu/src/unit_budget.rs(qemu-unit-budget,stm32h725ig-unit-budget-cycles) with plain/Fixedpairs that must write the same bytes, andexamples_mcu/fw_budgetprinting instructions (Cortex-M3 under QEMU), flash and stack, at the shipped firmware's profile (fw-z, fat LTO,opt-level = "z", andfw-3). Baseline of 27 Oct 2026: struct encode/decode, aSeriescolumn and aRangecheck within Β±7 % of the plain instructions atfw-z, +0 bytes of flash (+68 atfw-3), equal stack frames. Hardware check left: DWT cycles on the STM32H725 for the pairs that differ (the board was not behind the probe). Proved by ELF instead (just units-elf): the pairs compared instruction by instruction on thumbv6m, thumbv7em and thumbv8m.main atfw-zandfw-3: range check andSeriesencode identical everywhere,Seriesdecode atfw-3; struct encode/decode differ by the derive's per-field write for aFixed(a few instructions). STD-63's rounding conversions were measured against hand-written ones on M0 and M4F atzand3(docs/std_library/rounding.md, "Cost on a device"). STD-72 adds nothing of its own to measure: its markers are zero-size types andFixed::convertisFixed::to's compile-time factor, so thetomeasurements cover it. Seedocs/articles/units_budget.md. - π§ TEST-9 Remaining test coverage Β·
v0.5: - properties: unimplemented, observe stream
- streams: promise timeout
- methods: return type evolving to a struct, adding arguments
- π§ TEST-10 Binary size tracking Β·
v0.5β flash and RAM per feature for a set of examples, in CI. Consider compile-time construction so that only final bytes remain. - π TEST-11 Keep an eye on coverage β around 70% on average as of 2026-10-01, the important crates are at
80-90%,
ww_framerat 97%. Don't let it drop: add tests with new features and fixes, and raise the crates that are behind. - π TEST-12 Coverage with hardware in the loop Β·
v0.5.x. - π TEST-13 TODOs round Β·
v0.5.xβ go through theTODOcomments in the source. - π TEST-14 Update all dependencies Β·
v0.5.x. - π TEST-15 Measure serialization speed on MCU and host (LE vs BE, array access vs pointers,
UNib32implementations). - π TEST-19 Lossy test medium Β·
v0.5β an in-process medium that drops, duplicates, reorders, stalls and disconnects on a seeded schedule, with the stream reliability scenarios S1-S6 as end-to-end tests (docs/api/stream_reliability.md). - π TEST-20 DFU transcript 1L-UPD-STEP races: after the power cut, the host annotation "the call fails (link
lost)" and the bootloader's "an update is under way" are recorded in either order, so
scenarios::pit_stop_shuttle_1l_upd_stepfails now and then (~2 in 10 alone, more often under the full paralleljust test). Seen before the TR-12 changes too. The recorder needs one order for events of the host and the simulated device that happen at the same time. Found 15 Oct 2026. - π§ TEST-21 Power cuts against a flash store on STM32 β
examples_mcu/ecc_flash_g0b1, a sequential-storage map onww_ecc_flash(SRV-19) in bank 2 of a G0B1: - reset loop: every boot loads and checks all keys against what earlier boots committed, then a SysTick
reset at a random time cuts the stores. 3000 boots on the B153A (J-Link, 15 Oct 2026): 655 stores, resets
during 28 programs, 453 erases and 1537 reads, every committed value loaded, none corrupted.
src/bin/reset_cut.rsshowed why no word ever failed its ECC check: on G0 a system reset waits for the flash operation (73 page erases cut 1-20 ms into their ~22 ms all left the page fully erased, programs only stop between double words). - real power cut against
ww_settings::StoreonEccSafeFlash, so the zeros path of SRV-19 runs on hardware: the B129A powered through the Donguru plug VBUS switch (U601, AOZ1375), switched from the PC in a loop with Donguru'splug.vbus_mode(typec_dongle API-11, in progress). The device can't keep its counters across a power cut: a host program reads the loaded and committed sequence numbers and the ECC count over SWD (Donguru CMSIS-DAP, session ended before each cut so SBU/CC don't back-feed the board) and checks every boot loads at least what was committed before the cut. - π‘ TEST-16 Host queue on a slow VM β does it lead to fewer or no missed packets?
Dropped and superseded¶
- CLT-37 No millisecond per request from the accumulation window β superseded by request priority (CLT-50)
and the fine window timer (CLT-53) (user, 27 Oct 2026). CLT-37 flushed whatever accumulated as soon as the tx
task had nothing else waiting, for windows under 1 ms (
TxInput::Idle,TxCore::wants_idle_flush, removed), so a request's latency depended on what else happened to be queued. Now default traffic waits for the window, never longer, and a request that must not wait says so with.urgent(). Its measurement (B135 rev B, USB HS, 21 Oct 2026): a property-like call 1.3 ms β 0.17 ms median, a 4 KB SD read 4.5 ms β 1.8 ms, CAN frames through a credit sink 3.8k β 7.2k/s. - STD-57
Quantityused at one fixed scale becomesFixedβ dropped (Roman, 27 Oct 2026): the per-use list ofQuantityfields found none whose scale is fixed by contract, so nothing to change. - API-25 Refactor codegen to use an IR β decided against (18 Oct 2026). There are two wire-layout engines by
design:
shrink_wrap_derivekeeps its own AST, asww_self's own types are encoded with#[derive_shrink_wrap]and the derive can't depend onww_self(a bootstrapping cycle); the dynamic path (ww_selfto the dynamic client, Python, the CLI and GUI) computes the layout again from the introspection model, and generated C (API-26) is one more consumer of that model. Instead of an IR, the conformance matrix (TEST-23), one corpus of cases run through every consumer, is the contract that keeps them byte-identical;ww_selfis the shared model for everything outside the derive. Fallback if drift ever bites: a dependency-free crate holding only the layout rules (bit offsets, flags, nibble alignment), called by bothshrink_wrap_deriveandww_self, breaks no cycle. - STD-30
ww_gpioport access β dropped (Roman, 20 Oct 2026),Bank::read_inputs/write_outputs,PinLeveland thesimultaneous_*capabilities removed beforeww_gpio0.1.0 was published.ww_gpiostays simple and low speed, one pin per call (IO family D9): many pins in one round trip are per-pin calls in one multi request (CLT-15,docs/std_library/ww_gpio.md#several-pins); several pins at one instant or with precise timing (bit-banged parallel buses, a strobe with its data lines) belong to the signal generator (STD-22) and the logic analyzer (STD-21). The extra methods only cost flash on every GPIO server. - SW-31
Delta/DeltaOfDelta, SW-32XorFloat, SW-50 compressed sequences in APIs β superseded bySeries<T>(SW-60): its numeric columns use the SW-31 prefix code with the delta or delta of delta predictor, its float columns the SW-32 XOR code, plus packed, constant and plain and the smallest picked per column, in every API position and in dynamic clients and Python (CLT-29); the types were removed (18 Oct 2026). - API-15 Return streams from functions β superseded by API-31 stream functions:
#[stream(T)] fn name(args)opens a stream with typed arguments and result. - Document things (old notes) β superseded by
docs/serdes/shrink_wrap.mdanddocs/evolution/rules.md; the notes described an earlier format (field ids,vlu16n_rev). Remaining ideas moved to their areas. - LEB numbers β superseded by
UNib32andUVlq32(ULeb128is still in the derive AST, unimplemented). - Field ids β decided against for now: fields are identified by position, which works well, and ids would
add confusion together with flag relocation. The
idleftovers inshrink_wrap_derive/src/transform/can go. - Option and Result taking two id numbers β dropped with field ids; flags are separate fields.
- Put length in front into a specified field β done as
UVlq32Backfill. - CLI common blocks AST generation and excluding common blocks from the AST β done as API snapshots.
- Compatibility check using AST signatures on connect β done.
- Introspect as a trait, removed from
ww_client_serverβ won't do. - WebUSB descriptors β only some browsers support WebUSB and it needs separate host support; NCM is more appealing, especially with pre-programmed unique IPs.