#include "RadioSet.h" #include "Str.h" #include #include #include #include #include #include "tt_app_alertdialog.h" constexpr const char* TAG = "RadioSet"; void crash(const char* const message) { tt_app_alertdialog_start("RadioSet has crashed!", message, nullptr, 0); } // 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; const RadioHandle handle; const RadioParameter param; 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; virtual void updatePreview() = 0; virtual void activate() = 0; virtual void deactivate() = 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(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { Str txt; txt.appendf(fmt, value); lv_textarea_set_text(input, txt.c_str()); } } void storeToRadio() { 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); } }; 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); loadFromRadio(); 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); updatePreview(); 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(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { lv_slider_set_value(slider, value, LV_ANIM_ON); } } void storeToRadio() { 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); } }; 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); loadFromRadio(); 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)); updatePreview(); 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(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { lv_slider_set_value(slider, get_selection_index(value), LV_ANIM_ON); } } void storeToRadio() { 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); } }; 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(); }, LV_EVENT_VALUE_CHANGED, this); } void loadFromRadio() { float value; if (tt_hal_radio_get_parameter(handle, param, &value) == RADIO_PARAM_SUCCESS) { if (value != 0.0) { lv_obj_add_state(input, LV_STATE_CHECKED); } else { lv_obj_clear_state(input, LV_STATE_CHECKED); } } } void storeToRadio() { 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); } }; 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; 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 *propertiesForm = nullptr; public: 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, it's safe and this loop costs nothing' for (size_t i = 0; i < MAX_PARAMS; ++i) { 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); paramInputs[paramsAvailableCount] = 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]); //clownvomit(propertiesForm); } } void selectRadio(int index) { if (radioStateSubId > -1) { tt_hal_radio_unsubscribe_state(radioSelected, radioStateSubId); } radioSelected = radios[index]; assert(radioSelected); 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, 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"); 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(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); for (size_t i = 0; i < paramsAvailableCount; ++i) { assert(paramInputs[i]); paramInputs[i]->activate(); } } void deactivateConfig() { lv_obj_add_state(radioSwitch, LV_STATE_CHECKED); lv_obj_add_state(modemDropdown, LV_STATE_DISABLED); for (size_t i = 0; i < paramsAvailableCount; ++i) { assert(paramInputs[i]); paramInputs[i]->deactivate(); } } 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); //lv_obj_add_event_cb(btn, scrollbar_highlight, LV_EVENT_FOCUS, NULL); //lv_obj_add_event_cb(btn, scrollbar_restore, LV_EVENT_DEFOCUSED, NULL); // Create once (e.g. during init) static lv_style_t style_scroll_focus; lv_style_init(&style_scroll_focus); lv_style_set_bg_color(&style_scroll_focus, lv_color_make(0x40,0xA0,0xFF)); lv_style_set_bg_opa(&style_scroll_focus, LV_OPA_COVER); lv_style_set_border_width(&style_scroll_focus, 1); lv_style_set_border_color(&style_scroll_focus, lv_color_black()); // Apply style targeted to the scrollbar part when the object is FOCUSED // LV_PART_SCROLLBAR | LV_STATE_FOCUSED will ensure the style is used only while focused. lv_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); } RadioSet::~RadioSet() { if (termView) { delete termView; } if (settingsView) { delete settingsView; } } // ???? extern "C" void __cxa_pure_virtual() { crash("Entered the Virtual Zone..."); }