From caf5da201baa0c73bebdc654b6a9bff9dfc599de Mon Sep 17 00:00:00 2001 From: cuicuiV5 Date: Thu, 27 Aug 2026 21:03:03 +0800 Subject: [PATCH] feat: implement Web UI dashboard, Wi-Fi AP/STA dual mode, Captive Portal, mDNS, and RESTful APIs --- src/log/app_log.cpp | 68 ++++++++ src/log/app_log.h | 21 +++ src/main.cpp | 49 ++++-- src/web/web_server.cpp | 132 ++++++++++++++++ src/web/web_server.h | 15 ++ src/web/web_ui.h | 325 ++++++++++++++++++++++++++++++++++++++ src/wifi/wifi_manager.cpp | 123 +++++++++++++++ src/wifi/wifi_manager.h | 22 +++ 8 files changed, 739 insertions(+), 16 deletions(-) create mode 100644 src/log/app_log.cpp create mode 100644 src/log/app_log.h create mode 100644 src/web/web_server.cpp create mode 100644 src/web/web_server.h create mode 100644 src/web/web_ui.h create mode 100644 src/wifi/wifi_manager.cpp create mode 100644 src/wifi/wifi_manager.h diff --git a/src/log/app_log.cpp b/src/log/app_log.cpp new file mode 100644 index 0000000..8c54e6a --- /dev/null +++ b/src/log/app_log.cpp @@ -0,0 +1,68 @@ +#include "app_log.h" +#include +#include +#include +#include + +static char s_log_lines[MAX_LOG_LINES][MAX_LOG_LINE_LEN]; +static size_t s_log_head = 0; +static size_t s_log_count = 0; +static portMUX_TYPE s_log_mux = portMUX_INITIALIZER_UNLOCKED; + +void app_log_init(void) { + s_log_head = 0; + s_log_count = 0; + memset(s_log_lines, 0, sizeof(s_log_lines)); +} + +void app_log(const char* tag, const char* format, ...) { + char msg_buf[128]; + va_list args; + va_start(args, format); + vsnprintf(msg_buf, sizeof(msg_buf), format, args); + va_end(args); + + uint32_t now_ms = millis(); + uint32_t sec = now_ms / 1000; + uint32_t ms = now_ms % 1000; + + char full_line[MAX_LOG_LINE_LEN]; + snprintf(full_line, sizeof(full_line), "[%04u.%03u] [%s] %s", (unsigned int)sec, (unsigned int)ms, tag, msg_buf); + + // 1. Output to Serial + Serial.println(full_line); + + // 2. Store to circular buffer + taskENTER_CRITICAL(&s_log_mux); + strncpy(s_log_lines[s_log_head], full_line, MAX_LOG_LINE_LEN - 1); + s_log_lines[s_log_head][MAX_LOG_LINE_LEN - 1] = '\0'; + s_log_head = (s_log_head + 1) % MAX_LOG_LINES; + if (s_log_count < MAX_LOG_LINES) { + s_log_count++; + } + taskEXIT_CRITICAL(&s_log_mux); +} + +String app_log_get_json(void) { + JsonDocument doc; + JsonArray arr = doc["logs"].to(); + + taskENTER_CRITICAL(&s_log_mux); + size_t start_idx = (s_log_count < MAX_LOG_LINES) ? 0 : s_log_head; + for (size_t i = 0; i < s_log_count; i++) { + size_t idx = (start_idx + i) % MAX_LOG_LINES; + arr.add(s_log_lines[idx]); + } + taskEXIT_CRITICAL(&s_log_mux); + + String out; + serializeJson(doc, out); + return out; +} + +void app_log_clear(void) { + taskENTER_CRITICAL(&s_log_mux); + s_log_head = 0; + s_log_count = 0; + taskEXIT_CRITICAL(&s_log_mux); +} diff --git a/src/log/app_log.h b/src/log/app_log.h new file mode 100644 index 0000000..3344f87 --- /dev/null +++ b/src/log/app_log.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#define MAX_LOG_LINES 60 +#define MAX_LOG_LINE_LEN 160 + +#ifdef __cplusplus +extern "C" { +#endif + +void app_log_init(void); +void app_log(const char* tag, const char* format, ...); +String app_log_get_json(void); +void app_log_clear(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/main.cpp b/src/main.cpp index 5c53408..6c5cd27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,9 @@ #include #include "app_config.h" #include "version.h" +#include "log/app_log.h" +#include "wifi/wifi_manager.h" +#include "web/web_server.h" #include "audio/audio_pipeline.h" #include "keymap/key_state_machine.h" #include "usb/usb_composite.h" @@ -11,7 +14,7 @@ key_mapper_engine_t g_key_engine; // Task running on Core 0: BLE Central & Audio Decoding static void ble_task_core0(void* param) { - Serial.printf("[SYSTEM] BLE & Audio Task started on Core %d\n", xPortGetCoreID()); + app_log("SYSTEM", "BLE & Audio Task started on Core %d", xPortGetCoreID()); ble_remote_init(); while (true) { @@ -22,29 +25,37 @@ static void ble_task_core0(void* param) { void setup() { Serial.begin(115200); - delay(500); + delay(400); - Serial.println("=================================================="); - Serial.printf(" %s v%s (%s)\n", FIRMWARE_NAME, FIRMWARE_VERSION, HARDWARE_TARGET); - Serial.println(" Xiaomi Remote Hardware Bridge (BLE -> USB)"); - Serial.println("=================================================="); + // 1. Initialize Global Log System + app_log_init(); + app_log("SYSTEM", "=================================================="); + app_log("SYSTEM", " %s v%s (%s)", FIRMWARE_NAME, FIRMWARE_VERSION, HARDWARE_TARGET); + app_log("SYSTEM", " Xiaomi Remote Hardware Bridge (BLE -> USB + Web)"); + app_log("SYSTEM", "=================================================="); - // 1. Initialize Audio Pipeline + // 2. Initialize Audio Pipeline audio_pipeline_init(&g_audio_pipeline); - Serial.println("[INIT] Audio Pipeline initialized (16kHz 16-bit Mono)"); + app_log("INIT", "Audio Pipeline initialized (16kHz 16-bit Mono UAC 1.0)"); - // 2. Initialize Key Engine with USB HID dispatcher callback + // 3. Initialize Key Engine with USB HID dispatcher callback key_engine_init(&g_key_engine, usb_hid_dispatch_action); - Serial.printf("[INIT] Key Engine initialized with %u mappings\n", (unsigned int)g_key_engine.binding_count); + app_log("INIT", "Key Engine initialized with %u mappings", (unsigned int)g_key_engine.binding_count); - // 3. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC) + // 4. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC) usb_composite_init(); - Serial.println("[INIT] USB Composite Device ready (UAC Mic + HID Keyboard/Consumer)"); + app_log("INIT", "USB Composite Device ready"); - // 4. Initialize CLI Manager + // 5. Initialize Serial / CDC CLI Manager cli_manager_init(); - // 5. Launch BLE Central Task pinned to Core 0 + // 6. Initialize Wi-Fi AP + STA & Captive Portal + wifi_manager_init(); + + // 7. Initialize Embedded Web Server & REST APIs + web_server_init(); + + // 8. Launch BLE Central Task pinned to Core 0 xTaskCreatePinnedToCore( ble_task_core0, "ble_audio_task", @@ -55,7 +66,7 @@ void setup() { TASK_CORE_BLE ); - Serial.println("[SYSTEM] System initialization complete. Ready!"); + app_log("SYSTEM", "System initialization complete. Web available at http://192.168.4.1 or http://remotemapper.local"); } void loop() { @@ -67,7 +78,13 @@ void loop() { // 2. Service Key State Machine timers (long press, double click, repeat) key_engine_tick(&g_key_engine, now); - // 3. Service Serial / WebSerial CLI commands + // 3. Service Wi-Fi & DNS tasks + wifi_manager_task(); + + // 4. Service HTTP Web Server + web_server_task(); + + // 5. Service Serial / WebSerial CLI commands cli_manager_task(); delay(2); diff --git a/src/web/web_server.cpp b/src/web/web_server.cpp new file mode 100644 index 0000000..4bd8e04 --- /dev/null +++ b/src/web/web_server.cpp @@ -0,0 +1,132 @@ +#include "web_server.h" +#include "web_ui.h" +#include "log/app_log.h" +#include "wifi/wifi_manager.h" +#include "version.h" +#include "ble/ble_remote_client.h" +#include "audio/audio_pipeline.h" +#include "keymap/key_state_machine.h" +#include +#include + +static WebServer s_server(80); +extern key_mapper_engine_t g_key_engine; + +static void handle_root() { + s_server.sendHeader("Connection", "close"); + s_server.send(200, "text/html; charset=utf-8", INDEX_HTML); +} + +static void handle_status() { + JsonDocument doc; + doc["firmware"] = FIRMWARE_NAME; + doc["version"] = FIRMWARE_VERSION; + doc["uptime_sec"] = millis() / 1000; + doc["ble_state"] = (int)ble_remote_get_state(); + doc["frames_decoded"] = g_audio_pipeline.total_frames_decoded; + doc["samples_pushed"] = g_audio_pipeline.total_samples_pushed; + doc["free_heap"] = ESP.getFreeHeap(); + doc["free_psram"] = ESP.getFreePsram(); + doc["ap_ip"] = wifi_manager_get_ap_ip(); + doc["sta_ip"] = wifi_manager_get_sta_ip(); + doc["sta_connected"] = wifi_manager_is_sta_connected(); + + String out; + serializeJson(doc, out); + s_server.send(200, "application/json", out); +} + +static void handle_logs() { + String json = app_log_get_json(); + s_server.send(200, "application/json", json); +} + +static void handle_logs_clear() { + app_log_clear(); + s_server.send(200, "application/json", "{\"status\":\"cleared\"}"); +} + +static void handle_wifi_scan() { + String json = wifi_manager_scan_json(); + s_server.send(200, "application/json", json); +} + +static void handle_wifi_config() { + if (!s_server.hasArg("plain")) { + s_server.send(400, "application/json", "{\"error\":\"missing_body\"}"); + return; + } + JsonDocument doc; + DeserializationError err = deserializeJson(doc, s_server.arg("plain")); + if (err) { + s_server.send(400, "application/json", "{\"error\":\"invalid_json\"}"); + return; + } + + String ssid = doc["ssid"] | ""; + String pass = doc["pass"] | ""; + + if (ssid.length() == 0) { + s_server.send(400, "application/json", "{\"error\":\"empty_ssid\"}"); + return; + } + + wifi_manager_save_sta_config(ssid, pass); + s_server.send(200, "application/json", "{\"status\":\"ok\"}"); +} + +static void handle_keymap_reset() { + key_engine_load_defaults(&g_key_engine); + app_log("KEYMAP", "Reset keymap to factory defaults via Web API"); + s_server.send(200, "application/json", "{\"status\":\"reset_ok\"}"); +} + +static void handle_ble_reconnect() { + app_log("BLE", "Triggered manual reconnect scan via Web API"); + ble_remote_trigger_reconnect(); + s_server.send(200, "application/json", "{\"status\":\"reconnecting\"}"); +} + +static void handle_system_restart() { + app_log("SYSTEM", "Rebooting ESP32 via Web API..."); + s_server.send(200, "application/json", "{\"status\":\"rebooting\"}"); + delay(500); + ESP.restart(); +} + +static void handle_captive_portal() { + String host = s_server.hostHeader(); + if (host != "192.168.4.1" && host != "remotemapper.local") { + s_server.sendHeader("Location", "http://192.168.4.1/", true); + s_server.send(302, "text/plain", ""); + return; + } + handle_root(); +} + +void web_server_init(void) { + // API Routes + s_server.on("/", HTTP_GET, handle_root); + s_server.on("/api/status", HTTP_GET, handle_status); + s_server.on("/api/logs", HTTP_GET, handle_logs); + s_server.on("/api/logs/clear", HTTP_POST, handle_logs_clear); + s_server.on("/api/wifi/scan", HTTP_GET, handle_wifi_scan); + s_server.on("/api/wifi/config", HTTP_POST, handle_wifi_config); + s_server.on("/api/keymap/reset", HTTP_POST, handle_keymap_reset); + s_server.on("/api/ble/reconnect", HTTP_POST, handle_ble_reconnect); + s_server.on("/api/system/restart", HTTP_POST, handle_system_restart); + + // Captive Portal probe redirects + s_server.on("/generate_204", HTTP_GET, handle_captive_portal); + s_server.on("/hotspot-detect.html", HTTP_GET, handle_captive_portal); + s_server.on("/canonical.html", HTTP_GET, handle_captive_portal); + s_server.on("/connecttest.txt", HTTP_GET, handle_captive_portal); + s_server.onNotFound(handle_captive_portal); + + s_server.begin(); + app_log("WEB", "HTTP Web Server started on port 80"); +} + +void web_server_task(void) { + s_server.handleClient(); +} diff --git a/src/web/web_server.h b/src/web/web_server.h new file mode 100644 index 0000000..414c8ed --- /dev/null +++ b/src/web/web_server.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void web_server_init(void); +void web_server_task(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/web/web_ui.h b/src/web/web_ui.h new file mode 100644 index 0000000..0ce72ec --- /dev/null +++ b/src/web/web_ui.h @@ -0,0 +1,325 @@ +#pragma once + +#include + +static const char INDEX_HTML[] PROGMEM = R"rawliteral( + + + + + + RemoteMapper 控制台 + + + +
+
+ +
+
BLE: 已连接
+
Wi-Fi: AP+STA
+
IP: 192.168.4.1
+
+
+ +
+ + + + + +
+ + +
+
+
+
🎙️ BLE 遥控器与音频流
+
连接状态Connected
+
音频规格16kHz 16-bit Mono (UAC 1.0)
+
已解码音频帧0 帧
+
已推流采样点0 点
+
+ +
+
+ +
+
⚡ 硬件与系统资源
+
主控芯片ESP32-S3 N16R8
+
运行时间0s
+
空闲堆内存 (Heap)0 KB
+
空闲 PSRAM0 KB
+
+ +
+
+
+ +
+
🌐 网络访问信息
+
AP 热点 IP (直连)192.168.4.1 (SSID: RemoteMapper-AP)
+
家庭局域网 IP (STA)获取中...
+
局域网 mDNS 快速访问http://remotemapper.local
+
+
+ + +
+
+
+ 🎮 默认物理按键映射规则 + +
+
+
音量加 (+) / 音量减 (-)
0x80 / 0x81 (原始 Android 键码)
+
USB Consumer Vol Up/Down (连续连发)
+
+
+
返回键 (Back)
0xF1 (原始 Android 键码)
+
USB Consumer AC Back (浏览器/播放器后退)
+
+
+
语音键 (Voice HTT)
按住说话 / 松开结束
+
UAC 麦克风音频流 + 注入 RAlt+Comma
+
+
+
电源键 (Power)
单击 / 长按
+
单击: Alt+Tab | 长按: Sleep
+
+
+
主页键 (Home)
单击
+
USB Keyboard Win+D (显示桌面)
+
+
+
菜单键 (Menu)
单击
+
USB Keyboard Space (播放/暂停)
+
+
+
直播/TV 键
单击
+
USB Keyboard F8
+
+
+
+ + +
+
+
+ 📡 扫描周围蓝牙遥控器 + +
+
+

