From 2779b867cc83fa2b6ec6b96f87260afd5665f329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Sat, 27 Sep 2025 12:12:07 +0200 Subject: [PATCH] RadioSet: Add presets The preset dropdown reset any time the value is changed, which includes on parameter loads from the radio. It should only reset on user input, but it's not worth finding out how right now. --- ExternalApps/RadioSet/main/Source/Dequeue.h | 95 ++++++++ .../RadioSet/main/Source/LinkedList.h | 156 +++++++++++++ ExternalApps/RadioSet/main/Source/Preset.h | 37 +-- .../RadioSet/main/Source/RadioSet.cpp | 214 ++++++++++++++---- 4 files changed, 429 insertions(+), 73 deletions(-) create mode 100644 ExternalApps/RadioSet/main/Source/Dequeue.h create mode 100644 ExternalApps/RadioSet/main/Source/LinkedList.h diff --git a/ExternalApps/RadioSet/main/Source/Dequeue.h b/ExternalApps/RadioSet/main/Source/Dequeue.h new file mode 100644 index 00000000..9d3f2d53 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/Dequeue.h @@ -0,0 +1,95 @@ +#pragma once + +template +class Dequeue { + + struct Node { + DataType data; + Node* next; + Node* previous; + + Node(DataType data, Node* next, Node* previous): + data(data), + next(next), + previous(previous) + {} + }; + + int count = 0; + Node* head = nullptr; + Node* tail = nullptr; + +public: + + void pushFront(DataType data) { + auto* new_node = new Node(data, head, nullptr); + + if (head != nullptr) { + head->previous = new_node; + } + + if (tail == nullptr) { + tail = new_node; + } + + head = new_node; + count++; + } + + void pushBack(DataType data) { + auto* new_node = new Node(data, nullptr, tail); + + if (head == nullptr) { + head = new_node; + } + + if (tail != nullptr) { + tail->next = new_node; + } + + tail = new_node; + count++; + } + + void popFront() { + if (head != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = head; + head = node_to_delete->next; + if (is_last_node) { + tail = nullptr; + } + delete node_to_delete; + count--; + } + } + + void popBack() { + if (tail != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = tail; + tail = node_to_delete->previous; + if (is_last_node) { + head = nullptr; + } + delete node_to_delete; + count--; + } + } + + DataType back() const { + assert(tail != nullptr); + return tail->data; + } + + DataType front() const { + assert(head != nullptr); + return head->data; + } + + bool empty() const { + return head == nullptr; + } + + int size() const { return count; } +}; \ No newline at end of file diff --git a/ExternalApps/RadioSet/main/Source/LinkedList.h b/ExternalApps/RadioSet/main/Source/LinkedList.h new file mode 100644 index 00000000..42837608 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/LinkedList.h @@ -0,0 +1,156 @@ +#pragma once + +template +class LinkedList { + + struct Node { + DataType data; + Node* next; + Node* previous; + + Node(DataType data, Node* next, Node* previous): + data(data), + next(next), + previous(previous) + {} + }; + + int count = 0; + Node* head = nullptr; + Node* tail = nullptr; + +public: + class Iterator { + Node *node = nullptr; + public: + Iterator(Node* node) + : node(node) {} + + bool advance(size_t n) { + size_t i = 0; + if (n == 0) { + return true; + } + + for (; node && (i < n); ++i) { + node = node->next; + } + return (i > 0); + } + + DataType& operator* () + { + return node->data; + } + + DataType* operator-> () + { + return &(node->data); + } + + Iterator operator++ (int) { + assert(advance(1)); + Iterator i(node); + return i; + } + + bool operator==(const Iterator& right) const { + return node == right.node; + } + + bool operator!=(const Iterator& right) const { + return node != right.node; + } + }; + + void pushFront(DataType data) { + auto* new_node = new Node(data, head, nullptr); + + if (head != nullptr) { + head->previous = new_node; + } + + if (tail == nullptr) { + tail = new_node; + } + + head = new_node; + count++; + } + + void pushBack(DataType data) { + auto* new_node = new Node(data, nullptr, tail); + + if (head == nullptr) { + head = new_node; + } + + if (tail != nullptr) { + tail->next = new_node; + } + + tail = new_node; + count++; + } + + void popFront() { + if (head != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = head; + head = node_to_delete->next; + if (is_last_node) { + tail = nullptr; + } + delete node_to_delete; + count--; + } + } + + void popBack() { + if (tail != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = tail; + tail = node_to_delete->previous; + if (is_last_node) { + head = nullptr; + } + delete node_to_delete; + count--; + } + } + + DataType back() const { + assert(tail != nullptr); + return tail->data; + } + + DataType front() const { + assert(head != nullptr); + return head->data; + } + + Iterator begin() const { + return Iterator(head); + } + + Iterator end() const { + return Iterator(nullptr); + } + + bool empty() const { + return head == nullptr; + } + + int size() const { return count; } + + DataType operator [] (int i) const { + auto iter = begin(); + assert(iter.advance(i)); + return *iter; + } + DataType& operator [] (int i) { + auto iter = begin(); + assert(iter.advance(i)); + return *iter; + } +}; diff --git a/ExternalApps/RadioSet/main/Source/Preset.h b/ExternalApps/RadioSet/main/Source/Preset.h index dcc9b716..16885221 100644 --- a/ExternalApps/RadioSet/main/Source/Preset.h +++ b/ExternalApps/RadioSet/main/Source/Preset.h @@ -3,48 +3,35 @@ #include #include "Str.h" +#include "LinkedList.h" class Preset { +public: struct PresetItem { RadioParameter parameter; float value; - PresetItem* next = nullptr; }; Str name; Modulation modulation; - PresetItem* first = nullptr; - PresetItem* last = nullptr; -public: + LinkedList items; + Preset(const char* const name, Modulation modulation) : name(name) , modulation(modulation) {} - virtual ~Preset() { - PresetItem* n = first; - while(n) { - auto next = n->next; - delete n; - n = next; - } - } + virtual ~Preset() = default; void addParameter(RadioParameter parameter, float value) { - auto node = new PresetItem; - node->parameter = parameter; - node->value = value; - - if (last) { - last->next = node; - last = node; - } else { - first = node; - last = node; - } + items.pushBack({parameter, value}); } - PresetItem* first() { - return first; + LinkedList::Iterator begin() { + return items.begin(); + } + + LinkedList::Iterator end() { + return items.end(); } }; diff --git a/ExternalApps/RadioSet/main/Source/RadioSet.cpp b/ExternalApps/RadioSet/main/Source/RadioSet.cpp index 8dbeb989..dbe9de54 100644 --- a/ExternalApps/RadioSet/main/Source/RadioSet.cpp +++ b/ExternalApps/RadioSet/main/Source/RadioSet.cpp @@ -1,21 +1,36 @@ #include "RadioSet.h" #include "Str.h" +#include "LinkedList.h" #include "Preset.h" #include #include #include #include - - -#include "tt_app_alertdialog.h" +#include constexpr const char* TAG = "RadioSet"; +template +Iterator next(Iterator i) { + return i++; +} + +template +bool is_last(Iterator i, const Container& c) { + return (i != c.end()) && (next(i) == c.end()); +} + void crash(const char* const message) { tt_app_alertdialog_start("RadioSet has crashed!", message, nullptr, 0); } +void crashassert(bool assertion, const char* const message) { + if (!assertion) { + crash(message); + } +} + // Debug function which colors all children randomly // TODO: Remove before flight void clownvomit(lv_obj_t *obj) { @@ -117,8 +132,12 @@ static lv_obj_t* createGridDropdownInput(lv_obj_t *container, int row, const cha struct ParameterInput { static constexpr auto LV_STATE_INVALID = LV_STATE_USER_1; + typedef void (*Callback)(void* ctx); + const RadioHandle handle; const RadioParameter param; + Callback userChangeCallback = nullptr; + void* userChangeCtx = nullptr; static void apply_error_style(lv_obj_t* obj) { static bool init = false; @@ -137,9 +156,23 @@ struct ParameterInput { , param(param) {} virtual ~ParameterInput() = default; + + void onUserChange(Callback cb, void* ctx) { + userChangeCallback = cb; + userChangeCtx = ctx; + } + + void emitUserChange() { + if (userChangeCallback) { + userChangeCallback(userChangeCtx); + } + } + + virtual void storeToRadio() = 0; virtual void updatePreview() = 0; virtual void activate() = 0; virtual void deactivate() = 0; + virtual void setValue(float value) = 0; }; struct NumericParameterInput : public ParameterInput { @@ -199,19 +232,18 @@ struct NumericParameterInput : public ParameterInput { lv_obj_add_event_cb(input, [](lv_event_t * e) { NumericParameterInput* self = (NumericParameterInput*)lv_event_get_user_data(e); self->storeToRadio(); + self->emitUserChange(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { - Str txt; - txt.appendf(fmt, value); - lv_textarea_set_text(input, txt.c_str()); + setValue(value); } } - void storeToRadio() { + virtual void storeToRadio() override { float value; if (sscanf(lv_textarea_get_text(input), "%f", &value) == 1) { if (tt_hal_radio_set_parameter(handle, param, value) != RADIO_PARAM_SUCCESS) { @@ -233,6 +265,12 @@ struct NumericParameterInput : public ParameterInput { virtual void deactivate() override { lv_obj_add_state(input, LV_STATE_DISABLED); } + + virtual void setValue(float value) override { + Str txt; + txt.appendf(fmt, value); + lv_textarea_set_text(input, txt.c_str()); + } }; struct SliderParameterInput : public ParameterInput { @@ -268,7 +306,6 @@ struct SliderParameterInput : public ParameterInput { lv_obj_set_size(slider, lv_pct(100), 10); lv_slider_set_range(slider, min, max); apply_error_style(slider); - loadFromRadio(); preview = lv_label_create(container); lv_obj_set_grid_cell(preview, @@ -276,12 +313,15 @@ struct SliderParameterInput : public ParameterInput { LV_GRID_ALIGN_CENTER, row, 1); lv_obj_set_size(preview, lv_pct(100), LV_SIZE_CONTENT); lv_obj_set_style_text_align(preview , LV_TEXT_ALIGN_LEFT, 0); - updatePreview(); + + loadFromRadio(); + lv_obj_add_event_cb(slider, [](lv_event_t * e) { lv_obj_t* slider = lv_event_get_target_obj(e); SliderParameterInput* self = (SliderParameterInput*)lv_event_get_user_data(e); self->updatePreview(); self->storeToRadio(); + self->emitUserChange(); }, LV_EVENT_VALUE_CHANGED, this); } @@ -289,11 +329,11 @@ struct SliderParameterInput : public ParameterInput { void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { - lv_slider_set_value(slider, value, LV_ANIM_ON); + setValue(value); } } - void storeToRadio() { + virtual void storeToRadio() override { if (tt_hal_radio_set_parameter(handle, param, lv_slider_get_value(slider)) != RADIO_PARAM_SUCCESS) { lv_obj_add_state(slider, LV_STATE_INVALID); } else { @@ -314,6 +354,11 @@ struct SliderParameterInput : public ParameterInput { virtual void deactivate() override { lv_obj_add_state(slider, LV_STATE_DISABLED); } + + virtual void setValue(float value) override { + lv_slider_set_value(slider, value, LV_ANIM_ON); + updatePreview(); + } }; @@ -379,7 +424,6 @@ struct SliderSelectParameterInput : public ParameterInput { lv_obj_set_size(slider, lv_pct(100), 10); lv_slider_set_range(slider, 0, selectionsSize - 1); apply_error_style(slider); - loadFromRadio(); preview = lv_label_create(container); lv_obj_set_grid_cell(preview, @@ -389,13 +433,15 @@ struct SliderSelectParameterInput : public ParameterInput { lv_obj_set_style_text_align(preview , LV_TEXT_ALIGN_LEFT, 0); tt_hal_radio_get_parameter_unit_str(handle, param, unit, sizeof(unit)); - updatePreview(); + + loadFromRadio(); lv_obj_add_event_cb(slider, [](lv_event_t * e) { lv_obj_t* slider = lv_event_get_target_obj(e); SliderSelectParameterInput* self = (SliderSelectParameterInput*)lv_event_get_user_data(e); self->updatePreview(); self->storeToRadio(); + self->emitUserChange(); }, LV_EVENT_VALUE_CHANGED, this); } @@ -403,11 +449,11 @@ struct SliderSelectParameterInput : public ParameterInput { void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { - lv_slider_set_value(slider, get_selection_index(value), LV_ANIM_ON); + setValue(value); } } - void storeToRadio() { + virtual void storeToRadio() override { if (tt_hal_radio_set_parameter(handle, param, selections[lv_slider_get_value(slider)]) != RADIO_PARAM_SUCCESS) { lv_obj_add_state(slider, LV_STATE_INVALID); } else { @@ -429,6 +475,11 @@ struct SliderSelectParameterInput : public ParameterInput { virtual void deactivate() override { lv_obj_add_state(slider, LV_STATE_DISABLED); } + + virtual void setValue(float value) override { + lv_slider_set_value(slider, get_selection_index(value), LV_ANIM_ON); + updatePreview(); + } }; struct FlagParameterInput : public ParameterInput { @@ -464,21 +515,18 @@ struct FlagParameterInput : public ParameterInput { lv_obj_t* slider = lv_event_get_target_obj(e); FlagParameterInput* self = (FlagParameterInput*)lv_event_get_user_data(e); self->storeToRadio(); + self->emitUserChange(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { - if (value != 0.0) { - lv_obj_add_state(input, LV_STATE_CHECKED); - } else { - lv_obj_clear_state(input, LV_STATE_CHECKED); - } + setValue(value); } } - void storeToRadio() { + virtual void storeToRadio() override { float value = lv_obj_has_state(input, LV_STATE_CHECKED) ? 1.0 : 0.0; if (tt_hal_radio_set_parameter(handle, param, value) != RADIO_PARAM_SUCCESS) { lv_obj_add_state(input, LV_STATE_INVALID); @@ -497,6 +545,14 @@ struct FlagParameterInput : public ParameterInput { virtual void deactivate() override { lv_obj_add_state(input, LV_STATE_DISABLED); } + + virtual void setValue(float value) override { + if (value != 0.0) { + lv_obj_add_state(input, LV_STATE_CHECKED); + } else { + lv_obj_clear_state(input, LV_STATE_CHECKED); + } + } }; static ParameterInput* makeLoraInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row) { @@ -548,7 +604,6 @@ class SettingsView { static constexpr Modulation LAST_MODULATION = MODULATION_LRFHSS; static constexpr size_t MAX_MODEMS = LAST_MODULATION + 1; static constexpr size_t MAX_PARAMS = RADIO_NARROWGRID + 1; - static constexpr size_t MAX_PRESETS = 32; RadioHandle radios[MAX_RADIOS] = {0}; size_t radioCount = 0; @@ -561,8 +616,8 @@ class SettingsView { ParameterInput* paramInputs[MAX_PARAMS] = {0}; size_t paramsAvailableCount = 0; - Preset* presets[MAX_PRESETS] = {0}; - size_t presetsCount = 0; + LinkedList presets; + LinkedList presetsByModulation[MAX_MODEMS]; lv_obj_t* mainPanel = nullptr; lv_obj_t* deviceForm = nullptr; @@ -570,13 +625,14 @@ class SettingsView { lv_obj_t* radioSwitch = nullptr; lv_obj_t* radioStateLabel = nullptr; lv_obj_t* modemDropdown = nullptr; + lv_obj_t* modemPresetDropdown = nullptr; lv_obj_t *propertiesForm = nullptr; public: void addPreset(Preset* preset) { - presets[presetsCount] = preset; - presetsCount++; + presets.pushBack(preset); + presetsByModulation[preset->modulation].pushBack(preset); } void queryRadios() { @@ -653,8 +709,9 @@ public: char unit_buffer[32] = {0}; - // Clean up any input, it's safe and this loop costs nothing' + // Clean up any input for (size_t i = 0; i < MAX_PARAMS; ++i) { + // As this is a LUT, only some may be set if (paramInputs[i]) { delete paramInputs[i]; paramInputs[i] = nullptr; @@ -669,7 +726,11 @@ public: auto status = tt_hal_radio_get_parameter(radioSelected, param, &value); if (status == RADIO_PARAM_SUCCESS) { auto input = makeParameterInput(radioSelected, param, modem, container, paramsAvailableCount); - paramInputs[paramsAvailableCount] = input; + input->onUserChange([](void* ctx) { + SettingsView* self = (SettingsView*)ctx; + self->onParameterInput(); + }, this); + paramInputs[param] = input; //lv_group_focus_obj(input); paramsAvailable[paramsAvailableCount] = param; paramsAvailableCount++; @@ -686,7 +747,60 @@ public: lv_dropdown_set_selected(modemDropdown, modemIndex); if (tt_hal_radio_set_modulation(radioSelected, modemsAvailable[modemIndex])) { propertiesForm = initParameterFormGeneric(mainPanel, modemsAvailable[modemIndex]); - //clownvomit(propertiesForm); + } + + updatePresets(); + } + + void selectPreset(int presetIndex) { + // The first index is always "No preset" or "None available" + // Other indices are the presets for the current modulation + 1 + + auto modem = tt_hal_radio_get_modulation(radioSelected); + auto& presets = presetsByModulation[modem]; + if ((presetIndex > 0) && ((presetIndex - 1) < presets.size())) { + auto preset = presets[presetIndex - 1]; + + for (auto iter = preset->items.begin(); iter != preset->items.end(); iter++) { + if (paramInputs[iter->parameter]) { + paramInputs[iter->parameter]->setValue(iter->value); + paramInputs[iter->parameter]->storeToRadio(); + } + } + } else { + crash("Selected preset does not exist"); + } + } + + void onParameterInput() { + // As the user did an input, this makes any applied + // preset inconsistent, revert back to "None". + lv_dropdown_set_selected(modemPresetDropdown, 0); + } + + void updatePresets() { + auto modemIndexConfigured = tt_hal_radio_get_modulation(radioSelected); + + Str preset_list("Select..."); + auto& presets = presetsByModulation[modemIndexConfigured]; + if (!presets.empty()) { + preset_list.append("\n"); + } + for (auto iter = presets.begin(); iter != presets.end(); iter++) { + auto place_sep = !is_last(iter, presets); + auto& name = (*iter)->name; + preset_list.append(name.c_str()); + if (place_sep) { + preset_list.append("\n"); + } + } + + if (preset_list.empty()) { + lv_obj_add_state(modemPresetDropdown, LV_STATE_DISABLED); + lv_dropdown_set_options(modemPresetDropdown, "None"); + } else { + lv_obj_clear_state(modemPresetDropdown, LV_STATE_DISABLED); + lv_dropdown_set_options(modemPresetDropdown, preset_list.c_str()); } } @@ -696,7 +810,7 @@ public: } radioSelected = radios[index]; - assert(radioSelected); + crashassert(radioSelected, "Radio selected not allocated"); for (size_t i = 0; i < MAX_MODEMS; ++i) { modemsAvailable[i] = MODULATION_NONE; @@ -774,6 +888,7 @@ public: grid_row_size, grid_row_size, grid_row_size, + grid_row_size, LV_GRID_TEMPLATE_LAST}; lv_obj_set_layout(container, LV_LAYOUT_GRID); lv_obj_set_grid_dsc_array(container, lora_col_dsc, lora_row_dsc); @@ -801,8 +916,8 @@ public: LV_GRID_ALIGN_CENTER, 1, 1); lv_obj_set_size(radioStateLabel, lv_pct(100), LV_SIZE_CONTENT); - modemDropdown = createGridDropdownInput(container, 2, "Modulation", "none available"); - + modemDropdown = createGridDropdownInput(container, 2, "Modulation", "None available"); + modemPresetDropdown = createGridDropdownInput(container, 3, "Preset", "None"); lv_obj_add_event_cb(modemDropdown, [](lv_event_t * e) { SettingsView* self = (SettingsView*)lv_event_get_user_data(e); @@ -810,6 +925,12 @@ public: self->selectModulation(lv_dropdown_get_selected(input)); }, LV_EVENT_VALUE_CHANGED, this); + lv_obj_add_event_cb(modemPresetDropdown, [](lv_event_t * e) { + SettingsView* self = (SettingsView*)lv_event_get_user_data(e); + lv_obj_t* input = lv_event_get_target_obj(e); + self->selectPreset(lv_dropdown_get_selected(input)); + }, LV_EVENT_VALUE_CHANGED, this); + lv_obj_add_event_cb(radioSwitch, [](lv_event_t * e) { lv_obj_t* input = lv_event_get_target_obj(e); SettingsView* self = (SettingsView*)lv_event_get_user_data(e); @@ -868,18 +989,22 @@ public: void activateConfig() { lv_obj_clear_state(modemDropdown, LV_STATE_DISABLED); lv_obj_clear_state(radioSwitch, LV_STATE_CHECKED); - for (size_t i = 0; i < paramsAvailableCount; ++i) { - assert(paramInputs[i]); - paramInputs[i]->activate(); + lv_obj_clear_state(modemPresetDropdown, LV_STATE_DISABLED); + for (size_t i = 0; i < MAX_PARAMS; ++i) { + if (paramInputs[i]) { + paramInputs[i]->activate(); + } } } void deactivateConfig() { lv_obj_add_state(radioSwitch, LV_STATE_CHECKED); lv_obj_add_state(modemDropdown, LV_STATE_DISABLED); - for (size_t i = 0; i < paramsAvailableCount; ++i) { - assert(paramInputs[i]); - paramInputs[i]->deactivate(); + lv_obj_add_state(modemPresetDropdown, LV_STATE_DISABLED); + for (size_t i = 0; i < MAX_PARAMS; ++i) { + if (paramInputs[i]) { + paramInputs[i]->deactivate(); + } } } @@ -894,19 +1019,13 @@ public: lv_obj_add_flag(mainPanel, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLL_ON_FOCUS)); auto* group = lv_group_get_default(); lv_group_add_obj(group, mainPanel); - //lv_obj_add_event_cb(btn, scrollbar_highlight, LV_EVENT_FOCUS, NULL); - //lv_obj_add_event_cb(btn, scrollbar_restore, LV_EVENT_DEFOCUSED, NULL); - // Create once (e.g. during init) static lv_style_t style_scroll_focus; lv_style_init(&style_scroll_focus); lv_style_set_bg_color(&style_scroll_focus, lv_color_make(0x40,0xA0,0xFF)); lv_style_set_bg_opa(&style_scroll_focus, LV_OPA_COVER); lv_style_set_border_width(&style_scroll_focus, 1); - lv_style_set_border_color(&style_scroll_focus, lv_color_black()); - - // Apply style targeted to the scrollbar part when the object is FOCUSED - // LV_PART_SCROLLBAR | LV_STATE_FOCUSED will ensure the style is used only while focused. + lv_style_set_border_color(&style_scroll_focus, lv_theme_get_color_primary(nullptr)); lv_obj_add_style(mainPanel, &style_scroll_focus, LV_PART_SCROLLBAR | LV_STATE_FOCUSED); deviceForm = initDeviceForm(mainPanel); @@ -950,7 +1069,7 @@ void RadioSet::onShow(AppHandle appHandle, lv_obj_t* parent) { settingsView = new SettingsView(wrapper); - auto presetMtEu868LongFast = new Preset("Meshtastic EU868 LongFast", MODULATION_LORA); + auto presetMtEu868LongFast = new Preset("MT EU868 LongFast", MODULATION_LORA); presetMtEu868LongFast->addParameter(RADIO_FREQUENCY, 869.525); presetMtEu868LongFast->addParameter(RADIO_BANDWIDTH, 250.0); presetMtEu868LongFast->addParameter(RADIO_SPREADFACTOR, 11); @@ -959,8 +1078,7 @@ void RadioSet::onShow(AppHandle appHandle, lv_obj_t* parent) { presetMtEu868LongFast->addParameter(RADIO_PREAMBLES, 16); settingsView->addPreset(presetMtEu868LongFast); - - + settingsView->updatePresets(); }