Skip to content

Command-Line Tools

Command-line host tools live here. Most hardware-facing tools link the Qt-free tagcore target from ../libraries/tagcore. dataprocessing links the Qt-free sensoranalysis_core and SQLite instead, and is built in any host build; like every tool here it uses no Qt.

Rust ports of the tools that need only the tag interface are in ../commandline-rs.

Distributed tools -- these are the ones in host_cli_install_targets in ../CMakeLists.txt, the ones documented in the user guide, and the ones that reach a release:

  • dataprocessing: copy SQLite logs and materialize derived/calibrated sensor streams. The initial implementation supports CompassTag calibrated vectors and canonical orientation streams.
  • tag-dwnld: download tag logs using the shared tag log writer interface.
  • tag-info: inspect tag/base information.
  • tag-reset: reset a tag.
  • tag-start: start logging.
  • tag-stop: stop logging and print the resulting tag status.
  • tag-cal: calibration helper.
  • tag-test: run RTC checks and tag self-tests. Despite the name this is a field tool -- it is what confirms a freshly programmed board works -- so it ships.
  • tag-txt-import: convert a legacy BitTag or PresTag .txt log to SQLite, verified by rendering it back to text. User page: cli/tag-txt-import.md. See below.

Developer and bench tools -- built from this directory, never installed. They exist to exercise hardware and firmware during development, they carry no user guide pages, and adding one to the install list is what put an unsigned, undocumented tag-attach-cycle.app into every macOS release until it was caught by the signature check in host/tools/release-macos.sh:

  • tag-test-example: minimal example that sets the tag RTC, waits, and prints the measured clock error. A worked example of the tagcore API, not a tool.
  • tag-monitor-test: exercises the debug monitor interface through the link adapter.
  • tag-attach-cycle: repeatedly attaches and detaches a tag over USB, to shake out enumeration and attach-path faults.
  • tag-peek: read arbitrary memory through the monitor interface.
  • tag-cal-write: write an identity (no-op) calibration constant set, to unblock a mass-erased CompassTag. See below.
  • tag-log-import: write a tagclient JSONL download as a SQLite log, with no tag. See below.
  • qtmonitor-fixture-capture: capture TagInfo, default Config, Status, and voltage from a real tag into the fixture JSON consumed by qtmonitor documentation screenshot automation.

All of the tag-attached tools share -d/--debug, -b/--base BUS:DEVICE and -h/--help.

Keep direct tag-operation tools independent of Qt so they remain lightweight and usable in scripts. Processing tools may link host analysis libraries when that avoids duplicating sensor math.

Example qtmonitor fixture capture preserving the real tag identity:

qtmonitor-fixture-capture \
  --id compasstag \
  --label CompassTag \
  --state idle \
  --fallback-config embedded/proto-c/compasstag-proto-c/default-config.json \
  --output host/docs/fixtures/qtmonitor/compasstag.json \
  --print-summary

Example sanitized capture for screenshots that should not show a real device UUID:

qtmonitor-fixture-capture \
  --id prestag \
  --label PresTag \
  --state idle \
  --sanitize \
  --fallback-config embedded/proto-c/prestag-proto-c/default-config.json \
  --output host/docs/fixtures/qtmonitor/prestag.json

--state names the captured status slot in the fixture; it does not drive the tag into that state. Put the tag in the desired state before running the tool.

tag-capture

Captures a tag's state over SWD without booting its firmware: the backup registers (where the tag keeps its pState), the reset flags, option bytes, flash ECC registers, RTC, unique ID, OTP and the whole internal flash. Run it first on a returned tag, before tag-info or any other tool here. Those attach a monitor, which lets the firmware boot, and the boot clears the reset flags and can rewrite pState and the marker log.

build-host/bin/tag-capture --reason "returned from site 3, would not start"

It writes captures/capture-YYYYmmdd-HHMMSS/ with one .bin per region and a manifest.json holding the decoded registers and each file's SHA-256. The tag is then reset and boots normally. --sram also captures SRAM, which is rarely useful because Shutdown and Standby do not keep it. --leave-halted leaves the core stopped.

It then captures the external flash. It reads the tag's identity record from the internal flash just captured, finds the loader the record names (<loader>.stldr under --loader-dir, $TAG_LOADER_DIR or build-host/embedded/loaders), and checks the part's JEDEC ID against the record. The output is: - for NOR: external_flash.bin; - for SPI NAND: external_raw.bin, external_ecc.bin and external_pages.csv, with blank blocks skipped (--external-full reads them).

