mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
group() / settingRow / listRow / actionRow / choiceRow / groupNote: one card per section, hairlines between rows, hints that wrap. Dropdowns replaced by a centred picker list. Settings regrouped (Device, Connections, Map & data, System); Display & power split into Display, Power and Time on L2 only (the shared schema is unchanged), Keyboard and About get their own pages. Radio and Repeater share one LoRa parameter block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
8.7 KiB
C++
199 lines
8.7 KiB
C++
#pragma once
|
|
// Settings > Radio: preset, frequency, SF / bandwidth / coding rate, TX power
|
|
// and Adaptive Power Control -- ui-new's Settings > Radio; the saved presets
|
|
// and scopes below them are RadioExtras.h. Every change is
|
|
// applied to the radio at once and saved (ui-core/RadioControl.h does the
|
|
// applying for both frontends).
|
|
//
|
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp after ClockScreen.h.
|
|
|
|
namespace radioview {
|
|
|
|
enum : uint8_t { R_PRESET, R_SF, R_BW, R_CR, R_TX, R_APC };
|
|
static const int TX_MIN = 2;
|
|
#ifdef LORA_TX_POWER
|
|
static const int TX_MAX = LORA_TX_POWER;
|
|
#else
|
|
static const int TX_MAX = 22;
|
|
#endif
|
|
|
|
static lv_obj_t* s_overlay = nullptr; // frequency entry
|
|
static lv_obj_t* s_ta = nullptr;
|
|
static bool s_freq_rpt = false; // the entry is for the repeater profile (RepeaterScreen.h)
|
|
static const int OPTS_LEN = 1024;
|
|
static char* s_opts = psramBuf<char>(OPTS_LEN); // dropdown options (LVGL copies them)
|
|
|
|
// A choice at the right of a settings row (its label titles the picker).
|
|
static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width, lv_event_cb_t cb, uintptr_t which) {
|
|
(void)width;
|
|
lv_obj_t* t = rowTitle(row);
|
|
lv_obj_t* c = choiceCreate(row, opts, sel, t ? lv_label_get_text(t) : "");
|
|
lv_obj_align(c, LV_ALIGN_RIGHT_MID, -4, 0);
|
|
lv_obj_add_event_cb(c, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
|
return c;
|
|
}
|
|
|
|
// The LoRa parameters, as Radio and the repeater's own profile both set them:
|
|
// preset (0 = "Custom"), frequency (typed in a popup), SF, bandwidth, coding
|
|
// rate. `ids` are the radioSet / repeaterSet codes for preset, SF, BW, CR.
|
|
static void paramRows(lv_obj_t* g, const NodePrefs* p, int preset, float freq, uint8_t sf_v, float bw_v, uint8_t cr_v,
|
|
lv_event_cb_t cb, const uint8_t ids[4], lv_event_cb_t freq_cb, const char* sf_hint) {
|
|
int o = snprintf(s_opts, OPTS_LEN, "Custom");
|
|
const char* name; float f, b; uint8_t sf, cr;
|
|
for (int i = 0; radioctl::presetAt(p, i, name, f, b, sf, cr) && o < OPTS_LEN - 24; i++)
|
|
o += snprintf(s_opts + o, OPTS_LEN - o, "\n%s", name);
|
|
rowDropdown(settingRow(g, "Preset", nullptr), s_opts, preset + 1, 0, cb, ids[0]);
|
|
char fs[16];
|
|
snprintf(fs, sizeof(fs), "%.3f MHz", freq);
|
|
rowValue(settingRow(g, "Frequency", nullptr), fs, freq_cb);
|
|
o = 0;
|
|
for (int v = 5; v <= 12; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n%d" : "%d", v);
|
|
rowDropdown(settingRow(g, "Spreading factor", sf_hint), s_opts, sf_v >= 5 && sf_v <= 12 ? sf_v - 5 : 0, 0, cb, ids[1]);
|
|
o = 0;
|
|
for (int i = 0; i < LORA_BW_OPT_COUNT; i++)
|
|
o += snprintf(s_opts + o, OPTS_LEN - o, "%s%g kHz", i ? "\n" : "", (double)LORA_BW_OPTS[i]);
|
|
rowDropdown(settingRow(g, "Bandwidth", nullptr), s_opts, nearestBwIndex(bw_v), 0, cb, ids[2]);
|
|
o = 0;
|
|
for (int v = 5; v <= 8; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > 5 ? "\n4/%d" : "4/%d", v);
|
|
rowDropdown(settingRow(g, "Coding rate", nullptr), s_opts, cr_v >= 5 && cr_v <= 8 ? cr_v - 5 : 0, 0, cb, ids[3]);
|
|
}
|
|
|
|
// A few choices side by side at the right of a row (a dropdown's list can run
|
|
// off the bottom of a popup). `map` ends with "".
|
|
static lv_obj_t* rowSegmented(lv_obj_t* row, const char** map, int sel, int width, lv_event_cb_t cb, uintptr_t which) {
|
|
lv_obj_t* seg = segmented(row, map, sel, width, 36);
|
|
lv_obj_align(seg, LV_ALIGN_RIGHT_MID, -4, 0);
|
|
lv_obj_add_event_cb(seg, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
|
return seg;
|
|
}
|
|
|
|
} // namespace radioview
|
|
|
|
static void onRadioDropdown(lv_event_t* e) {
|
|
s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e), choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onRadioSwitch(lv_event_t* e) {
|
|
s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e),
|
|
lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
|
}
|
|
static void onRadioFreq(lv_event_t* e) { (void)e; s_ui->radioFreqPopup(false); }
|
|
static void onRadioFreqKb(lv_event_t* e) { s_ui->radioFreqDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
|
static void onOpenRadio(lv_event_t* e) { (void)e; s_ui->showRadio(); }
|
|
|
|
void UITask::showRadio() {
|
|
_screen = SCR_RADIO;
|
|
buildRadio();
|
|
}
|
|
|
|
void UITask::buildRadio() {
|
|
using namespace radioview;
|
|
lv_obj_t* body = newScreen("Radio", true);
|
|
s_overlay = s_ta = nullptr;
|
|
NodePrefs* p = _prefs;
|
|
|
|
lv_obj_t* g = group(body, "LORA");
|
|
static const uint8_t IDS[4] = { R_PRESET, R_SF, R_BW, R_CR };
|
|
paramRows(g, p, radioctl::currentPreset(p), p->freq, p->sf, p->bw, p->cr, onRadioDropdown, IDS, onRadioFreq,
|
|
"Higher = longer range");
|
|
groupNote(body, "Everyone you talk to needs the same settings.");
|
|
|
|
g = group(body, "TRANSMIT");
|
|
int o = 0;
|
|
for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, OPTS_LEN - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v);
|
|
int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm;
|
|
rowDropdown(settingRow(g, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 0,
|
|
onRadioDropdown, R_TX);
|
|
lv_obj_t* sw = switchRow(g, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links", nullptr);
|
|
if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
|
if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED);
|
|
lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC);
|
|
|
|
buildRadioExtras(body); // my presets, scopes (RadioExtras.h)
|
|
}
|
|
|
|
void UITask::radioSet(int which, int v) {
|
|
using namespace radioview;
|
|
NodePrefs* p = _prefs;
|
|
if (!p) return;
|
|
switch (which) {
|
|
case R_PRESET:
|
|
if (v == 0) return; // "Custom": nothing to take
|
|
radioctl::choosePreset(p, v - 1);
|
|
break;
|
|
case R_SF: p->sf = (uint8_t)(5 + v); radioctl::applyParams(); break;
|
|
case R_BW: if (v >= 0 && v < LORA_BW_OPT_COUNT) { p->bw = LORA_BW_OPTS[v]; radioctl::applyParams(); } break;
|
|
case R_CR: p->cr = (uint8_t)(5 + v); radioctl::applyParams(); break;
|
|
case R_TX: p->tx_power_dbm = (int8_t)(TX_MIN + v); radioctl::applyTxPower(p); break;
|
|
case R_APC: p->tx_apc = (uint8_t)v; radioctl::applyApc(); break;
|
|
}
|
|
the_mesh.savePrefs();
|
|
rebuildRadio(); // preset name / hints follow
|
|
}
|
|
|
|
// Rebuilds the screen where it was scrolled to.
|
|
void UITask::rebuildRadio() {
|
|
int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0;
|
|
buildRadio();
|
|
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); }
|
|
}
|
|
|
|
void UITask::radioFreqPopup(bool repeater) {
|
|
using namespace radioview;
|
|
if (s_overlay) return;
|
|
s_freq_rpt = repeater;
|
|
s_overlay = dimOverlay(screen());
|
|
lv_obj_t* panel = lv_obj_create(s_overlay);
|
|
anim::popup(s_overlay);
|
|
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
|
|
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4);
|
|
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
|
|
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
|
|
lv_obj_set_style_border_width(panel, 1, 0);
|
|
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
|
|
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
|
|
lv_obj_set_style_pad_row(panel, 6, 0);
|
|
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
|
float lo, hi;
|
|
radio_driver.getFreqBounds(lo, hi);
|
|
char t[48];
|
|
snprintf(t, sizeof(t), "Frequency, MHz (%.0f - %.0f)", lo, hi);
|
|
label(panel, t, THEME_FONT_BODY, theme::TEXT);
|
|
s_ta = textField(panel);
|
|
lv_textarea_set_accepted_chars(s_ta, "0123456789.");
|
|
lv_textarea_set_max_length(s_ta, 10);
|
|
char fs[16];
|
|
snprintf(fs, sizeof(fs), "%.3f", repeater ? _prefs->repeater_freq : _prefs->freq);
|
|
lv_textarea_set_text(s_ta, fs);
|
|
lv_obj_add_state(s_ta, LV_STATE_FOCUSED);
|
|
lv_obj_t* kbd = kb::create(s_overlay, _prefs);
|
|
lv_obj_set_size(kbd, LV_PCT(100), 124);
|
|
lv_obj_align(kbd, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
lv_keyboard_set_textarea(kbd, s_ta);
|
|
kb::apply(kbd, kb::L_SYM);
|
|
lv_obj_add_event_cb(kbd, onRadioFreqKb, LV_EVENT_READY, NULL);
|
|
lv_obj_add_event_cb(kbd, onRadioFreqKb, LV_EVENT_CANCEL, NULL);
|
|
}
|
|
|
|
void UITask::radioFreqDone(bool ok) {
|
|
using namespace radioview;
|
|
if (ok && s_ta) {
|
|
float lo, hi;
|
|
radio_driver.getFreqBounds(lo, hi);
|
|
float f = strtof(lv_textarea_get_text(s_ta), nullptr);
|
|
if (f < lo || f > hi) { showToast("Out of the radio's range"); return; }
|
|
if (s_freq_rpt) { _prefs->repeater_freq = f; rptctl::applyProfile(); }
|
|
else { _prefs->freq = f; radioctl::applyParams(); }
|
|
the_mesh.savePrefs();
|
|
}
|
|
radioCloseFreq();
|
|
if (ok) { if (s_freq_rpt) rebuildRepeater(); else rebuildRadio(); }
|
|
}
|
|
|
|
void UITask::radioCloseFreq() {
|
|
if (radioview::s_overlay) lv_obj_delete_async(radioview::s_overlay);
|
|
radioview::s_overlay = radioview::s_ta = nullptr;
|
|
}
|
|
|
|
bool UITask::radioPopupOpen() const { return radioview::s_overlay != nullptr; }
|