BKPT LabsDOCS/VIEWALYZER CLI/CAPTURE AND STREAM VIEWALYZER · HEADLESS CLI
VIEWALYZER · CLI & AUTOMATION

Capture and stream

A capture connects to a target through a probe (or a socket), reads the recorder's stream for a while, and writes one .vadb. This page covers the connection file, the capture flags, the live stream, snapshots, polling, and hardware trace.

The connection file

A .vacf is flat JSON with the same names as the flags; keep one per board next to the firmware and pass overrides on the command line. See the ViewAlyzer Recorder section for the transport each firmware build needs.

{
  "transport": "stlink-rambuf",
  "target-device": "STM32G474RE",
  "speed-khz": 24000,
  "rambuf-scan-start": "0x20000000",
  "rambuf-scan-size": 131072
}
TRANSPORT THE KEYS THAT MATTER
stlink-rambuf, jlink-rambuf--elf (pins _VA_RAMBUF) or --rambuf-address, else the scan window; --rambuf-poll-ms; --no-reset
stlink-rtt, jlink-rtt--rtt-channel, --rtt-address or --elf; --no-reset
stlink-swo, jlink-swo--cpu-clock-hz (required), --swo-freq-hz, --itm-port, --init-swo
udp, serial--udp-ip, --udp-port; --serial-port, --baud; --cobs
swo-tcp--swo-tcp-port (a bkpt_gdbserver --swo-tcp side channel), --cpu-clock-hz, --itm-port, --swo-freq-hz; the debug server owns the probe and programs the target, the CLI only decodes

config --config board.vacf echoes the effective configuration and any notes without touching hardware. With several probes of one kind attached, pass --stlink-serial or --jlink-serial; probes lists them.

capture

$ viewalyzer-cli capture --config nucleo_g474_rambuf.vacf --elf build/rambuf/Nucleo_G474_VA.elf \
      --output run.vadb --duration 5
[headless] NOTE: Control block at 0x2000005C (resolved from ELF symbol _VA_RAMBUF)
[headless] Capture: stlink-rambuf target STM32G474RE cb 0x2000005C scan 0x20000000+0x20000 reset=true
[capture] Target reset; capture from boot
[capture] RAM buffer at 0x2000005C, 8192 bytes, CPU 170 MHz
[headless] State: Recording
[headless] t=1.8 s  13981 events  107 KB  54.5 KB/s
[headless] Duration reached; finalizing
[headless] Capture ended: 36614 events, 7 objects, 285717 bytes, lost 0, corrupt 0 bytes (0 runs), seq gaps 0
[headless] Recording saved: D:/tmp/run.vadb (2848 KB)
[headless] Recording registered: id=328d49aa4eb6
{"schema_version":2,"recording_id":"328d49aa4eb6","path":"D:/tmp/run.vadb","summary":{"events":36614,"objects":7,"bytes":285717,"lost_events":0,"corrupt_bytes":0,"duration_us":5111530,"wall_s":5.02,"os":"BareMetal","cpu_hz":170000000.0,"transport":"stlink-rambuf"}}
FLAG MEANING
--output <path>A .vadb (default <app dir>/recordings/capture-<stamp>.vadb), or a .va to keep only the byte-log
--duration <s>Seconds of recording, counted from the moment the transport reports Recording. Omit it to run until Ctrl-C, SIGTERM or --stop-file
--stop-file <path>The capture finalizes as soon as this file exists (content ignored); the way a host ends an open-ended capture cleanly on every OS
--streamJSON lines on stderr while running, below
--elf <file>Pins the control block from the image; also symbolicates hardware trace
--keep-vaKeep the .va byte-log next to the .vadb (it is written while the capture runs, so a host crash loses nothing)
--no-registerDo not add the recording to the index
--instrument-cmd "<cmd>", --instrument-series f, --instrument-live, --instrument-warmup-s nRun an external instrument recorder for the capture window and merge its series afterwards (or live), with sync-mark alignment (--sync-name, --sync-tol-ms)

Always read the envelope's summary.events and lost_events, not just the exit code: a capture that ends with 0 events is an error (empty_capture, exit 1, no file left behind), and the [capture] lines say why (no control block found, no sync marker, the wrong ITM port). Early stop (Stop requested at N s. Finalizing partial recording...) yields a normal recording.

Note: with no license the capture is capped at 5 s and a 5 s cooldown follows: a second capture inside it answers {"error":"cooldown_active","retry_after_s":N}.

The live stream

--stream writes one JSON object per line on stderr while the capture runs, so a host can plot or react without waiting for the file. Every line has a t kind:

