refactor(ui): share radio preset picker between Settings and Repeater

Settings › Radio and Tools › Repeater had near-identical preset-picker
logic (name/list-index lookup, save/delete, popup build, selection
handling) plus their own copy of the nearest-bandwidth search — only the
target fields (companion params vs. dedicated repeater profile) and the
apply call differed.

Extract the shared logic into RadioPresetPicker (a Target of field
pointers + a Result the screen acts on), with no dependency on UITask so
each screen keeps its own apply/dirty/keyboard/alert handling. Move the
nearest-bandwidth search to nearestBwIndex() in RadioPresets.h. Removes
~6 duplicated methods and 4 state fields from each screen.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-21 22:32:47 +02:00
co-authored by Claude Opus 4.8
parent a8c7a200dc
commit 0e333aeb5e
4 changed files with 249 additions and 295 deletions
+39 -145
View File
@@ -1,9 +1,9 @@
#pragma once
// Tools Repeater — consolidates the repeater toggle, its flood "politeness"
// filters, an optional dedicated radio profile, and live forwarding stats on one
// screen. A dedicated screen (vs. the old Settings Radio sub-items) gives
// full-width rows, so the longer labels no longer collide with the value column,
// and keeps the relaying controls next to the numbers that show them working.
// Tools Repeater — consolidates the repeater toggle, its flood forwarding
// filters, and an optional dedicated radio profile on one screen. A dedicated
// screen (vs. the old Settings Radio sub-items) gives full-width rows, so the
// longer labels no longer collide with the value column. Live forwarding stats
// live separately on Tools Diagnostics.
//
// Network modes:
// - Current — repeat on the companion's current frequency. Opt-in only:
@@ -19,8 +19,8 @@
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include "icons.h"
#include "PopupMenu.h"
#include "DigitEditor.h"
#include "RadioPresetPicker.h"
#include "../RadioPresets.h"
#include "../MyMesh.h"
@@ -40,22 +40,13 @@ class RepeaterScreen : public UIScreen {
uint8_t _items[12];
int _item_count;
PopupMenu _preset_menu;
RadioPresetPicker _picker;
DigitEditor _freq_editor;
uint8_t _preset_user_slot[NodePrefs::USER_RADIO_PRESET_MAX];
int _preset_user_count = 0;
bool _preset_saving = false; // keyboard is open to name a new preset
bool _preset_deleting = false; // _preset_menu is showing the delete sub-list
// Nearest entry in LORA_BW_OPTS to the repeater profile's bw.
int rptBwIndex(NodePrefs* p) const {
int best = 0;
float best_diff = 1e9f;
for (int i = 0; i < LORA_BW_OPT_COUNT; i++) {
float diff = fabsf(p->repeater_bw - LORA_BW_OPTS[i]);
if (diff < best_diff) { best_diff = diff; best = i; }
}
return best;
// The dedicated repeater profile's fields, as the shared preset picker's target
// (Settings Radio points the same picker at the companion's own params).
RadioPresetPicker::Target rptTarget(NodePrefs* p) const {
return { &p->repeater_freq, &p->repeater_bw, &p->repeater_sf, &p->repeater_cr };
}
void buildItems(NodePrefs* p) {
@@ -98,69 +89,12 @@ class RepeaterScreen : public UIScreen {
return "";
}
// Name of the preset matching the repeater profile, or "Custom".
const char* rptPresetName(NodePrefs* p) const {
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
const RadioPreset& r = RADIO_PRESETS[i];
if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, r.freq, r.bw, r.sf, r.cr))
return r.name;
}
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[i];
if (!u.name[0]) continue;
if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, u.freq, u.bw, u.sf, u.cr))
return u.name;
}
return "Custom";
}
// Position of the repeater profile within the popup list built by
// openPresetMenu() below ("Save current..." at 0, then built-ins, then
// non-empty user slots in slot order) — or -1 ("Custom") if nothing matches.
int currentPresetListIndex(NodePrefs* p) const {
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
const RadioPreset& r = RADIO_PRESETS[i];
if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, r.freq, r.bw, r.sf, r.cr))
return i + 1;
}
int pos = RADIO_PRESET_COUNT + 1;
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
const auto& u = p->user_radio_presets[i];
if (!u.name[0]) continue;
if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, u.freq, u.bw, u.sf, u.cr))
return pos;
pos++;
}
return -1;
}
// Save the current repeater profile as a named user preset: overwrite a slot
// with the same name if one exists, else the first empty slot, else slot 0.
// Shared with Settings' freq/bw/sf/cr — these are the same 4 user slots, just
// populated from the repeater profile instead of the companion's own params.
void saveCurrentAsPreset(NodePrefs* p, const char* name) {
if (!p || !name || !name[0]) return;
int slot = -1;
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
if (strcmp(p->user_radio_presets[i].name, name) == 0) { slot = i; break; }
if (slot < 0)
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
if (!p->user_radio_presets[i].name[0]) { slot = i; break; }
if (slot < 0) slot = 0;
NodePrefs::UserRadioPreset& u = p->user_radio_presets[slot];
strncpy(u.name, name, sizeof(u.name) - 1);
u.name[sizeof(u.name) - 1] = '\0';
u.freq = p->repeater_freq; u.bw = p->repeater_bw; u.sf = p->repeater_sf; u.cr = p->repeater_cr;
_dirty = true;
_task->showAlert("Preset saved", 800);
}
void itemValue(int item, NodePrefs* p, char* buf, size_t n) const {
if (!p) { strncpy(buf, "OFF", n); buf[n-1]=0; return; }
switch (item) {
case IT_REPEATER: strncpy(buf, p->client_repeat ? "ON" : "OFF", n); break;
case IT_NETWORK: strncpy(buf, p->repeater_use_profile ? "Custom" : "Current", n); break;
case IT_RPRESET: strncpy(buf, rptPresetName(p), n); break;
case IT_RPRESET: strncpy(buf, _picker.currentName(p, rptTarget(p)), n); break;
case IT_RFREQ: snprintf(buf, n, "%.3f", p->repeater_freq); break;
case IT_RSF: snprintf(buf, n, "%d", (int)p->repeater_sf); break;
case IT_RBW: snprintf(buf, n, "%.1f", p->repeater_bw); break;
@@ -191,45 +125,17 @@ class RepeaterScreen : public UIScreen {
p->repeater_sf = p->sf; p->repeater_cr = p->cr;
}
// Layout: [0]="+ Save current...", [1..RADIO_PRESET_COUNT]=built-ins,
// [..+_preset_user_count]=saved user presets, ["- Delete preset..." if any].
void openPresetMenu(NodePrefs* p) {
_preset_deleting = false;
_preset_menu.begin("Repeater Preset", 6);
_preset_menu.addItem("+ Save current...");
for (int i = 0; i < RADIO_PRESET_COUNT; i++) _preset_menu.addItem(RADIO_PRESETS[i].name);
_preset_user_count = 0;
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
if (!p->user_radio_presets[i].name[0]) continue;
_preset_menu.addItem(p->user_radio_presets[i].name);
_preset_user_slot[_preset_user_count++] = (uint8_t)i;
}
if (_preset_user_count > 0) _preset_menu.addItem("- Delete preset...");
int idx = currentPresetListIndex(p);
_preset_menu.setSelected(idx >= 0 ? idx : 0);
}
// Reuses _preset_menu for a second-level list of just the saved user presets
// (their slot mapping in _preset_user_slot is still the one openPresetMenu()
// just built, since this is only reached from within that same popup).
void openDeletePresetMenu(NodePrefs* p) {
_preset_menu.begin("Delete Preset", 6);
for (int i = 0; i < _preset_user_count; i++)
_preset_menu.addItem(p->user_radio_presets[_preset_user_slot[i]].name);
_preset_deleting = true;
}
public:
RepeaterScreen(UITask* task) : _task(task), _dirty(false), _sel(0), _scroll(0), _item_count(1) {}
void enter() {
_dirty = false; _sel = 0; _scroll = 0;
_preset_menu.active = false; _freq_editor.active = false;
_preset_saving = false; _preset_deleting = false;
_picker.menu.active = false; _freq_editor.active = false;
_picker.saving = false; _picker.deleting = false;
}
int render(DisplayDriver& display) override {
if (_preset_saving) return _task->keyboard().render(display);
if (_picker.saving) return _task->keyboard().render(display);
NodePrefs* p = _task->getNodePrefs();
buildItems(p);
@@ -271,61 +177,49 @@ public:
}
drawScrollIndicator(display, start_y, visible * item_h, total, visible, _scroll);
display.setColor(DisplayDriver::LIGHT);
if (_preset_menu.active) _preset_menu.render(display);
return (_preset_menu.active || _freq_editor.active) ? 50 : 500;
if (_picker.menu.active) _picker.menu.render(display);
return (_picker.menu.active || _freq_editor.active) ? 50 : 500;
}
bool handleInput(char c) override {
NodePrefs* p = _task->getNodePrefs();
// Keyboard editing mode for naming a new saved preset
if (_preset_saving) {
if (_picker.saving) {
auto res = _task->keyboard().handleInput(c);
if (res == KeyboardWidget::DONE) {
saveCurrentAsPreset(p, _task->keyboard().buf);
_preset_saving = false;
if (_picker.save(p, _task->keyboard().buf, rptTarget(p))) {
_dirty = true;
_task->showAlert("Preset saved", 800);
}
_picker.saving = false;
} else if (res == KeyboardWidget::CANCELLED) {
_preset_saving = false;
_picker.saving = false;
}
return true;
}
// Modal overlays first.
if (_preset_menu.active) {
auto res = _preset_menu.handleInput(c);
if (_picker.menu.active) {
auto res = _picker.menu.handleInput(c);
if (res == PopupMenu::SELECTED && p) {
int idx = _preset_menu.selectedIndex();
if (_preset_deleting) {
if (idx >= 0 && idx < _preset_user_count) {
p->user_radio_presets[_preset_user_slot[idx]].name[0] = '\0';
_dirty = true;
_task->showAlert("Preset deleted", 800);
}
_preset_deleting = false;
} else {
int save_idx = 0;
int builtin_base = 1;
int user_base = builtin_base + RADIO_PRESET_COUNT;
int delete_idx = user_base + _preset_user_count;
if (idx == save_idx) {
_preset_saving = true;
switch (_picker.onSelected(_picker.menu.selectedIndex(), p, rptTarget(p))) {
case RadioPresetPicker::START_SAVE:
_task->keyboard().begin("", (int)sizeof(p->user_radio_presets[0].name) - 1);
} else if (idx >= builtin_base && idx < user_base) {
const RadioPreset& r = RADIO_PRESETS[idx - builtin_base];
p->repeater_freq = r.freq; p->repeater_bw = r.bw; p->repeater_sf = r.sf; p->repeater_cr = r.cr;
break;
case RadioPresetPicker::APPLIED:
the_mesh.applyRepeaterRadio(); // live if currently relaying on the profile
_dirty = true;
} else if (idx >= user_base && idx < delete_idx) {
const NodePrefs::UserRadioPreset& up = p->user_radio_presets[_preset_user_slot[idx - user_base]];
p->repeater_freq = up.freq; p->repeater_bw = up.bw; p->repeater_sf = up.sf; p->repeater_cr = up.cr;
the_mesh.applyRepeaterRadio();
break;
case RadioPresetPicker::DELETED:
_dirty = true;
} else if (_preset_user_count > 0 && idx == delete_idx) {
openDeletePresetMenu(p);
}
_task->showAlert("Preset deleted", 800);
break;
case RadioPresetPicker::NONE:
break;
}
} else if (res == PopupMenu::CANCELLED) {
_preset_deleting = false;
_picker.deleting = false;
}
return true;
}
@@ -372,7 +266,7 @@ public:
_dirty = true;
return true;
}
if (item == IT_RPRESET && enter) { openPresetMenu(p); return true; }
if (item == IT_RPRESET && enter) { _picker.open(p, rptTarget(p), "Repeater Preset"); return true; }
if (item == IT_RFREQ && enter) {
float lo, hi; radio_driver.getFreqBounds(lo, hi);
_freq_editor.begin(p->repeater_freq, lo, hi, 3, 3);
@@ -383,7 +277,7 @@ public:
if (left && p->repeater_sf > 5) { p->repeater_sf--; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
}
if (item == IT_RBW) {
int idx = rptBwIndex(p);
int idx = nearestBwIndex(p->repeater_bw);
if (right && idx < LORA_BW_OPT_COUNT - 1) { p->repeater_bw = LORA_BW_OPTS[idx + 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
if (left && idx > 0) { p->repeater_bw = LORA_BW_OPTS[idx - 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
}