点击上方按钮扫描周围的小米蓝牙语音遥控器 (MI RC)...

+
+
+
+ + +
+
+
📶 连接家庭局域网 Wi-Fi
+

配置连接路由器后,您可以在家庭局域网内任意手机或电脑通过 http://remotemapper.local 直接打开本配置面板。

+
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+
+ 📜 ESP32-S3 实时运行日志 +
+ + +
+
+
加载日志中...
+
+
+
+ + + + +)rawliteral"; diff --git a/src/wifi/wifi_manager.cpp b/src/wifi/wifi_manager.cpp new file mode 100644 index 0000000..78cae58 --- /dev/null +++ b/src/wifi/wifi_manager.cpp @@ -0,0 +1,123 @@ +#include "wifi_manager.h" +#include "log/app_log.h" +#include +#include +#include +#include +#include + +#define AP_SSID "RemoteMapper-AP" +#define AP_PASS "" // Open network for quick onboarding +#define DNS_PORT 53 +#define MDNS_HOSTNAME "remotemapper" + +static DNSServer s_dns_server; +static Preferences s_prefs; +static IPAddress s_ap_ip(192, 168, 4, 1); +static IPAddress s_ap_netmask(255, 255, 255, 0); + +static bool s_sta_configured = false; +static uint32_t s_last_sta_check = 0; + +void wifi_manager_init(void) { + s_prefs.begin("wifi_conf", false); + String sta_ssid = s_prefs.getString("ssid", ""); + String sta_pass = s_prefs.getString("pass", ""); + + // Set Wi-Fi Mode + WiFi.mode(WIFI_AP_STA); + + // 1. Configure and start AP Mode + WiFi.softAPConfig(s_ap_ip, s_ap_ip, s_ap_netmask); + WiFi.softAP(AP_SSID, AP_PASS); + app_log("WIFI", "AP Started: %s (IP: 192.168.4.1)", AP_SSID); + + // 2. Start Captive Portal DNS + s_dns_server.setErrorReplyCode(DNSReplyCode::NoError); + s_dns_server.start(DNS_PORT, "*", s_ap_ip); + + // 3. Connect to Home Wi-Fi if saved + if (sta_ssid.length() > 0) { + s_sta_configured = true; + app_log("WIFI", "Connecting to Home Wi-Fi: %s...", sta_ssid.c_str()); + WiFi.begin(sta_ssid.c_str(), sta_pass.c_str()); + } else { + app_log("WIFI", "No Home Wi-Fi configured, running in AP Setup mode"); + } + + // 4. Start mDNS + if (MDNS.begin(MDNS_HOSTNAME)) { + MDNS.addService("http", "tcp", 80); + app_log("WIFI", "mDNS responder started: http://%s.local", MDNS_HOSTNAME); + } +} + +void wifi_manager_task(void) { + s_dns_server.processNextRequest(); + + uint32_t now = millis(); + if (s_sta_configured && (now - s_last_sta_check > 5000)) { + s_last_sta_check = now; + if (WiFi.status() == WL_CONNECTED) { + static bool s_logged_connected = false; + if (!s_logged_connected) { + app_log("WIFI", "Connected to Home Wi-Fi! Local IP: %s", WiFi.localIP().toString().c_str()); + s_logged_connected = true; + } + } + } +} + +String wifi_manager_get_ap_ip(void) { + return WiFi.softAPIP().toString(); +} + +String wifi_manager_get_sta_ip(void) { + if (WiFi.status() == WL_CONNECTED) { + return WiFi.localIP().toString(); + } + return "Disconnected"; +} + +bool wifi_manager_is_sta_connected(void) { + return WiFi.status() == WL_CONNECTED; +} + +int8_t wifi_manager_get_sta_rssi(void) { + if (WiFi.status() == WL_CONNECTED) { + return WiFi.RSSI(); + } + return 0; +} + +String wifi_manager_scan_json(void) { + app_log("WIFI", "Scanning for 2.4GHz Wi-Fi networks..."); + int n = WiFi.scanNetworks(); + JsonDocument doc; + JsonArray arr = doc["networks"].to(); + + for (int i = 0; i < n; i++) { + JsonObject obj = arr.add(); + obj["ssid"] = WiFi.SSID(i); + obj["rssi"] = WiFi.RSSI(i); + obj["secure"] = (WiFi.encryptionType(i) != WIFI_AUTH_OPEN); + } + + String out; + serializeJson(doc, out); + WiFi.scanDelete(); + return out; +} + +bool wifi_manager_save_sta_config(const String& ssid, const String& password) { + if (ssid.length() == 0) return false; + + s_prefs.putString("ssid", ssid); + s_prefs.putString("pass", password); + s_sta_configured = true; + + app_log("WIFI", "Saved new Wi-Fi credentials for: %s, connecting...", ssid.c_str()); + WiFi.disconnect(); + WiFi.begin(ssid.c_str(), password.c_str()); + return true; +} diff --git a/src/wifi/wifi_manager.h b/src/wifi/wifi_manager.h new file mode 100644 index 0000000..aa54ad5 --- /dev/null +++ b/src/wifi/wifi_manager.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void wifi_manager_init(void); +void wifi_manager_task(void); +String wifi_manager_get_ap_ip(void); +String wifi_manager_get_sta_ip(void); +bool wifi_manager_is_sta_connected(void); +int8_t wifi_manager_get_sta_rssi(void); +String wifi_manager_scan_json(void); +bool wifi_manager_save_sta_config(const String& ssid, const String& password); + +#ifdef __cplusplus +} +#endif