qtmonitor architecture
qtmonitor is three layers. - A tag session owns the tag. In live use its worker thread makes every monitor call. - A Qt-free core holds every rule: which commands are available, the start checks, what each status means, the configuration document and the download loop. - The window draws the core's model and turns the user's gestures into session commands.
The window never touches a tag and never waits on one. The layers are plain C++ (the core), a Qt interface with two implementations (the session), and Qt Widgets (the window). Each part is checked on its own, mostly without a tag. This layout is also the shape a Rust and Tauri port would take; see decision 0036.
Layers and files
MainWindow (mainwindow.cpp) ── draws the model; commands → session
│ ConfigTab (tagconfiguration/) ── panels and dialogs over ConfigDocument
│ TagStatusStrip, LogWindow ── views
▼ commands ▲ reports (signals, GUI thread)
TagSession (session/tagsession.h)
├─ LiveTagSession ── TagWorker on its own thread owns tagcore's Tag
└─ FixtureSession ── replays a captured tag for screenshots
core/ (qtmon_core, no Qt)
commands · menus · startchecks · tagrules · monitormodel · configdocument · downloadjob
| Part | Files | Holds |
|---|---|---|
| Command rule | core/commands.h |
qtmon::Command, availability(): the one rule for every command; actionStateTable() |
| Menus | core/menus.h |
Every command's text, tooltip and shortcut, the window's own items, and the menu bar as a table; the window builds its actions and menus from it in one loop, and every popup menu shows the menu bar's menus as submenus |
| Start checks | core/startchecks.h |
startCheckFailures(): clock within 2 s, self-test ALL_PASSED, approved; the sentence for each |
| Tag rules | core/tagrules.h |
Downloadable states, erase sector count, clock offset, the version floor, how a base is listed |
| Monitor model | core/monitormodel.h |
The attached tag as known: identity, latest status, waits (self-test, start), erase progress, debug lines; applyStatus() returns what a status settled |
| Configuration document | core/configdocument.h |
The Config, source, file, unsaved-changes baseline, approval; reading and writing configuration files; what attaching does to it (onAttach()) |
| Download loop | core/downloadjob.h |
DownloadJob::run(): status, header, pages until the writer consumes nothing; cancel between pages. Which formats a tag offers, the suggested name, and the chosen file's extension (downloadPath()) |
| Session interface | session/tagsession.h |
Commands in, reports out as signals |
| Live session | session/livetagsession.h |
TagWorker (owns the Tag, polls every 400 ms, runs commands and downloads) on its own QThread; LiveTagSession queues commands to it |
| Fixture session | session/fixturesession.h |
A captured tag from host/docs/fixtures/qtmonitor/; commands are logged and ignored |
| Window | mainwindow.cpp, mainwindow_fixture.cpp |
Actions and menus built from core/menus.h; session reports into the model; render(); dialogs; screenshot capture, the wiring check and the menu printer |
| Configuration tab | tagconfiguration/configtab.h |
The panels over the document, file dialogs, recent files; editability from the tag's state |
How a status flows
- Read.
TagWorker::poll(), everyqtmon::kStatusPollMs(400 ms), reads the voltage andStatuson the worker thread and emitsstatusRead. - Delivered. The signal crosses to the GUI thread queued, through
LiveTagSession. AFixtureSessionemits the same signal directly. - Applied.
MainWindow::onStatus()callsMonitorModel::applyStatus(). That records the status and clock offset, and returns events for anything it settled: - a whole line of the tag's debug text;
- a self-test result;
- a start that succeeded or failed;
- an erase that finished.
- Drawn.
render()redraws from the model: the Tag page, the status strip, the Configuration tab's editability, the actions (throughqtmon::availability()), and erase progress. - Reported. The window acts on the events: Error Log lines, and non-modal message boxes.
A command goes the other way. A menu action calls a window slot, which calls
a TagSession method. That returns at once; the worker runs the call and
reports with a signal (startSent, eraseSent, commandFailed,
downloadFinished, ...).
Rules that live in the model, not in the calls
- Start is judged by the state the tag reaches.
Tag::Start()can lose its acknowledgement after the tag accepted the command. So the model records the start and waits for RUNNING or CONFIGURED. It reports failure only if the tag is still in another statekStartWaitMs(7 s) later. A start error the tag overcame is logged as a warning. - A self-test result is taken only from a status at least
kTestMinimumMs(1 s) after sending. An earlier status can still show the previous run's result. With no result within 60 s, the wait times out. - Erase progress comes from sRESET statuses. The maximum is the tag's own
erase_sectors_total_plus_onewhen it reports one, or an estimate otherwise, and never decreases. The erase ends when the tag reports IDLE. - A tag that stops answering is shown as such. A failed status read is
skipped silently, so the model records when the last status got through.
After
kNoAnswerMs(3 s) without one, the status strip marks the state "(no answer N s)" in red, and an erase in progress says so too. A download pauses the polls on purpose, so it does not count. The window redraws once a second with a live session to notice; a fixture session is never marked. - Approval belongs to the document. Any load, reset, clear or edit withdraws it; saving does not. The status strip's box only shows it and reports the user's clicks.
Threading
- One thread owns the tag. Only
TagWorker, on the session's thread, callsTag, so monitor calls never overlap and need no lock. Commands queue on that thread and run in order. - A download runs inside the worker's
download()slot. It blocks the status polls until it ends, which is what the tag needs anyway.cancelDownload()sets an atomic flag, the one thing another thread may do. The job stops after the current page and commits what it wrote. - Polls pause after Synchronize Clock and Self-Test. For 1.5 s after
either is accepted, while the tag cannot answer debug accesses reliably,
the worker skips its polls, then reads the status once. Polling through
that window only produced bursts of base errors (0x81; see
embedded/bases/TODO.md). The pause is shorter thankNoAnswerMs. - Logging is thread-safe. The worker logs through
qInfo()and tagcore'slog.c.main.cpp'slogOutput()queues any message from another thread to the Error Log's text box on the GUI thread. - Shutdown detaches on the worker's own thread.
LiveTagSession's destructor cancels any download, runsTagWorker::shutdown()there (blocking), then ends the thread. - No nested event loops while attached. Message boxes that only report
are shown with
open(), so a status arriving meanwhile cannot re-enter the window. Confirmations stay modal: erase, Start Anyway, choosing a base, unsaved changes.
Checks
| Check | Covers | Needs |
|---|---|---|
qtmonitor --check-action-states |
availability() against test/action-states.txt |
nothing |
qtmonitor --check-start-checks |
startCheckFailures() against test/start-checks.txt |
nothing |
qtmonitor --check-action-wiring |
The window applies the rule to the right actions, found by menu text | a display (offscreen works) |
qtmonitor --check-menus |
The menu bar and popup menus against test/menus.txt |
a display (offscreen works) |
qtmonitor --config-probe |
The Configuration tab over every case, against test/golden/ |
a display |
qtmon_core_check |
The document's and the model's rules | nothing (no Qt) |
qtmon_session_check |
One whole tag cycle through LiveTagSession and MonitorModel |
a bench tag; changes it |
| Screenshot capture | The window drawn from a FixtureSession |
a display |
How to run each is in test/README.md. The three tables, the probe goldens and the core checks are language-neutral, so a port of the core is checked against the same files.