BKPT LabsDOCS/VIEWALYZER CLI/EXTERNAL INSTRUMENT SERIES: THE IMPORT CONTRACT VIEWALYZER · HEADLESS CLI
VIEWALYZER · CLI & AUTOMATION

External instrument series: the import contract

Any bench instrument that samples on its own clock (power analyzer, DAQ, logic analyzer, thermal camera ROI, battery cycler, ...) can land its data on a recording's time axis. Nothing in ViewAlyzer knows any instrument by name: an adapter converts the instrument's native capture into this NDJSON contract, and viewalyzer-cli import does the rest. This is the sync recipe from the instrument-port vision doc (joulescope-demo/docs/instrument-port-vision.md section 2) made concrete. How the alignment actually works, from first principles: docs/TIME-SYNC.md.

The sync recipe

  1. The firmware raises a GPIO edge and logs a VA_LogTrace("jsync", seq) event back to back with interrupts masked, at LFSR-dithered intervals (400..700 ms in 20 ms steps; see Nucleo_C071_VA/Core/Src/main.c for the reference block). The dither makes the interval sequence a unique fingerprint, so alignment is unambiguous no matter when either capture started.
  2. The instrument records that edge on its own clock (any tick unit).
  3. import matches edges to events by interval pattern, rejects noise edges and missed marks (including mid-train insertions), fits a piecewise-linear map from instrument ticks to recording cycles, and inserts the series.

On a BKPT probe with the instrument port, hardware probe_trigger events replace the firmware marks automatically (stream-position exact).

Instruments that cannot see a pin can still import with --offset-s <t> (pure linear map, no drift correction): honest about being unsynchronized.

File format (NDJSON, one JSON object per line)

{"type":"meta","source":"bench-daq","rate_hz":100000}
{"type":"channel","id":0,"name":"rail_v","unit":"V","display":"graph","group":"Bench"}
{"type":"sync","edges":[171234, 215002, ...]}
{"type":"block","channel":0,"t0":0,"dt":200,"v":[3.301, 3.299, null, 3.302]}
{"type":"sample","channel":0,"t":171000,"v":3.3}
  • meta (required, once): source names the producer (re-imports with the same source replace, never duplicate); rate_hz is instrument ticks per second (1e6 for microsecond sample ids, 1 for seconds).
  • channel: one per series; unit/display/group land in the object extra exactly like a Trace Domain descriptor would set them (display uses the same vocabulary; default graph).
  • sync: edge times in instrument ticks (may repeat / accumulate lines).
  • block: uniform-rate run; sample i is at t0 + (i + 0.5) * dt. A null keeps its time slot (instrument dropout) and inserts nothing.
  • sample: single point, for sparse or irregular series.

What lands in the .vadb

va_objects(ns='external', va_id=<channel id>, name, extra={display, unit, group, source}) plus va_events(kind='external_trace', t_cycles, value_f). Both are ordinary store rows: charts, stats, query, SQL and fingerprints see them like any firmware channel, and the file stays valid with nothing installed. A .vadomain descriptor can additionally claim ns='external' channels for branding and overrides, but is not required: the file is self-describing.

CLI

viewalyzer-cli import demo.vadb --series daq.ndjson [--sync-name jsync]
                      [--sync-tol-ms 40] [--max-resid-us 500] [--offset-s t]

Or in one step with the capture (spawns the instrument's own recorder for the capture window, waits for it, then imports):

viewalyzer-cli capture --config board.vacf --elf app.elf --output demo.vadb \
    --duration 15 \
    --instrument-cmd "python js_record.py -o run.ndjson --duration 20" \
    --instrument-series run.ndjson

The import prints matched pairs, clock drift (ppm) and fit residuals; use --max-resid-us as a quality gate in CI.