- 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.
363 lines
12 KiB
C++
363 lines
12 KiB
C++
#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;
|
|
}
|