feat: Implement Discord app UI and settings management
- Added MainUI class for displaying voice state, status icon, and buttons. - Introduced MainUIHandler to manage UI interactions and bridge communication. - Created SettingsUI for displaying QR code and configuration instructions. - Implemented SettingsUIHandler to manage settings and web server interactions. - Developed WebHandler for handling HTTP requests for settings configuration. - Updated AppRegistry to initialize with the new Discord app descriptor. - Enhanced InteractionHandler to support keyboard interactions across app switches. - Updated UIHandler to manage app switching and rendering of app icons. - Enabled QR code support in LVGL configuration.
This commit is contained in:
256
main/ui/apps/iotdis/bridge/bridge.cpp
Normal file
256
main/ui/apps/iotdis/bridge/bridge.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
#include "ui/apps/iotdis/bridge/bridge.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#define TAG "IotDisBridge"
|
||||
#define MUTE_COMMAND "MUTE"
|
||||
#define STATUS_COMMAND "STATUS"
|
||||
#define MUTED_RESPONSE "MUTED"
|
||||
#define UNMUTED_RESPONSE "UNMUTED"
|
||||
|
||||
IotDisBridge::~IotDisBridge() {
|
||||
stop_polling_task();
|
||||
}
|
||||
|
||||
void IotDisBridge::start_polling_task() {
|
||||
if (poll_task_handle_) {
|
||||
ESP_LOGW(TAG, "Polling task already running");
|
||||
return;
|
||||
}
|
||||
|
||||
udp_client_.init(setting_handler_->get_local_port());
|
||||
udp_client_.configure(
|
||||
setting_handler_->get_remote_ip(),
|
||||
setting_handler_->get_remote_port()
|
||||
);
|
||||
|
||||
stop_polling_ = false;
|
||||
xTaskCreate(poll_task_, "discord_poll", 4096, this, 5, &poll_task_handle_);
|
||||
}
|
||||
|
||||
void IotDisBridge::stop_polling_task() {
|
||||
if (!poll_task_handle_) {
|
||||
if (udp_client_.is_configured()) {
|
||||
udp_client_.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stopping polling task");
|
||||
stop_polling_ = true;
|
||||
|
||||
// Wait for task to finish (max 2 seconds)
|
||||
int wait_count = 0;
|
||||
while (poll_task_handle_ && wait_count < 20) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
wait_count++;
|
||||
}
|
||||
|
||||
if (poll_task_handle_) {
|
||||
ESP_LOGW(TAG, "Force deleting polling task");
|
||||
vTaskDelete(poll_task_handle_);
|
||||
poll_task_handle_ = nullptr;
|
||||
}
|
||||
|
||||
if (udp_client_.is_configured()) {
|
||||
udp_client_.close();
|
||||
}
|
||||
on_status_update_callback_ = nullptr;
|
||||
status_event_user_data_ = nullptr;
|
||||
consecutive_failures_ = 0;
|
||||
}
|
||||
|
||||
|
||||
esp_err_t IotDisBridge::send_mute_command() {
|
||||
if (!setting_handler_->is_configured()) {
|
||||
ESP_LOGW(TAG, "Cannot send command: not configured");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Sending MUTE command");
|
||||
esp_err_t err = udp_client_.send_command(MUTE_COMMAND);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send MUTE command");
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool IotDisBridge::test_connection(const std::string& ip, uint16_t port, uint16_t local_port) {
|
||||
ESP_LOGI(TAG, "Testing connection to %s:%u (local port: %u)", ip.c_str(), port, local_port);
|
||||
|
||||
// Create temporary UDP client for testing
|
||||
UDPClient test_client;
|
||||
esp_err_t err = test_client.init(local_port);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to initialize test UDP client");
|
||||
return false;
|
||||
}
|
||||
|
||||
err = test_client.configure(ip, port);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to configure test UDP client");
|
||||
return false;
|
||||
}
|
||||
|
||||
err = test_client.send_command(STATUS_COMMAND);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send STATUS command");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "STATUS command sent, waiting for response (timeout: %dms)", RESPONSE_TIMEOUT_MS);
|
||||
|
||||
std::string response;
|
||||
err = test_client.receive_response(response, RESPONSE_TIMEOUT_MS);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Received response: %s", response.c_str());
|
||||
bool valid = (response == MUTED_RESPONSE || response == UNMUTED_RESPONSE);
|
||||
if (!valid) {
|
||||
ESP_LOGW(TAG, "Unexpected response (expected MUTED or UNMUTED)");
|
||||
}
|
||||
test_client.close();
|
||||
return valid;
|
||||
} else if (err == ESP_ERR_TIMEOUT) {
|
||||
ESP_LOGW(TAG, "Timeout waiting for response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error receiving response: %d", err);
|
||||
}
|
||||
|
||||
test_client.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// private methods
|
||||
//
|
||||
|
||||
void IotDisBridge::poll_task_(void* param) {
|
||||
IotDisBridge* bridge = static_cast<IotDisBridge*>(param);
|
||||
|
||||
ESP_LOGI(TAG, "Polling task started");
|
||||
|
||||
while (!bridge->stop_polling_) {
|
||||
ESP_LOGI(TAG, "Polling for status update...");
|
||||
bridge->poll_status_();
|
||||
|
||||
// Yield to allow display updates to complete
|
||||
taskYIELD();
|
||||
|
||||
// Use longer interval if in error state
|
||||
int interval = (bridge->consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR)
|
||||
? ERROR_POLL_INTERVAL_MS
|
||||
: POLL_INTERVAL_MS;
|
||||
ESP_LOGI(TAG, "Next poll in %d ms", interval);
|
||||
vTaskDelay(pdMS_TO_TICKS(interval));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Polling task stopped");
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void IotDisBridge::poll_status_() {
|
||||
if (!setting_handler_->is_configured()) {
|
||||
// Don't poll if not configured
|
||||
return;
|
||||
}
|
||||
|
||||
// First check for any unsolicited push messages (non-blocking)
|
||||
std::string push_message;
|
||||
esp_err_t err = udp_client_.receive_response(push_message, 0); // 0 = non-blocking
|
||||
|
||||
if (err == ESP_OK && !push_message.empty()) {
|
||||
// Received push update from remote
|
||||
ESP_LOGI(TAG, "Received push update: %s", push_message.c_str());
|
||||
StatusUpdateEventData event_data {
|
||||
.state = StatusUpdateEventData::VoiceState::UNKNOWN
|
||||
};
|
||||
|
||||
if (push_message == MUTED_RESPONSE) {
|
||||
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
|
||||
} else if (push_message == UNMUTED_RESPONSE) {
|
||||
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
|
||||
}
|
||||
|
||||
if (on_status_update_callback_) {
|
||||
on_status_update_callback_(event_data, status_event_user_data_);
|
||||
}
|
||||
|
||||
// VoiceState new_state = VoiceState::UNKNOWN;
|
||||
// if (push_message == MUTED_RESPONSE) {
|
||||
// new_state = VoiceState::MUTED;
|
||||
// } else if (push_message == UNMUTED_RESPONSE) {
|
||||
// new_state = VoiceState::UNMUTED;
|
||||
// }
|
||||
|
||||
// if (new_state != VoiceState::UNKNOWN) {
|
||||
// consecutive_failures_ = 0;
|
||||
// if (main_ui_ && current_page_ == Page::MAIN) {
|
||||
// main_ui_->show_error_notification(false);
|
||||
// }
|
||||
|
||||
// if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
// current_state_ = new_state;
|
||||
// xSemaphoreGive(state_mutex_);
|
||||
// }
|
||||
|
||||
// update_main_ui();
|
||||
// return; // Got push update, skip polling
|
||||
// }
|
||||
}
|
||||
|
||||
// Send STATUS command for polling
|
||||
err = udp_client_.send_command(STATUS_COMMAND);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to send STATUS command");
|
||||
consecutive_failures_++;
|
||||
|
||||
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
|
||||
if (on_status_update_callback_) {
|
||||
on_status_update_callback_(
|
||||
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
|
||||
status_event_user_data_
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for response to STATUS command
|
||||
std::string response;
|
||||
err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
// Success - reset failure counter
|
||||
consecutive_failures_ = 0;
|
||||
|
||||
StatusUpdateEventData event_data {
|
||||
.state = StatusUpdateEventData::VoiceState::UNKNOWN
|
||||
};
|
||||
if (response == MUTED_RESPONSE) {
|
||||
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
|
||||
} else if (response == UNMUTED_RESPONSE) {
|
||||
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
|
||||
}
|
||||
if (on_status_update_callback_) {
|
||||
on_status_update_callback_(event_data, status_event_user_data_);
|
||||
}
|
||||
} else {
|
||||
// Timeout or error
|
||||
consecutive_failures_++;
|
||||
ESP_LOGW(TAG, "No response to STATUS (failures: %d)", consecutive_failures_);
|
||||
|
||||
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
|
||||
if (on_status_update_callback_) {
|
||||
on_status_update_callback_(
|
||||
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
|
||||
status_event_user_data_
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user