Files
ink-board/main/ui/apps/iotdis/bridge/bridge.cpp
2026-02-02 21:12:54 +08:00

257 lines
7.4 KiB
C++

#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");
}