Tag Calibrator (Rust): UI/engine contract
Tag Calibrator in Rust is split as Tag Monitor is:
core/(tagcalibrate-core): the engine (src/engine). No UI toolkit. Every rule and all state are here.ui/: a page onhost/ui-kitthat draws the view state -- including the sample sphere, the heading indicator and the tag model -- and turns clicks into commands. It holds no rules and computes nothing about the tag.app/: the Tauri shell, which runs the engine, forwards commands and queries, emits the engine's events, and turns a window close intoquit.
The behaviour is qtcalibrate's (MainWindow, and CompassData beneath it).
The calibration itself is not ported: it is the C++
calibcore::CalibrationSession that qtcalibrate's CompassData now runs, in
sensoranalysis's Qt-free core, reached through tagcore_c's tcs_* functions
and the calibsession crate.
Where this port differs from qtcalibrate, it says so below.
Threads
| Thread | Owns | Does |
|---|---|---|
tag (engine/tagthread.rs) |
the tagcore::Tag |
attaches; while streaming, reads the calibration log every 100 ms (qtcalibrate's sample timer); otherwise reads the status every second, which keeps the tag awake; writes and reads constants |
engine (engine/calibrator.rs) |
the calibration session, the sample capture, the display settings, the prompt, the timers | applies commands, the tag thread's reports and the timers; publishes the view state (engine/view.rs) |
The engine thread never touches the tag. While collecting from a tag it recomputes the quality figures every 200 ms (qtcalibrate's quality timer).
Stopping a stream waits for IDLE. The tag keeps its CALIBRATE state in
its backup registers and acknowledges Stop before its main thread acts on it,
so a tag detached on the acknowledgement stays calibrating through the next
attach's reset, and refuses everything but Stop. The tag thread therefore
polls the status after Stop until the tag reports IDLE (up to 2 s), whether
the stream ends by Stream off, Detach or the thread ending; quit waits for
the detach before the app exits (up to 5 s); and Attach stops a tag it finds
still calibrating rather than refuse it.
Replay
TAGCALIBRATE_REPLAY=<capture.json> replays a sample capture
(qtcalibrate's tag-designs.qtcalibrate.calibration-capture.v1) instead of
attaching, as qtcalibrate's --replay-capture does: the window looks attached
to the file, Stream feeds one sample per 100 ms, and the quality update runs
every second sample rather than on a timer, so the result depends on the
capture alone. At the end of the capture the stream turns itself off. The
engine's replay_period shortens the tick for tests; nothing else depends on
it.
Transport
As Tag Monitor's: invoke("command", {cmd, args}), invoke("query",
{name, args}), and events state and log. In a plain browser
ui/mock-engine.js stands in, with scenarios (?scenario=).
Nothing happens until the page sends ready. ready starts a replay when
one is configured, or attaches (unless TAGCALIBRATE_NO_ATTACH is set), as
qtcalibrate does when its window opens.
View state (event state)
{
"title": "Tag Calibrator v1.0",
"link": { "attached": true, "text": "IMUTAG · UUID 2035..." }, // or "Replay: <file>"
"stream": { "on": false, "enabled": true }, // the Stream check box
"actions": { // { visible, enabled } for each command
"attach": ..., "detach": ..., "start": ..., "stop": ..., "clear": ...,
"tag_write": ..., "tag_read": ..., "save_capture": ...
},
"calibration": { // "--" until a calibration exists
"offset": ["+0.027", "+30.893", "+210.320"], // V, µT, %+.3f
"mapping": ["+0.980 -0.000 +0.014", "...", "..."], // rows of A
"field": "45.66", // B, µT
"accelOffset": { "text": "-29.4 -5.2 -67.1 = 73.4 mg", "warn": false } // " CHECK", warn, beyond 250 mg
},
"quality": { // "--" before the first quality update
"spread": "0.43", "worst5": "0.94", "hardIron": "0.02", "fitError": "0.43",
"coverage": "90/100", "evenness": "0.72", "attitude": "2.06",
"dip": "63.5 ± 2.7" // "no accel" without accelerometer figures
},
"sphere": {
"radius": 45.66, // B, µT: the fit's, or 50 before one
"points": [x, y, z, ...], // the buffer (up to 650), calibrated, µT
"newest": [x, y, z] // the sample last collected, as plotted; or null
},
"orientation": { // the live compass, while streaming
"valid": true,
"heading": 315.2, // degrees, declination and battery applied: the dial
"quaternion": [w, x, y, z], // the tag model's rotation
"readouts": { "heading": "315", "roll": "-18", "pitch": "12",
"dip": "63", "field": "46", "gravity": "1,038" }
},
"settings": { "declination": 0.0, "declinationText": "0.00", "batteryForward": true },
"prompt": null // see Prompts
}
Every text is formatted by the engine as qtcalibrate formats it: the
constants and figures as its labels do (calibration_update,
updateMagQualityDisplay), the readouts as MyCompass.qml does -- heading
plus declination, and with the battery backward the heading turned round and
pitch and roll negated, each rounded to a whole number with thousands
separators. The heading, pitch and roll come from display_angles() and the
model's rotation from model_rotation(), both the C++ conversions qtcalibrate
uses.
The page turns the sphere as magPlot does: the newest point to the centre by the shortest rotation, plus the user's drag. That is drawing, not a rule, so it is the page's.
Prompts
As in Tag Monitor: the engine shows one prompt at a time with a fresh id,
and the page answers with answer {id, button, index?, path?}.
kind |
Fields | Buttons |
|---|---|---|
alert |
title, text, informative, style (warning, info) |
ok |
chooseBase |
title, label, items[] (each base with its identity, as in Tag Monitor) |
ok with index, cancel |
saveFile |
title, suggested (qtcalibrate-samples-<stop time>.json in the home folder), filters |
ok with path, cancel |
Commands
cmd |
args |
Does |
|---|---|---|
ready |
-- | the page is listening: start the replay, or attach |
attach, detach |
-- | find the bases (choosing when there are several) and attach; or detach, ending any capture and stream |
set_stream |
{on} |
the Stream box: on sets the tag's clock and enters calibration; off acts as Stop first, then stops the tag |
start, stop, clear |
-- | collect samples into the session and the capture; stop; discard the samples, the fit and the capture |
tag_write |
-- | append the constants to the tag: the magnetometer's, and the accelerometer offset beside them (zero, meaning no correction, without a fit) |
tag_read |
-- | install the tag's last stored constants; the field is recomputed from the buffer |
save_capture |
-- | asks where, then writes the capture in qtcalibrate's schema |
set_declination |
{degrees} |
degrees east, added to the heading |
set_battery_forward |
{on} |
which end of the tag is forward |
answer |
{id, button, index?, path?} |
answers the shown prompt |
quit |
-- | File ▸ Quit and window close: stops the stream, detaches, then emits Exit |
set_log_level |
{level} |
TRACE..ERROR |
save_text |
{path, text} |
Save Log |
Each command is ignored unless actions enables it. Attach refuses a tag
that is not IDLE after the recovery above: one running a deployment or
holding data is not one to calibrate.
Queries
name |
Returns |
|---|---|
menus |
{menus: [{title, entries: [{kind, id, label, tooltip, shortcut}]}]} |
about |
{title, text} |
user_guide_url |
URL |
Logging
Each quality update logs qtcalibrate's four DEBUG lines -- magquality:,
residual:, gravity:, retention: -- in its format, so a replay's log can
be compared with qtcalibrate's line for line (core/tests/replay_log.rs).
The capture file
Written as qtcalibrate's saveSampleCapture writes it: the same keys, sorted
as QJsonObject sorts them, four-space indentation, each float written as the
double it widens to, and a tag block without the UUID or source path, so a
capture is safe to commit as a fixture. A capture saved from a replay carries
the replayed file's tag block. Captures from either program replay in both.
Deliberate differences from qtcalibrate
- The sphere is three.js, drawn and turned as magPlot draws and turns it. It shows the buffer, calibrated with the current fit, rather than magPlot's list, which grows by each point as collected until the next fit replaces it.
- The quality figures are a labelled grid rather than Courier columns.
- The menus add a Tag menu (the collection commands, Stream, Tag Write and Read) and a Log menu, as in Tag Monitor; qtcalibrate's File ▸ Save Calibration and Read Data File had no handlers and are not carried over.
- The bottom bar names the attached tag, or the replayed file.
- Quit stops the stream and waits for IDLE, and Attach recovers a tag left calibrating; qtcalibrate does neither.
- No screenshot automation in the engine:
ui/screenshots.shcaptures pictures from the browser mock.