qtprogram as a batch programming station
qtprogram finds its programmer, base, tag and firmware by itself, and remembers them between sessions. For each tag the operator presses Program, types the board's label and confirms. The station stops and erases the tag, programs and tests it, and leaves it IDLE. It appends one row per attempt, failures included, to a CSV. Next Tag releases the tag for the next board. The choices behind this design are in decision 0034.
| Piece | File | Owns |
|---|---|---|
Station |
station.* |
The checks, the base and tag, the firmware choice, the record, settings |
ProgramSession |
programsession.* |
The program sequence for one tag |
ProgrammerCli |
programmercli.* |
Finding, running and parsing STM32_Programmer_CLI |
FirmwareCatalog |
firmwarecatalog.* |
GitHub releases, their download, extraction and cache |
FirmwareImage |
firmwareimage.* |
One ELF: identity record, manifest check, SHA-256, erase plan |
RecordLog |
recordlog.* |
The CSV |
MainWindow |
mainwindow.* |
Display and dialogs only |
planFlash() |
flashplan.* |
The erase rule (README) |
Start-up checks
The station panel shows one light per check. The hardware checks run in order, and a failure stops the checks after it. The programmer, firmware and record checks stand alone.
| Check | Passes when | Otherwise |
|---|---|---|
| Programmer | The saved path, or findProgrammer()'s search, runs --version |
Red. Choose… takes the tool itself, or a folder or bundle holding it: its bin, the install folder or the macOS bundle (a file dialog, since Qt's dialog shows a bundle as a file and cannot enter it) |
| Base | An ST-LINK is on USB and, when there are several, it can be named | No base: amber, and USB is polled every second, without attaching. Several: the operator picks one, and the serial is saved; Station ▸ Choose Base… picks again at any time |
| SWD | -c port=SWD mode=UR (with sn=<serial> when several ST-LINKs are on USB) reports a device ID |
Red, with the CLI's first Error: line |
| Supply | The ST-LINK's target voltage is in range | Red above 3.6 V on an STM32L432 (its absolute maximum), and Program is disabled. Amber below 1.8 V |
| Tag | tagcore attaches and GetTagInfo answers |
Amber: a blank board, one in its bootloader, or foreign firmware. The tag type must then be chosen by hand |
| Tag state | IDLE | FINISHED or ABORTED: amber, with Erase, which the tag's own firmware does. Deployed: amber, with Stop. Program does both in any case |
| Firmware | The image is present, matches its manifest, and is for the tag's MCU | Red, and Program is disabled. Amber if the image is for a different target from the one the tag runs, or was built from a dirty tree |
| Record | The CSV is open and can be appended to | Red, and Program is disabled until it is fixed |
Attaching resets the tag. The station attaches by itself only after the operator has answered yes, once, to "Attach automatically?". The answer is saved, and Station ▸ Attach Automatically changes it. Until then the checks stop after Base and wait for Attach.
The SWD check and the tagcore attach both use the ST-LINK. On macOS the first open after the CLI exits fails, so the attach retries every 200 ms, up to ten times. Nothing reaches the tag until an attempt succeeds.
The base's serial
The base is named to the CLI by its ST-LINK serial (sn=). tagcore reads that
serial from the USB descriptor in LinkAdapt::Available(), which opens the
device without claiming it, so the tag is not reset. On the bench base, an
ST-LINK/V2 with firmware V2J37S7, macOS, tagcore and STM32_Programmer_CLI -l
st-link all report 0037001A5556431520303537. Old ST-LINK/V2 firmware reports
its 12-byte serial as raw bytes; tagcore hex-encodes it as the CLI and OpenOCD
do. A serial tagcore still cannot read is taken from the CLI's probe list when
exactly one listed probe is not already named by another base.
With a single ST-LINK on USB the serial is recorded but not passed: the CLI is
called without sn= and uses the only probe, so no serial, and no difference
between tagcore's and the CLI's reading of one, can keep a lone base from
working. The serial matters only for telling several bases apart.
Identifying images and tags
Every image this tree builds carries a tag identity record after its vectors
(tag_identity.h). The record holds the build target, the board, the TagType
and the git hash. A running tag reports the same fields through TagInfo, so:
- The Tag type drop-down lists build targets (
BitTag,CompassTagAT25, …), the names of a release's directories. It does not list TagTypes, because one TagType covers several targets. - A tag that answers selects its target, from the last part of
TagInfo.source_path(/embedded/tags/BitTag→BitTag), and the drop-down says "detected from the tag". The bench BitTag reports TagTypeBITTAG_LEand boardBitTag V6, which is why the target, not the TagType, is matched. - A file chosen by hand names its own target, from its record.
- The MCU family is checked against the SWD device ID: 0x435 is the STM32L432 and 0x454 the STM32U375. The image's family comes from its flash page size.
Build manifests also record tag_type and board, from the same record, from
the release after fw-v0.7 onwards. The station reads the record itself, so it
handles older releases and custom images the same way.
Firmware
The Firmware drop-down lists the published fw-v* releases, newest first,
then recent ELF files, then Other ELF file…. The repository is public,
so GET /repos/tag-designs/software/releases needs no token, and drafts are
not listed. A release without a tag-firmware-<tag>.tar.gz asset is not
offered.
Choosing a release downloads its archive once, into
QStandardPaths::AppDataLocation/firmware/<tag>/. The station lists the
archive with the system tar and keeps only member names of the form
firmware/<T>/<T>.elf and firmware/<T>/<T>-build-manifest.json. It then
extracts exactly those names, verifies every image against its manifest, and
only then marks the release cached. The last listing is kept in
releases.json, so cached releases are still offered offline.
An image is loaded again, and hashed again, whenever it is chosen. An image
whose bytes do not match its manifest is refused; a release image must have a
manifest. An image with no __tag_flash_page_size__ symbol (the L432 images
of every release before fw-v0.7) gets a mass erase: its calibration and
persistent regions cannot be trusted to be where a current tree puts them, and
the release notes for fw-v0.7 already require a mass erase when a calibrated
tag leaves fw-v0.6. The flash size ChibiOS linked it for
(__flash0_base__..__flash0_end__) still names the MCU, so the check that
the image fits the tag stands. A file without a manifest is usable: its SHA-256 is still recorded,
and its version is custom. Release Notes opens the release page, which
says what was qualified.
The Firmware light turns amber, and the Program dialog repeats the warning,
when the chosen release is not qualified for the chosen target. A release
counts as qualified for a target when it carries that target's
qualification-<Target>-<date>.tar.gz asset, the evidence the release
procedure uploads as each target is cleared; the listing already returns the
asset names, so this needs no extra request and no parsing of the release
notes. Offline, before any listing has been read, qualification is unknown and
says so. A local ELF file is never qualified. It is a warning, not a refusal:
bench and development programming use unqualified images. A matching hash shows that the image is the archived
one, not that it is fit to fly.
Programming one tag
Program opens one dialog. It holds the label, typed by the operator with nothing suggested. It shows the tag, the image (release, commit and SHA-256), the erase, and what is lost and what is kept. It warns, without refusing, when the label is already recorded for another UUID, or the tag under another label, or when the image is for a target other than the one the tag runs.
ProgramSession then runs the sequence with a one-second tick, and the
programmer runs asynchronously:
- Stop a running tag, and have its own firmware erase a FINISHED or ABORTED log. A tag that does not answer skips this step.
- Program: the erase and the write in one CLI call with
sn=, after detaching tagcore. - Connect to the new image, retrying every second, six times. If it does not answer, the window asks for the base to be unplugged and plugged in again, and offers Retry and Give Up. The station writes no flash registers (FLASH_SR.PEMPTY).
- Test: read
TagInfo, compare its git hash with the image's, set the clock, and run the self-tests. The tag is left attached and IDLE.
The programmer does not start the tag: a start belongs to a deployment. An
earlier version finished by starting, stopping and erasing it once more; on a
PresTag the start left the RTC write-protected, and the clock setting after
the final erase hung the firmware (embedded/tags/TODO.md, Defects).
Cancel is accepted before step 2 and at step 3, never while the programmer is
writing. The self-tests disturb the tag's clock, so the session sets it again
(Tag::SetRtc()) once they report. The
Tag panel shows the clock offset from every status poll, amber beyond
qtmonitor's 2 s tolerance, with Sync to set it by hand. Retest runs step 4 alone on the attached tag, and records it as a
row of its own. It first finds the image the tag should be running: the chosen
one when the tag reports its commit, otherwise any downloaded release's image
for the tag's target built from that commit, so a tag left on an older release
is checked against that release. A commit is a claim the firmware makes about
itself, so when the tag is IDLE the station then reads back, with
STM32_Programmer_CLI -u, the span of internal flash the image's loaded
segments occupy, and compares every loaded byte. NOLOAD regions (calibration,
stored configuration, the persistent area, the NAND map) have no file data and
are not compared, so a calibrated tag still matches. The read resets the tag,
which is then attached again for its tests. The row names the image and its
release when the flash matches; when it differs the result is MISMATCH.
When no image matches the commit, or the flash cannot be read, the image and
version stay empty and the row carries only the commit the tag reports.
The record
The CSV holds one row per attempt that reached the programmer, and one per retest. Rows are appended and flushed, and the file is never rewritten. A file whose header does not start with the first seven columns below is refused.
| Column | Value |
|---|---|
label |
Typed by the operator |
tag_type |
Build target |
uuid |
From the tag after programming, else from before |
firmware_version |
fw-vX.Y, or custom |
git_hash |
Reported by the tag after programming, else the image's |
date |
UTC, ISO 8601 |
test_status |
TestResult name |
result |
OK, TEST_FAILED, MISMATCH (the tag's commit is not the image's), NO_ANSWER, PROGRAM_FAILED: …, FAILED: … |
image_sha256 |
Computed when programming, since a tag cannot report the hash of its own image |
image |
ELF file name |
erase |
none, below-calibration, all |
voltage_v |
From the SWD check |
probe_serial |
The base's ST-LINK serial |
host_version |
The host package version |
Show CSV opens the file's folder. The first time, the station offers to
create Documents/Tag Programmer/programmed-tags.csv, to open an existing
file, or to create one elsewhere. The window's batch line counts this
session's OK and other rows.
Settings
QSettings, organization tag-designs, application qtprogram:
programmer, firmwareRelease or firmwareFile, recentFiles, target,
recordPath, baseSerial, resetsAcknowledged and lastLabel. qtprogram
1.x's lastImage is read as firmwareFile when nothing else is set.
Checking it
qtprogram_checks(-DBUILD_QTPROGRAM_CHECKS=ON) needs no hardware or network. It checks the CLI parsers against captured output, the CSV, the archive member filter and the release-list parser. Given a release archive, it also checks the whole download, extract and verify path through afile://URL. See test/README.md.qtprogram --verify <elf|dir>…checks images as Program would, without hardware.qtprogram --bench-program <label>runs the checks and then programs the tag on the base without the dialog, and exits with the result. On 2026-10-24 it programmedfw-v0.7onto the bench BitTag (UUID2035374D303150190059002F, 3.23 V) in 13 s: mass erase, ALL_PASSED, andtag-infothen reported commit169cda2cand IDLE. No power cycle was needed.