feat: Implement Discord app UI and settings management

- Added MainUI class for displaying voice state, status icon, and buttons.
- Introduced MainUIHandler to manage UI interactions and bridge communication.
- Created SettingsUI for displaying QR code and configuration instructions.
- Implemented SettingsUIHandler to manage settings and web server interactions.
- Developed WebHandler for handling HTTP requests for settings configuration.
- Updated AppRegistry to initialize with the new Discord app descriptor.
- Enhanced InteractionHandler to support keyboard interactions across app switches.
- Updated UIHandler to manage app switching and rendering of app icons.
- Enabled QR code support in LVGL configuration.
This commit is contained in:
GW_MC
2026-02-02 20:47:27 +08:00
parent 12ad5be48a
commit e467951b8c
28 changed files with 1927 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram esp_lvgl_port) set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_http_server esp_wifi esp_psram esp_lvgl_port)
file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.cpp" "ui/**/*.cpp" "ui/**/*.c" "external/**/*.cpp" "external/**/*.c") file(GLOB_RECURSE SRCS "main.cpp" "*.cpp" "*.c" "ui/**/*.cpp" "ui/**/*.c" "external/**/*.cpp" "external/**/*.c")
# Path to the source JSON in this component # Path to the source JSON in this component

View File

@@ -8,10 +8,12 @@
#include "esp_flash.h" #include "esp_flash.h"
#include "esp_system.h" #include "esp_system.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h"
// //
#include "common/constants.h" #include "common/constants.h"
#include "common/queue_defs.h" #include "common/queue_defs.h"
#include "common/system_context.h"
#include "io/nvs_handler.h" #include "io/nvs_handler.h"
#include "io/fs_handler.h" #include "io/fs_handler.h"
#include "info/info.h" #include "info/info.h"
@@ -42,6 +44,15 @@ void init_queues(
void app_main(void) { void app_main(void) {
display_chip_info(); display_chip_info();
// Initialize default event loop early - required for UI events
esp_err_t err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to create default event loop: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
ESP_LOGI(TAG, "Default event loop created.\n");
QueueHandle_t touch_event_queue = NULL; QueueHandle_t touch_event_queue = NULL;
EventGroupHandle_t system_event_group = NULL, system_lifecycle_event_group = NULL; EventGroupHandle_t system_event_group = NULL, system_lifecycle_event_group = NULL;
@@ -76,7 +87,7 @@ void app_main(void) {
// LVGL Handler // LVGL Handler
std::unique_ptr<EInkDisplayHandler> display_uptr(display_handler); std::unique_ptr<EInkDisplayHandler> display_uptr(display_handler);
LVGLHandler lvgl_handler(std::move(display_uptr)); LVGLHandler lvgl_handler(std::move(display_uptr));
esp_err_t err = lvgl_handler.initLVGL(system_event_group); err = lvgl_handler.initLVGL(system_event_group);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize LVGL handler: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to initialize LVGL handler: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS); vTaskDelay(5000 / portTICK_PERIOD_MS);
@@ -87,6 +98,9 @@ void app_main(void) {
kv_storage_handler->init(system_event_group); kv_storage_handler->init(system_event_group);
network_handler->init(system_event_group); network_handler->init(system_event_group);
// Make network handler available to apps
SystemContext::instance().set_network_handler(network_handler);
// //
ESP_LOGI(TAG, "Waiting for system to be ready...\n"); ESP_LOGI(TAG, "Waiting for system to be ready...\n");
xEventGroupWaitBits( xEventGroupWaitBits(
@@ -100,7 +114,13 @@ void app_main(void) {
); );
ESP_LOGI(TAG, "System is ready. Starting main application...\n"); ESP_LOGI(TAG, "System is ready. Starting main application...\n");
// DiscordAppDescriptor::instance(); AppRegistry& app_registry = AppRegistry::instance();
err = app_registry.init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize App Registry: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
UIHandler ui_handler; UIHandler ui_handler;
err = ui_handler.init(); err = ui_handler.init();
if (err != ESP_OK) { if (err != ESP_OK) {

View File

@@ -5,6 +5,9 @@
#include <string> #include <string>
#include <memory> #include <memory>
// Forward declaration
class InteractionHandler;
/** /**
* @brief Base class for all UI applications * @brief Base class for all UI applications
* *
@@ -25,9 +28,10 @@ public:
* between the header and navigation bar. * between the header and navigation bar.
* *
* @param container LVGL container object for this app * @param container LVGL container object for this app
* @param interaction_handler Pointer to interaction handler for keyboard support
* @return ESP_OK on success, error code otherwise * @return ESP_OK on success, error code otherwise
*/ */
virtual esp_err_t init(lv_obj_t* container) = 0; virtual esp_err_t init(lv_obj_t* container, InteractionHandler* interaction_handler) = 0;
/** /**
* @brief Deinitialize and clean up app resources * @brief Deinitialize and clean up app resources

131
main/ui/apps/iotdis/app.cpp Normal file
View File

@@ -0,0 +1,131 @@
#include "ui/apps/iotdis/app.h"
#include "ui/apps/iotdis/ui/main_handler.h"
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "common/system_context.h"
#include "esp_log.h"
static const char* TAG = "IotDisApp";
// ============================================================================
// IotDisApp Implementation
// ============================================================================
IotDisApp::IotDisApp()
: main_ui_handler_(nullptr)
, settings_ui_handler_(nullptr)
, current_page_(Page::MAIN)
, setting_handler_(nullptr)
, interaction_handler_(nullptr) {
setting_handler_ = std::make_unique<SettingHandler>(
std::make_unique<NVSStorageHandler>(IotDisApp::NVS_NAMESPACE)
);
}
IotDisApp::~IotDisApp() { }
esp_err_t IotDisApp::init(lv_obj_t* container, InteractionHandler* interaction_handler) {
ESP_LOGI(TAG, "Initializing Discord app");
container_ = container;
interaction_handler_ = interaction_handler;
// Initialize storage
setting_handler_->init(nullptr);
// Load saved settings
setting_handler_->load_settings();
// Create main UI handler
main_ui_handler_ = std::make_unique<MainUIHandler>();
main_ui_handler_->init(container, interaction_handler_, setting_handler_.get());
// Register settings button callback
main_ui_handler_->register_on_settings_button_clicked(on_settings_button_clicked_static, this);
current_page_ = Page::MAIN;
return ESP_OK;
}
esp_err_t IotDisApp::deinit() {
ESP_LOGI(TAG, "Deinitializing Discord app");
// Clean up UI handlers
if (settings_ui_handler_) {
settings_ui_handler_->deinit();
settings_ui_handler_.reset();
}
if (main_ui_handler_) {
main_ui_handler_->deinit();
main_ui_handler_.reset();
}
return ESP_OK;
}
std::string IotDisApp::get_name() const {
return "Discord";
}
bool IotDisApp::on_back_button_pressed() {
// If on settings page, go back to main page
if (current_page_ == Page::SETTINGS) {
// Clean up settings handler
if (settings_ui_handler_) {
settings_ui_handler_->deinit();
settings_ui_handler_.reset();
}
// Reload settings in case they were updated
setting_handler_->load_settings();
// Recreate main UI handler with updated settings
if (!main_ui_handler_) {
main_ui_handler_ = std::make_unique<MainUIHandler>();
main_ui_handler_->init(container_, interaction_handler_, setting_handler_.get());
main_ui_handler_->register_on_settings_button_clicked(on_settings_button_clicked_static, this);
}
// Update UI with configuration status
main_ui_handler_->update_config_prompt(setting_handler_->is_configured());
current_page_ = Page::MAIN;
return true;
}
// Let system handle back (return to app icons)
return false;
}
// ============================================================================
// Private Methods
// ============================================================================
// Settings page with web server and QR code
void IotDisApp::show_settings_page() {
ESP_LOGI(TAG, "Showing settings page");
// Hide main UI handler
if (main_ui_handler_) {
main_ui_handler_->deinit();
main_ui_handler_.reset();
}
// Create settings UI handler
settings_ui_handler_ = std::make_unique<SettingsUIHandler>();
settings_ui_handler_->init(container_, interaction_handler_, setting_handler_.get());
current_page_ = Page::SETTINGS;
}
// ============================================================================
// Static Callbacks
// ============================================================================
void IotDisApp::on_settings_button_clicked_static(lv_event_t* e) {
IotDisApp* app = static_cast<IotDisApp*>(lv_event_get_user_data(e));
if (app) {
app->show_settings_page();
}
}

61
main/ui/apps/iotdis/app.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include "ui/apps/app.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/ui/main_handler.h"
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "io/nvs_handler.h"
#include <string>
#include <memory>
// Forward declarations
class MainUIHandler;
class SettingsUIHandler;
/**
* @brief IotDis (Discord Integration) App
*
* Manages Discord voice state monitoring and control via UDP bridge.
* Features:
* - Real-time voice state monitoring (muted/unmuted)
* - Manual mute/unmute control
* - Settings for bridge IP/port configuration
* - Connection error detection and notification
* - NVS storage for persistent settings
*/
class IotDisApp : public UIApp {
public:
IotDisApp();
~IotDisApp() override;
esp_err_t init(lv_obj_t* container, InteractionHandler* interaction_handler) override;
esp_err_t deinit(void) override;
std::string get_name(void) const override;
bool on_back_button_pressed(void) override;
private:
// UI handlers
std::unique_ptr<MainUIHandler> main_ui_handler_;
std::unique_ptr<SettingsUIHandler> settings_ui_handler_;
// Current page tracking
enum class Page {
MAIN,
SETTINGS
};
Page current_page_;
// Settings handler (shared across handlers)
std::unique_ptr<SettingHandler> setting_handler_;
// Interaction handler (not owned)
InteractionHandler* interaction_handler_;
static constexpr const char* NVS_NAMESPACE = "discord_app";
// Private methods
void show_settings_page();
// UI callback forwarders
static void on_settings_button_clicked_static(lv_event_t* e);
};

View File

@@ -0,0 +1,256 @@
#include "ui/apps/iotdis/bridge/bridge.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#define TAG "IotDisBridge"
#define MUTE_COMMAND "MUTE"
#define STATUS_COMMAND "STATUS"
#define MUTED_RESPONSE "MUTED"
#define UNMUTED_RESPONSE "UNMUTED"
IotDisBridge::~IotDisBridge() {
stop_polling_task();
}
void IotDisBridge::start_polling_task() {
if (poll_task_handle_) {
ESP_LOGW(TAG, "Polling task already running");
return;
}
udp_client_.init(setting_handler_->get_local_port());
udp_client_.configure(
setting_handler_->get_remote_ip(),
setting_handler_->get_remote_port()
);
stop_polling_ = false;
xTaskCreate(poll_task_, "discord_poll", 4096, this, 5, &poll_task_handle_);
}
void IotDisBridge::stop_polling_task() {
if (!poll_task_handle_) {
if (udp_client_.is_configured()) {
udp_client_.close();
}
return;
}
ESP_LOGI(TAG, "Stopping polling task");
stop_polling_ = true;
// Wait for task to finish (max 2 seconds)
int wait_count = 0;
while (poll_task_handle_ && wait_count < 20) {
vTaskDelay(pdMS_TO_TICKS(100));
wait_count++;
}
if (poll_task_handle_) {
ESP_LOGW(TAG, "Force deleting polling task");
vTaskDelete(poll_task_handle_);
poll_task_handle_ = nullptr;
}
if (udp_client_.is_configured()) {
udp_client_.close();
}
on_status_update_callback_ = nullptr;
status_event_user_data_ = nullptr;
consecutive_failures_ = 0;
}
esp_err_t IotDisBridge::send_mute_command() {
if (!setting_handler_->is_configured()) {
ESP_LOGW(TAG, "Cannot send command: not configured");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Sending MUTE command");
esp_err_t err = udp_client_.send_command(MUTE_COMMAND);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send MUTE command");
return err;
}
return ESP_OK;
}
bool IotDisBridge::test_connection(const std::string& ip, uint16_t port, uint16_t local_port) {
ESP_LOGI(TAG, "Testing connection to %s:%u (local port: %u)", ip.c_str(), port, local_port);
// Create temporary UDP client for testing
UDPClient test_client;
esp_err_t err = test_client.init(local_port);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize test UDP client");
return false;
}
err = test_client.configure(ip, port);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure test UDP client");
return false;
}
err = test_client.send_command(STATUS_COMMAND);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send STATUS command");
return false;
}
ESP_LOGI(TAG, "STATUS command sent, waiting for response (timeout: %dms)", RESPONSE_TIMEOUT_MS);
std::string response;
err = test_client.receive_response(response, RESPONSE_TIMEOUT_MS);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Received response: %s", response.c_str());
bool valid = (response == MUTED_RESPONSE || response == UNMUTED_RESPONSE);
if (!valid) {
ESP_LOGW(TAG, "Unexpected response (expected MUTED or UNMUTED)");
}
test_client.close();
return valid;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Timeout waiting for response");
} else {
ESP_LOGE(TAG, "Error receiving response: %d", err);
}
test_client.close();
return false;
}
//
// private methods
//
void IotDisBridge::poll_task_(void* param) {
IotDisBridge* bridge = static_cast<IotDisBridge*>(param);
ESP_LOGI(TAG, "Polling task started");
while (!bridge->stop_polling_) {
ESP_LOGI(TAG, "Polling for status update...");
bridge->poll_status_();
// Yield to allow display updates to complete
taskYIELD();
// Use longer interval if in error state
int interval = (bridge->consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR)
? ERROR_POLL_INTERVAL_MS
: POLL_INTERVAL_MS;
ESP_LOGI(TAG, "Next poll in %d ms", interval);
vTaskDelay(pdMS_TO_TICKS(interval));
}
ESP_LOGI(TAG, "Polling task stopped");
vTaskDelete(nullptr);
}
void IotDisBridge::poll_status_() {
if (!setting_handler_->is_configured()) {
// Don't poll if not configured
return;
}
// First check for any unsolicited push messages (non-blocking)
std::string push_message;
esp_err_t err = udp_client_.receive_response(push_message, 0); // 0 = non-blocking
if (err == ESP_OK && !push_message.empty()) {
// Received push update from remote
ESP_LOGI(TAG, "Received push update: %s", push_message.c_str());
StatusUpdateEventData event_data {
.state = StatusUpdateEventData::VoiceState::UNKNOWN
};
if (push_message == MUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
} else if (push_message == UNMUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
}
if (on_status_update_callback_) {
on_status_update_callback_(event_data, status_event_user_data_);
}
// VoiceState new_state = VoiceState::UNKNOWN;
// if (push_message == MUTED_RESPONSE) {
// new_state = VoiceState::MUTED;
// } else if (push_message == UNMUTED_RESPONSE) {
// new_state = VoiceState::UNMUTED;
// }
// if (new_state != VoiceState::UNKNOWN) {
// consecutive_failures_ = 0;
// if (main_ui_ && current_page_ == Page::MAIN) {
// main_ui_->show_error_notification(false);
// }
// if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
// current_state_ = new_state;
// xSemaphoreGive(state_mutex_);
// }
// update_main_ui();
// return; // Got push update, skip polling
// }
}
// Send STATUS command for polling
err = udp_client_.send_command(STATUS_COMMAND);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to send STATUS command");
consecutive_failures_++;
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
if (on_status_update_callback_) {
on_status_update_callback_(
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
status_event_user_data_
);
}
}
return;
}
// Wait for response to STATUS command
std::string response;
err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS);
if (err == ESP_OK) {
// Success - reset failure counter
consecutive_failures_ = 0;
StatusUpdateEventData event_data {
.state = StatusUpdateEventData::VoiceState::UNKNOWN
};
if (response == MUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
} else if (response == UNMUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
}
if (on_status_update_callback_) {
on_status_update_callback_(event_data, status_event_user_data_);
}
} else {
// Timeout or error
consecutive_failures_++;
ESP_LOGW(TAG, "No response to STATUS (failures: %d)", consecutive_failures_);
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
if (on_status_update_callback_) {
on_status_update_callback_(
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
status_event_user_data_
);
}
}
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include <string>
#include <cstdint>
#include <memory>
#include "esp_err.h"
#include "network/udp_client.h"
struct StatusUpdateEventData {
enum class VoiceState {
UNKNOWN,
MUTED,
UNMUTED,
ERROR
} state;
};
using StatusEventCallback = void(*)(StatusUpdateEventData, void*);
class IotDisBridge {
public:
IotDisBridge(
SettingHandler* setting_handler
) : setting_handler_(setting_handler) { }
~IotDisBridge();
void start_polling_task();
void stop_polling_task();
esp_err_t send_mute_command();
bool test_connection(const std::string& ip, uint16_t port) {
return test_connection(ip, port, setting_handler_->get_local_port());
}
bool test_connection(const std::string& ip, uint16_t port, uint16_t local_port);
void register_on_status_update_callback(
StatusEventCallback callback,
void* status_event_user_data
) {
on_status_update_callback_ = callback;
status_event_user_data_ = status_event_user_data;
}
private:
static constexpr int POLL_INTERVAL_MS = 2000;
static constexpr int ERROR_POLL_INTERVAL_MS = 5000;
static constexpr int RESPONSE_TIMEOUT_MS = 1000;
static constexpr int MAX_FAILURES_BEFORE_ERROR = 3;
void poll_status_();
// Polling task
static void poll_task_(void* param);
TaskHandle_t poll_task_handle_ = nullptr;
bool stop_polling_ = false;
int consecutive_failures_ = 0;
SettingHandler* setting_handler_ = nullptr;
UDPClient udp_client_;
StatusEventCallback on_status_update_callback_ = nullptr;
void* status_event_user_data_ = nullptr;
};

View File

@@ -0,0 +1,11 @@
#include "ui/apps/iotdis/descriptor.h"
IotDisDescriptor::IotDisDescriptor()
: AppDescriptor("IotDis", std::make_unique<IotDisApp>()) { }
void IotDisDescriptor::draw_icon(lv_obj_t* parent) {
// Draw Discord icon (call/phone symbol)
lv_obj_t* icon = lv_label_create(parent);
lv_label_set_text(icon, LV_SYMBOL_CALL);
lv_obj_center(icon);
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "ui/apps/app.h"
#include "ui/apps/iotdis/app.h"
class IotDisDescriptor : public AppDescriptor {
public:
IotDisDescriptor();
~IotDisDescriptor() override = default;
void draw_icon(lv_obj_t* parent) override;
};

View File

@@ -0,0 +1,52 @@
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "esp_log.h"
#define TAG "SettingHandler"
void SettingHandler::load_settings() {
remote_ip_ = storage_->get(NVS_KEY_IP);
std::string port_str = storage_->get(NVS_KEY_PORT);
std::string local_port_str = storage_->get(NVS_KEY_LOCAL_PORT);
if (!remote_ip_.empty() && !port_str.empty()) {
remote_port_ = static_cast<uint16_t>(atoi(port_str.c_str()));
// Load local port, default to DEFAULT_LOCAL_PORT if not configured
if (!local_port_str.empty()) {
local_port_ = static_cast<uint16_t>(atoi(local_port_str.c_str()));
} else {
local_port_ = DEFAULT_LOCAL_PORT;
}
ESP_LOGI(TAG, "Loaded settings: %s:%u (local port: %u)", remote_ip_.c_str(), remote_port_, local_port_);
} else {
local_port_ = DEFAULT_LOCAL_PORT;
ESP_LOGI(TAG, "No settings found, user setup required");
}
}
void SettingHandler::save_settings(const std::string& ip, uint16_t port, uint16_t local_port) {
if (ip.empty() || port == 0 || local_port == 0) {
ESP_LOGW(TAG, "Cannot save: invalid settings");
return;
}
// Save to NVS
storage_->put(NVS_KEY_IP, ip);
char port_str[8];
snprintf(port_str, sizeof(port_str), "%u", port);
storage_->put(NVS_KEY_PORT, port_str);
char local_port_str[8];
snprintf(local_port_str, sizeof(local_port_str), "%u", local_port);
storage_->put(NVS_KEY_LOCAL_PORT, local_port_str);
// Update local config
remote_ip_ = ip;
remote_port_ = port;
local_port_ = local_port;
ESP_LOGI(TAG, "Settings saved: %s:%u (local port: %u)", remote_ip_.c_str(), remote_port_, local_port_);
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include <string>
#include <memory>
#include "io/nvs_handler.h"
class SettingHandler {
public:
SettingHandler(std::unique_ptr<NVSStorageHandler> storage) :
remote_ip_(""),
remote_port_(0),
local_port_(0),
storage_(std::move(storage)) { }
~SettingHandler() = default;
esp_err_t init(const EventGroupHandle_t& system_event_group) {
storage_->init(system_event_group);
return ESP_OK;
}
void load_settings();
void save_settings(const std::string& ip, uint16_t port) {
save_settings(ip, port, local_port_);
}
void save_settings(const std::string& ip, uint16_t port, uint16_t local_port);
bool is_configured() const { return !remote_ip_.empty() && remote_port_ != 0 && local_port_ != 0; }
std::string get_remote_ip() const { return remote_ip_; }
uint16_t get_remote_port() const { return remote_port_; }
uint16_t get_local_port() const { return local_port_; }
private:
static constexpr uint16_t DEFAULT_LOCAL_PORT = 4212;
static constexpr const char* NVS_KEY_IP = "bridge_ip";
static constexpr const char* NVS_KEY_PORT = "bridge_port";
static constexpr const char* NVS_KEY_LOCAL_PORT = "local_port";
std::string remote_ip_;
uint16_t remote_port_;
uint16_t local_port_;
std::unique_ptr<NVSStorageHandler> storage_;
};

View File

@@ -0,0 +1,173 @@
#include "ui/apps/iotdis/ui/main.h"
#include "ui/apps/iotdis/app.h"
#include "ui/interaction_handler.h"
#include "esp_log.h"
static const char* TAG = "MainUI";
MainUI::~MainUI() {
deinit();
}
esp_err_t MainUI::init(lv_obj_t* parent, InteractionHandler* interaction_handler) {
container_ = parent;
create_ui_(parent);
return ESP_OK;
}
esp_err_t MainUI::deinit(void) {
// LVGL will clean up children automatically when parent is deleted
error_notification_ = nullptr;
status_icon_label_ = nullptr;
status_text_label_ = nullptr;
mute_button_ = nullptr;
settings_button_ = nullptr;
config_prompt_ = nullptr;
container_ = nullptr;
return ESP_OK;
}
void MainUI::create_ui_(lv_obj_t* parent) {
// Set up main page with flex column layout
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(parent, 10, 0);
// === Top Section: Error Notification ===
error_notification_ = lv_obj_create(parent);
lv_obj_set_width(error_notification_, LV_PCT(90));
lv_obj_set_height(error_notification_, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(error_notification_, lv_color_white(), 0);
lv_obj_set_style_bg_opa(error_notification_, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(error_notification_, lv_color_black(), 0);
lv_obj_set_style_border_width(error_notification_, 2, 0);
lv_obj_set_style_pad_all(error_notification_, 10, 0);
lv_obj_set_style_radius(error_notification_, 8, 0);
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_flex_flow(error_notification_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(error_notification_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* error_label = lv_label_create(error_notification_);
lv_label_set_text(error_label, LV_SYMBOL_WARNING " Connection Lost");
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
// === Center Section: Main Content ===
lv_obj_t* center_container = lv_obj_create(parent);
lv_obj_set_size(center_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(center_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_container, 0, 0);
lv_obj_set_style_pad_all(center_container, 0, 0);
lv_obj_set_flex_flow(center_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(center_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(center_container, 15, 0);
lv_obj_set_flex_grow(center_container, 1);
// Status icon (large, centered)
status_icon_label_ = lv_label_create(center_container);
lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE);
// Status text
status_text_label_ = lv_label_create(center_container);
lv_label_set_text(status_text_label_, "Unknown Status");
// Mute button
mute_button_ = lv_btn_create(center_container);
lv_obj_set_size(mute_button_, 200, 60);
lv_obj_t* mute_label = lv_label_create(mute_button_);
lv_label_set_text(mute_label, "MUTE");
lv_obj_center(mute_label);
// === Bottom Section: Settings and Config Prompt ===
lv_obj_t* bottom_container = lv_obj_create(parent);
lv_obj_set_size(bottom_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(bottom_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(bottom_container, 0, 0);
lv_obj_set_style_pad_all(bottom_container, 0, 0);
lv_obj_set_flex_flow(bottom_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bottom_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Config prompt (left side)
config_prompt_ = lv_label_create(bottom_container);
lv_label_set_text(config_prompt_, "Tap " LV_SYMBOL_SETTINGS " to configure");
lv_obj_set_style_text_color(config_prompt_, lv_color_black(), 0);
// Settings button (right side)
settings_button_ = lv_btn_create(bottom_container);
lv_obj_set_size(settings_button_, 60, 60);
lv_obj_t* settings_icon = lv_label_create(settings_button_);
lv_label_set_text(settings_icon, LV_SYMBOL_SETTINGS);
lv_obj_center(settings_icon);
ESP_LOGI(TAG, "Main UI created");
}
esp_err_t MainUI::register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data) {
if (!settings_button_) {
return ESP_ERR_INVALID_STATE;
}
lv_obj_add_event_cb(settings_button_, cb, LV_EVENT_CLICKED, user_data);
return ESP_OK;
}
esp_err_t MainUI::register_on_mute_button_clicked(lv_event_cb_t cb, void* user_data) {
if (!mute_button_) {
return ESP_ERR_INVALID_STATE;
}
lv_obj_add_event_cb(mute_button_, cb, LV_EVENT_CLICKED, user_data);
return ESP_OK;
}
void MainUI::update_status(VoiceState state) {
if (!status_icon_label_ || !status_text_label_) {
return;
}
switch (state) {
case VoiceState::MUTED:
lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE);
lv_label_set_text(status_text_label_, "Muted");
lv_obj_set_style_text_color(status_icon_label_, lv_color_black(), 0);
break;
case VoiceState::UNMUTED:
lv_label_set_text(status_icon_label_, LV_SYMBOL_VOLUME_MAX);
lv_label_set_text(status_text_label_, "Unmuted");
lv_obj_set_style_text_color(status_icon_label_, lv_color_black(), 0);
break;
case VoiceState::ERROR:
lv_label_set_text(status_icon_label_, LV_SYMBOL_WARNING);
lv_label_set_text(status_text_label_, "Connection Error");
lv_obj_set_style_text_color(status_icon_label_, lv_color_black(), 0);
break;
case VoiceState::UNKNOWN:
default:
lv_label_set_text(status_icon_label_, LV_SYMBOL_BLUETOOTH);
lv_label_set_text(status_text_label_, "Unknown Status");
lv_obj_set_style_text_color(status_icon_label_, lv_color_black(), 0);
break;
}
}
void MainUI::show_error_notification(bool show) {
if (error_notification_) {
if (show) {
lv_obj_clear_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
}
}
}
void MainUI::update_config_prompt(bool configured) {
if (config_prompt_) {
if (configured) {
lv_obj_add_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
}
}
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include "lvgl.h"
#include "esp_err.h"
#include <string>
#include "ui/events.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/interaction_handler.h"
// Voice state enumeration
enum class VoiceState {
UNKNOWN,
MUTED,
UNMUTED,
ERROR
};
// Forward declarations
class InteractionHandler;
/**
* @brief Main UI for Discord app
*
* Displays:
* - Current voice state (muted/unmuted/error/unknown)
* - Large status icon
* - Status text
* - Mute toggle button
* - Error notification banner (when connection lost)
* - Settings button
* - Configuration prompt (if not configured)
*/
class MainUI {
public:
MainUI() = default;
~MainUI();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler);
esp_err_t deinit(void);
/**
* @brief Register callback for settings button clicks
* @param cb Callback function
* @param user_data User data to pass to callback
*/
esp_err_t register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data);
/**
* @brief Register callback for mute button clicks
* @param cb Callback function
* @param user_data User data to pass to callback
*/
esp_err_t register_on_mute_button_clicked(lv_event_cb_t cb, void* user_data);
/**
* @brief Update status display with current voice state
* @param state Current voice state
*/
void update_status(VoiceState state);
/**
* @brief Show or hide error notification banner
* @param show true to show, false to hide
*/
void show_error_notification(bool show);
/**
* @brief Update configuration prompt visibility
* @param configured true if settings are configured
*/
void update_config_prompt(bool configured);
private:
void create_ui_(lv_obj_t* parent);
lv_obj_t* container_ = nullptr;
// UI elements
lv_obj_t* error_notification_ = nullptr;
lv_obj_t* status_icon_label_ = nullptr;
lv_obj_t* status_text_label_ = nullptr;
lv_obj_t* mute_button_ = nullptr;
lv_obj_t* settings_button_ = nullptr;
lv_obj_t* config_prompt_ = nullptr;
};

View File

@@ -0,0 +1,170 @@
#include "ui/apps/iotdis/ui/main_handler.h"
#include "esp_log.h"
static const char* TAG = "MainUIHandler";
MainUIHandler::MainUIHandler() {
state_mutex_ = xSemaphoreCreateMutex();
}
MainUIHandler::~MainUIHandler() {
deinit();
if (state_mutex_) {
vSemaphoreDelete(state_mutex_);
state_mutex_ = nullptr;
}
}
esp_err_t MainUIHandler::init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler) {
ESP_LOGI(TAG, "Initializing Main UI Handler");
setting_handler_ = setting_handler;
// Create unique bridge instance for this handler
bridge_ = std::make_unique<IotDisBridge>(setting_handler_);
// Register status update callback
bridge_->register_on_status_update_callback(on_status_update_static_, this);
// Create main UI
main_ui_ = std::make_unique<MainUI>();
main_ui_->init(parent, interaction_handler);
// Register mute button callback
main_ui_->register_on_mute_button_clicked(on_mute_button_clicked_static_, this);
// Update UI with current configuration
main_ui_->update_config_prompt(setting_handler_->is_configured());
update_ui_();
// Start polling task
bridge_->start_polling_task();
return ESP_OK;
}
esp_err_t MainUIHandler::deinit(void) {
ESP_LOGI(TAG, "Deinitializing Main UI Handler");
// Stop polling
if (bridge_) {
bridge_->stop_polling_task();
bridge_.reset();
}
// Clean up UI
if (main_ui_) {
main_ui_->deinit();
main_ui_.reset();
}
return ESP_OK;
}
esp_err_t MainUIHandler::register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data) {
on_settings_callback_ = cb;
settings_callback_user_data_ = user_data;
if (main_ui_) {
main_ui_->register_on_settings_button_clicked(cb, user_data);
} else {
ESP_LOGE(TAG, "Main UI not initialized");
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
void MainUIHandler::update_config_prompt(bool is_configured) {
if (main_ui_) {
main_ui_->update_config_prompt(is_configured);
} else {
ESP_LOGE(TAG, "Main UI not initialized");
}
}
void MainUIHandler::update_status() {
update_ui_();
}
// ============================================================================
// Private Methods
// ============================================================================
void MainUIHandler::send_mute_command_() {
if (!setting_handler_->is_configured()) {
ESP_LOGW(TAG, "Cannot send command: not configured");
return;
}
ESP_LOGI(TAG, "Sending MUTE command");
esp_err_t err = bridge_->send_mute_command();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send MUTE command");
}
}
void MainUIHandler::on_status_update_(StatusUpdateEventData data) {
// Update state in thread-safe manner
if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
current_state_ = data.state;
xSemaphoreGive(state_mutex_);
}
// Update UI
update_ui_();
}
void MainUIHandler::update_ui_() {
if (main_ui_) {
StatusUpdateEventData::VoiceState state;
if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
state = current_state_;
xSemaphoreGive(state_mutex_);
} else {
state = StatusUpdateEventData::VoiceState::UNKNOWN;
}
// Convert to MainUI VoiceState
VoiceState ui_state;
switch (state) {
case StatusUpdateEventData::VoiceState::MUTED:
ui_state = VoiceState::MUTED;
break;
case StatusUpdateEventData::VoiceState::UNMUTED:
ui_state = VoiceState::UNMUTED;
break;
case StatusUpdateEventData::VoiceState::ERROR:
ui_state = VoiceState::ERROR;
break;
default:
ui_state = VoiceState::UNKNOWN;
break;
}
main_ui_->update_status(ui_state);
}
}
// ============================================================================
// Static Callbacks
// ============================================================================
void MainUIHandler::on_mute_button_clicked_static_(lv_event_t* e) {
MainUIHandler* handler = static_cast<MainUIHandler*>(lv_event_get_user_data(e));
if (handler) {
handler->on_mute_button_clicked_();
}
}
void MainUIHandler::on_mute_button_clicked_() {
send_mute_command_();
}
void MainUIHandler::on_status_update_static_(StatusUpdateEventData data, void* user_data) {
MainUIHandler* handler = static_cast<MainUIHandler*>(user_data);
if (handler) {
handler->on_status_update_(data);
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "ui/apps/iotdis/ui/main.h"
#include "ui/interaction_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_err.h"
#include <memory>
/**
* @brief Main UI Handler for Discord App
*
* Manages the MainUI instance and interaction with the InteractionHandler.
* Each handler instance has its own IotDisBridge to prevent conflicts.
*/
class MainUIHandler {
public:
MainUIHandler();
~MainUIHandler();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler);
esp_err_t deinit(void);
esp_err_t register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data);
void update_config_prompt(bool is_configured);
void update_status();
private:
static void on_mute_button_clicked_static_(lv_event_t* e);
static void on_status_update_static_(StatusUpdateEventData data, void* user_data);
void on_mute_button_clicked_();
void on_status_update_(StatusUpdateEventData data);
void send_mute_command_();
void update_ui_();
std::unique_ptr<MainUI> main_ui_ = nullptr;
std::unique_ptr<IotDisBridge> bridge_ = nullptr;
SettingHandler* setting_handler_ = nullptr; // Not owned
// Voice state tracking
StatusUpdateEventData::VoiceState current_state_ = StatusUpdateEventData::VoiceState::UNKNOWN;
SemaphoreHandle_t state_mutex_ = nullptr;
// Callback for settings button
lv_event_cb_t on_settings_callback_ = nullptr;
void* settings_callback_user_data_ = nullptr;
};

View File

@@ -0,0 +1,74 @@
#include "ui/apps/iotdis/ui/settings.h"
#include "ui/interaction_handler.h"
#include "esp_log.h"
#include <cstring>
static const char* TAG = "SettingsUI";
SettingsUI::~SettingsUI() {
deinit();
}
esp_err_t SettingsUI::init(lv_obj_t* parent, InteractionHandler* interaction_handler) {
container_ = parent;
create_ui_(parent, interaction_handler);
return ESP_OK;
}
esp_err_t SettingsUI::deinit(void) {
// LVGL will clean up children automatically when parent is deleted
qr_code_ = nullptr;
status_label_ = nullptr;
container_ = nullptr;
return ESP_OK;
}
void SettingsUI::create_ui_(lv_obj_t* parent, InteractionHandler* interaction_handler) {
// Title
lv_obj_t* title = lv_label_create(parent);
lv_label_set_text(title, "Scan to Configure");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
// Instruction text
lv_obj_t* instruction = lv_label_create(parent);
lv_label_set_text(instruction, "Scan this QR code with your mobile\ndevice to configure settings");
lv_obj_align(instruction, LV_ALIGN_TOP_MID, 0, 60);
lv_obj_set_style_text_align(instruction, LV_TEXT_ALIGN_CENTER, 0);
// QR code (centered)
qr_code_ = lv_qrcode_create(parent);
lv_qrcode_set_size(qr_code_, 250);
lv_qrcode_set_dark_color(qr_code_, lv_color_black());
lv_qrcode_set_light_color(qr_code_, lv_color_white());
lv_obj_align(qr_code_, LV_ALIGN_CENTER, 0, 0);
// Status label below QR code
status_label_ = lv_label_create(parent);
lv_label_set_text(status_label_, "Initializing...");
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_MID, 0, -40);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
ESP_LOGI(TAG, "Settings UI created with QR code display");
}
void SettingsUI::set_config_url(const std::string& url) {
if (!qr_code_ || url.empty()) {
ESP_LOGW(TAG, "Cannot set config URL: qr_code=%p, url=%s", qr_code_, url.c_str());
return;
}
lv_result_t result = lv_qrcode_update(qr_code_, url.c_str(), url.length());
if (result != LV_RESULT_OK) {
ESP_LOGE(TAG, "Failed to update QR code");
set_status_message("Error: Failed to generate QR code");
} else {
ESP_LOGI(TAG, "QR code updated with URL: %s", url.c_str());
}
}
void SettingsUI::set_status_message(const std::string& message) {
if (status_label_) {
lv_label_set_text(status_label_, message.c_str());
}
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include "lvgl.h"
#include "esp_err.h"
#include <string>
// Forward declaration
class InteractionHandler;
/**
* @brief Settings UI for Discord app
*
* Displays a QR code that links to a web-based configuration interface
*/
class SettingsUI {
public:
SettingsUI() = default;
~SettingsUI();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler);
esp_err_t deinit(void);
/**
* @brief Set the configuration URL to display in QR code
* @param url Full URL including IP, port, and auth key
*/
void set_config_url(const std::string& url);
/**
* @brief Update status message below QR code
* @param message Status message to display
*/
void set_status_message(const std::string& message);
private:
void create_ui_(lv_obj_t* parent, InteractionHandler* interaction_handler);
lv_obj_t* container_ = nullptr;
lv_obj_t* qr_code_ = nullptr;
lv_obj_t* status_label_ = nullptr;
};

View File

@@ -0,0 +1,90 @@
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "network/network.h"
#include "esp_log.h"
#include <sstream>
#include <iomanip>
static const char* TAG = "SettingsUIHandler";
SettingsUIHandler::SettingsUIHandler() { }
SettingsUIHandler::~SettingsUIHandler() {
deinit();
}
esp_err_t SettingsUIHandler::init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler) {
ESP_LOGI(TAG, "Initializing Settings UI Handler");
setting_handler_ = setting_handler;
// Create unique bridge instance for this handler
bridge_ = std::make_unique<IotDisBridge>(setting_handler_);
// Create web handler with unique bridge
web_handler_ = std::make_unique<WebHandler>(setting_handler_, bridge_.get());
// Create settings UI
settings_ui_ = std::make_unique<SettingsUI>();
settings_ui_->init(parent, interaction_handler);
// Start web server and setup
setup_web_server_();
return ESP_OK;
}
esp_err_t SettingsUIHandler::deinit(void) {
ESP_LOGI(TAG, "Deinitializing Settings UI Handler");
// Stop web server
if (web_handler_) {
web_handler_->stop_web_server();
web_handler_.reset();
}
// Stop bridge
if (bridge_) {
bridge_->stop_polling_task();
bridge_.reset();
}
// Clean up UI
if (settings_ui_) {
settings_ui_->deinit();
settings_ui_.reset();
}
return ESP_OK;
}
// ============================================================================
// Private Methods
// ============================================================================
void SettingsUIHandler::setup_web_server_() {
// Start web server
web_handler_->start_web_server();
if (web_handler_->is_running()) {
std::string device_ip = web_handler_->get_device_ip();
uint16_t port = web_handler_->get_port();
if (!device_ip.empty()) {
std::string url = web_handler_->get_url();
settings_ui_->set_config_url(url);
std::ostringstream status;
status << "Server running on " << device_ip << ":" << port;
settings_ui_->set_status_message(status.str());
ESP_LOGI(TAG, "QR code URL: %s", url.c_str());
} else {
settings_ui_->set_status_message("Error: No IP address");
ESP_LOGE(TAG, "Failed to get device IP address");
}
} else {
settings_ui_->set_status_message("Error: Failed to start server");
ESP_LOGE(TAG, "Web server failed to start");
}
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "ui/apps/iotdis/ui/settings.h"
#include "ui/interaction_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/web/web_handlers.h"
#include "esp_err.h"
#include <memory>
/**
* @brief Settings UI Handler for Discord App
*
* Manages the SettingsUI instance, web server, and interaction with the InteractionHandler.
* Each handler instance has its own IotDisBridge to prevent conflicts.
*/
class SettingsUIHandler {
public:
SettingsUIHandler();
~SettingsUIHandler();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler);
esp_err_t deinit(void);
private:
void setup_web_server_();
std::unique_ptr<SettingsUI> settings_ui_ = nullptr;
std::unique_ptr<IotDisBridge> bridge_ = nullptr;
std::unique_ptr<WebHandler> web_handler_ = nullptr;
SettingHandler* setting_handler_ = nullptr; // Not owned
};

View File

@@ -0,0 +1,362 @@
#include "web_handlers.h"
#include "../app.h"
#include "esp_log.h"
#include "network/network.h"
#include "common/system_context.h"
#include "esp_random.h"
#include <sstream>
#include <iomanip>
static const char* TAG = "DiscordWebHandler";
WebHandler::~WebHandler() {
stop_web_server();
}
esp_err_t WebHandler::start_web_server() {
if (web_server_ && web_server_->is_running()) {
ESP_LOGI(TAG, "Web server already running");
return ESP_OK;
}
auth_key_ = generate_auth_key_();
esp_err_t ret = web_server_->start(
auth_key_,
8080
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start web server");
return ret;
}
ret = register_web_endpoints_();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to register web endpoints");
web_server_->stop();
return ret;
}
ESP_LOGI(TAG, "Web server started");
return ESP_OK;
}
esp_err_t WebHandler::stop_web_server() {
if (web_server_) {
web_server_->stop();
ESP_LOGI(TAG, "Web server stopped");
}
auth_key_.clear();
return ESP_OK;
}
std::string WebHandler::get_url() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
std::string device_ip = wifi_handler.get_current_ip();
if (device_ip.empty()) {
ESP_LOGW(TAG, "Device not connected to WiFi");
return "";
}
uint16_t port = web_server_->get_port();
std::ostringstream url;
url << "http://" << device_ip << ":" << port << "/?auth=" << auth_key_;
return url.str();
}
return "";
}
std::string WebHandler::get_device_ip() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
return wifi_handler.get_current_ip();
}
return "";
}
uint16_t WebHandler::get_port() const {
if (web_server_ && web_server_->is_running()) {
return web_server_->get_port();
}
return 0;
}
//
//
//
std::string WebHandler::generate_auth_key_() {
// Generate 128-bit random key using ESP32 hardware RNG
uint32_t rand_values[4];
for (int i = 0; i < 4; i++) {
rand_values[i] = esp_random();
}
// Convert to hex string
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < 4; i++) {
oss << std::setw(8) << rand_values[i];
}
return oss.str();
}
esp_err_t WebHandler::register_web_endpoints_() {
if (!web_server_ || !web_server_->is_running()) {
return ESP_FAIL;
}
// GET / - Serve settings page
httpd_uri_t settings_page_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = settings_page_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&settings_page_uri);
// POST /save - Save settings
httpd_uri_t save_settings_uri = {
.uri = "/save",
.method = HTTP_POST,
.handler = save_settings_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&save_settings_uri);
// POST /test - Test connection
httpd_uri_t test_connection_uri = {
.uri = "/test",
.method = HTTP_POST,
.handler = test_connection_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&test_connection_uri);
return ESP_OK;
}
esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Validate auth
size_t query_len = httpd_req_get_url_query_len(req);
if (query_len == 0) {
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
char* query = new char[query_len + 1];
if (httpd_req_get_url_query_str(req, query, query_len + 1) != ESP_OK) {
delete[] query;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request");
return ESP_FAIL;
}
if (!self->web_server_->validate_auth(query)) {
delete[] query;
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
delete[] query;
// Get current settings (access private members via friend)
std::string current_ip = self->setting_handler_->get_remote_ip();
uint16_t current_port = self->setting_handler_->get_remote_port();
uint16_t current_local_port = self->setting_handler_->get_local_port();
// Build HTML page
std::ostringstream html;
html << "<!DOCTYPE html><html><head>"
<< "<meta name='viewport' content='width=device-width, initial-scale=1'>"
<< "<title>Discord Bridge Settings</title>"
<< "<style>"
<< "body{font-family:Arial,sans-serif;max-width:600px;margin:50px auto;padding:20px;}"
<< "h1{color:#333;}"
<< "label{display:block;margin-top:15px;font-weight:bold;}"
<< "input{width:100%;padding:10px;margin-top:5px;box-sizing:border-box;font-size:16px;}"
<< "button{width:100%;padding:12px;margin-top:20px;font-size:16px;cursor:pointer;}"
<< ".btn-primary{background:#4CAF50;color:white;border:none;}"
<< ".btn-secondary{background:#008CBA;color:white;border:none;}"
<< "#result{margin-top:20px;padding:10px;border-radius:5px;display:none;}"
<< ".success{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}"
<< ".error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}"
<< "</style></head><body>"
<< "<h1>Discord Bridge Settings</h1>"
<< "<form id='settingsForm'>"
<< "<label for='ip'>Bridge IP Address:</label>"
<< "<input type='text' id='ip' name='ip' placeholder='e.g., 192.168.1.100' value='" << current_ip << "' required>"
<< "<label for='port'>Bridge Port:</label>"
<< "<input type='number' id='port' name='port' placeholder='e.g., 4211' value='" << current_port << "' required min='1' max='65535'>"
<< "<label for='localPort'>ESP32 Local Port:</label>"
<< "<input type='number' id='localPort' name='localPort' placeholder='e.g., 4212' value='" << current_local_port << "' required min='1' max='65535'>"
<< "<button type='button' class='btn-secondary' onclick='testConnection()'>Test Connection</button>"
<< "<button type='submit' class='btn-primary'>Save Settings</button>"
<< "</form>"
<< "<div id='result'></div>"
<< "<script>"
<< "const form=document.getElementById('settingsForm');"
<< "const result=document.getElementById('result');"
<< "function showResult(msg,isSuccess){"
<< "result.textContent=msg;"
<< "result.className=isSuccess?'success':'error';"
<< "result.style.display='block';"
<< "}"
<< "function testConnection(){"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "showResult('Testing connection...',false);"
<< "fetch('/test',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>showResult(data.message,data.success))"
<< ".catch(()=>showResult('Request failed',false));"
<< "}"
<< "form.addEventListener('submit',function(e){"
<< "e.preventDefault();"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "fetch('/save',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>{showResult(data.message,data.success);"
<< "if(data.success)setTimeout(()=>result.style.display='none',3000);})"
<< ".catch(()=>showResult('Request failed',false));"
<< "});"
<< "</script></body></html>";
std::string html_str = html.str();
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, html_str.c_str(), html_str.length());
return ESP_OK;
}
esp_err_t WebHandler::save_settings_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Save settings
if (self && self->setting_handler_) {
self->setting_handler_->save_settings(std::string(ip), port, local_port);
}
const char* resp = "{\"success\":true,\"message\":\"Settings saved successfully\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Settings saved via web interface: %s:%u (local port: %u)", ip, port, local_port);
return ESP_OK;
}
esp_err_t WebHandler::test_connection_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
IotDisBridge* bridge = self ? self->bridge_ : nullptr;
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Test connection
bool success = false;
if (bridge) {
success = bridge->test_connection(std::string(ip), port, local_port);
} else {
ESP_LOGE(TAG, "IotDisBridge pointer is null, cannot test connection");
}
const char* resp = success
? "{\"success\":true,\"message\":\"Connection successful!\"}"
: "{\"success\":false,\"message\":\"No response from bridge\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Connection test via web interface: %s:%u (local port: %u) - %s", ip, port, local_port, success ? "SUCCESS" : "FAILED");
return ESP_OK;
}

