diff --git a/ExternalApps/RadioSet/main/Source/RadioSet.cpp b/ExternalApps/RadioSet/main/Source/RadioSet.cpp index 3b8449bf..e0764732 100644 --- a/ExternalApps/RadioSet/main/Source/RadioSet.cpp +++ b/ExternalApps/RadioSet/main/Source/RadioSet.cpp @@ -2,35 +2,14 @@ #include "Str.h" #include "LinkedList.h" #include "Preset.h" +#include "Utils.h" #include #include #include -#include -#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) { @@ -60,983 +39,11 @@ 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_BOOSTEDGAIN: - return "RX Boosted Gain"; - case RADIO_FREQUENCY: - return "Center Frequency"; - case RADIO_BANDWIDTH: - return "Bandwidth"; - case RADIO_SPREADFACTOR: - return "Spread Factor"; - case RADIO_CODINGRATE: - return "Coding Rate"; - 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_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* makeBaseInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row) { - switch (param) { - case RADIO_POWER: //no break - case RADIO_FREQUENCY: - return new NumericParameterInput(handle, param, container, row); - case RADIO_BOOSTEDGAIN: - return new FlagParameterInput(handle, param, container, row); - default: - return nullptr; - } -} - -static ParameterInput* makeParameterInput(RadioHandle handle, const RadioParameter param, const Modulation modulation, lv_obj_t* container, int row) { - auto base_input = makeBaseInput(handle, param, container, row); - if (base_input) return base_input; - - 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 presets; - LinkedList 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 = 60; - 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((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((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); - - 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); diff --git a/ExternalApps/RadioSet/main/Source/RadioSet.h b/ExternalApps/RadioSet/main/Source/RadioSet.h index deda2619..006d6318 100644 --- a/ExternalApps/RadioSet/main/Source/RadioSet.h +++ b/ExternalApps/RadioSet/main/Source/RadioSet.h @@ -1,11 +1,12 @@ #pragma once +#include "SettingsView.h" + #include "tt_app.h" #include "tt_hal_radio.h" #include class TermView; -class SettingsView; class RadioSet { lv_obj_t* mainView = nullptr; diff --git a/ExternalApps/RadioSet/main/Source/SettingsView.cpp b/ExternalApps/RadioSet/main/Source/SettingsView.cpp new file mode 100644 index 00000000..08a55fb0 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/SettingsView.cpp @@ -0,0 +1,889 @@ +#include "SettingsView.h" + +#include "Utils.h" + +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_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* makeBaseInput(RadioHandle handle, const RadioParameter param, lv_obj_t* container, int row) { + switch (param) { + case RADIO_POWER: //no break + case RADIO_FREQUENCY: + return new NumericParameterInput(handle, param, container, row); + case RADIO_BOOSTEDGAIN: + return new FlagParameterInput(handle, param, container, row); + default: + return nullptr; + } +} + +static ParameterInput* makeParameterInput(RadioHandle handle, const RadioParameter param, const Modulation modulation, lv_obj_t* container, int row) { + auto base_input = makeBaseInput(handle, param, container, row); + if (base_input) return base_input; + + 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); + } +} + +void SettingsView::addPreset(Preset* preset) { + presets.pushBack(preset); + presetsByModulation[preset->modulation].pushBack(preset); +} + +void SettingsView::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 SettingsView::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 SettingsView::getModemAvailableIndex(Modulation m) { + for (size_t i = 0; i < modemsAvailableCount; ++i) { + if (modemsAvailable[i] == m) { + return i; + } + } + return -1; +} + +lv_obj_t* SettingsView::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 = 60; + 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((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 SettingsView::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 SettingsView::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 SettingsView::onParameterInput() { + // As the user did an input, this makes any applied + // preset inconsistent, revert back to "None". + lv_dropdown_set_selected(modemPresetDropdown, 0); +} + +void SettingsView::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 SettingsView::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((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* SettingsView::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 SettingsView::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 SettingsView::enableSelectedRadio(bool enable) { + if (radioSelected) { + if (enable) { + return tt_hal_radio_start(radioSelected); + } else { + return tt_hal_radio_stop(radioSelected); + } + } + return false; +} + +void SettingsView::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 SettingsView::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 SettingsView::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); + + deviceForm = initDeviceForm(mainPanel); +} diff --git a/ExternalApps/RadioSet/main/Source/SettingsView.h b/ExternalApps/RadioSet/main/Source/SettingsView.h new file mode 100644 index 00000000..6da20976 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/SettingsView.h @@ -0,0 +1,72 @@ +#pragma once + +#include "Preset.h" + +#include +#include + +class ParameterInput; + +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 presets; + LinkedList 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); + void queryRadios(); + void getRadioNames(Str &names, const char* const separator); + + int getModemAvailableIndex(Modulation m); + + lv_obj_t* initParameterFormGeneric(lv_obj_t *parent, const Modulation modem); + + void selectModulation(int modemIndex); + void selectPreset(int presetIndex); + + void onParameterInput(); + void updatePresets(); + + void selectRadio(int index); + + lv_obj_t *initDeviceForm(lv_obj_t *parent); + + void updateSelectedRadioState(RadioState state); + bool enableSelectedRadio(bool enable); + + void activateConfig(); + void deactivateConfig(); + + void initUi(lv_obj_t *parent); + + explicit SettingsView(lv_obj_t *parent) { + queryRadios(); + initUi(parent); + } +}; diff --git a/ExternalApps/RadioSet/main/Source/Utils.cpp b/ExternalApps/RadioSet/main/Source/Utils.cpp new file mode 100644 index 00000000..d19c5e36 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/Utils.cpp @@ -0,0 +1,63 @@ +#include "Utils.h" + +#include + +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); + } +} + +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_BOOSTEDGAIN: + return "RX Boosted Gain"; + case RADIO_FREQUENCY: + return "Center Frequency"; + case RADIO_BANDWIDTH: + return "Bandwidth"; + case RADIO_SPREADFACTOR: + return "Spread Factor"; + case RADIO_CODINGRATE: + return "Coding Rate"; + 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"; +} diff --git a/ExternalApps/RadioSet/main/Source/Utils.h b/ExternalApps/RadioSet/main/Source/Utils.h new file mode 100644 index 00000000..468ef3b4 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/Utils.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +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); +void crashassert(bool assertion, const char* const message); + +char *const toString(Modulation m); +char *const toString(RadioParameter p);