{"t":"stream_init","schema_version":2,"transport":"stlink-rambuf","started_utc":"2026-08-29T08:29:33Z"}
{"t":"stream_meta","id":42,"name":"Sine Wave","display":"graph"}
{"t":"stream_meta","id":46,"name":"Workload","display":"bar"}
{"t":"stream_sample","id":42,"t_us":2203,"value":10.0}
{"t":"stream_sample","id":46,"t_us":2207,"value":3541.0}
T FIELDS WHEN
stream_initschema_version, transport, started_utcOnce, first
stream_metaid (the firmware's trace id), name, display (graph, bar, gauge, ...)As channels are discovered
stream_sampleid, t_us, valuePer value, in recording time
itm_textport, t_us, textText the firmware wrote to an ITM stimulus port (a printf on port 0), SWO transports
swo_loadbytes_per_s, share_pct, overflowsEvery 500 ms: the pin's load in that window and the ITM overflow count so far, SWO transports
pc_samplestotal, sleep, pcs[]DWT program-counter samples since the previous line (pcs keep the Thumb bit); PC sampling on
dwt_datarows[] of {cmp, t_us, v, size, w, pc}Data-watch values as the comparator emitted them: raw v, size bytes, w true for a write, pc when traced
exctotal, max_depth, exceptions[] of {num, name, enter, exit, return, max_depth}Cumulative exception-trace table, at most every 200 ms; exception trace on

The poll verb streams the same stream_init (with source: "poll" and poll_hz), stream_meta (with the symbol's type) and stream_sample lines. Deltas arrive every 16 ms at most; there is no end marker, the stream ends when the process exits.

A consumer in Python (the SDK does this for you):

import json, subprocess

p = subprocess.Popen(["viewalyzer-cli", "capture", "--config", "board.vacf", "--output", "run.vadb",
                      "--duration", "10", "--stream"], stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True)
names = {}
for line in p.stderr:
    ev = json.loads(line)
    if ev["t"] == "stream_meta":
        names[ev["id"]] = ev["name"]
    elif ev["t"] == "stream_sample" and names.get(ev["id"]) == "Sine Wave":
        print(ev["t_us"], ev["value"])

snapshot

Reads the firmware's RAM ring through the probe without resetting the target (the core is halted for the dump and resumed), for a post-mortem look at a crashed or wedged board. Needs a rambuf transport and a ring that holds a window: the recorder's VA_RAMBUF_MODE_WRAP or VA_SNAPSHOT, frozen by VA_SnapshotFreeze() in the fault handler.

$ viewalyzer-cli snapshot --config nucleo_g474_rambuf.vacf --elf firmware.elf --output crash.vadb
{"schema_version":2,"recording_id":"a82ee8857447","path":"crash.vadb",
 "summary":{"ring":"post-mortem","events":1895,"window_bytes":16380,"discarded_packets":0,"wrapped":true,"frozen":true,
            "wire_version":1,"recorder_version":65792,"cpu_hz":170000000,"control_block":"0x2000005C","lost_events":0,"corrupt_bytes":0}}

A live drop-mode ring (the default VA_RAMBUF_MODE_DROP) that the last capture drained to empty answers empty_snapshot ("the live ring window contains no sync marker"): there was nothing left to read, which is the expected state after a clean capture. reset resets the target afterwards.

poll: variables without instrumentation

poll samples global variables over the probe at a fixed rate; the firmware needs no recorder. Symbols come from the ELF (symbols --elf f lists the pollable ones with their inferred type), or raw addresses.

$ viewalyzer-cli poll --config nucleo_g474_rambuf.vacf --elf firmware.elf \
      --symbols uwTick:u32,adc_value:u16 --poll-hz 200 --duration-s 3 --output poll.vadb --stream
[poll] uwTick @ 0x20000058 (u32, 4 bytes)
[poll] Polling 2 address(es) at 200 Hz (period 5.000 ms)
[headless] Recording saved: D:/tmp/poll.vadb (148 KB)
{"schema_version":2,"recording_id":"30e62c927656","path":"D:/tmp/poll.vadb","summary":{"span_seconds":3.0,"sample_count":600,"sample_loss_percent":0.0,"symbols_polled":1,"poll_hz":200.0}}

Types are u8 u16 u32 i8 i16 i32 f32 (default: the symbol's size, signed). An unknown symbol or type is an error before polling starts. --coalesce-gap <bytes> merges nearby registers into ranged reads when you can assert the gaps are safe to read. Samples are poll_trace rows served by query user-traces like any channel. A debug-probe transport is required.

Hardware trace

SWO transports carry the DWT and ITM alongside the recorder stream: program-counter samples, exception entry and exit, data watches on the DWT comparators, event counters, printf on ITM port 0. RAM buffer and RTT transports sample the PC by polling DWT_PCSR over the debug port instead (--dwt --dwt-pc N).

$ viewalyzer-cli capture --config nucleo_g474_swo.vacf --elf firmware.elf --output run.vadb --duration 10 \
      --dwt --dwt-exc --dwt-pc 16384 --dwt-watch "sig_noise@0x200000E4:4:data-w,g_state@0x20000410:4:data-rw:pc"
KEY MEANING
--dwt, --dwt-exc, --dwt-pc NDWT on, exception trace, PC sampling every N cycles (rounded onto the 64 to 16384 POSTCNT grid; the applied value lands in the recording's pc_sample_interval_cycles)
--dwt-watch spec[,spec]Up to 4 comparators, positional: [name@]0xADDR[:size][:data-rw|data-r|data-w|pc|address][:pc]; default data-w, size 4; :pc adds the PC of the access
--dwt-counters cpi,exc,sleep,lsu,fold,cycEvent counters (cyc is refused while PC sampling is on: both use POSTCNT)
--itm-ports, --itm-privilege, --itm-timestamps off|1|4|16|64Stimulus-port mask (the recorder port and port 0 by default), privilege mask, timestamp prescaler
--dwt-path auto|poll|swopoll forces DWT_PCSR polling on an SWO session
--hardware-trace <json | @file>The whole block at once, the same shape the BKPT Debug extension and bkpt_gdbserver use
"hardware-trace": {
  "itm":  { "ports": 3, "privilege": 0, "timestamps": 1 },
  "dwt":  { "enable": true, "exception-trace": true, "pc-sample-cyc": 16384,
            "counters": ["cpi", "sleep"],
            "watch": [ { "addr": "0x200000E4", "size": 4, "function": "data-w", "pc": false, "name": "sig_noise" } ] },
  "trace-port": { "swo-hz": 10000000, "protocol": "nrz" }
}

What the core lacks is refused with the reason in the capture log ([hwtrace] ... refused: ...) and in the recording's hardware_trace meta, never silently. The queries profile, itm-console, dwt-data, dwt-exc, dwt-counters and swo-load read the result (Queries).

The pin budget: PC sampling every 16384 cycles at 170 MHz is about 10 kHz of 5-byte packets, 13% of a 10 MHz pin and 65% of a 2 MHz one; data watches add 5 to 9 bytes per write, exception trace 3 per entry or exit. swo_load on the stream and swo-load in the recording report what was used and how many ITM overflows there were; a STLINK-V3 receiver takes up to 24 MHz, so raise --swo-freq-hz before thinning the sources.

Dry run

hwtrace --dry-run prints the register image a capture would program on a given core, without a target, for review and for conformance tests:

$ viewalyzer-cli hwtrace --dry-run --arch v7m --cpu-clock-hz 170000000 --swo-freq-hz 10000000 --itm-port 1 \
      --hardware-trace '{"dwt":{"enable":true,"exception-trace":true,"pc-sample-cyc":16384,"watch":[{"addr":"0x200000E4","size":4,"function":"data-w","name":"sig_noise"}]}}'
{"schema":1,"arch":"v7m","cpu_hz":170000000,"swo_hz":10000000,
 "writes":[{"reg":"DEMCR","addr":"0xE000EDFC","value":"0x01000000"},{"reg":"ITM_LAR","addr":"0xE0000FB0","value":"0xC5ACCE55"}, ...,
           {"reg":"TPIU_ACPR","addr":"0xE0040010","value":"0x00000010"}, ..., {"reg":"DWT_COMP0","addr":"0xE0001020","value":"0x200000E4"},
           {"reg":"DWT_FUNCTION0","addr":"0xE0001028","value":"0x0000000D"},{"reg":"DWT_CTRL","addr":"0xE0001000","value":"0x000117FF"}],
 "refused":[],"applied":{"itm":{"ports":3,"privilege":0,"timestamps":1},"dwt":{"enable":true,"exception-trace":true,"pc-sample-cyc":16384,"counters":[],"watch":[...]}}}

--caps '{"numcomp":4,"notrcpkt":0,"nocyccnt":0,"noprfcnt":0,"itm":1,"tpiu":1}' describes the core (default four comparators, everything present); bkpt_gdbserver --dry-run prints the same image for the same inputs, pinned by a shared conformance table.