View File

@@ -0,0 +1,81 @@
#pragma once
#include "esp_http_server.h"
#include <string>
#include "network/web_server_handler.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
/**
* @brief HTTP request handlers for Discord Bridge settings web interface
*
* These handlers serve the web configuration page and process
* settings updates and connection tests.
*/
class WebHandler {
public:
WebHandler(
SettingHandler* setting_handler,
IotDisBridge* bridge
) :
web_server_(std::make_unique<WebServerHandler>())
, setting_handler_(setting_handler)
, bridge_(bridge) { }
~WebHandler();
esp_err_t start_web_server();
esp_err_t stop_web_server();
std::string get_url() const;
std::string get_device_ip() const;
uint16_t get_port() const;
bool is_running() const {
return web_server_ && web_server_->is_running();
}
private:
std::string generate_auth_key_();
esp_err_t register_web_endpoints_();
/**
* @brief Serve the main settings configuration page
*
* Validates authentication and serves an HTML form with current settings.
* Requires auth query parameter matching the session key.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t settings_page_handler_(httpd_req_t* req);
/**
* @brief Save bridge connection settings
*
* Parses POST data containing ip, port, and localPort fields.
* Validates and persists settings to NVS storage.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t save_settings_handler_(httpd_req_t* req);
/**
* @brief Test connection to Discord bridge
*
* Creates temporary UDP client to test connectivity with provided settings.
* Returns JSON response indicating success or failure.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t test_connection_handler_(httpd_req_t* req);
std::unique_ptr<WebServerHandler> web_server_;
SettingHandler* setting_handler_ = nullptr; ///< Pointer to settings handler (not owned)
std::string auth_key_;
IotDisBridge* bridge_ = nullptr; ///< Pointer to IotDisBridge (not owned)
};

View File

@@ -0,0 +1,9 @@
#include "ui/apps/registry.h"
#include "ui/apps/iotdis/descriptor.h"
esp_err_t AppRegistry::init(void) {
register_app(std::make_unique<IotDisDescriptor>());
return ESP_OK;
}

View File

@@ -13,6 +13,12 @@ public:
return registry; return registry;
} }
/**
* @brief Initialize the app registry with built-in apps
*
*/
esp_err_t init(void);
void register_app(std::unique_ptr<AppDescriptor> app_descriptor) { void register_app(std::unique_ptr<AppDescriptor> app_descriptor) {
if (app_descriptors_.find(app_descriptor->get_name()) != app_descriptors_.end()) { if (app_descriptors_.find(app_descriptor->get_name()) != app_descriptors_.end()) {
// App already registered // App already registered

View File

@@ -12,13 +12,20 @@ InteractionHandler::~InteractionHandler() {
} }
} }
esp_err_t InteractionHandler::init(lv_obj_t* app_container) { esp_err_t InteractionHandler::init(lv_obj_t* parent_container) {
if (!app_container) { if (!parent_container) {
ESP_LOGE(TAG, "Invalid argument: app_container is nullptr"); ESP_LOGE(TAG, "Invalid argument: parent_container is nullptr");
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
app_container_ = app_container; parent_container_ = parent_container;
keyboard_ = lv_keyboard_create(app_container_);
keyboard_ = lv_keyboard_create(parent_container_);
if (!keyboard_) {
ESP_LOGE(TAG, "Failed to create keyboard object");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Keyboard created successfully at %p", keyboard_);
lv_obj_add_flag(keyboard_, LV_OBJ_FLAG_HIDDEN); // start hidden lv_obj_add_flag(keyboard_, LV_OBJ_FLAG_HIDDEN); // start hidden
lv_obj_add_event_cb( lv_obj_add_event_cb(
keyboard_, keyboard_,
@@ -102,11 +109,24 @@ void InteractionHandler::on_keyboard_event_(lv_event_t* e) {
} }
esp_err_t InteractionHandler::show_keyboard_for_textarea_(lv_obj_t* textarea) { esp_err_t InteractionHandler::show_keyboard_for_textarea_(lv_obj_t* textarea) {
if (!keyboard_ || !textarea) { if (!textarea) {
ESP_LOGE(TAG, "Invalid state or argument in show_keyboard_for_textarea_"); ESP_LOGE(TAG, "Invalid argument: textarea is nullptr");
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
if (!keyboard_) {
ESP_LOGE(TAG, "Keyboard object is nullptr - was InteractionHandler properly initialized?");
return ESP_ERR_INVALID_STATE;
}
// Verify keyboard object is still valid
if (!lv_obj_is_valid(keyboard_)) {
ESP_LOGE(TAG, "Keyboard object is no longer valid - it may have been deleted");
keyboard_ = nullptr;
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Showing keyboard for textarea %p", textarea);
focused_textarea_ = textarea; focused_textarea_ = textarea;
lv_keyboard_set_textarea(keyboard_, textarea); lv_keyboard_set_textarea(keyboard_, textarea);
lv_obj_clear_flag(keyboard_, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(keyboard_, LV_OBJ_FLAG_HIDDEN);

View File

@@ -28,9 +28,10 @@ public:
* *
* Sets up necessary event listeners and state. * Sets up necessary event listeners and state.
* *
* @param parent_container Parent container for keyboard (typically the screen)
* @return ESP_OK on success, error code otherwise * @return ESP_OK on success, error code otherwise
*/ */
esp_err_t init(lv_obj_t* app_container); esp_err_t init(lv_obj_t* parent_container);
/** /**
* @brief Deinitialize the Interaction Handler * @brief Deinitialize the Interaction Handler
@@ -58,8 +59,8 @@ private:
esp_err_t show_keyboard_for_textarea_(lv_obj_t* textarea); esp_err_t show_keyboard_for_textarea_(lv_obj_t* textarea);
esp_err_t hide_keyboard_(void); esp_err_t hide_keyboard_(void);
// Pointers to key UI objects, owned by UIHandler // Parent container (typically screen), reference only
lv_obj_t* app_container_ = nullptr; lv_obj_t* parent_container_ = nullptr;
// owned keyboard object // owned keyboard object
lv_obj_t* keyboard_ = nullptr; lv_obj_t* keyboard_ = nullptr;
// Currently focused textarea, reference only // Currently focused textarea, reference only

View File

@@ -1,8 +1,14 @@
#include "ui/ui_handler.h" #include "ui/ui_handler.h"
#include "ui/apps/registry.h"
#include "esp_log.h" #include "esp_log.h"
#define TAG "UIHandler" #define TAG "UIHandler"
struct AppClickUserData {
UIHandler* ui_handler;
std::string app_name;
};
UIHandler::~UIHandler() { UIHandler::~UIHandler() {
deinit(); deinit();
} }
@@ -18,7 +24,9 @@ esp_err_t UIHandler::init(void) {
return ret; return ret;
} }
ret = interaction_handler_.init(root_layout_.get_app_container()); // Initialize InteractionHandler with screen as parent (not app_container)
// so keyboard survives app switches
ret = interaction_handler_.init(screen);
if (ret != ESP_OK) { if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize InteractionHandler"); ESP_LOGE(TAG, "Failed to initialize InteractionHandler");
return ret; return ret;
@@ -61,7 +69,7 @@ esp_err_t UIHandler::deinit(void) {
return ESP_OK; return ESP_OK;
} }
esp_err_t UIHandler::switch_app(std::shared_ptr<AppDescriptor> app_descriptor) { esp_err_t UIHandler::switch_app(AppDescriptor* app_descriptor) {
if (!app_descriptor) { if (!app_descriptor) {
ESP_LOGE(TAG, "Invalid app descriptor"); ESP_LOGE(TAG, "Invalid app descriptor");
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
@@ -100,7 +108,7 @@ esp_err_t UIHandler::switch_app(std::shared_ptr<AppDescriptor> app_descriptor) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
ret = new_app->init(app_container); ret = new_app->init(app_container, &interaction_handler_);
if (ret != ESP_OK) { if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize app: %s", new_app->get_name().c_str()); ESP_LOGE(TAG, "Failed to initialize app: %s", new_app->get_name().c_str());
active_descriptor_ = nullptr; active_descriptor_ = nullptr;
@@ -239,6 +247,45 @@ esp_err_t UIHandler::create_main_screen_(lv_obj_t* parent) {
return ret; return ret;
} }
// render all apps
for (const auto& [name, descriptor] : AppRegistry::instance()) {
lv_obj_t* app_icon_container = lv_obj_create(root_layout_.get_app_container());
lv_obj_set_size(app_icon_container, 100, 100);
lv_obj_set_style_pad_all(app_icon_container, 10, 0);
lv_obj_set_flex_flow(app_icon_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(app_icon_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Draw the app icon
descriptor->draw_icon(app_icon_container);
// App name label
lv_obj_t* label = lv_label_create(app_icon_container);
lv_label_set_text(label, name.c_str());
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
// Center the icon container
lv_obj_center(app_icon_container);
// Register click event to switch to the app
lv_obj_add_event_cb(app_icon_container,
[](lv_event_t* e) {
AppClickUserData* user_data = static_cast<AppClickUserData*>(lv_event_get_user_data(e));
UIHandler* ui_handler = user_data->ui_handler;
std::string app_name = user_data->app_name;
AppDescriptor* descriptor = AppRegistry::instance()[app_name];
if (descriptor) {
ui_handler->switch_app(descriptor);
} else {
ESP_LOGE(TAG, "App descriptor not found for app: %s", app_name.c_str());
}
},
LV_EVENT_CLICKED,
new AppClickUserData { this, name }
);
}
// Register back button callback // Register back button callback
lv_event_dsc_t* back_event_dsc = nullptr; lv_event_dsc_t* back_event_dsc = nullptr;
ret = root_layout_.register_back_button_callback( ret = root_layout_.register_back_button_callback(

View File

@@ -53,13 +53,13 @@ public:
* @brief Switch to a new app by its descriptor * @brief Switch to a new app by its descriptor
* *
* Deinitializes the current app (if any), initializes the new app, * Deinitializes the current app (if any), initializes the new app,
* and updates the display. Holds shared ownership of the descriptor * and updates the display. The descriptor must remain valid in the
* to ensure the app remains valid while active. * AppRegistry for the lifetime of the app.
* *
* @param app_descriptor Shared pointer to the app descriptor * @param app_descriptor Pointer to the app descriptor (managed by AppRegistry)
* @return ESP_OK on success, error code otherwise * @return ESP_OK on success, error code otherwise
*/ */
esp_err_t switch_app(std::shared_ptr<AppDescriptor> app_descriptor); esp_err_t switch_app(AppDescriptor* app_descriptor);
/** /**
* @brief Display shutdown screen * @brief Display shutdown screen
@@ -114,5 +114,5 @@ private:
lv_obj_t* main_screen_ = nullptr; ///< Root screen lv_obj_t* main_screen_ = nullptr; ///< Root screen
RootLayout root_layout_; ///< Main screen layout manager RootLayout root_layout_; ///< Main screen layout manager
std::shared_ptr<AppDescriptor> active_descriptor_ = nullptr; ///< Currently active app descriptor (shared ownership) AppDescriptor* active_descriptor_ = nullptr; ///< Currently active app descriptor (managed by AppRegistry)
}; };

View File

@@ -2338,7 +2338,7 @@ CONFIG_LV_FS_DEFAULT_DRIVER_LETTER=0
# CONFIG_LV_USE_GIF is not set # CONFIG_LV_USE_GIF is not set
# CONFIG_LV_BIN_DECODER_RAM_LOAD is not set # CONFIG_LV_BIN_DECODER_RAM_LOAD is not set
# CONFIG_LV_USE_RLE is not set # CONFIG_LV_USE_RLE is not set
# CONFIG_LV_USE_QRCODE is not set CONFIG_LV_USE_QRCODE=y
# CONFIG_LV_USE_BARCODE is not set # CONFIG_LV_USE_BARCODE is not set
# CONFIG_LV_USE_FREETYPE is not set # CONFIG_LV_USE_FREETYPE is not set
# CONFIG_LV_USE_TINY_TTF is not set # CONFIG_LV_USE_TINY_TTF is not set