Skip to content

type: design status: current summary: Chapter 11 of the Host Software Guide - the four Rust applications one by one (Tag Programmer, Tag Monitor, Tag Calibrator, Tag View): what each does, how it is built from the pattern, its threads, its state, the C++ it reaches, its code map, the oracles that hold it to the Qt application it ports, and where it deliberately differs.


11. The applications

This chapter assumes chapter 10. Each section gives the application's job, the shape it takes within the pattern, its state, a code map, and the checks. The per-application design documents remain the detailed sources and are linked at the end of each section.

Tag Programmer (tagprogram)

Job. A station for programming and testing a batch of tags: find the base and the tag, offer the published firmware releases, program one with the right erase, run its self tests, set its clock, and record every attempt in a CSV. A port of qtprogram (decision 0034 describes the station it reproduces).

Shape. The first port, and the one that fixed the pattern. One engine thread owns a Station that holds everything: eight checks (Programmer, Base, SWD, Supply, Tag, Tag state, Firmware, Record), the base and the Tag, the chosen image, the record file, the open prompt and the programming session. Monitor calls run on the engine thread; the slow work (the connect check, programming, the flash read-back, listing and downloading releases) runs on worker threads started per job, whose results come back as closures applied on the engine thread. A worker that owns the base sets worker_busy, and nothing else touches the USB device meanwhile.

The session is a step machine ticked once a second while it waits on the tag:

stateDiagram-v2
  direction LR
  state "program" as program {
    [*] --> Stopping
    Stopping --> Erasing: FINISHED or ABORTED, Erase sent
    Erasing --> Stopping: tag IDLE
    Stopping --> Programming: IDLE
    Programming --> Connecting: worker done
    Connecting --> Testing: TagInfo answers
    Connecting --> PowerCycle: no answer for 6 s
    PowerCycle --> Connecting: Retry
    Testing --> [*]: tests done, tag IDLE
  }
  state "retest" as retest {
    [*] --> Reading: an image matches
    [*] --> Connecting2: no image
    Reading --> Connecting2: worker done
    Connecting2 --> Testing2
    Testing2 --> [*]: tag IDLE
  }

Erasing asks the tag's own firmware to erase; Programming detaches the monitor, runs tagprobe::program and reattaches; Testing compares the tag's commit with the image's, sets the clock and runs the self tests. The session ends with the tag IDLE and never starts it.

Code map (host/applications/tagprogram-rs):

