0032. Host: configuration shape from default configs, fixed panels per tag type
Date: 2026-10-23
Context
proto3 does not record whether a zero-valued scalar was set, so a decoded
Config cannot say which fields a tag supports. qtmonitor answered that at
build time. A CMake script regex-scanned each tag's default-config.json into
a table of twenty booleans per tag type. Four configuration modules showed and
hid their widgets from it, field by field, and relabelled themselves by tag
type.
That arrangement had these costs:
- One field was described in about seven places: the proto; the struct;
the generator's field list; the generator's regex; each module's
SetConfig(), an unused value-guessingSetConfig(), andGetConfig(); and a hand-written restore merge. - The regex ignored nesting. It matched
range,freq,filter,odrandchannelsanywhere in a file. - Restore and
tag-start --mergedisagreed. Restore skipped zeros and copied whole sub-messages;MergeFromskipped zeros and copied fields the tag lacked. Neither could set a value to zero. - What Start sent depended on the selected sub-tab. The accelerometer
module wrote four fields only when their widgets
isVisible().
Decision
Shape. tagcore's ConfigShape holds each tag type's supported fields as
dotted paths. They are read at run time from the default configs, whose text
tagcore's CMake embeds without interpreting. The walk is checked against the
Config descriptor at each level and keeps zero-valued keys.
One merge. ConfigShape::merge() copies a field only if the tag supports
it and the file speaks for it. Restore and tag-start --merge both use it.
ConfigShape::forFile() limits a file to its own tag type's fields, because
qtmonitor's Save pads files with zeros.
Fixed panels. qtmonitor's Configuration tab is one page of panels, each built once with fixed labels and ranges. The tag type selects the panels shown, from one table. Each panel reads and writes only the fields it lists, whatever is on screen.
- The schedule panels are three variants of one class, because they share the "Logging Schedule" box.
- The accelerometer has one variant per meaning of the reused
adxl362fields. qtmonitor --check-config-panelschecks the table against every shape.
Equivalence proven on the host. A headless probe
(qtmonitor --config-probe) runs 686 generated cases through the real tab.
It was added to the old code first, and its results were committed as goldens.
Every later step was diffed against them, and each accepted difference is
listed with its reason.
Deferred: tag-reported presence. Making Config's scalars proto3
optional, with a marker field, would let the tag report its own shape. It is
kept as a proposal:
tag-reported configuration presence.
The model as it now stands is described in Tag Configuration Model.
Alternatives considered
- Tag-reported presence now. It is wire-compatible:
optionalchanges no bytes, and old tags can be recognised by a missing marker. But it needs every nanopb output regenerated, firmware work in each family, and an audit of JSON consumers. Its only advantage over default configs is catching a firmware/default-config mismatch, which has not yet caused trouble. - A frozen, checked-in table of shapes. It would cut the build's tie to
embedded/proto-c/, but it would be a second copy of each tag's field list. - A form generated from the protobuf descriptors. Most of what the panels do is tag-specific: limits that follow other fields, fields that mean different things on different tags, and the schedule's date handling. A generic form would still need all of that written by hand.
- Generic binding tables (widget ↔ path) for load and store. These were
planned, but exact equivalence needed panel-specific code, so each panel's
load()andstore()are explicit instead: - the accelerometer's load order, and its clamping;
- the filter's fallback to 0;
- the BitTag log panel hiding itself;
- BitTagNG's
accel_type. - Separate start-delay and sampling-period panels. They would have changed the screen. The three schedule variants keep it as it was.
- Per-tag hardware testing. The configuration screen is a function from
ConfigtoConfig, and the firmware did not change. The probe proves the output identical for every tag type; one live start covers the USB path.
Evidence
From host/applications/qtmon/test/expected-differences.md and two live runs
on a BitTag LE (UUID 2035374D303150190059002F, firmware 56e5e6a0):
Sub-tab defect. It was confirmed in 69 of 686 cases, and fixed by testing
!isHidden() in place of isVisible(). Only those 69 cases changed. Live: an
inactive threshold of 0.50 g, set on the Sensors tab with Start pressed from
the Schedule tab, was stored as 0.498 (it had been sent as the old 0.35).
ConfigShape and the merge. ConfigShape reproduced the generator's table
for all eight tag types (config_shape_check). The merge changed 68 restore
cases and nothing else:
- one-field files no longer wipe the rest of a message. Before, a one-field
adxl362file switched a BitTagNG to ADXL362; - stated zeros are copied;
- another tag's file contributes only shared fields.
Live: tag-start --merge with a padded BitTagNG file sent the LE 0.3 g and 6
samples, and kept its own 0.35 g and log format, with no accel_type. The tag
stored exactly that.
Panels. Nothing sent or shown changed, except hibernation capped at the
firmware's two intervals (7 cases). 130 restore cases differed only by stale
rows in the old goldens, an artefact of the old deleteLater(); each was
checked to be exactly the new rows plus the base configuration's.
Live, on the same BitTag LE:
- in the new panels, the inactive threshold was set to 0.40 g, the inactivity count to 5 samples, and the log format to bit per second;
- the configuration was saved, the active threshold changed as a decoy, the file restored, and the tag started;
- the saved file and the tag's stored configuration both held exactly those values, with the active threshold back at 0.50 g (stored as 0.498).
Consequences
Adding a field takes four places: the proto, the default configs that list it, the firmware conversion, and the panel that owns it. The cross-check fails if the default config or the panel is missed.
Adding a tag type needs a default config and one row in the panel table.
The default configs must list every supported field, zeros included. A
mismatch with the firmware is not caught on the host: UIUCTag's firmware
reports accel_type, which its default config omits, so it passes through
unchanged.
The configuration probe is the gate for any change to the configuration
screen. Its goldens are regenerated only with an expected-differences.md
entry.
Generated documentation screenshots are one page per tag type.