#include "RadioSet.h" #include "Str.h" #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"; 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* initGridDropdownInput(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; } static lv_obj_t* initGridTextInput(lv_obj_t *container, int row, const char* const label, const char* const defval, const char* const unit) { const int height = LV_SIZE_CONTENT; 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), height); lv_obj_t* 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_text(input, defval); lv_textarea_set_one_line(input, true); lv_obj_t* unit_obj = lv_label_create(container); lv_label_set_text(unit_obj, unit); lv_obj_set_grid_cell(unit_obj, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_CENTER, row, 1); lv_obj_set_size(unit_obj, lv_pct(100), height); lv_obj_set_style_text_align(unit_obj , LV_TEXT_ALIGN_CENTER, 0); return input; } struct ParameterInput { const RadioHandle handle; const RadioParameter param; ParameterInput(RadioHandle handle, const RadioParameter param) : handle(handle) , param(param) {} virtual void updatePreview() = 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(); } void loadFromRadio() { } 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); //TODO: LOAD VALUE //lv_textarea_set_text(input, defval); lv_textarea_set_one_line(input, true); 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_CENTER, 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); } } virtual void updatePreview() override {} }; 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); } 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); 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(); }, LV_EVENT_VALUE_CHANGED, this); } 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); } }; 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); } 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); 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(); }, LV_EVENT_VALUE_CHANGED, this); } 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()); } }; 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}; 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* 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); 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; 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 *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}; 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) { 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); } } 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, 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 = initGridDropdownInput(container, 0, "Device", radio_names.c_str()); modemDropdown = initGridDropdownInput(container, 1, "Modulation", "none available"); 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_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); selectRadio(0); return container; } 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..."); }