Compare commits
5 Commits
06e81301b2
...
d0c9a7c4cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c9a7c4cc | ||
|
|
1dff88ed1a | ||
|
|
e467951b8c | ||
|
|
12ad5be48a | ||
|
|
bcbde510e0 |
@@ -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
|
||||||
|
|||||||
30
main/common/system_context.h
Normal file
30
main/common/system_context.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class NetworkHandler;
|
||||||
|
class WifiHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief System context providing access to global system components
|
||||||
|
*/
|
||||||
|
class SystemContext {
|
||||||
|
public:
|
||||||
|
static SystemContext& instance() {
|
||||||
|
static SystemContext context;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_network_handler(NetworkHandler* handler) {
|
||||||
|
network_handler_ = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkHandler* get_network_handler() const {
|
||||||
|
return network_handler_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SystemContext() = default;
|
||||||
|
SystemContext(const SystemContext&) = delete;
|
||||||
|
SystemContext& operator=(const SystemContext&) = delete;
|
||||||
|
|
||||||
|
NetworkHandler* network_handler_ = nullptr;
|
||||||
|
};
|
||||||
@@ -61,9 +61,9 @@ EInkDisplayHandler::~EInkDisplayHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::deep_sleep_display(void) {
|
esp_err_t EInkDisplayHandler::deep_sleep_display(void) {
|
||||||
ESP_LOGI(TAG, "Putting display into deep sleep mode...");
|
ESP_LOGV(TAG, "Putting display into deep sleep mode...");
|
||||||
if (is_deep_sleep_) {
|
if (is_deep_sleep_) {
|
||||||
ESP_LOGI(TAG, "Display is already in deep sleep mode");
|
ESP_LOGW(TAG, "Display is already in deep sleep mode");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -111,7 +111,7 @@ esp_err_t EInkDisplayHandler::refresh_display() {
|
|||||||
} else {
|
} else {
|
||||||
// refresh does not correctly work after recovering from deep sleep due to sram reset
|
// refresh does not correctly work after recovering from deep sleep due to sram reset
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "Waiting for display to be idle...");
|
ESP_LOGV(TAG, "Waiting for display to be idle...");
|
||||||
TransactionGuard transaction_guard(this->epd_handler_);
|
TransactionGuard transaction_guard(this->epd_handler_);
|
||||||
err = transaction_guard.begin(pdMS_TO_TICKS(10000));
|
err = transaction_guard.begin(pdMS_TO_TICKS(10000));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -123,7 +123,7 @@ esp_err_t EInkDisplayHandler::refresh_display() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
epd_handler_.wait_for_idle();
|
epd_handler_.wait_for_idle();
|
||||||
ESP_LOGI(TAG, "Starting display refresh...");
|
ESP_LOGV(TAG, "Starting display refresh...");
|
||||||
err = epd_handler_.epd_write_cmd(0x92, transaction_guard.transaction_id()); // enter normal mode
|
err = epd_handler_.epd_write_cmd(0x92, transaction_guard.transaction_id()); // enter normal mode
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to enter normal mode: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to enter normal mode: %s", esp_err_to_name(err));
|
||||||
@@ -149,12 +149,12 @@ esp_err_t EInkDisplayHandler::refresh_display() {
|
|||||||
force_full_refresh_ = false;
|
force_full_refresh_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Refresh complete");
|
ESP_LOGV(TAG, "Refresh complete");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool white_basemap) {
|
esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool white_basemap) {
|
||||||
ESP_LOGI(TAG, "Starting full refresh (3 seconds)...");
|
ESP_LOGV(TAG, "Starting full refresh (3 seconds)...");
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
|
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // at least 200us delay
|
vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // at least 200us delay
|
||||||
ESP_LOGI(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY));
|
ESP_LOGV(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY));
|
||||||
|
|
||||||
// Wait for refresh to complete
|
// Wait for refresh to complete
|
||||||
epd_handler_.wait_for_idle();
|
epd_handler_.wait_for_idle();
|
||||||
@@ -229,13 +229,13 @@ esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool
|
|||||||
refresh_area_.reset();
|
refresh_area_.reset();
|
||||||
memcpy(old_buffer_, draw_buffer_, DISPLAY_BUFFER_SIZE);
|
memcpy(old_buffer_, draw_buffer_, DISPLAY_BUFFER_SIZE);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Full refresh complete");
|
ESP_LOGV(TAG, "Full refresh complete");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Partial refresh is inverted in color
|
// TODO: Partial refresh is inverted in color
|
||||||
esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_framebuffer, const RefreshArea& incoming_area, const bool is_last_partial_update) {
|
esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_framebuffer, const RefreshArea& incoming_area, const bool is_last_partial_update) {
|
||||||
ESP_LOGI(TAG, "Starting partial refresh (0.3 seconds)...");
|
ESP_LOGV(TAG, "Starting partial refresh (0.3 seconds)...");
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
|
|
||||||
write_to_buffer_(incoming_partial_framebuffer, incoming_area);
|
write_to_buffer_(incoming_partial_framebuffer, incoming_area);
|
||||||
@@ -244,7 +244,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
refresh_area_.expand_to_include(incoming_area);
|
refresh_area_.expand_to_include(incoming_area);
|
||||||
|
|
||||||
if (!is_last_partial_update) {
|
if (!is_last_partial_update) {
|
||||||
ESP_LOGI(TAG, "Partial refresh skipped (not last partial update)");
|
ESP_LOGV(TAG, "Partial refresh skipped (not last partial update)");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
RefreshArea area = refresh_area_;
|
RefreshArea area = refresh_area_;
|
||||||
if (area.x1 % 8 != 0 || area.x2 % 8 != 7) {
|
if (area.x1 % 8 != 0 || area.x2 % 8 != 7) {
|
||||||
ESP_LOGE(TAG, "Partial refresh area x1 and x2 must be byte-aligned (x1 %% 8 == 0 and x2 %% 8 == 7)");
|
ESP_LOGE(TAG, "Partial refresh area x1 and x2 must be byte-aligned (x1 %% 8 == 0 and x2 %% 8 == 7)");
|
||||||
ESP_LOGI(TAG, "Given area: x1=%d, x2=%d", area.x1, area.x2);
|
ESP_LOGV(TAG, "Given area: x1=%d, x2=%d", area.x1, area.x2);
|
||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,9 +348,9 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
static_cast<uint8_t>(area.y2 & 0xFF),
|
static_cast<uint8_t>(area.y2 & 0xFF),
|
||||||
0x01 // Gates scan both inside and outside of the partial window
|
0x01 // Gates scan both inside and outside of the partial window
|
||||||
};
|
};
|
||||||
ESP_LOGI(TAG, "Setting partial window: x1=%d, y1=%d, x2=%d, y2=%d",
|
ESP_LOGV(TAG, "Setting partial window: x1=%d, y1=%d, x2=%d, y2=%d",
|
||||||
area.x1, area.y1, area.x2, area.y2);
|
area.x1, area.y1, area.x2, area.y2);
|
||||||
ESP_LOGI(TAG, "Partial window data: %02X %02X %02X %02X %02X %02X %02X %02X",
|
ESP_LOGV(TAG, "Partial window data: %02X %02X %02X %02X %02X %02X %02X %02X",
|
||||||
window_data[0], window_data[1], window_data[2], window_data[3], window_data[4],
|
window_data[0], window_data[1], window_data[2], window_data[3], window_data[4],
|
||||||
window_data[5], window_data[6], window_data[7]);
|
window_data[5], window_data[6], window_data[7]);
|
||||||
err = epd_handler_.epd_write_cmd_with_data(0x90, window_data, transaction_guard.transaction_id()); // Set partial window
|
err = epd_handler_.epd_write_cmd_with_data(0x90, window_data, transaction_guard.transaction_id()); // Set partial window
|
||||||
@@ -370,7 +370,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send only the partial area data, not the full display buffer
|
// Send only the partial area data, not the full display buffer
|
||||||
ESP_LOGI(TAG, "Sending new partial buffer: %zu bytes (area: %dx%d)",
|
ESP_LOGV(TAG, "Sending new partial buffer: %zu bytes (area: %dx%d)",
|
||||||
partial_buffer_size, area_width_bytes * 8, area_height);
|
partial_buffer_size, area_width_bytes * 8, area_height);
|
||||||
err = epd_handler_.transfer_spi_data(partial_buffer, partial_buffer_size, transaction_guard.transaction_id(), true); // Inverted for partial refresh
|
err = epd_handler_.transfer_spi_data(partial_buffer, partial_buffer_size, transaction_guard.transaction_id(), true); // Inverted for partial refresh
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -403,7 +403,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Partial refresh complete");
|
ESP_LOGV(TAG, "Partial refresh complete");
|
||||||
|
|
||||||
err = deep_sleep_display();
|
err = deep_sleep_display();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -412,7 +412,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (force_full_refresh_) {
|
if (force_full_refresh_) {
|
||||||
ESP_LOGI(TAG, "Full refresh already requested, skipping partial refresh count increment");
|
ESP_LOGV(TAG, "Full refresh already requested, skipping partial refresh count increment");
|
||||||
err = refresh_display();
|
err = refresh_display();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to perform forced full refresh: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to perform forced full refresh: %s", esp_err_to_name(err));
|
||||||
@@ -432,7 +432,7 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
partial_refresh_count_++;
|
partial_refresh_count_++;
|
||||||
}
|
}
|
||||||
if (partial_refresh_count_ >= PARTIAL_REFRESH_THRESHOLD) {
|
if (partial_refresh_count_ >= PARTIAL_REFRESH_THRESHOLD) {
|
||||||
ESP_LOGI(TAG, "Partial refresh count %u reached threshold %u, next refresh will be full",
|
ESP_LOGV(TAG, "Partial refresh count %u reached threshold %u, next refresh will be full",
|
||||||
partial_refresh_count_, PARTIAL_REFRESH_THRESHOLD);
|
partial_refresh_count_, PARTIAL_REFRESH_THRESHOLD);
|
||||||
force_full_refresh_ = true;
|
force_full_refresh_ = true;
|
||||||
partial_refresh_count_ = 0;
|
partial_refresh_count_ = 0;
|
||||||
@@ -447,14 +447,14 @@ esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_fr
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::clear_display(void) {
|
esp_err_t EInkDisplayHandler::clear_display(void) {
|
||||||
ESP_LOGI(TAG, "Clearing display to all white...");
|
ESP_LOGV(TAG, "Clearing display to all white...");
|
||||||
|
|
||||||
esp_err_t err = full_write(white_data, false);
|
esp_err_t err = full_write(white_data, false);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Display cleared to all white");
|
ESP_LOGV(TAG, "Display cleared to all white");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ void EInkDisplayHandler::request_full_refresh(void) {
|
|||||||
if (guard.take(pdMS_TO_TICKS(100))) {
|
if (guard.take(pdMS_TO_TICKS(100))) {
|
||||||
force_full_refresh_ = true;
|
force_full_refresh_ = true;
|
||||||
partial_refresh_count_ = 0;
|
partial_refresh_count_ = 0;
|
||||||
ESP_LOGI(TAG, "Full refresh requested");
|
ESP_LOGV(TAG, "Full refresh requested");
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "Failed to take refresh mutex to request full refresh");
|
ESP_LOGE(TAG, "Failed to take refresh mutex to request full refresh");
|
||||||
}
|
}
|
||||||
@@ -506,13 +506,13 @@ esp_err_t EInkDisplayHandler::init_devices(EventGroupHandle_t system_event_group
|
|||||||
if (system_event_group != nullptr) {
|
if (system_event_group != nullptr) {
|
||||||
// Indicate that display is ready
|
// Indicate that display is ready
|
||||||
xEventGroupSetBits(system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT);
|
xEventGroupSetBits(system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT);
|
||||||
ESP_LOGI(TAG, "Display marked as ready");
|
ESP_LOGV(TAG, "Display marked as ready");
|
||||||
}
|
}
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::init_display_pins_(void) {
|
esp_err_t EInkDisplayHandler::init_display_pins_(void) {
|
||||||
ESP_LOGI(TAG, "Initializing E-Ink display handler...");
|
ESP_LOGV(TAG, "Initializing E-Ink display handler...");
|
||||||
|
|
||||||
esp_err_t ret;
|
esp_err_t ret;
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ esp_err_t EInkDisplayHandler::init_display_pins_(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::epd_init_internal_(uint32_t transaction_id) {
|
esp_err_t EInkDisplayHandler::epd_init_internal_(uint32_t transaction_id) {
|
||||||
ESP_LOGI(TAG, "Initializing EPD...");
|
ESP_LOGV(TAG, "Initializing EPD...");
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
|
|
||||||
@@ -585,8 +585,8 @@ esp_err_t EInkDisplayHandler::epd_init_internal_(uint32_t transaction_id) {
|
|||||||
vTaskDelay(pdMS_TO_TICKS(MINIMUM_POWER_ON_DELAY_MS)); // Wait for power on
|
vTaskDelay(pdMS_TO_TICKS(MINIMUM_POWER_ON_DELAY_MS)); // Wait for power on
|
||||||
|
|
||||||
// Check BUSY pin with detailed logging
|
// Check BUSY pin with detailed logging
|
||||||
ESP_LOGI(TAG, "Waiting for EPD to be ready after power on...");
|
ESP_LOGV(TAG, "Waiting for EPD to be ready after power on...");
|
||||||
ESP_LOGI(TAG, "BUSY pin level after power on: %d (0=BUSY, 1=FREE)", gpio_get_level(PIN_BUSY));
|
ESP_LOGV(TAG, "BUSY pin level after power on: %d (0=BUSY, 1=FREE)", gpio_get_level(PIN_BUSY));
|
||||||
|
|
||||||
epd_handler_.wait_for_idle();
|
epd_handler_.wait_for_idle();
|
||||||
std::vector<uint8_t> booster_data = { 0x27, 0x27, 0x18, 0x17 };
|
std::vector<uint8_t> booster_data = { 0x27, 0x27, 0x18, 0x17 };
|
||||||
@@ -618,7 +618,7 @@ esp_err_t EInkDisplayHandler::epd_init_internal_(uint32_t transaction_id) {
|
|||||||
|
|
||||||
// Internal version that uses an existing transaction (no separate TransactionGuard)
|
// Internal version that uses an existing transaction (no separate TransactionGuard)
|
||||||
esp_err_t EInkDisplayHandler::epd_init_partial_internal_(uint32_t transaction_id) {
|
esp_err_t EInkDisplayHandler::epd_init_partial_internal_(uint32_t transaction_id) {
|
||||||
ESP_LOGI(TAG, "Initializing EPD for partial refresh (internal)...");
|
ESP_LOGV(TAG, "Initializing EPD for partial refresh (internal)...");
|
||||||
esp_err_t err = ESP_OK;
|
esp_err_t err = ESP_OK;
|
||||||
|
|
||||||
// 1. Hardware Reset
|
// 1. Hardware Reset
|
||||||
@@ -676,12 +676,12 @@ esp_err_t EInkDisplayHandler::epd_init_partial_internal_(uint32_t transaction_id
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "EPD partial init (internal) complete");
|
ESP_LOGV(TAG, "EPD partial init (internal) complete");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::init_touch_() {
|
esp_err_t EInkDisplayHandler::init_touch_() {
|
||||||
ESP_LOGI(TAG, "Initializing touch...");
|
ESP_LOGV(TAG, "Initializing touch...");
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
// 1. Initialize I2C Bus
|
// 1. Initialize I2C Bus
|
||||||
@@ -703,10 +703,10 @@ esp_err_t EInkDisplayHandler::init_touch_() {
|
|||||||
ESP_LOGE(TAG, "Failed to install I2C driver: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to install I2C driver: %s", esp_err_to_name(err));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
ESP_LOGI("DisplayHandler", "I2C driver installed");
|
ESP_LOGV("DisplayHandler", "I2C driver installed");
|
||||||
|
|
||||||
// 2. Initialize GT911
|
// 2. Initialize GT911
|
||||||
ESP_LOGI("DisplayHandler", "Initializing GT911 touch controller...");
|
ESP_LOGV("DisplayHandler", "Initializing GT911 touch controller...");
|
||||||
esp_lcd_panel_io_i2c_config_t tp_io_config = {};
|
esp_lcd_panel_io_i2c_config_t tp_io_config = {};
|
||||||
// temporarily disable -Wmissing-field-initializers, as ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG macro does not set all fields
|
// temporarily disable -Wmissing-field-initializers, as ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG macro does not set all fields
|
||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
@@ -734,16 +734,16 @@ esp_err_t EInkDisplayHandler::init_touch_() {
|
|||||||
|
|
||||||
err = esp_lcd_touch_new_i2c_gt911(tp_io_handle_, &tp_cfg, &tp_handle_);
|
err = esp_lcd_touch_new_i2c_gt911(tp_io_handle_, &tp_cfg, &tp_handle_);
|
||||||
if (err == ESP_OK && tp_handle_ != nullptr) {
|
if (err == ESP_OK && tp_handle_ != nullptr) {
|
||||||
ESP_LOGI("DisplayHandler", "GT911 touch controller initialized successfully");
|
ESP_LOGV(TAG, "GT911 touch controller initialized successfully");
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE("DisplayHandler", "GT911 touch controller initialization failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "GT911 touch controller initialization failed: %s", esp_err_to_name(err));
|
||||||
tp_handle_ = nullptr;
|
tp_handle_ = nullptr;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EInkDisplayHandler::refresh_old_buffer_(uint32_t transaction_id) {
|
esp_err_t EInkDisplayHandler::refresh_old_buffer_(uint32_t transaction_id) {
|
||||||
ESP_LOGI(TAG, "Refreshing display SRAM to restore state after wake...");
|
ESP_LOGV(TAG, "Refreshing display SRAM to restore state after wake...");
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
err = epd_handler_.epd_write_cmd(0x92, transaction_id); // enter normal mode
|
err = epd_handler_.epd_write_cmd(0x92, transaction_id); // enter normal mode
|
||||||
@@ -784,6 +784,6 @@ esp_err_t EInkDisplayHandler::refresh_old_buffer_(uint32_t transaction_id) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Display SRAM restored successfully");
|
ESP_LOGV(TAG, "Display SRAM restored successfully");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,23 +73,23 @@ bool EPDHandler::is_busy(void) const {
|
|||||||
return gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL; // BUSY is active LOW
|
return gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL; // BUSY is active LOW
|
||||||
}
|
}
|
||||||
void EPDHandler::wait_for_idle(void) const {
|
void EPDHandler::wait_for_idle(void) const {
|
||||||
ESP_LOGI(TAG, "Waiting for display ready (BUSY pin)...");
|
ESP_LOGV(TAG, "Waiting for display ready (BUSY pin)...");
|
||||||
int initial_level = gpio_get_level(PIN_BUSY);
|
int initial_level = gpio_get_level(PIN_BUSY);
|
||||||
ESP_LOGI(TAG, "Initial BUSY pin level: %d (0=BUSY, 1=FREE)", initial_level);
|
ESP_LOGV(TAG, "Initial BUSY pin level: %d (0=BUSY, 1=FREE)", initial_level);
|
||||||
|
|
||||||
// If already free, no need to wait
|
// If already free, no need to wait
|
||||||
if (initial_level == BUSY_INACTIVE_LEVEL) {
|
if (initial_level == BUSY_INACTIVE_LEVEL) {
|
||||||
ESP_LOGI(TAG, "Display already ready (BUSY pin = 1)");
|
ESP_LOGV(TAG, "Display already ready (BUSY pin = 1)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
while (gpio_get_level(PIN_BUSY) != BUSY_INACTIVE_LEVEL) {
|
while (gpio_get_level(PIN_BUSY) != BUSY_INACTIVE_LEVEL) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Display is now ready (BUSY pin = 1)");
|
ESP_LOGV(TAG, "Display is now ready (BUSY pin = 1)");
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::epd_write_cmd(const uint8_t cmd, uint32_t transaction_id) {
|
esp_err_t EPDHandler::epd_write_cmd(const uint8_t cmd, uint32_t transaction_id) {
|
||||||
ESP_LOGI(TAG, "epd_write_cmd: waiting to send 0x%02X", cmd);
|
ESP_LOGV(TAG, "epd_write_cmd: waiting to send 0x%02X", cmd);
|
||||||
|
|
||||||
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
@@ -106,12 +106,12 @@ esp_err_t EPDHandler::epd_write_cmd(const uint8_t cmd, uint32_t transaction_id)
|
|||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
err = dangerous_epd_write_cmd_without_lock_(cmd);
|
err = dangerous_epd_write_cmd_without_lock_(cmd);
|
||||||
ESP_LOGI(TAG, "epd_write_cmd: 0x%02X done", cmd);
|
ESP_LOGV(TAG, "epd_write_cmd: 0x%02X done", cmd);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::epd_write_data(const uint8_t data, uint32_t transaction_id) {
|
esp_err_t EPDHandler::epd_write_data(const uint8_t data, uint32_t transaction_id) {
|
||||||
ESP_LOGI(TAG, "epd_write_data: waiting to send 0x%02X", data);
|
ESP_LOGV(TAG, "epd_write_data: waiting to send 0x%02X", data);
|
||||||
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
|
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
|
||||||
@@ -126,13 +126,13 @@ esp_err_t EPDHandler::epd_write_data(const uint8_t data, uint32_t transaction_id
|
|||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
err = dangerous_epd_write_data_without_lock_(data);
|
err = dangerous_epd_write_data_without_lock_(data);
|
||||||
ESP_LOGI(TAG, "epd_write_data: 0x%02X done", data);
|
ESP_LOGV(TAG, "epd_write_data: 0x%02X done", data);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::epd_write_cmd_with_data(const uint8_t cmd, std::vector<uint8_t>& data, uint32_t transaction_id) {
|
esp_err_t EPDHandler::epd_write_cmd_with_data(const uint8_t cmd, std::vector<uint8_t>& data, uint32_t transaction_id) {
|
||||||
const size_t data_len = data.size();
|
const size_t data_len = data.size();
|
||||||
ESP_LOGI(TAG, "epd_write_cmd_with_data: waiting to send cmd 0x%02X with %u bytes of data", cmd, data_len);
|
ESP_LOGV(TAG, "epd_write_cmd_with_data: waiting to send cmd 0x%02X with %u bytes of data", cmd, data_len);
|
||||||
|
|
||||||
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
@@ -158,13 +158,13 @@ esp_err_t EPDHandler::epd_write_cmd_with_data(const uint8_t cmd, std::vector<uin
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "epd_write_cmd_with_data: cmd 0x%02X with %u bytes of data done", cmd, data_len);
|
ESP_LOGV(TAG, "epd_write_cmd_with_data: cmd 0x%02X with %u bytes of data done", cmd, data_len);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
esp_err_t EPDHandler::dangerous_epd_write_cmd_without_lock_(const uint8_t cmd) {
|
esp_err_t EPDHandler::dangerous_epd_write_cmd_without_lock_(const uint8_t cmd) {
|
||||||
ESP_LOGI(TAG, "dangerous_epd_write_cmd_without_lock_: sending 0x%02X", cmd);
|
ESP_LOGV(TAG, "dangerous_epd_write_cmd_without_lock_: sending 0x%02X", cmd);
|
||||||
gpio_set_level(PIN_DC, 0); // Command mode
|
gpio_set_level(PIN_DC, 0); // Command mode
|
||||||
spi_transaction_t t {};
|
spi_transaction_t t {};
|
||||||
t.length = 8;t.tx_buffer = &cmd;
|
t.length = 8;t.tx_buffer = &cmd;
|
||||||
@@ -172,13 +172,13 @@ esp_err_t EPDHandler::dangerous_epd_write_cmd_without_lock_(const uint8_t cmd) {
|
|||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to send data 0x%02X", cmd);
|
ESP_LOGE(TAG, "Failed to send data 0x%02X", cmd);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGI(TAG, "dangerous_epd_write_cmd_without_lock_: 0x%02X sent", cmd);
|
ESP_LOGV(TAG, "dangerous_epd_write_cmd_without_lock_: 0x%02X sent", cmd);
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::dangerous_epd_write_data_without_lock_(const uint8_t data) {
|
esp_err_t EPDHandler::dangerous_epd_write_data_without_lock_(const uint8_t data) {
|
||||||
ESP_LOGI(TAG, "dangerous_epd_write_data_without_lock_: sending 0x%02X", data);
|
ESP_LOGV(TAG, "dangerous_epd_write_data_without_lock_: sending 0x%02X", data);
|
||||||
gpio_set_level(PIN_DC, 1); // Data mode
|
gpio_set_level(PIN_DC, 1); // Data mode
|
||||||
spi_transaction_t t = { };
|
spi_transaction_t t = { };
|
||||||
t.length = 8; t.tx_buffer = &data;
|
t.length = 8; t.tx_buffer = &data;
|
||||||
@@ -186,13 +186,13 @@ esp_err_t EPDHandler::dangerous_epd_write_data_without_lock_(const uint8_t data)
|
|||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to send data 0x%02X", data);
|
ESP_LOGE(TAG, "Failed to send data 0x%02X", data);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGI(TAG, "dangerous_epd_write_data_without_lock_: 0x%02X sent", data);
|
ESP_LOGV(TAG, "dangerous_epd_write_data_without_lock_: 0x%02X sent", data);
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id, bool inverted) {
|
esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id, bool inverted) {
|
||||||
ESP_LOGI(TAG, "transfer_spi_data: waiting to send %zu bytes of data", length);
|
ESP_LOGV(TAG, "transfer_spi_data: waiting to send %zu bytes of data", length);
|
||||||
|
|
||||||
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
|
||||||
esp_err_t err =
|
esp_err_t err =
|
||||||
@@ -207,7 +207,7 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
|
|||||||
ESP_LOGE(TAG, "SPI mutex timeout for data transfer of %zu bytes", length);
|
ESP_LOGE(TAG, "SPI mutex timeout for data transfer of %zu bytes", length);
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "transfer_spi_data: starting to send %zu bytes of data", length);
|
ESP_LOGV(TAG, "transfer_spi_data: starting to send %zu bytes of data", length);
|
||||||
|
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
size_t remaining = length;
|
size_t remaining = length;
|
||||||
@@ -264,7 +264,7 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
|
|||||||
|
|
||||||
// Yield every 16KB to prevent watchdog timeout
|
// Yield every 16KB to prevent watchdog timeout
|
||||||
if (offset % (16 * 1024) == 0) {
|
if (offset % (16 * 1024) == 0) {
|
||||||
ESP_LOGI(TAG, "New data progress: %zu/%zu bytes sent, yielding...", offset, length);
|
ESP_LOGV(TAG, "New data progress: %zu/%zu bytes sent, yielding...", offset, length);
|
||||||
vTaskDelay(pdMS_TO_TICKS(1));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,30 +274,30 @@ esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& lengt
|
|||||||
heap_caps_free(temp_transfer_buffer);
|
heap_caps_free(temp_transfer_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "transfer_spi_data: completed sending %zu bytes of data", length);
|
ESP_LOGV(TAG, "transfer_spi_data: completed sending %zu bytes of data", length);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::begin_transaction_(TickType_t timeout, uint32_t& out_id) {
|
esp_err_t EPDHandler::begin_transaction_(TickType_t timeout, uint32_t& out_id) {
|
||||||
ESP_LOGI(TAG, "begin_transaction_: waiting to obtain transaction mutex");
|
ESP_LOGV(TAG, "begin_transaction_: waiting to obtain transaction mutex");
|
||||||
if (xSemaphoreTake(spi_transaction_mutex_, timeout) != pdTRUE) {
|
if (xSemaphoreTake(spi_transaction_mutex_, timeout) != pdTRUE) {
|
||||||
ESP_LOGE(TAG, "begin_transaction_: transaction mutex timeout");
|
ESP_LOGE(TAG, "begin_transaction_: transaction mutex timeout");
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
out_id = ++spi_transaction_id;
|
out_id = ++spi_transaction_id;
|
||||||
ESP_LOGI(TAG, "begin_transaction_: transaction mutex obtained");
|
ESP_LOGV(TAG, "begin_transaction_: transaction mutex obtained");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t EPDHandler::end_transaction_(void) {
|
esp_err_t EPDHandler::end_transaction_(void) {
|
||||||
ESP_LOGI(TAG, "end_transaction_: releasing transaction mutex");
|
ESP_LOGV(TAG, "end_transaction_: releasing transaction mutex");
|
||||||
if (xSemaphoreGive(spi_transaction_mutex_) != pdTRUE) {
|
if (xSemaphoreGive(spi_transaction_mutex_) != pdTRUE) {
|
||||||
ESP_LOGE(TAG, "end_transaction_: failed to release transaction mutex");
|
ESP_LOGE(TAG, "end_transaction_: failed to release transaction mutex");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "end_transaction_: transaction mutex released");
|
ESP_LOGV(TAG, "end_transaction_: transaction mutex released");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ UDPClient::~UDPClient() {
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t UDPClient::init() {
|
esp_err_t UDPClient::init(uint16_t local_port) {
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
ESP_LOGW(TAG, "Already initialized");
|
ESP_LOGW(TAG, "Already initialized");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
@@ -31,6 +31,23 @@ esp_err_t UDPClient::init() {
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind to local port if specified
|
||||||
|
if (local_port > 0) {
|
||||||
|
struct sockaddr_in local_addr;
|
||||||
|
memset(&local_addr, 0, sizeof(local_addr));
|
||||||
|
local_addr.sin_family = AF_INET;
|
||||||
|
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
local_addr.sin_port = htons(local_port);
|
||||||
|
|
||||||
|
if (bind(sock_fd_, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to bind to port %u: errno %d", local_port, errno);
|
||||||
|
::close(sock_fd_);
|
||||||
|
sock_fd_ = -1;
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Bound to local port %u", local_port);
|
||||||
|
}
|
||||||
|
|
||||||
// Set socket to non-blocking mode
|
// Set socket to non-blocking mode
|
||||||
esp_err_t err = set_nonblocking();
|
esp_err_t err = set_nonblocking();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -95,7 +112,7 @@ esp_err_t UDPClient::send_command(const std::string& command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ssize_t sent = sendto(sock_fd_, command.c_str(), command.length(), 0,
|
ssize_t sent = sendto(sock_fd_, command.c_str(), command.length(), 0,
|
||||||
(struct sockaddr*)&remote_addr_, sizeof(remote_addr_));
|
(struct sockaddr*)&remote_addr_, sizeof(remote_addr_));
|
||||||
|
|
||||||
if (sent < 0) {
|
if (sent < 0) {
|
||||||
ESP_LOGE(TAG, "Failed to send command '%s': errno %d", command.c_str(), errno);
|
ESP_LOGE(TAG, "Failed to send command '%s': errno %d", command.c_str(), errno);
|
||||||
@@ -144,7 +161,7 @@ esp_err_t UDPClient::receive_response(std::string& response, int timeout_ms) {
|
|||||||
socklen_t from_len = sizeof(from_addr);
|
socklen_t from_len = sizeof(from_addr);
|
||||||
|
|
||||||
ssize_t received = recvfrom(sock_fd_, buffer, sizeof(buffer) - 1, 0,
|
ssize_t received = recvfrom(sock_fd_, buffer, sizeof(buffer) - 1, 0,
|
||||||
(struct sockaddr*)&from_addr, &from_len);
|
(struct sockaddr*)&from_addr, &from_len);
|
||||||
|
|
||||||
if (received < 0) {
|
if (received < 0) {
|
||||||
ESP_LOGE(TAG, "recvfrom() failed: errno %d", errno);
|
ESP_LOGE(TAG, "recvfrom() failed: errno %d", errno);
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initialize UDP socket
|
* @brief Initialize UDP socket
|
||||||
|
* @param local_port Local port to bind to (0 = system assigns port)
|
||||||
* @return ESP_OK on success, error code otherwise
|
* @return ESP_OK on success, error code otherwise
|
||||||
*/
|
*/
|
||||||
esp_err_t init();
|
esp_err_t init(uint16_t local_port = 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Configure remote endpoint
|
* @brief Configure remote endpoint
|
||||||
|
|||||||
113
main/network/web_server_handler.cpp
Normal file
113
main/network/web_server_handler.cpp
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#include "web_server_handler.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static const char* TAG = "WebServerHandler";
|
||||||
|
|
||||||
|
WebServerHandler::WebServerHandler() { }
|
||||||
|
|
||||||
|
WebServerHandler::~WebServerHandler() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t WebServerHandler::start(const std::string& auth_key, uint16_t base_port) {
|
||||||
|
if (server_ != nullptr) {
|
||||||
|
ESP_LOGW(TAG, "Server already running on port %d", current_port_);
|
||||||
|
return current_port_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_key_ = auth_key;
|
||||||
|
|
||||||
|
// Try to find a free port
|
||||||
|
uint16_t port = base_port;
|
||||||
|
const uint16_t max_attempts = 100;
|
||||||
|
|
||||||
|
for (uint16_t attempt = 0; attempt < max_attempts; attempt++) {
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
config.server_port = port;
|
||||||
|
config.ctrl_port = port + 1000; // Control port
|
||||||
|
config.max_open_sockets = 7;
|
||||||
|
config.lru_purge_enable = true;
|
||||||
|
|
||||||
|
esp_err_t err = httpd_start(&server_, &config);
|
||||||
|
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
current_port_ = port;
|
||||||
|
ESP_LOGI(TAG, "Web server started successfully on port %d", current_port_);
|
||||||
|
return current_port_;
|
||||||
|
} else if (err == ESP_ERR_HTTPD_ALLOC_MEM) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate memory for web server");
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
// Port likely in use, try next port
|
||||||
|
ESP_LOGD(TAG, "Port %d in use, trying next port", port);
|
||||||
|
port++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGE(TAG, "Failed to find free port after %d attempts", max_attempts);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t WebServerHandler::stop() {
|
||||||
|
if (server_ == nullptr) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = httpd_stop(server_);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to stop web server: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
server_ = nullptr;
|
||||||
|
current_port_ = 0;
|
||||||
|
auth_key_.clear();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Web server stopped");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t WebServerHandler::register_uri_handler(const httpd_uri_t* uri_handler) {
|
||||||
|
if (server_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Server not running, cannot register URI handler");
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = httpd_register_uri_handler(server_, uri_handler);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to register URI handler: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebServerHandler::validate_auth(const char* query_string) const {
|
||||||
|
if (!query_string || auth_key_.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for "auth=<key>" in query string
|
||||||
|
const char* auth_param = strstr(query_string, "auth=");
|
||||||
|
if (!auth_param) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip "auth="
|
||||||
|
auth_param += 5;
|
||||||
|
|
||||||
|
// Find end of auth value (& or end of string)
|
||||||
|
const char* end = strchr(auth_param, '&');
|
||||||
|
size_t auth_len = end ? (size_t)(end - auth_param) : strlen(auth_param);
|
||||||
|
|
||||||
|
// Compare with stored auth key
|
||||||
|
if (auth_len != auth_key_.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strncmp(auth_param, auth_key_.c_str(), auth_len) == 0;
|
||||||
|
}
|
||||||
42
main/network/web_server_handler.h
Normal file
42
main/network/web_server_handler.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esp_http_server.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class WebServerHandler {
|
||||||
|
public:
|
||||||
|
WebServerHandler();
|
||||||
|
~WebServerHandler();
|
||||||
|
|
||||||
|
// Start web server, finds a free port starting from base_port
|
||||||
|
// Returns the actual port used, or 0 on failure
|
||||||
|
uint16_t start(const std::string& auth_key, uint16_t base_port = 8080);
|
||||||
|
|
||||||
|
// Stop web server
|
||||||
|
esp_err_t stop();
|
||||||
|
|
||||||
|
// Check if server is running
|
||||||
|
bool is_running() const { return server_ != nullptr; }
|
||||||
|
|
||||||
|
// Get the current port
|
||||||
|
uint16_t get_port() const { return current_port_; }
|
||||||
|
|
||||||
|
// Get the auth key
|
||||||
|
std::string get_auth_key() const { return auth_key_; }
|
||||||
|
|
||||||
|
// Register a URI handler
|
||||||
|
esp_err_t register_uri_handler(const httpd_uri_t* uri_handler);
|
||||||
|
|
||||||
|
// Validate auth key from query string
|
||||||
|
bool validate_auth(const char* query_string) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Prevent copying
|
||||||
|
WebServerHandler(const WebServerHandler&) = delete;
|
||||||
|
WebServerHandler& operator=(const WebServerHandler&) = delete;
|
||||||
|
|
||||||
|
httpd_handle_t server_ = nullptr;
|
||||||
|
uint16_t current_port_ = 0;
|
||||||
|
std::string auth_key_;
|
||||||
|
};
|
||||||
@@ -539,3 +539,23 @@ EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) {
|
|||||||
ticks_to_wait
|
ticks_to_wait
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string WifiHandler::get_current_ip() const {
|
||||||
|
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||||
|
if (!netif) {
|
||||||
|
ESP_LOGW(TAG, "Failed to get netif handle");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
esp_err_t err = esp_netif_get_ip_info(netif, &ip_info);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to get IP info: %s", esp_err_to_name(err));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IP address to string
|
||||||
|
char ip_str[16];
|
||||||
|
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||||
|
return std::string(ip_str);
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public:
|
|||||||
uint16_t& ap_count
|
uint16_t& ap_count
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get current IP address (empty string if not connected)
|
||||||
|
std::string get_current_ip() const;
|
||||||
|
|
||||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
|
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -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
131
main/ui/apps/iotdis/app.cpp
Normal 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
61
main/ui/apps/iotdis/app.h
Normal 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);
|
||||||
|
};
|
||||||
257
main/ui/apps/iotdis/bridge/bridge.cpp
Normal file
257
main/ui/apps/iotdis/bridge/bridge.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#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_();
|
||||||
|
ESP_LOGI(TAG, "poll_status_() returned");
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
bridge->poll_task_handle_ = nullptr;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset failure counter on successful push update
|
||||||
|
if (event_data.state != StatusUpdateEventData::VoiceState::UNKNOWN) {
|
||||||
|
consecutive_failures_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (on_status_update_callback_) {
|
||||||
|
on_status_update_callback_(event_data, status_event_user_data_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got push update, skip polling
|
||||||
|
if (event_data.state != StatusUpdateEventData::VoiceState::UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send STATUS command for polling
|
||||||
|
ESP_LOGI(TAG, "Sending STATUS command for polling");
|
||||||
|
err = udp_client_.send_command(STATUS_COMMAND);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
consecutive_failures_++;
|
||||||
|
ESP_LOGW(TAG, "Failed to send STATUS command for polling. Consecutive 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_
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for response to STATUS command
|
||||||
|
ESP_LOGI(TAG, "Waiting for response to STATUS command (timeout: %dms)", RESPONSE_TIMEOUT_MS);
|
||||||
|
std::string response;
|
||||||
|
err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS);
|
||||||
|
ESP_LOGI(TAG, "Received response from STATUS command: err=%d", err);
|
||||||
|
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
// Success - reset failure counter
|
||||||
|
consecutive_failures_ = 0;
|
||||||
|
ESP_LOGI(TAG, "STATUS response: %s", response.c_str());
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Invoking status update callback with state: %d", event_data.state);
|
||||||
|
if (on_status_update_callback_) {
|
||||||
|
on_status_update_callback_(event_data, status_event_user_data_);
|
||||||
|
ESP_LOGI(TAG, "Status update callback returned");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Timeout or error
|
||||||
|
consecutive_failures_++;
|
||||||
|
ESP_LOGW(TAG, "No response to STATUS (failures: %d, error: %d)", consecutive_failures_, err);
|
||||||
|
|
||||||
|
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
|
||||||
|
ESP_LOGW(TAG, "Max failures reached, sending ERROR state");
|
||||||
|
if (on_status_update_callback_) {
|
||||||
|
on_status_update_callback_(
|
||||||
|
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
|
||||||
|
status_event_user_data_
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "poll_status_ complete");
|
||||||
|
}
|
||||||
67
main/ui/apps/iotdis/bridge/bridge.h
Normal file
67
main/ui/apps/iotdis/bridge/bridge.h
Normal 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 = 10000;
|
||||||
|
static constexpr int ERROR_POLL_INTERVAL_MS = 20000;
|
||||||
|
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;
|
||||||
|
|
||||||
|
};
|
||||||
11
main/ui/apps/iotdis/descriptor.cpp
Normal file
11
main/ui/apps/iotdis/descriptor.cpp
Normal 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);
|
||||||
|
}
|
||||||
12
main/ui/apps/iotdis/descriptor.h
Normal file
12
main/ui/apps/iotdis/descriptor.h
Normal 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;
|
||||||
|
};
|
||||||
52
main/ui/apps/iotdis/settings/settings_handler.cpp
Normal file
52
main/ui/apps/iotdis/settings/settings_handler.cpp
Normal 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_);
|
||||||
|
}
|
||||||
|
|
||||||
45
main/ui/apps/iotdis/settings/settings_handler.h
Normal file
45
main/ui/apps/iotdis/settings/settings_handler.h
Normal 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_;
|
||||||
|
};
|
||||||
205
main/ui/apps/iotdis/ui/main.cpp
Normal file
205
main/ui/apps/iotdis/ui/main.cpp
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
#include "ui/apps/iotdis/ui/main.h"
|
||||||
|
#include "ui/apps/iotdis/app.h"
|
||||||
|
#include "ui/interaction_handler.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_lvgl_port.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) {
|
||||||
|
if (!parent) {
|
||||||
|
ESP_LOGE(TAG, "Parent LVGL object is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lvgl_port_lock(pdMS_TO_TICKS(100))) {
|
||||||
|
ESP_LOGE(TAG, "Failed to acquire LVGL lock for UI creation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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");
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lvgl_port_lock(pdMS_TO_TICKS(100))) {
|
||||||
|
ESP_LOGW(TAG, "Failed to acquire LVGL lock for status update");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainUI::show_error_notification(bool show) {
|
||||||
|
if (!error_notification_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lvgl_port_lock(pdMS_TO_TICKS(100))) {
|
||||||
|
ESP_LOGW(TAG, "Failed to acquire LVGL lock for error notification update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (show) {
|
||||||
|
lv_obj_clear_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainUI::update_config_prompt(bool configured) {
|
||||||
|
if (!config_prompt_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lvgl_port_lock(pdMS_TO_TICKS(100))) {
|
||||||
|
ESP_LOGW(TAG, "Failed to acquire LVGL lock for config prompt update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (configured) {
|
||||||
|
lv_obj_add_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
lvgl_port_unlock();
|
||||||
|
|
||||||
|
}
|
||||||
85
main/ui/apps/iotdis/ui/main.h
Normal file
85
main/ui/apps/iotdis/ui/main.h
Normal 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;
|
||||||
|
};
|
||||||
194
main/ui/apps/iotdis/ui/main_handler.cpp
Normal file
194
main/ui/apps/iotdis/ui/main_handler.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#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) {
|
||||||
|
ESP_LOGI(TAG, "on_status_update_ called with state: %d", data.state);
|
||||||
|
|
||||||
|
// Update state in thread-safe manner
|
||||||
|
bool update_ui = false;
|
||||||
|
if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||||
|
if (data.state != current_state_) {
|
||||||
|
update_ui = true;
|
||||||
|
}
|
||||||
|
current_state_ = data.state;
|
||||||
|
xSemaphoreGive(state_mutex_);
|
||||||
|
ESP_LOGI(TAG, "State updated in mutex");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
if (!update_ui) {
|
||||||
|
ESP_LOGI(TAG, "State unchanged, skipping UI update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Calling update_ui_()");
|
||||||
|
update_ui_();
|
||||||
|
ESP_LOGI(TAG, "on_status_update_ complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainUIHandler::update_ui_() {
|
||||||
|
ESP_LOGI(TAG, "update_ui_ called");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Converting state: %d", state);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Calling main_ui_->update_status() with ui_state: %d", ui_state);
|
||||||
|
|
||||||
|
// Lock LVGL before calling UI functions from another task
|
||||||
|
main_ui_->update_status(ui_state);
|
||||||
|
ESP_LOGI(TAG, "main_ui_->update_status() returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "update_ui_ complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
main/ui/apps/iotdis/ui/main_handler.h
Normal file
52
main/ui/apps/iotdis/ui/main_handler.h
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
74
main/ui/apps/iotdis/ui/settings.cpp
Normal file
74
main/ui/apps/iotdis/ui/settings.cpp
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
41
main/ui/apps/iotdis/ui/settings.h
Normal file
41
main/ui/apps/iotdis/ui/settings.h
Normal 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;
|
||||||
|
};
|
||||||
90
main/ui/apps/iotdis/ui/settings_handler.cpp
Normal file
90
main/ui/apps/iotdis/ui/settings_handler.cpp
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
33
main/ui/apps/iotdis/ui/settings_handler.h
Normal file
33
main/ui/apps/iotdis/ui/settings_handler.h
Normal 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
|
||||||
|
};
|
||||||
362
main/ui/apps/iotdis/web/web_handlers.cpp
Normal file
362
main/ui/apps/iotdis/web/web_handlers.cpp
Normal 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;
|
||||||
|
}
|
||||||
81
main/ui/apps/iotdis/web/web_handlers.h
Normal file
81
main/ui/apps/iotdis/web/web_handlers.h
Normal 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)
|
||||||
|
};
|
||||||
9
main/ui/apps/registry.cpp
Normal file
9
main/ui/apps/registry.cpp
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user