ww_log¶
ww_log is the log of a device: its defmt frames, decoded on the host with the firmware's ELF (LogDefmt), or
text records for firmware without defmt (LogText). Both are reliable streams:
the device keeps what it logged until the host has it, also while no host is connected, and tells the host exactly
how many messages it dropped and where. It is no_std by default and depends only on wire_weaver.
The device side of LogDefmt is the ww_defmt_logger crate: the firmware's defmt global logger, which also
tees every frame to RTT (see below).
LogDefmt¶
#[ww_trait]
pub trait LogDefmt {
const DEFMT: DefmtInfo<'i>;
#[stream(DefmtFrames<'i>, reliable)]
fn frames() -> Result<(), LogError<'i>>;
}
pub struct DefmtInfo<'i> {
pub encoding: DefmtEncoding, // Raw, Rzcobs
pub build_id: MaxLen<RefVec<'i, u8>, 32>,
}
pub struct DefmtFrames<'i> {
pub bytes: MaxLen<RefVec<'i, u8>, 512>, // one or more whole frames
}
DEFMT- the encodingdefmtis built with (itsencoding-raw/encoding-rzcobscargo feature), and the GNU build id of the firmware: the host takes the ELF with this build id to decode the frames (the format strings are interned there, not sent).frames- the log. Its unit is onedefmtframe, so the stream head'sunitsis the number of frames in an item and itslostis the exact number of log messages the host won't get. An item never holds half a frame: a decoder fed one item after the other never loses sync, also with raw encoding and after a loss.
Timestamps are defmt's own (defmt::timestamp!), inside each frame; the head's t stays off.
Logs from before the host connects¶
The device starts the stream's session at boot, without a host, and keeps logging into the store. A host that wants these logs resumes that session from its start instead of opening the stream (opening starts a new session and empties the store):
let status = device.log().frames_status().read().await?;
let cursor = StreamCursor { session: status.session.unwrap(), pos: UVlq64(0) };
let mut frames = device.log().frames_resume(cursor).call().await?;
The backlog arrives as the backfill run while new logs come live, see
stream reliability. When the store is full, its FullPolicy decides: with
DropOldest it keeps the latest logs, which matter most after a fault, and the host gets the dropped ones as one
gap. A store on SD or NOR flash (ww_stream's LogStore) keeps the logs across reboots and power cuts with nothing
changed in the API.
Decoding on the host¶
defmt-decoder does the decoding: Table::parse(&elf) for the table, a stream decoder fed with the bytes of each
item, frames out of it until it needs more bytes. examples/defmt_log is a complete host program: it reads
DEFMT, refuses an ELF whose build id differs from the device's (--force to use it anyway), resumes the session
and prints decoded frames and gaps, something like:
The build id¶
The linker writes a GNU build id when asked (-C link-arg=--build-id=sha1, a hash of the linked output: it
changes with every byte of code); a section in memory.x puts it into flash where the firmware reads it at run
time:
SECTIONS {
.note.gnu.build-id : ALIGN(4)
{
__build_id_start = .;
KEEP(*(.note.gnu.build-id));
__build_id_end = .;
} > FLASH
} INSERT AFTER .rodata;
ww_firmware_info::build_id() (its build-id feature) reads the id between the two symbols. examples_mcu/rp2
does all three steps (build.rs, memory.x, src/bin/usb_log.rs). Without the note, build_id is empty and a
host can only guess the ELF.
The device side: ww_defmt_logger¶
defmt allows one global logger, called from any context, interrupts included, and it can't wait; a reliable
stream's store is async. ww_defmt_logger sits between them:
global_logger!(LOG, 4096)makes aDefmtLoggerwith a ring of 4 KiB the#[defmt::global_logger]. Adefmtmacro takes a critical section (asdefmt-rttdoes), encodes the frame into the ring and, with therttfeature, into an RTT up channel. The whole frame is committed at once; a frame that doesn't fit the free space (or is longer than 512 bytes) is dropped whole and counted. The RTT tee never blocks, also when a probe sets the channel to blocking on attach (probe-rsdoes): each frame goes to the channel in one write, whole or skipped when the channel has no room (LOG.rtt_skipped()), so the firmware runs the same with or without a probe and the host never decodes half a frame.LOG.pump(&mut stream)in the event loop takes the committed frames out of the ring, without a critical section, packs up to 512 bytes of whole frames into oneDefmtFramesitem and pushes it into the store,units= frames. Frames the ring dropped become the stream'slostexactly where they were dropped.
ww_defmt_logger::global_logger!(LOG, 4096);
// at boot
let channels = rtt_init! { up: { 0: { size: 1024, mode: NoBlockSkip, name: "defmt" } } };
LOG.set_rtt_channel(channels.up.0); // probe-rs and `cnt run` keep showing the logs
state.log_stream.start_session(1).await?; // keep logs until a host resumes
// the event loop
loop {
if let Either::First(ready) = select(server.wait(), Timer::after_millis(5)).await
&& let Some(LinkEvent::Down(_)) = server.handle(ready, state).await
{
state.log_stream.detach();
}
LOG.pump(&mut state.log_stream).await?;
if server.is_up() {
state.poll_streams(&mut server.sink(), now_ms, &mut buf, 2048).await?;
}
}
- RAM: the ring (a power of two; each frame takes its encoded length plus 6 bytes) and about 1 KiB for packing an item. The ring only has to hold what is logged between two pumps; the store holds the rest.
- Encoding:
ww_defmt_logger::encoding()tells which onedefmtis built with, forDEFMT. With rzCOBS the very first frame after boot starts with an extra0x00(defmt's resync marker), which decoders skip. - Contexts: frames from an interrupt never interleave with one being written. A
defmtmacro inside adefmtmacro panics, as withdefmt-rtt.defmt::flush()doesn't wait, the ring is emptied bypumponly. - Other global loggers: none may be linked next to it: not
defmt-rtt, notrtt-target'sdefmtfeature. - Async server: a reliable stream needs
use_async = true.
The ring is tested on the host with four threads logging into a 512-byte ring while another pumps: every frame
arrives whole and in order, and received plus lost is exactly what was logged. examples_mcu/rp2's rp2_usb_log
logs over USB and RTT at once (just run rp2 usb_log, host: examples/defmt_log, --flood 500 to overload it).
LogText¶
For firmware without defmt: the log crate on embassy or std targets, ESP-IDF.
#[ww_trait]
pub trait LogText {
#[stream(LogRecord<'i>, reliable)]
fn records() -> Result<(), LogError<'i>>;
property!(rw level: LevelFilter, LogError<'i>);
}
pub struct LogRecord<'i> {
pub severity: Severity, // Error, Warn, Info, Debug, Trace
pub target: MaxLen<&'i str, 48>, // module path
pub message: MaxLen<&'i str, 256>,
pub truncated: bool, // the message was cut at the device's limit
}
pub enum LevelFilter { Off, Error, Warn, Info, Debug, Trace }
records- one record per message, unit: record. The head'stcarries the device time of the record. Logs from before the host connects come the same way as withLogDefmt, by resuming.level- records less severe than this are not produced (LevelFilter::allows(severity)); filters compiled into the firmware still apply. Writing it may fail withCommon(Unsupported)when the level is fixed at compile time.
Severity and LevelFilter are complete as they are (no_future_version): the five levels of log, tracing
and defmt.
Errors¶
LogError has only Common(CommonError) for now (common errors): Busy when another host has the
log open, Unsupported when the device logs elsewhere only. It stays an enum so variants can be added.