Dominic Höglinger 2779b867cc 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 18:59:01 +02:00

1096 lines
38 KiB
C++

#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 <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) {
static auto rng = []() {
static int color = 0xC0FE;
const int a = 4711;
const int m = 0x10001;
color = (a * color) % m;
return color;
};
const int darken = 0x0E0E0E;
const int lighten = 0xFEFEFE;
lv_obj_set_style_bg_color(obj, lv_color_hex(rng() & darken), 0);
uint32_t i;
for(i = 0; i < lv_obj_get_child_count(obj); i++) {
lv_obj_t * child = lv_obj_get_child(obj, i);
lv_obj_set_style_bg_color(child, lv_color_hex(rng() & darken), 0);
lv_obj_set_style_border_color(child, lv_color_hex(rng() | lighten), 0);
lv_obj_set_style_border_width(child, 1, 0);
clownvomit(child);
}
}
/*
void scrollbar_highlight(lv_event_t * e) {
lv_obj_t * obj = lv_event_get_target(e);
}*/
char *const toString(Modulation m) {
switch (m) {
case MODULATION_NONE:
return "None";
case MODULATION_LORA:
return "LoRa";
case MODULATION_FSK:
return "FSK";
case MODULATION_LRFHSS:
return "LR-FHSS";
default:
break;
}
crash("Unknown modulation passed.");
return "Unknown";
}
char *const toString(RadioParameter p) {
switch (p) {
case RADIO_POWER:
return "Power";
case RADIO_FREQUENCY:
return "Center Frequency";
case RADIO_BANDWIDTH:
return "Bandwidth";
case RADIO_SPREADFACTOR:
return "Spread Factor";
case RADIO_CODINGRATE:
return "Coding Rate Denominator";
case RADIO_SYNCWORD:
return "Sync Word";
case RADIO_PREAMBLES:
return "Preamble Length";
case RADIO_FREQDIV:
return "Frequency Deviation";
case RADIO_DATARATE:
return "Data Rate";
case RADIO_ADDRWIDTH:
return "Address Width";
case RADIO_NARROWGRID:
return "Narrow Grid";
default:
break;
}
crash("Unknown parameter passed.");
return "Unknown";
}
class TermView {
};
static lv_obj_t* createGridDropdownInput(lv_obj_t *container, int row, const char* const label, const char* const items) {
lv_obj_t* label_obj = lv_label_create(container);
lv_label_set_text(label_obj, label);
lv_obj_set_grid_cell(label_obj,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_t* input = lv_dropdown_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, lv_pct(100), LV_SIZE_CONTENT);
lv_dropdown_set_options(input, items);
return input;
}
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;
static lv_style_t style_invalid;
if (!init) {
lv_style_init(&style_invalid);
lv_style_set_border_color(&style_invalid, lv_color_hex(0xFFBF00));
lv_style_set_border_width(&style_invalid, 2);
init = true;
}
lv_obj_add_style(obj, &style_invalid, LV_STATE_INVALID);
}
ParameterInput(RadioHandle handle, const RadioParameter param)
: handle(handle)
, 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 {
lv_obj_t* label = nullptr;
lv_obj_t* input = nullptr;
lv_obj_t* unitlabel = nullptr;
char* fmt;
NumericParameterInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row, char* fmt = "%f", char* unit_override = nullptr)
: ParameterInput(handle, param)
, fmt(fmt) {
initUi(container, row, unit_override);
loadFromRadio();
}
virtual ~NumericParameterInput() {
//lv_obj_clean(label);
//lv_obj_clean(input);
//lv_obj_clean(unitlabel);
}
void initUi(lv_obj_t* container, int row, char* unit_override) {
const int height = LV_SIZE_CONTENT;
label = lv_label_create(container);
lv_label_set_text(label, toString(param));
lv_obj_set_grid_cell(label,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label, lv_pct(100), height);
input = lv_textarea_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, lv_pct(100), height);
lv_textarea_set_accepted_chars(input, "0123456789.+-");
loadFromRadio();
lv_textarea_set_one_line(input, true);
apply_error_style(input);
unitlabel = lv_label_create(container);
lv_obj_set_grid_cell(unitlabel,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(unitlabel, lv_pct(100), height);
lv_obj_set_style_text_align(unitlabel , LV_TEXT_ALIGN_LEFT, 0);
if (unit_override) {
lv_label_set_text(unitlabel, unit_override);
} else {
char unit[64] = {0};
tt_hal_radio_get_parameter_unit_str(handle, param, unit, sizeof(unit));
lv_label_set_text(unitlabel, unit);
}
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) {
setValue(value);
}
}
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) {
lv_obj_add_state(input, LV_STATE_INVALID);
} else {
lv_obj_clear_state(input, LV_STATE_INVALID);
}
} else {
lv_obj_add_state(input, LV_STATE_INVALID);
}
}
virtual void updatePreview() override {}
virtual void activate() override {
lv_obj_clear_state(input, LV_STATE_DISABLED);
}
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 {
char* fmt = nullptr;
lv_obj_t* label = nullptr;
lv_obj_t* slider = nullptr;
lv_obj_t* preview = nullptr;
SliderParameterInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row, int min, int max, char* fmt = "%i")
: ParameterInput(handle, param)
, fmt(fmt) {
initUi(container, row, min, max);
}
virtual ~SliderParameterInput() {
//lv_obj_clean(label);
//lv_obj_clean(slider);
//lv_obj_clean(preview);
}
void initUi(lv_obj_t* container, int row, int min, int max) {
label = lv_label_create(container);
lv_label_set_text(label, toString(param));
lv_obj_set_grid_cell(label,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT);
slider = lv_slider_create(container);
lv_obj_set_grid_cell(slider,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(slider, lv_pct(100), 10);
lv_slider_set_range(slider, min, max);
apply_error_style(slider);
preview = lv_label_create(container);
lv_obj_set_grid_cell(preview,
LV_GRID_ALIGN_STRETCH, 2, 1,
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);
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);
}
void loadFromRadio() {
float value;
if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) {
setValue(value);
}
}
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 {
lv_obj_clear_state(slider, LV_STATE_INVALID);
}
}
virtual void updatePreview() override {
char buf[64] = {0};
lv_snprintf(buf, sizeof(buf), fmt, lv_slider_get_value(slider));
lv_label_set_text(preview, buf);
}
virtual void activate() override {
lv_obj_clear_state(slider, LV_STATE_DISABLED);
}
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();
}
};
struct SliderSelectParameterInput : public ParameterInput {
static constexpr float SELECT_END = -1;
const float* selections;
const size_t selectionsSize;
char unit[64] = {0};
char* fmt;
lv_obj_t* label = nullptr;
lv_obj_t* slider = nullptr;
lv_obj_t* preview = nullptr;
static constexpr size_t get_selection_num(const float selections[]) {
constexpr size_t MAX_SELECTIONS = 32;
if (selections) {
for (size_t i = 0; i < MAX_SELECTIONS; ++i) {
if (selections[i] == SELECT_END) {
return i;
}
}
}
return 0;
}
SliderSelectParameterInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row, const float selections[] = nullptr, char* fmt = "%f")
: ParameterInput(handle, param)
, selections(selections)
, selectionsSize(get_selection_num(selections))
, fmt(fmt) {
initUi(container, row);
}
virtual ~SliderSelectParameterInput() {
//lv_obj_clean(label);
//lv_obj_clean(slider);
//lv_obj_clean(preview);
}
int get_selection_index(const float value) {
for (int i = 0; i < selectionsSize; ++i) {
if (selections[i] == value) {
return i;
}
}
return 0;
}
void initUi(lv_obj_t* container, int row) {
label = lv_label_create(container);
lv_label_set_text(label, toString(param));
lv_obj_set_grid_cell(label,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT);
slider = lv_slider_create(container);
lv_obj_set_grid_cell(slider,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(slider, lv_pct(100), 10);
lv_slider_set_range(slider, 0, selectionsSize - 1);
apply_error_style(slider);
preview = lv_label_create(container);
lv_obj_set_grid_cell(preview,
LV_GRID_ALIGN_STRETCH, 2, 1,
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);
tt_hal_radio_get_parameter_unit_str(handle, param, unit, sizeof(unit));
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);
}
void loadFromRadio() {
float value;
if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) {
setValue(value);
}
}
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 {
lv_obj_clear_state(slider, LV_STATE_INVALID);
}
}
virtual void updatePreview() override {
Str text;
text.appendf(fmt, selections[lv_slider_get_value(slider)]);
text.append(unit);
lv_label_set_text(preview, text.c_str());
}
virtual void activate() override {
lv_obj_clear_state(slider, LV_STATE_DISABLED);
}
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 {
lv_obj_t* label = nullptr;
lv_obj_t* input = nullptr;
FlagParameterInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row)
: ParameterInput(handle, param) {
initUi(container, row);
}
virtual ~FlagParameterInput() {
//lv_obj_clean(label);
//lv_obj_clean(input);
}
void initUi(lv_obj_t* container, int row) {
label = lv_label_create(container);
lv_label_set_text(label, toString(param));
lv_obj_set_grid_cell(label,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT);
input = lv_switch_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, 30, 20);
apply_error_style(input);
loadFromRadio();
lv_obj_add_event_cb(input, [](lv_event_t * e) {
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) {
setValue(value);
}
}
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);
} else {
lv_obj_clear_state(input, LV_STATE_INVALID);
}
}
virtual void updatePreview() override {
}
virtual void activate() override {
lv_obj_clear_state(input, LV_STATE_DISABLED);
}
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) {
static constexpr float bw_values[] = {7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0, 500.0, SliderSelectParameterInput::SELECT_END};
// LoRa is standardized, so we get to use fancy inputs
switch (param) {
case RADIO_POWER: //no break
case RADIO_FREQUENCY:
return new NumericParameterInput(handle, param, container, row);
case RADIO_BANDWIDTH:
return new SliderSelectParameterInput(handle, param, container, row, bw_values, "%.1f");
case RADIO_SPREADFACTOR:
return new SliderParameterInput(handle, param, container, row, 7, 12);
case RADIO_CODINGRATE:
return new SliderParameterInput(handle, param, container, row, 5, 8);
case RADIO_SYNCWORD:
return new SliderParameterInput(handle, param, container, row, 0, 255, "%02X");
case RADIO_PREAMBLES:
return new SliderParameterInput(handle, param, container, row, 0, 0xFFFF);
default:
return new NumericParameterInput(handle, param, container, row);
}
}
static ParameterInput* makeLrFhssInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row) {
switch (param) {;
case RADIO_NARROWGRID:
return new FlagParameterInput(handle, param, container, row);
default:
return new NumericParameterInput(handle, param, container, row);
}
}
static ParameterInput* makeParameterInput(RadioHandle handle, const RadioParameter param, const Modulation modulation, lv_obj_t* container, int row) {
switch (modulation) {
case MODULATION_LORA:
return makeLoraInput(handle, param, container, row);
case MODULATION_LRFHSS:
return makeLrFhssInput(handle, param, container, row);
default:
return new NumericParameterInput(handle, param, container, row);
}
}
class SettingsView {
static constexpr size_t MAX_RADIOS = 32;
static constexpr Modulation FIRST_MODULATION = MODULATION_NONE;
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;
RadioHandle radios[MAX_RADIOS] = {0};
size_t radioCount = 0;
RadioHandle radioSelected = nullptr;
RadioStateSubscriptionId radioStateSubId = -1;
Modulation modemsAvailable[MAX_MODEMS] = {};
size_t modemsAvailableCount = 0;
RadioParameter paramsAvailable[MAX_PARAMS] = {};
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;
lv_obj_t* radioDropdown = nullptr;
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;
if(!tt_hal_device_find(DEVICE_TYPE_RADIO, devices, &device_count, MAX_RADIOS)) {
// TT_LOG_W(TAG, "No radios registered with the system?");
} else {
size_t radios_allocated = 0;
for (size_t i = 0; (i < device_count) && (i < MAX_RADIOS); ++i) {
auto radio = tt_hal_radio_alloc(devices[i]);
if (radio) {
// TT_LOG_I(TAG, "Discovered radio \"%s\"", tt_hal_radio_get_name(radio));
radios[radios_allocated] = radio;
radios_allocated++;
} else {
// TT_LOG_E(TAG, "Error allocating radio handle for id=%d", devId);
}
}
radioCount = radios_allocated;
}
}
void getRadioNames(Str &names, const char* const separator) {
int count = 1;
names.clear();
//for (auto radio : radios) {
for (size_t i = 0; i < radioCount; ++i) {
Str name(tt_hal_radio_get_name(radios[i]));
auto last = (i == (radioCount - 1));
if (name == "") {
name.appendf("Unknown Radio %d", count);
}
names.append(name.c_str());
count++;
if (!last) {
names.append(separator);
}
}
}
int getModemAvailableIndex(Modulation m) {
for (size_t i = 0; i < modemsAvailableCount; ++i) {
if (modemsAvailable[i] == m) {
return i;
}
}
return -1;
}
lv_obj_t* initParameterFormGeneric(lv_obj_t *parent, const Modulation modem) {
lv_obj_t *container = propertiesForm;
if (container) {
lv_obj_clean(container);
lv_obj_del(container);
}
paramsAvailableCount = 0;
container = lv_obj_create(parent);
lv_obj_set_style_pad_all(container, 0, 0);
lv_obj_set_layout(container, LV_LAYOUT_GRID);
lv_obj_align(container, LV_ALIGN_TOP_MID, 0, 0);
const int grid_row_size = 40;
const int grid_col_size = 140;
static constexpr size_t row_dsc_last = RADIO_NARROWGRID + 1;
static lv_coord_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), grid_col_size, LV_GRID_TEMPLATE_LAST};
static lv_coord_t row_dsc[row_dsc_last] = {0};
for (size_t i = 0; i < row_dsc_last; ++i) {
row_dsc[i] = grid_row_size; //LV_GRID_FR(1);
}
row_dsc[row_dsc_last - 1] = LV_GRID_TEMPLATE_LAST;
lv_obj_set_grid_dsc_array(container, col_dsc, row_dsc);
char unit_buffer[32] = {0};
// 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;
}
}
for (RadioParameter param = RADIO_POWER;
param <= RADIO_NARROWGRID;
param = static_cast<RadioParameter>((size_t)param + 1)) {
float value = 0.0;
Str value_buffer;
auto status = tt_hal_radio_get_parameter(radioSelected, param, &value);
if (status == RADIO_PARAM_SUCCESS) {
auto input = makeParameterInput(radioSelected, param, modem, container, paramsAvailableCount);
input->onUserChange([](void* ctx) {
SettingsView* self = (SettingsView*)ctx;
self->onParameterInput();
}, this);
paramInputs[param] = input;
//lv_group_focus_obj(input);
paramsAvailable[paramsAvailableCount] = param;
paramsAvailableCount++;
}
}
row_dsc[paramsAvailableCount] = LV_GRID_TEMPLATE_LAST;
lv_obj_set_grid_dsc_array(container, col_dsc, row_dsc);
lv_obj_set_size(container, lv_pct(100), lv_pct(100));
return container;
}
void selectModulation(int modemIndex) {
lv_dropdown_set_selected(modemDropdown, modemIndex);
if (tt_hal_radio_set_modulation(radioSelected, modemsAvailable[modemIndex])) {
propertiesForm = initParameterFormGeneric(mainPanel, modemsAvailable[modemIndex]);
}
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());
}
}
void selectRadio(int index) {
if (radioStateSubId > -1) {
tt_hal_radio_unsubscribe_state(radioSelected, radioStateSubId);
}
radioSelected = radios[index];
crashassert(radioSelected, "Radio selected not allocated");
for (size_t i = 0; i < MAX_MODEMS; ++i) {
modemsAvailable[i] = MODULATION_NONE;
}
Str modulation_list;
modemsAvailableCount = 1;
modemsAvailable[0] = MODULATION_NONE;
modulation_list.append(LV_SYMBOL_MINUS);
modulation_list.append(" ");
modulation_list.append(LV_SYMBOL_MINUS);
modulation_list.append(" ");
modulation_list.append("Disabled\n");
for (Modulation mod = FIRST_MODULATION;
mod <= LAST_MODULATION;
mod = static_cast<Modulation>((size_t)mod + 1)) {
bool canRx = tt_hal_radio_can_receive(radioSelected, mod);
bool canTx = tt_hal_radio_can_transmit(radioSelected, mod);
bool place_sep = (canRx || canTx) && (mod != LAST_MODULATION);
if (!canRx && !canTx) {
continue;
}
modemsAvailable[modemsAvailableCount] = mod;
modemsAvailableCount++;
if (canRx) {
modulation_list.append(LV_SYMBOL_DOWNLOAD);
modulation_list.append(" ");
} else {
modulation_list.append(LV_SYMBOL_MINUS);
modulation_list.append(" ");
}
if (canTx) {
modulation_list.append(LV_SYMBOL_UPLOAD);
modulation_list.append(" ");
} else {
modulation_list.append(LV_SYMBOL_MINUS);
modulation_list.append(" ");
}
modulation_list.append(toString(mod));
if (place_sep) {
modulation_list.append("\n");
}
}
lv_dropdown_set_options(modemDropdown, modulation_list.c_str());
auto modemIndexConfigured = getModemAvailableIndex(tt_hal_radio_get_modulation(radioSelected));
if (modemIndexConfigured > -1) {
lv_dropdown_set_selected(modemDropdown, modemIndexConfigured);
selectModulation(modemIndexConfigured);
}
updateSelectedRadioState(tt_hal_radio_get_state(radioSelected));
radioStateSubId = tt_hal_radio_subscribe_state(radioSelected, [](DeviceId id, RadioState state, void* ctx) {
SettingsView* self = (SettingsView*)ctx;
self->updateSelectedRadioState(state);
}, this);
}
lv_obj_t *initDeviceForm(lv_obj_t *parent) {
lv_obj_t *container = lv_obj_create(parent);
lv_obj_set_size(container, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(container, 0, 0);
const int grid_row_size = 40;
const int grid_col_size = 45;
static lv_coord_t lora_col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(1), grid_col_size, LV_GRID_TEMPLATE_LAST};
static lv_coord_t lora_row_dsc[] = {
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);
Str radio_names;
getRadioNames(radio_names, "\n");
radioDropdown = createGridDropdownInput(container, 0, "Device", radio_names.c_str());
radioSwitch = lv_switch_create(container);
lv_obj_set_grid_cell(radioSwitch,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_size(radioSwitch, lv_pct(100), 20);
lv_obj_t* state_text = lv_label_create(container);
lv_label_set_text(state_text, "State");
lv_obj_set_grid_cell(state_text,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_size(state_text, lv_pct(100), LV_SIZE_CONTENT);
radioStateLabel = lv_label_create(container);
lv_label_set_text(radioStateLabel, "Unknown");
lv_obj_set_grid_cell(radioStateLabel,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_size(radioStateLabel, lv_pct(100), LV_SIZE_CONTENT);
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);
lv_obj_t* input = lv_event_get_target_obj(e);
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);
self->enableSelectedRadio(lv_obj_has_state(input, LV_STATE_CHECKED));
}, LV_EVENT_VALUE_CHANGED, this);
selectRadio(0);
return container;
}
void updateSelectedRadioState(RadioState state) {
switch (state) {
case RADIO_PENDING_ON:
lv_label_set_text(radioStateLabel, "Activating...");
break;
case RADIO_ON:
lv_label_set_text(radioStateLabel, "Activated");
break;
case RADIO_ERROR:
lv_label_set_text(radioStateLabel, "Error");
break;
case RADIO_PENDING_OFF:
lv_label_set_text(radioStateLabel, "Deactivating...");
break;
case RADIO_OFF:
lv_label_set_text(radioStateLabel, "Deactivated");
break;
default:
lv_label_set_text(radioStateLabel, "Unknown");
break;
}
switch (state) {
case RADIO_OFF:
case RADIO_ERROR:
activateConfig();
break;
default:
deactivateConfig();
break;
}
}
bool enableSelectedRadio(bool enable) {
if (radioSelected) {
if (enable) {
return tt_hal_radio_start(radioSelected);
} else {
return tt_hal_radio_stop(radioSelected);
}
}
return false;
}
void activateConfig() {
lv_obj_clear_state(modemDropdown, LV_STATE_DISABLED);
lv_obj_clear_state(radioSwitch, LV_STATE_CHECKED);
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);
lv_obj_add_state(modemPresetDropdown, LV_STATE_DISABLED);
for (size_t i = 0; i < MAX_PARAMS; ++i) {
if (paramInputs[i]) {
paramInputs[i]->deactivate();
}
}
}
void initUi(lv_obj_t *parent) {
mainPanel = lv_obj_create(parent);
lv_obj_set_size(mainPanel, lv_pct(100), lv_pct(80));
lv_obj_set_flex_flow(mainPanel, LV_FLEX_FLOW_COLUMN);
lv_obj_align(mainPanel, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_border_width(mainPanel, 0, 0);
lv_obj_set_style_pad_all(mainPanel, 0, 0);
// Only needed if container needs to be scrollable through encoder long long press
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);
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_theme_get_color_primary(nullptr));
lv_obj_add_style(mainPanel, &style_scroll_focus, LV_PART_SCROLLBAR | LV_STATE_FOCUSED);
deviceForm = initDeviceForm(mainPanel);
}
explicit SettingsView(lv_obj_t *parent) {
queryRadios();
initUi(parent);
}
};
void RadioSet::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
uiDropDownMenu = lv_dropdown_create(toolbar);
lv_dropdown_set_options(uiDropDownMenu, LV_SYMBOL_ENVELOPE " Terminal\n" LV_SYMBOL_SETTINGS " Settings");
lv_dropdown_set_text(uiDropDownMenu, "Menu");
lv_dropdown_set_symbol(uiDropDownMenu, LV_SYMBOL_DOWN);
lv_dropdown_set_selected_highlight(uiDropDownMenu, true);
lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN);
lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_width(uiDropDownMenu, 120);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_height(wrapper, LV_PCT(100));
lv_obj_set_style_pad_all(wrapper, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(wrapper, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(wrapper, 0, 0);
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();
}
RadioSet::~RadioSet() {
if (termView) {
delete termView;
}
if (settingsView) {
delete settingsView;
}
}
// ????
extern "C" void __cxa_pure_virtual() { crash("Entered the Virtual Zone..."); }