--loader names the image explicitly, for an image with no identity record. --no-external skips this stage. See SWD Capture and Recovery Library.

tag-xflash

Reads a tag's external flash over SWD, again without booting its firmware. It halts the tag at its reset vector, downloads an external-flash loader from embedded/loaders into SRAM, and calls the loader's Init and Read from the host. CubeProgrammer is not involved, and internal flash is not written.

build-host/bin/tag-xflash dump \
    --loader build-host/embedded/loaders/AT25XE_PresTagv3/AT25XE_PresTagv3/build/AT25XE_PresTagv3.stldr \
    -o xflash.bin

A loader with a Serve() entry point (the loaders in this tree) is driven through it, and the tool prints the part's JEDEC ID and status register as found. Any other .stldr is driven through the STM32CubeProgrammer entry points; --st forces that path. --offset and --length read part of the device; by default the whole part is read, its size taken from the loader.

For SPI NAND, tag-xflash nand -l <loader> -o <dir> reads whole pages, data and spare, both raw and through the part's on-die ECC: - page 0 of each block is read raw, and a blank block is skipped (--full reads every block); - --first-block and --blocks select a range; - it writes raw.bin, ecc.bin, pages.csv (each page's ECC verdict and status registers) and summary.txt.

On an IMUTagNandBmp581 the whole 256 MiB part scans in about 75 s when only one block is in use. The loader must match the board; for now it is chosen by hand. Downloading the loader overwrites the start of SRAM1, so run tag-capture first if SRAM matters. A 4 MiB part takes about a minute. See Loader Runtime Design.

tag-sramcall

Calls one function of an SRAM-resident image on a halted tag and prints the transfer buffer. It halts the tag at its reset vector, downloads the image into SRAM1 with the same convention as the external-flash loaders, calls the named function with up to four arguments, hex-dumps the buffer, and resets the tag. An argument written buf becomes the buffer's address.

build-host/bin/tag-sramcall \
    --image build-host/embedded/loaders/RV3028_PresTagv3/RV3028_PresTagv3/build/RV3028_PresTagv3.elf \
    --call Rv3028ReadRegs --args buf,0x30,8 --dump 8

That reads the RV3028's configuration registers, its factory clock offset among them, with no firmware on the tag. See RV3028_PresTagv3. Exit status: 0 when the function returned 1, 2 for any other result, 1 when the call could not be made.

tag-rebuild

Rebuilds a SQLite download from a tag-capture directory, without the tag. The capture's internal flash, backup registers and external flash are turned back into the messages the firmware would have served, and written by the same SQLite writer tag-dwnld uses:

build-host/bin/tag-rebuild <capture-dir> -o tag.db3

The file is "as captured": the tag as it was found, before the reset recovery a live attach would run. Its info table records source = capture, the capture directory and the capture time. Families with a capture decoder are accepted: IMUTag, PresTag, CompassTag, UIUCTag and BitTag. A capture whose layout version the decoder does not know is refused. How to capture, and how to keep the decoders in step with the firmware, is in Capturing a tag; the design choice is decision 0018.

tag-log-import

Writes a download in the JSONL format of tagclient's tag_dwnld.py as a SQLite log, with the same writer tag-dwnld uses. It needs no tag:

build-host/bin/tag-log-import download.jsonl -o tag.db3

The input has two parts:

  • The first line is the header. config and info are required. states (State messages) and calibration (CalibrationConstants, oldest first) are optional. Other members are ignored, such as tag_sha and status.
  • Every later line is one Ack in protobuf JSON. Each is written with one writeLog() call, in order.

A line the writer takes no records from is an error: it means the Acks do not match the header's tag type. The tool is tag-agnostic.

The info table gains source = import and import_file, and --info KEY=VALUE adds more rows. On failure, the partial output is removed.

tagsim uses it to turn simulated IMUTag pages into a genuine log.

tag-txt-import

Converts a legacy BitTag or PresTag .txt log to a SQLite log, and proves the conversion before keeping it (proposal):