Path Holds
core/src/model/ Pure logic over files, each with unit tests: elf.rs (a minimal ELF32 reader), flashplan.rs (the erase from the image's linker symbols), identity.rs (the TGID record), image.rs (a FirmwareImage with its SHA-256 and manifest check), catalog.rs (GitHub releases: list, download, extract, verify, cache), record.rs (the CSV), settings.rs
core/src/engine/mod.rs Engine, Msg, the loop, UiLogger
core/src/engine/station.rs Station: checks, base and tag, firmware, record, prompts, commands, queries
core/src/engine/session.rs The step machine, as a second impl Station
core/src/engine/view.rs The view state from the station
core/examples/smoke.rs, bench.rs A headless run that never attaches; qtprogram's --bench-program, which programs one tag
app/, ui/ The shell and the page

Checks. The model's unit tests; examples/bench.rs on hardware; Program and Retest passed on a BitTag through both base generations, Retest on an IMUTag, Program on a UIUCTag through a field base, and a batch of PresTags in one session.

Deliberate differences from qtprogram. No STM32CubeProgrammer, so no "Choose Programmer" and the Programmer row names the probe driver; a label already recorded for another tag must be confirmed; a session's result stays on the step line until Next Tag; host_version in the record reads tagprogram-rs <version>.

Read: README.md, design/engine.md, design/ui-engine-contract.md, design/build-and-packaging.md, decision 0035.

Tag Monitor (tagmonitor)

Job. The everyday tool: attach to a tag, see its state, clock error, voltage and self-test result, read and edit its configuration (or open a configuration file), synchronise its clock, run the self tests, start it, stop it, download its data and erase it. A port of qtmonitor, module for module.

Shape. Two threads. The tag thread owns the Tag, polls Status every 400 ms while attached (pausing 1.5 s after Synchronize Clock and Self-Test, when an L432 tag cannot answer debug accesses reliably), runs each TagCmd in order and runs downloads. The engine thread owns the MonitorModel (the tag as known, and what each status settles), the ConfigEditor (the document and the form), the prompts and the progress displays. The two exchange TagCmd and Report messages and share nothing.

The model (core/src/model/) is qtmonitor's Qt-free core, file for file: the twelve commands and the one rule for when each is available; the start checks and their sentences; small tag rules (downloadable states, the erase size estimate, the clock offset, the version floor); the monitor model; the configuration document (its values, source, unsaved-changes baseline, approval, and the file format); the menus as data; the configuration form; and the editor that joins document and form.

The form is the hard part and the largest file (configform.rs). What Start sends depends not only on qtmonitor's panels but on how Qt's widgets behave: date boxes that clamp to a minimum and re-run the panel's whole-hour handlers on every change, spin boxes that round half away from zero and re-round their limits when their decimals change. So the file models those widgets (DateEdit, DoubleSpin, IntSpin, RadioGroup), builds each panel from them exactly as the Qt constructor does, and a table says which panels each tag type shows. The page draws the form generically from the engine's description of each control (kind, label, range, step, decimals, enabled) and sends each edit as set_field {id, value}; the engine clamps, rounds and applies the panel's rules, and the next state shows the result.

Download (engine/download.rs) is chapter 5's loop through the C++ writer, behind the sqlitelog feature; built without it, Download refuses with a message and everything else works. It runs on the tag thread and blocks only that thread; Cancel stops it after the current page and keeps what was written.

Code map (host/applications/tagmonitor-rs): the architecture document has the full table, with the qtmonitor file each one ports and a "where to change what" table. The skeleton:

Path Holds
core/src/model/{commands,menus,startchecks,tagrules,monitormodel,configdocument,configform,configeditor}.rs The rules, as above
core/src/engine/{mod,tagthread,monitor,view,download}.rs The loop; the tag thread; Monitor, the engine's state and what follows each prompt; the view state and menus query; the download
core/tests/{oracles,core_check,config_probe,panels}.rs The checks against qtmonitor's files
core/examples/session_check.rs, form_view.rs A whole tag cycle on hardware; one tag type's form as the page sees it

Checks. The command table, start checks and menu layout match qtmonitor's committed oracle files; qtmon_core_check's assertions pass; all 710 configuration probe cases match the goldens (the bytes Start sends, the document, the panels, the dialogs, every control's value, range, step, decimals and enabled state; deliberately breaking the rounding made 22 cases fail); the panel table agrees with every default configuration; a full cycle (attach, sync, self-test, read, start, run, stop, download, erase) passed on an IMUTag.

Deliberate differences. The Configuration tab comes first and the Tag tab is "Tag Info"; the date boxes are the browser's; bases are named by their identity in the chooser; there is no fixture mode (the mock serves for screenshots).

Adding a tag type is a written procedure: design/adding-a-tag-type.md, which covers both Tag Monitors, the default configuration, the panel table, a new panel, the probe cases and goldens, and the download path.

Read: README.md, design/architecture.md, design/ui-engine-contract.md, design/build-and-packaging.md, decisions 0036 and 0037.

Tag Calibrator (tagcalibrate)

Job. Calibrate a CompassTag's or IMUTag's magnetometer and accelerometer: stream raw samples while the operator turns the tag through every attitude, fit the hard-iron offset and soft-iron matrix and the accelerometer's zero-g offset, show quality figures and a sphere of the calibrated samples, write the constants to the tag, and show the live compass that results. A port of qtcalibrate's window.

Shape. Two threads as in Tag Monitor. The tag thread attaches, puts the tag in CALIBRATE, reads the calibration log every 100 ms (qtcalibrate's sample timer), otherwise polls status every second to keep the tag awake, and writes and reads constants; stopping the stream waits for IDLE, because a tag detached on the acknowledgement stays calibrating through the next reset. The engine thread owns the calibration session, which is not Rust: it is calibsession::Session, the C++ CalibrationSession that qtcalibrate's CompassData runs, reached through tagcore_c (chapter 8). The engine feeds it samples, ticks its quality update every 200 ms, formats its figures as qtcalibrate's labels did, logs the same four debug lines per update, and keeps the capture file.

Replay. TAGCALIBRATE_REPLAY=<capture.json> feeds a saved capture one sample per tick instead of attaching, as qtcalibrate's --replay-capture did, so the whole engine runs without a tag and its tests compare its log with qtcalibrate's for the same capture.

Code map (host/applications/tagcalibrate-rs):

Path Holds
core/src/engine/mod.rs The loop, as Tag Monitor's
core/src/engine/calibrator.rs Calibrator: every command, report and timer; the session; prompts
core/src/engine/tagthread.rs Attach, the stream, the status poll, constants, the stop-and-wait
core/src/engine/view.rs The view state: constants, quality, sphere points, orientation readouts
core/src/engine/capture.rs The capture file, read and written in qtcalibrate's schema
core/tests/replay_log.rs, imutag_axes.rs The replay log against qtcalibrate's; the IMUTag fixture's dip
ui/views.js, three.min.js The sphere and tag model (three.js), the heading dial

Checks. calibsession's replay test reaches every figure qtcalibrate logged for the reference capture; the engine's replay logs the same 1,552 quality lines character for character; the user calibrated an IMUTag through it, which found a firmware axis-mapping fault and became a fixture.

Deliberate differences. three.js graphics in place of magPlot and the QML views (drawn and turned as those did); quality figures in a grid rather than Courier columns; a Tag menu and a Log menu; Quit waits for IDLE and Attach recovers a tag left calibrating, which qtcalibrate did not.

Read: README.md, design/architecture.md, design/ui-engine-contract.md, decision 0039.

Tag View (tagview)

Job. View a downloaded SQLite log of any family: the streams on stacked axes against time, with hover readouts, cursors, zoom, event markers, gap breaks, per-stream ranges, colours and axis sides, a compass panel for compass logs, per-tag preference files, File Info, and PDF export. A port of the Qt SensorViz's window; the Qt application keeps its name while both ship.

Shape. One engine thread and no tag. The engine owns the open log as a sensorviz_session::Session: sensorViz's C++ core (chapter 5), so the streams, transforms, plot specification, hover text and preference files are the Qt application's. The page draws the core's plot spec with uPlot, fetching M4 envelopes from the engine for the visible range at the plot's width, and asks for the hover readout and the compass sample at the pointer. Every setter on the session returns change bits, so the engine republishes only what changed.

Printing is File ▸ Export PDF: the page draws the view again offscreen at A4 landscape and 300 dpi from envelopes fetched for that width, and places it in a PDF with jsPDF, the title, metadata box and legend as PDF text. The renderer and the printing method were chosen by a spike whose results are in the README.

Code map (host/applications/sensorviz-rs):

Path Holds
core/src/engine.rs Engine, the loop, Viewer: every command and query (tabled in its doc comment), status line, File Info, preferences, the PDF save
core/tests/engine.rs The engine on the committed fixtures
ui/plot.js The uPlot chart from the spec: stacked axes, time axes, markers, gaps, cursors, zoom, the metadata box and legend, the PDF export
ui/app.js The Qt menus and dialogs, the tabs, the compass panel
ui/uPlot.iife.min.js, jspdf.umd.min.js uPlot 1.6.32 and jsPDF 2.5.2, vendored
tools/export_mock_data.py Writes fixture data for the browser mock (generated, not committed)

Checks. sensorviz-session's tests against the core's state dump; core/tests/engine.rs on the fixtures; the core itself against the Qt application by session_cli and the state dump.

Deliberate differences. Auto axes take the data range padded by 5 % (the Qt plot also includes zero and the previous range); printing is PDF export rather than the system print dialog; no activity filter (it stays in the Qt application); Axis Sides lists streams rather than axis groups.

Read: README.md, design/architecture.md, the leaving-Qt proposal (accepted; the plan this is step 4 of), and the Qt application's toolkit-neutral core proposal.

The Qt applications, briefly

They are the references the ports are held to, and they ship beside them until retired. qtmon is structured like its port (a Qt-free core/, a tag session on a worker thread, a window that draws), by design. qtprogram is the batch station, through STM32_Programmer_CLI. qtcalibrate's CompassData is now an adapter over the C++ session the Rust application uses. sensorviz's core/ is the one Tag View runs; its Qt layer holds QCustomPlot, the activity filter and the screenshot hooks. btdataviz is a deprecated BitTag viewer. Each has a README; qtmon/design/architecture.md is worth reading alongside Tag Monitor's.