Compare commits

...

2 Commits

Author SHA1 Message Date
659542f094 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.
2025-09-27 12:12:07 +02:00
487d75fd73 RadioSet: Add MT868 LongFast preset definion 2025-09-27 07:59:43 +02:00
4 changed files with 467 additions and 41 deletions

View File

@ -0,0 +1,95 @@
#pragma once
template <typename DataType>
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; }
};

View File

@ -0,0 +1,156 @@
#pragma once
template <typename DataType>
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;
}
};

View File

@ -0,0 +1,37 @@
#pragma once
#include <tt_hal_radio.h>
#include "Str.h"
#include "LinkedList.h"
class Preset {
public:
struct PresetItem {
RadioParameter parameter;
float value;
};
Str name;
Modulation modulation;
LinkedList<PresetItem> items;
Preset(const char* const name, Modulation modulation)
: name(name)
, modulation(modulation)
{}
virtual ~Preset() = default;
void addParameter(RadioParameter parameter, float value) {
items.pushBack({parameter, value});
}
LinkedList<PresetItem>::Iterator begin() {
return items.begin();
}
LinkedList<PresetItem>::Iterator end() {
return items.end();
}
};

View File

@ -1,21 +1,36 @@
#include "RadioSet.h"
#include "Str.h"
#include "LinkedList.h"
#include "Preset.h"
#include <cstdio>
#include <ctype.h>
#include <tt_lvgl_toolbar.h>
#include <tt_message_queue.h>
#include <initializer_list>
#include "tt_app_alertdialog.h"
#include <tt_app_alertdialog.h>
constexpr const char* TAG = "RadioSet";
template <typename Iterator>
Iterator next(Iterator i) {
return i++;
}
template <typename Iterator, typename Container>
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) {
@ -560,6 +616,8 @@ class SettingsView {
ParameterInput* paramInputs[MAX_PARAMS] = {0};
size_t paramsAvailableCount = 0;
LinkedList<Preset*> presets;
LinkedList<Preset*> presetsByModulation[MAX_MODEMS];
lv_obj_t* mainPanel = nullptr;
lv_obj_t* deviceForm = nullptr;
@ -567,10 +625,16 @@ 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.pushBack(preset);
presetsByModulation[preset->modulation].pushBack(preset);
}
void queryRadios() {
DeviceId devices[MAX_RADIOS];
uint16_t device_count = 0;
@ -645,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;
@ -661,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++;
@ -678,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());
}
}
@ -688,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;
@ -766,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);
@ -793,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);
@ -802,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);
@ -860,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();
}
}
}
@ -886,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);
@ -941,6 +1068,17 @@ void RadioSet::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE);
settingsView = new SettingsView(wrapper);
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);
presetMtEu868LongFast->addParameter(RADIO_CODINGRATE, 6);
presetMtEu868LongFast->addParameter(RADIO_SYNCWORD, 0x2B);
presetMtEu868LongFast->addParameter(RADIO_PREAMBLES, 16);
settingsView->addPreset(presetMtEu868LongFast);
settingsView->updatePresets();
}