diff --git a/ExternalApps/RadioSet/main/Source/RadioSet.cpp b/ExternalApps/RadioSet/main/Source/RadioSet.cpp index 7d096497..0ca44b19 100644 --- a/ExternalApps/RadioSet/main/Source/RadioSet.cpp +++ b/ExternalApps/RadioSet/main/Source/RadioSet.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -72,7 +73,7 @@ char *const toString(RadioParameter p) { case RADIO_SPREADFACTOR: return "Spread Factor"; case RADIO_CODINGRATE: - return "Coding Rate"; + return "Coding Rate Denominator"; case RADIO_SYNCWORD: return "Sync Word"; case RADIO_PREAMBLES: @@ -96,8 +97,7 @@ class TermView { }; - -static lv_obj_t* initGridDropdownInput(lv_obj_t *container, int row, const char* const label, const char* const items) { +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, @@ -115,42 +115,31 @@ static lv_obj_t* initGridDropdownInput(lv_obj_t *container, int row, const char* 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 { + 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 { @@ -167,8 +156,10 @@ struct NumericParameterInput : public ParameterInput { loadFromRadio(); } - void 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) { @@ -185,16 +176,17 @@ struct NumericParameterInput : public ParameterInput { 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_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_CENTER, 0); + lv_obj_set_style_text_align(unitlabel , LV_TEXT_ALIGN_LEFT, 0); if (unit_override) { lv_label_set_text(unitlabel, unit_override); @@ -203,9 +195,44 @@ struct NumericParameterInput : public ParameterInput { 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 { @@ -220,6 +247,12 @@ struct SliderParameterInput : public ParameterInput { 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)); @@ -234,6 +267,8 @@ struct SliderParameterInput : public ParameterInput { 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, @@ -246,15 +281,39 @@ struct SliderParameterInput : public ParameterInput { 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); + } }; @@ -290,6 +349,21 @@ struct SliderSelectParameterInput : public ParameterInput { 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)); @@ -304,6 +378,8 @@ struct SliderSelectParameterInput : public ParameterInput { 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, @@ -319,20 +395,113 @@ struct SliderSelectParameterInput : public ParameterInput { 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: @@ -352,10 +521,22 @@ static ParameterInput* makeLoraInput(RadioHandle handle, const RadioParameter pa } } + +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); } @@ -372,6 +553,7 @@ class SettingsView { size_t radioCount = 0; RadioHandle radioSelected = nullptr; + RadioStateSubscriptionId radioStateSubId = -1; Modulation modemsAvailable[MAX_MODEMS] = {}; size_t modemsAvailableCount = 0; RadioParameter paramsAvailable[MAX_PARAMS] = {}; @@ -379,11 +561,12 @@ class SettingsView { 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* 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; @@ -461,6 +644,15 @@ public: 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)) { @@ -491,6 +683,10 @@ public: } void selectRadio(int index) { + if (radioStateSubId > -1) { + tt_hal_radio_unsubscribe_state(radioSelected, radioStateSubId); + } + radioSelected = radios[index]; assert(radioSelected); @@ -549,6 +745,13 @@ public: 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) { @@ -560,6 +763,7 @@ public: 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}; @@ -568,14 +772,29 @@ public: Str radio_names; getRadioNames(radio_names, "\n"); - radioDropdown = initGridDropdownInput(container, 0, "Device", radio_names.c_str()); - modemDropdown = initGridDropdownInput(container, 1, "Modulation", "none available"); + 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); @@ -583,10 +802,79 @@ public: 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));