diff --git a/firmware/main/apps/app_launcher/app_launcher.cpp b/firmware/main/apps/app_launcher/app_launcher.cpp index e1e3d20..26a0f72 100644 --- a/firmware/main/apps/app_launcher/app_launcher.cpp +++ b/firmware/main/apps/app_launcher/app_launcher.cpp @@ -5,6 +5,7 @@ */ #include "app_launcher.h" #include +#include #include #include #include @@ -70,7 +71,16 @@ void AppLauncher::onLauncherDestroy() void AppLauncher::create_launcher_view() { _view = std::make_unique(); - _view->init(getAppProps()); + + auto app_props = getAppProps(); + std::vector filtered_props; + for (const auto& props : app_props) { + if (hal_toggles::is_app_enabled(props.info.name)) { + filtered_props.push_back(props); + } + } + + _view->init(filtered_props); _view->onAppClicked = [&](int appID) { mclog::tagInfo(getAppInfo().name, "handle open app, app id: {}", appID); openApp(appID); diff --git a/firmware/main/apps/app_setup/app_setup.cpp b/firmware/main/apps/app_setup/app_setup.cpp index a50a917..6c4307a 100644 --- a/firmware/main/apps/app_setup/app_setup.cpp +++ b/firmware/main/apps/app_setup/app_setup.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: MIT */ #include "app_setup.h" +#include "workers/feature_toggle.h" #include #include #include @@ -117,6 +118,14 @@ void AppSetup::onOpen() // }} }, }, + { + "Others", + {{"Feature Toggles", + [&]() { + _destroy_menu = true; + _worker = std::make_unique(); + }}}, + }, }; LvglLockGuard lock; diff --git a/firmware/main/apps/app_setup/workers/feature_toggle.cpp b/firmware/main/apps/app_setup/workers/feature_toggle.cpp new file mode 100644 index 0000000..d063b8a --- /dev/null +++ b/firmware/main/apps/app_setup/workers/feature_toggle.cpp @@ -0,0 +1,88 @@ +#include "feature_toggle.h" +#include "toggle_row.h" +#include +#include +#include "common.h" + +using namespace setup_workers; +using namespace hal_toggles; + +static const char* TAG = "FeatureToggle"; + +FeatureToggleWorker::FeatureToggleWorker() +{ + if (!hal_toggles::init_nvs()) { + mclog::tagError(TAG, "NVS init failed"); + return; + } + + for (int i = 0; i < TOGGLE_COUNT; i++) { + _states[i] = hal_toggles::get_toggle_state(TOGGLE_LIST[i].key); + } + + create_toggle_ui(); +} + +void FeatureToggleWorker::create_toggle_ui() +{ + auto screen = lv_screen_active(); + + _panel = std::make_unique(screen); + _panel->setAlign(LV_ALIGN_CENTER); + _panel->setPos(0, 0); + _panel->setSize(320, 240); + _panel->setBgColor(lv_color_hex(0xEDF4FF)); + _panel->setRadius(0); + _panel->setBorderWidth(0); + _panel->setFlexFlow(LV_FLEX_FLOW_COLUMN); + _panel->setFlexAlign(LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + _panel->setPadding(15, 20, 10, 20); + _panel->setPadRow(10); + + _title = std::make_unique(_panel->get()); + _title->setText("Feature Toggles"); + _title->setTextFont(&lv_font_montserrat_20); + _title->setTextColor(lv_color_hex(0x26206A)); + _title->setTextAlign(LV_TEXT_ALIGN_CENTER); + + _panel->setScrollbarMode(LV_SCROLLBAR_MODE_OFF); + + for (int i = 0; i < TOGGLE_COUNT; i++) { + auto row = std::make_unique(_panel->get(), TOGGLE_LIST[i].display_name); + row->set_state(_states[i]); + _toggle_rows_widgets.push_back(std::move(row)); + } + + _reset_button = std::make_unique(_panel->get()); + _reset_button->setSize(260, 40); + apply_button_common_style(*_reset_button); + _reset_button->label().setText("Reset"); + _reset_button->onClick().connect([this]() { + hal_toggles::reset_toggles_to_defaults(); + for (int i = 0; i < TOGGLE_COUNT; i++) { + _states[i] = true; + _toggle_rows_widgets[i]->set_state(true); + } + mclog::tagInfo(TAG, "Toggles reset to defaults"); + }); + + _back_button = std::make_unique(_panel->get()); + _back_button->setSize(260, 40); + apply_button_common_style(*_back_button); + _back_button->label().setText("Back"); + _back_button->onClick().connect([this]() { + _is_done = true; + }); +} + +void FeatureToggleWorker::update() +{ + for (int i = 0; i < TOGGLE_COUNT; i++) { + bool current_state = _toggle_rows_widgets[i]->get_state(); + if (current_state != _states[i]) { + _states[i] = current_state; + hal_toggles::set_toggle_state(TOGGLE_LIST[i].key, current_state); + mclog::tagInfo(TAG, "Toggle %s = %s", TOGGLE_LIST[i].display_name, current_state ? "ON" : "OFF"); + } + } +} \ No newline at end of file diff --git a/firmware/main/apps/app_setup/workers/feature_toggle.h b/firmware/main/apps/app_setup/workers/feature_toggle.h new file mode 100644 index 0000000..c19c143 --- /dev/null +++ b/firmware/main/apps/app_setup/workers/feature_toggle.h @@ -0,0 +1,26 @@ +#pragma once +#include "workers.h" +#include "toggle_row.h" +#include + +namespace setup_workers { + +class FeatureToggleWorker : public WorkerBase { +public: + FeatureToggleWorker(); + void update() override; + +private: + void create_toggle_ui(); + void update_toggle_display(); + + std::unique_ptr _panel; + std::unique_ptr _title; + std::vector> _toggle_rows_widgets; + std::unique_ptr _reset_button; + std::unique_ptr _back_button; + + bool _states[hal_toggles::TOGGLE_COUNT]; +}; + +} // namespace setup_workers \ No newline at end of file diff --git a/firmware/main/apps/app_setup/workers/toggle_row.cpp b/firmware/main/apps/app_setup/workers/toggle_row.cpp new file mode 100644 index 0000000..c510c73 --- /dev/null +++ b/firmware/main/apps/app_setup/workers/toggle_row.cpp @@ -0,0 +1,47 @@ +#include "toggle_row.h" +#include + +namespace setup_workers { + +ToggleRow::ToggleRow(lv_obj_t* parent, const char* label_text) + : _state(false) +{ + auto row = lv_obj_create(parent); + lv_obj_set_size(row, 280, 28); + lv_obj_add_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(row, 0, LV_PART_MAIN); + lv_obj_set_style_border_width(row, 0, LV_PART_MAIN); + lv_obj_set_style_pad_all(row, 0, LV_PART_MAIN); + + auto label = lv_label_create(row); + lv_label_set_text(label, label_text); + lv_obj_add_flag(label, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(label, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_text_font(label, &lv_font_montserrat_14, LV_PART_MAIN); + lv_obj_set_style_text_color(label, lv_color_hex(0x26206A), LV_PART_MAIN); + lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); + + _sw = lv_switch_create(row); + lv_obj_set_style_bg_color(_sw, lv_color_hex(0xB8D3FD), LV_PART_MAIN); + lv_obj_set_style_bg_color(_sw, lv_color_hex(0xB8D3FD), LV_PART_INDICATOR); +} + +void ToggleRow::set_state(bool enabled) +{ + _state = enabled; + if (enabled) { + lv_obj_add_state(_sw, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(_sw, LV_STATE_CHECKED); + } +} + +bool ToggleRow::get_state() const +{ + return lv_obj_has_state(_sw, LV_STATE_CHECKED); +} + +} // namespace setup_workers \ No newline at end of file diff --git a/firmware/main/apps/app_setup/workers/toggle_row.h b/firmware/main/apps/app_setup/workers/toggle_row.h new file mode 100644 index 0000000..defcc2b --- /dev/null +++ b/firmware/main/apps/app_setup/workers/toggle_row.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +namespace setup_workers { + +class ToggleRow { +public: + ToggleRow(lv_obj_t* parent, const char* label_text); + void set_state(bool enabled); + bool get_state() const; + lv_obj_t* get_switch() const { return _sw; } + +private: + lv_obj_t* _sw; + bool _state; +}; + +} // namespace setup_workers \ No newline at end of file diff --git a/firmware/main/hal/hal_toggles.cpp b/firmware/main/hal/hal_toggles.cpp new file mode 100644 index 0000000..4d1a23f --- /dev/null +++ b/firmware/main/hal/hal_toggles.cpp @@ -0,0 +1,115 @@ +#include "hal_toggles.h" +#include +#include +#include +#include + +static const char* TAG = "hal_toggles"; + +namespace hal_toggles { + +static bool _initialized = false; + +bool init_nvs() +{ + if (_initialized) { + return true; + } + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase(); + err = nvs_flash_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS init failed after erase: %s", esp_err_to_name(err)); + return false; + } + } else if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(err)); + return false; + } + _initialized = true; + ESP_LOGI(TAG, "NVS toggles initialized"); + return true; +} + +bool is_first_boot() +{ + init_nvs(); + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + return true; + } + nvs_close(handle); + return false; +} + +bool get_toggle_state(std::string_view key) +{ + init_nvs(); + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to open NVS, returning default true for %.*s", (int)key.size(), key.data()); + return true; + } + + uint8_t value = 1; + err = nvs_get_u8(handle, key.data(), &value); + if (err != ESP_OK) { + value = 1; + } + nvs_close(handle); + return value != 0; +} + +void set_toggle_state(std::string_view key, bool value) +{ + init_nvs(); + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS for write"); + return; + } + err = nvs_set_u8(handle, key.data(), value ? 1 : 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set toggle %.*s", (int)key.size(), key.data()); + } + nvs_commit(handle); + nvs_close(handle); + ESP_LOGI(TAG, "Set toggle %.*s = %s", (int)key.size(), key.data(), value ? "ON" : "OFF"); +} + +void reset_toggles_to_defaults() +{ + init_nvs(); + nvs_handle_t handle; + esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS for reset"); + return; + } + + for (int i = 0; i < TOGGLE_COUNT; i++) { + err = nvs_set_u8(handle, TOGGLE_LIST[i].key, TOGGLE_LIST[i].default_value ? 1 : 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to reset toggle %s", TOGGLE_LIST[i].key); + } + } + nvs_commit(handle); + nvs_close(handle); + ESP_LOGI(TAG, "All toggles reset to defaults"); +} + +bool is_app_enabled(std::string_view app_name) +{ + for (int i = 0; i < TOGGLE_COUNT; i++) { + if (app_name == TOGGLE_LIST[i].display_name) { + return get_toggle_state(TOGGLE_LIST[i].key); + } + } + return true; +} + +} // namespace hal_toggles \ No newline at end of file diff --git a/firmware/main/hal/hal_toggles.h b/firmware/main/hal/hal_toggles.h new file mode 100644 index 0000000..6fea32e --- /dev/null +++ b/firmware/main/hal/hal_toggles.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include + +namespace hal_toggles { + +constexpr const char* NVS_NAMESPACE = "feature_toggles"; + +constexpr const char* KEY_AI_AGENT = "ai_agent"; +constexpr const char* KEY_AVATAR = "avatar"; +constexpr const char* KEY_ESPNOW_REMOTE = "espnow_remote"; +constexpr const char* KEY_APP_CENTER = "app_center"; +constexpr const char* KEY_EZDATA = "ezdata"; +constexpr const char* KEY_DANCE = "dance"; + +constexpr uint8_t TOGGLE_COUNT = 6; + +struct ToggleInfo { + const char* key; + const char* display_name; + bool default_value; +}; + +const ToggleInfo TOGGLE_LIST[TOGGLE_COUNT] = { + {KEY_AI_AGENT, "AI.AGENT", true}, + {KEY_AVATAR, "AVATAR", true}, + {KEY_ESPNOW_REMOTE, "ESPNOW.REMOTE", true}, + {KEY_APP_CENTER, "APP.CENTER", true}, + {KEY_EZDATA, "EZDATA", true}, + {KEY_DANCE, "DANCE", true}, +}; + +bool get_toggle_state(std::string_view key); +void set_toggle_state(std::string_view key, bool value); +void reset_toggles_to_defaults(); +bool is_first_boot(); +bool init_nvs(); +bool is_app_enabled(std::string_view app_name); + +} // namespace hal_toggles \ No newline at end of file