#include "RadioSet.h" #include "Str.h" #include "LinkedList.h" #include "Preset.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) { 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 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 = 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((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); 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..."); }