build-host/bin/tag-txt-import log.txt -o log.db3 --report log.json
  • What it reads: variants B (BitTag V5 header), C (the current header, including the C3/C4 packed word on V: lines), D (PresTag) and E (no header), each checked as below; and variant A, translated only. Files that are not logs are refused with exit status 2.
  • Variant A (BitTagRTCv4 firmware, 2019–2020; written by DumpPersistent.c in the old accel-tag repository) has no writer here to render it back, so it is decoded as btviz decodes it and not round-trip checked: each 5-minute hex word is five 6-bit counts of active seconds per minute, the minute at epoch − i·60 (i = 5…1) in bits (i−1)·6, as count / 0.6 percent; temperature T vdd V lines become CoreTemperature and Voltage. Bits 30–31 are dropped as btviz drops them (the report counts words with bit 30 set). The log's info says txt_translation; the tool checks only that the rows read back as decoded.
  • How it writes: with SqliteTagLogWriter::writeSamples(), into the same tables and streams a download writes.
  • The check, on every conversion: the written log is read back, rebuilt into the records the text writer consumes, rendered with TextTagLogWriter, and compared with the source: data lines byte for byte or, for older writers, within the source's printed precision; header fields one by one. A failure writes no log (--keep-failed keeps it, marked) and exits 1.
  • Repairs, all recorded: exact duplicate records removed; conflicting duplicates kept and counted; records before 2015 or after the read time dropped; FIVEMIN files from the pre-2021 host (nine 7-bit buckets per record) re-decoded as seven 9-bit ones and checked back through the buggy layout; CRLF and comment lines handled. Removed lines are kept verbatim in the report.
  • Not guessed at: a V: line with no activity before it, or activity no V: line closes, marks exports run together; the conversion fails.
  • PresTag voltages: PresTag firmware put a second supply voltage in the core-temperature slot, so TC: is a voltage. The tool warns, stores the mean of V: and TC: as one Voltage row (no CoreTemperature), and the round trip writes that mean back into both slots.
  • Provenance in info: source = txt-import, the source path (relative to --root), SHA-256 and size, the variant, the importer version, the verbatim header, txt_roundtrip and txt_conversion_report; --report FILE writes the report as JSON too.

On the legacy collection (2026-10-22), 398 files of variants B–E convert and pass the check and all 195 variant A files translate; 2 fail as exports run together; 24 non-logs (20 macOS AppleDouble files, 3 empty files, 1 page of notes) are refused. The check itself is exercised on anonymised fixtures by test/txt_import_check.py.

tag-cal-write

Several CompassTag-family state handlers refuse Start with "Device must be calibrated" (config.c, sensorsHaveCalibration()) until at least one magnetometer calibration entry is present in flash. A mass-erase reflash (STM32_Programmer_CLI -e all, or any full-chip erase) wipes that flash region along with everything else, which then blocks tag-start on an otherwise-healthy board with no other symptom.

tag-cal-write writes a single identity calibration entry (M' = A(M-V) with A = identity, V = 0, B = 1) so the tag will start again:

build-host/bin/tag-cal-write
build-host/bin/tag-reset --set-rtc
build-host/bin/tag-start --start-now

This unblocks power/lifecycle testing on a freshly reflashed board without a physical rotation-based calibration pass. It is not a real calibration -- magnetometer output taken under it is uncalibrated raw data passed straight through. Run the interactive tag-cal (or qtcalibrate) for a real calibration before trusting compass output from a board this was used on.

DataProcessing

dataprocessing is a post-processing tool for SQLite logs. It keeps the input database untouched, copies it to a new output path, writes derived tables and stream metadata, and records processing provenance in a ProcessingRun table. The design, output tables and provenance format are in design/dataprocessing.md; open work is in TODO.md.

Current processors:

  • compass-calibrated: writes CompassCalibrated with offset-corrected acceleration beside calibrated magnetometer x/y/z values.
  • compass-orientation: writes CompassOrientation with the magnetic-frame orientation quaternion, dip, field strength and acceleration magnitude. Euler angles are not stored; see Downstream Heading Conversion.
  • imu-compass-orientation: the same orientation columns for an IMUTag log, written to ImuCompassOrientation on ElapsedUs. Each magnetometer row is paired with the mean of its superframe's accelerometer rows. SensorViz plots the result when it opens the processed file.

Example:

dataprocessing \
  --input host/docs/fixtures/sensorviz/compasstag.db3 \
  --output /tmp/compasstag-processed.db3 \
  --processor compass-calibrated \
  --processor compass-orientation \
  --if-exists replace \
  --print-summary

dataprocessing \
  --input imutag.db3 \
  --output imutag-processed.db3 \
  --processor imu-compass-orientation \
  --print-summary

Use --list-processors to see whether a log contains the inputs needed by the current processors. Heading is not materialized; downstream tools can convert the stored yaw to a display heading by applying their own declination and mounting convention.