Skip to content

epic(#726): in-app radio preset catalog and picker (Companion/Observer) - #744

Merged
diffwireauto merged 9 commits into
devfrom
feat/726-preset-catalog
Oct 2, 2026
Merged

diffwireauto merged 9 commits into
devfrom
feat/726-preset-catalog

Conversation

@diffwireauto

Copy link
Copy Markdown
Member

Epic B of Feature #647 (plan of record: docs/initiatives/standalone/647-mesh500-presets/PLAN.md). Epic: #726.

What this changes

The companion and observer Radio Settings presets now come from a catalog instead of a hardcoded list:

What the user gets:

  • Presets work offline from copies bundled in assets/radio_presets/, and refresh without an app release from config-profiles. The picker refreshes when opened if the list is over 12 hours old, or on demand with its refresh button.
  • The picker is grouped by region, each preset labelled MeshCore or Offband, with a status line. A failed refresh stays on screen in the error colour and keeps the saved list.
  • Coding rates are corrected to MeshCore's values. For example EU/UK (Narrow) is now CR8, where the old list had CR5.
  • Applying a preset uses stock commands only: cmd 11 (radio params), cmd 12 (TX power, only when the preset sets one), and cmd 61 (path hash, converted from published bytes to the zero-based firmware mode). Off-grid repeat behaviour is unchanged.
  • Credits: an About card line and a licenses-page entry for Liam Cottle / MeshCore.
Task Commit
#727 preset model + parsers 0ed9bd7
#649 path hash bytes to firmware mode d4a7177
#728 bundled copies, refresh, merge, last-good cache (closes #648) 3c7c4d9
#729 grouped picker, id-based selection, hardcoded list removed 2f0f036
#730 apply path hash on save (and a corrected kHz comment in the protocol) c0ec6c6
#731 credits a4754b8
#732 pre-PR review fixes 67158bc

Verification (B6, #732)

CI-equivalent gates, run at the branch head:

  • check_no_emdash.py: OK.
  • flutter analyze --fatal-infos --fatal-warnings: no issues.
  • dart format --set-exit-if-changed: 0 files changed.
  • flutter test: all passed (1 skip predates this work).
  • Builds: Windows release, and a release-signed APK.

New tests:

  • the model and parsers;
  • the service, including an overlay edit reaching the app with no new build;
  • the picker widget (grouping, labels, headers not selectable, refresh-failure display);
  • frame bytes, against stock firmware's handlers.

Hardware validation is the owner's, at Epic D (#737), per the plan.

Pre-PR Gemini review

scripts/llm-consult.py --backend gemini --model gemini-2.5-pro. Audit log ~/.claude/llm-consultations/meshcore-open/2026-10-01-gemini-epicB-prompt-gemini-gemini-2.5-pro.log. Cost $0.0295.

# Finding Disposition
1 RadioPreset.frequencyHz returns kHz Fixed: renamed frequencyKHz in the new model. Its suggested call-site change used buildSetRadioFrame, which doesn't exist (rejected). The older _RadioSettingsSnapshot.frequencyHz is left as it was (out of scope)
2 A preset picked during a refresh can vanish when the refresh lands Fixed: the picker is disabled while a refresh runs
3 selectedItemBuilder looks up each title linearly Fixed: id-to-title map

Agent: MagentaHawk (session 5a8922d6)

🤖 Generated with Claude Code

diffwireauto and others added 7 commits October 1, 2026 04:13
RadioPreset carries frequency, bandwidth, SF, CR, and optional TX power,
path hash (bytes) and off-grid flag, plus its region and source.
parseUpstreamPresets reads the mirrored MeshCore list (string numbers);
parseOverlayPresets reads Offband's overlay (JSON numbers, retired
entries dropped). Malformed entries are skipped and counted; a wrong
format or a newer format_version is rejected. Upstream titles carry no
region, so upstreamRegionFor derives the picker group from the title.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Presets publish path hash size in bytes; firmware takes a zero-based
mode (1 byte = mode 0, 2 = 1, 3 = 2). RadioPreset.pathHashMode does
the conversion and returns null when the preset carries none or an
out-of-range size, so nothing is sent. Tests pin all three sizes and
that a published 2 never becomes mode 2.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom config-profiles

RadioPresetService starts from the last good copy of each preset file,
or the copy bundled in assets/radio_presets/ (config-profiles 857d4e9),
so the picker works offline. refresh() fetches both files from
config-profiles; each file that parses becomes the new saved copy, and
one that fails or comes back empty never replaces a good one. Failures
are logged and kept in refreshError for the picker to show.
refreshIfStale() refreshes at most every 12 hours. The merged list puts
the overlay over upstream on a matching title, sorted by region.

An overlay edit in config-profiles therefore reaches users on their
next refresh with no app release (test: "an overlay edit reaches the
app with no new build"). Closes the gap recorded in #648.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… source

Radio Settings now takes its presets from RadioPresetService instead of
the hardcoded list, which is removed (radio_settings.dart keeps only the
LoRa enums). The picker, extracted as RadioPresetPicker for reuse on the
remote-node screen, groups presets under non-selectable region headers,
labels each MeshCore or Offband, and shows where the list came from with
a refresh button; a failed refresh stays on screen in the error colour.

Selection is tracked by preset id rather than list index, because a
refresh can change the list under an open form. Upstream presets carry
no TX power, so a preset without one matches any power and applying it
leaves TX power as it is. The form refreshes a stale list when opened.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…save

Saving Radio Settings already sends radio params (cmd 11) and TX power
(cmd 12). When the selected preset carries a path hash size, save now
also sends CMD_SET_PATH_HASH_MODE (61) with the zero-based mode. It is
sent whenever the preset has one, not compared against the reported
width, because that reads as 1 when device info is short (#240).
Off-grid repeat handling is unchanged.

The CMD_SET_RADIO_PARAMS comment said the frequency was in Hz; stock
firmware stores freq / 1000.0 as MHz, so it is kHz. Comment corrected.
A frame test pins the bytes a preset puts on the wire.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The About card gains a credit line under the existing MeshCore Open
attribution, and the licenses page gains a "MeshCore suggested radio
presets" entry naming Liam Cottle, api.meshcore.nz, MeshCore and the
config-profiles mirror, alongside the Open-Meteo entry.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RadioPreset.frequencyHz returned kHz; renamed frequencyKHz (the unit
CMD_SET_RADIO_PARAMS carries). The picker is disabled while a refresh
runs, so a pick can't vanish when the refreshed list lands, and the
selected-item builder uses an id-to-title map.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as Canada

MeshCore's "Canada" and "USA" share 910.525 / 62.5 / SF7 / CR5 and
differ only in path hash (Canada 3-byte). Matching ignored path hash,
and Canada sorts first, so a USA radio showed "Canada" (D1 screenshot).
A preset with a path hash size now matches only a radio on that size;
presets without one match as before.

Epic: #726
Agent: MagentaHawk (session 5a8922d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every preset test faked the network, so presets missing from
config-profiles main passed them all and reached the owner as
"Couldn't update presets" on a test build twice. The live test
refreshes from the real URLs and fails on any refresh error; a second
live test proves a missing file comes back as HTTP 404. The picker
test now checks that a good refresh shows no error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task: preset list is a hardcoded 46-entry Dart literal with no update path -- and zero 500 kHz options

1 participant