From 8ab8a9a681052cf8d9aff656c1e9484d3e753ad3 Mon Sep 17 00:00:00 2001 From: cuicuiV5 Date: Fri, 28 Aug 2026 13:52:06 +0800 Subject: [PATCH] feat: add Chinese BLE name matching, Web manual scan, and 1-click pairing --- src/ble/ble_remote_client.cpp | 207 +++++++++++++++++++++++++++++----- src/ble/ble_remote_client.h | 21 ++++ src/main.cpp | 14 +-- src/web/web_server.cpp | 41 +++++++ src/web/web_ui.h | 142 +++++++++++++++++++---- tools/monitor_forever.py | 36 ++++++ tools/read_log.py | 39 +++++++ tools/stream_live.py | 20 ++++ 8 files changed, 461 insertions(+), 59 deletions(-) create mode 100644 tools/monitor_forever.py create mode 100644 tools/read_log.py create mode 100644 tools/stream_live.py diff --git a/src/ble/ble_remote_client.cpp b/src/ble/ble_remote_client.cpp index d58b4cc..0aed3c7 100644 --- a/src/ble/ble_remote_client.cpp +++ b/src/ble/ble_remote_client.cpp @@ -2,8 +2,12 @@ #include "audio/audio_pipeline.h" #include "keymap/key_state_machine.h" #include "usb/usb_composite.h" +#include "log/app_log.h" #include #include +#include +#include +#include static ble_remote_state_t s_ble_state = BLE_STATE_DISCONNECTED; static NimBLEClient* s_client = nullptr; @@ -11,6 +15,11 @@ static NimBLERemoteCharacteristic* s_char_cmd = nullptr; static NimBLERemoteCharacteristic* s_char_aud = nullptr; static NimBLERemoteCharacteristic* s_char_ctl = nullptr; +static Preferences s_ble_prefs; +static String s_bound_mac = ""; +static String s_connected_name = ""; +static String s_connected_mac = ""; + static uint8_t s_session_id = 0; static uint32_t s_last_audio_ms = 0; static uint32_t s_last_extend_ms = 0; @@ -22,6 +31,7 @@ extern key_mapper_engine_t g_key_engine; // Forward Declarations static void start_scan(); static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice); +static bool connect_to_mac_internal(const NimBLEAddress& address, const String& dev_name); // Audio Notification Callback (ATVV Char 0x03) static void on_audio_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { @@ -45,7 +55,7 @@ static void on_ctl_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, siz // Trigger Voice Hold action (hold hotkey & reset audio DSP) key_action_t act = { ACTION_VOICE_HOLD, DEFAULT_VOICE_MODIFIER, DEFAULT_VOICE_KEY, 0 }; usb_hid_dispatch_action(&act); - Serial.printf("[ATVV] >>> Voice button PRESSED (session %d)\n", s_session_id); + app_log("ATVV", ">>> Voice button PRESSED (session %d)", s_session_id); } // AUDIO_STOP / MIC_CLOSED / release op: else if (op == 0x00) { @@ -53,7 +63,7 @@ static void on_ctl_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, siz s_ble_state = BLE_STATE_CONNECTED; key_action_t act = { ACTION_VOICE_RELEASE, DEFAULT_VOICE_MODIFIER, DEFAULT_VOICE_KEY, 0 }; usb_hid_dispatch_action(&act); - Serial.println("[ATVV] <<< Voice button RELEASED"); + app_log("ATVV", "<<< Voice button RELEASED"); // Re-arm remote HTT standby if (s_char_cmd != nullptr) { @@ -67,14 +77,14 @@ static void on_ctl_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, siz uint16_t ver = (pData[1] << 8) | pData[2]; uint16_t fs = (pData[5] << 8) | pData[6]; if (fs > 0) s_frame_size = fs; - Serial.printf("[ATVV] CAPS: ver=0x%04X, frame_size=%d\n", ver, (int)s_frame_size); + app_log("ATVV", "CAPS: ver=0x%04X, frame_size=%d", ver, (int)s_frame_size); } // AUDIO_SYNC: op == 0x0A else if (op == 0x0A && length >= 7) { int16_t pred = (int16_t)((pData[4] << 8) | pData[5]); int8_t step_idx = (int8_t)pData[6]; audio_pipeline_sync(&g_audio_pipeline, pred, step_idx); - Serial.printf("[ATVV] SYNC: pred=%d, step=%d\n", pred, step_idx); + app_log("ATVV", "SYNC: pred=%d, step=%d", pred, step_idx); } } @@ -84,22 +94,58 @@ static void on_hogp_report_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pD uint8_t raw_key = pData[0]; bool is_pressed = (length > 1) ? (pData[1] != 0) : (raw_key != 0); - Serial.printf("[HOGP] Key byte: 0x%02X (%s)\n", raw_key, is_pressed ? "DOWN" : "UP"); + app_log("HOGP", "Key byte: 0x%02X (%s)", raw_key, is_pressed ? "DOWN" : "UP"); key_engine_feed_key(&g_key_engine, raw_key, is_pressed, millis()); } +static bool is_target_remote(NimBLEAdvertisedDevice* dev) { + String name = dev->getName().c_str(); + String addr = dev->getAddress().toString().c_str(); + addr.toLowerCase(); + + // 1. Exact match with previously bound remote + if (s_bound_mac.length() > 0 && addr.equalsIgnoreCase(s_bound_mac)) { + return true; + } + + // 2. Name contains Xiaomi / Remote keywords in Chinese & English + if (name.indexOf("小米") >= 0 || name.indexOf("遥控") >= 0 || + name.indexOf("MI RC") >= 0 || name.indexOf("Xiaomi") >= 0 || + name.indexOf("Remote") >= 0 || name.indexOf("RC") >= 0) { + return true; + } + + // 3. Service UUID matches ATVV or HID + if (dev->haveServiceUUID()) { + if (dev->isAdvertisingService(NimBLEUUID(ATVV_SVC_UUID)) || + dev->isAdvertisingService(NimBLEUUID((uint16_t)HOGP_SVC_UUID))) { + return true; + } + } + + // 4. Common Xiaomi Bluetooth OUI prefixes + if (addr.startsWith("c0:5d:39") || addr.startsWith("64:90:c1") || + addr.startsWith("7c:49:eb") || addr.startsWith("50:ec:50") || + addr.startsWith("04:cf:8c") || addr.startsWith("28:6c:07") || + addr.startsWith("34:ce:00") || addr.startsWith("5c:c3:06")) { + return true; + } + + return false; +} + // Advertised Device Scan Callbacks class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks { void onResult(NimBLEAdvertisedDevice* advertisedDevice) override { String name = advertisedDevice->getName().c_str(); String addr = advertisedDevice->getAddress().toString().c_str(); - addr.toLowerCase(); - bool match_name = name.indexOf(BLE_REMOTE_NAME_PREFIX) >= 0; - bool match_mac = addr.startsWith(BLE_REMOTE_MAC_PREFIX); + if (name.length() > 0) { + app_log("BLE_SCAN", "Device: %s (%s, RSSI: %d)", name.c_str(), addr.c_str(), advertisedDevice->getRSSI()); + } - if (match_name || match_mac) { - Serial.printf("[BLE] Found Target Remote: %s (%s), RSSI: %d\n", name.c_str(), addr.c_str(), advertisedDevice->getRSSI()); + if (is_target_remote(advertisedDevice)) { + app_log("BLE", "Matching Target Remote: %s (%s), connecting...", name.c_str(), addr.c_str()); NimBLEDevice::getScan()->stop(); connect_to_remote(advertisedDevice); } @@ -109,12 +155,12 @@ class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks { // Client Connection Callbacks class ClientCallbacks : public NimBLEClientCallbacks { void onConnect(NimBLEClient* pClient) override { - Serial.println("[BLE] Remote Connected"); + app_log("BLE", "Remote GATT Connected!"); s_ble_state = BLE_STATE_CONNECTED; } void onDisconnect(NimBLEClient* pClient) override { - Serial.println("[BLE] Remote Disconnected"); + app_log("BLE", "Remote Disconnected"); s_ble_state = BLE_STATE_DISCONNECTED; s_char_cmd = nullptr; s_char_aud = nullptr; @@ -129,22 +175,12 @@ static void start_scan() { pScan->setActiveScan(true); pScan->setInterval(BLE_SCAN_INTERVAL_MS); pScan->setWindow(BLE_SCAN_WINDOW_MS); - pScan->start(5, false); - Serial.println("[BLE] Scanning for Xiaomi Remote (MI RC)..."); + pScan->start(6, false); + app_log("BLE", "Scanning for Xiaomi Bluetooth Remote..."); } -static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice) { - s_ble_state = BLE_STATE_CONNECTING; - if (s_client == nullptr) { - s_client = NimBLEDevice::createClient(); - s_client->setClientCallbacks(new ClientCallbacks(), false); - } - - if (!s_client->connect(advDevice)) { - Serial.println("[BLE] Connection Failed"); - s_ble_state = BLE_STATE_DISCONNECTED; - return false; - } +static bool setup_services_and_handshake() { + if (!s_client || !s_client->isConnected()) return false; // Discover ATVV Service NimBLERemoteService* pAtvvSvc = s_client->getService(NimBLEUUID(ATVV_SVC_UUID)); @@ -167,7 +203,7 @@ static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice) { delay(150); uint8_t cmd_open[] = { 0x0C, 0x00 }; s_char_cmd->writeValue(cmd_open, sizeof(cmd_open), false); - Serial.println("[ATVV] Handshake completed successfully"); + app_log("ATVV", "Handshake completed successfully (MIC_OPEN active)"); } } @@ -177,7 +213,7 @@ static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice) { NimBLERemoteCharacteristic* pReportChar = pHogpSvc->getCharacteristic(NimBLEUUID((uint16_t)HOGP_REPORT_CHAR_UUID)); if (pReportChar && pReportChar->canNotify()) { pReportChar->subscribe(true, on_hogp_report_notify); - Serial.println("[HOGP] Subscribed to HID Key Reports"); + app_log("HOGP", "Subscribed to HID Key Reports"); } } @@ -185,9 +221,71 @@ static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice) { return true; } +static bool connect_to_remote(NimBLEAdvertisedDevice* advDevice) { + s_ble_state = BLE_STATE_CONNECTING; + if (s_client == nullptr) { + s_client = NimBLEDevice::createClient(); + s_client->setClientCallbacks(new ClientCallbacks(), false); + } + + if (!s_client->connect(advDevice)) { + app_log("BLE", "Connection Failed to %s", advDevice->getAddress().toString().c_str()); + s_ble_state = BLE_STATE_DISCONNECTED; + return false; + } + + s_connected_name = advDevice->getName().c_str(); + s_connected_mac = advDevice->getAddress().toString().c_str(); + if (s_connected_name.length() == 0) s_connected_name = "Xiaomi Voice Remote"; + + // Save bound MAC to NVS + s_bound_mac = s_connected_mac; + s_ble_prefs.putString("bound_mac", s_bound_mac); + s_ble_prefs.putString("bound_name", s_connected_name); + app_log("BLE", "Bound and saved remote: %s (%s)", s_connected_name.c_str(), s_bound_mac.c_str()); + + return setup_services_and_handshake(); +} + +static bool connect_to_mac_internal(const NimBLEAddress& address, const String& dev_name) { + s_ble_state = BLE_STATE_CONNECTING; + NimBLEDevice::getScan()->stop(); + + if (s_client == nullptr) { + s_client = NimBLEDevice::createClient(); + s_client->setClientCallbacks(new ClientCallbacks(), false); + } else if (s_client->isConnected()) { + s_client->disconnect(); + } + + app_log("BLE", "Connecting directly to MAC: %s...", address.toString().c_str()); + if (!s_client->connect(address)) { + app_log("BLE", "Direct connection to %s failed", address.toString().c_str()); + s_ble_state = BLE_STATE_DISCONNECTED; + return false; + } + + s_connected_mac = address.toString().c_str(); + s_connected_name = dev_name.length() > 0 ? dev_name : "Xiaomi Voice Remote"; + s_bound_mac = s_connected_mac; + s_ble_prefs.putString("bound_mac", s_bound_mac); + s_ble_prefs.putString("bound_name", s_connected_name); + app_log("BLE", "Manually paired and saved: %s (%s)", s_connected_name.c_str(), s_bound_mac.c_str()); + + return setup_services_and_handshake(); +} + extern "C" { void ble_remote_init(void) { + s_ble_prefs.begin("ble_conf", false); + s_bound_mac = s_ble_prefs.getString("bound_mac", ""); + String bound_name = s_ble_prefs.getString("bound_name", ""); + + if (s_bound_mac.length() > 0) { + app_log("BLE", "Loaded previously bound remote: %s (%s)", bound_name.c_str(), s_bound_mac.c_str()); + } + NimBLEDevice::init("ESP32-RemoteBridge"); NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEDevice::setSecurityAuth(true, true, true); @@ -209,7 +307,7 @@ void ble_remote_task(void) { // 2. Audio silence watchdog (if voice key released packet dropped) if (s_ble_state == BLE_STATE_TALKING) { if (now - s_last_audio_ms > BLE_SILENCE_WATCHDOG_MS) { - Serial.println("[ATVV] Silence watchdog expired -> force stopping speech"); + app_log("ATVV", "Silence watchdog expired -> force stopping speech"); s_ble_state = BLE_STATE_CONNECTED; key_action_t act = { ACTION_VOICE_RELEASE, DEFAULT_VOICE_MODIFIER, DEFAULT_VOICE_KEY, 0 }; usb_hid_dispatch_action(&act); @@ -235,4 +333,55 @@ void ble_remote_trigger_reconnect(void) { start_scan(); } +String ble_remote_scan_devices_json(void) { + app_log("BLE", "Performing full 4s BLE scan for nearby devices..."); + NimBLEScan* pScan = NimBLEDevice::getScan(); + pScan->stop(); + NimBLEScanResults results = pScan->start(4, false); + + JsonDocument doc; + JsonArray arr = doc["devices"].to(); + + for (int i = 0; i < results.getCount(); i++) { + NimBLEAdvertisedDevice dev = results.getDevice(i); + JsonObject obj = arr.add(); + String name = dev.getName().c_str(); + if (name.length() == 0) name = "Unnamed BLE Device"; + obj["name"] = name; + obj["mac"] = dev.getAddress().toString().c_str(); + obj["rssi"] = dev.getRSSI(); + } + + String out; + serializeJson(doc, out); + return out; +} + +bool ble_remote_connect_mac(const String& mac_str) { + if (mac_str.length() == 0) return false; + NimBLEAddress addr(mac_str.c_str()); + return connect_to_mac_internal(addr, "Xiaomi Remote"); +} + +void ble_remote_unpair(void) { + s_bound_mac = ""; + s_connected_name = ""; + s_connected_mac = ""; + s_ble_prefs.remove("bound_mac"); + s_ble_prefs.remove("bound_name"); + app_log("BLE", "Unpaired and cleared saved remote MAC"); + ble_remote_trigger_reconnect(); +} + +String ble_remote_get_connected_info(void) { + JsonDocument doc; + doc["connected"] = (s_ble_state >= BLE_STATE_CONNECTED); + doc["name"] = s_connected_name; + doc["mac"] = s_connected_mac; + doc["bound_mac"] = s_bound_mac; + String out; + serializeJson(doc, out); + return out; +} + } // extern "C" diff --git a/src/ble/ble_remote_client.h b/src/ble/ble_remote_client.h index 327cf9e..6c9233f 100644 --- a/src/ble/ble_remote_client.h +++ b/src/ble/ble_remote_client.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "app_config.h" #ifdef __cplusplus @@ -37,6 +38,26 @@ ble_remote_state_t ble_remote_get_state(void); */ void ble_remote_trigger_reconnect(void); +/** + * @brief Scan for nearby BLE devices and return JSON list + */ +String ble_remote_scan_devices_json(void); + +/** + * @brief Manually connect and pair to specific BLE device by MAC address + */ +bool ble_remote_connect_mac(const String& mac_str); + +/** + * @brief Unpair and clear saved remote MAC address + */ +void ble_remote_unpair(void); + +/** + * @brief Get connected remote name and MAC + */ +String ble_remote_get_connected_info(void); + #ifdef __cplusplus } #endif diff --git a/src/main.cpp b/src/main.cpp index 6c5cd27..d538d5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,28 +24,26 @@ static void ble_task_core0(void* param) { } void setup() { + // 1. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC) + usb_composite_init(); Serial.begin(115200); - delay(400); + delay(500); - // 1. Initialize Global Log System + // 2. 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", "=================================================="); - // 2. Initialize Audio Pipeline + // 3. Initialize Audio Pipeline audio_pipeline_init(&g_audio_pipeline); app_log("INIT", "Audio Pipeline initialized (16kHz 16-bit Mono UAC 1.0)"); - // 3. Initialize Key Engine with USB HID dispatcher callback + // 4. Initialize Key Engine with USB HID dispatcher callback key_engine_init(&g_key_engine, usb_hid_dispatch_action); app_log("INIT", "Key Engine initialized with %u mappings", (unsigned int)g_key_engine.binding_count); - // 4. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC) - usb_composite_init(); - app_log("INIT", "USB Composite Device ready"); - // 5. Initialize Serial / CDC CLI Manager cli_manager_init(); diff --git a/src/web/web_server.cpp b/src/web/web_server.cpp index 4bd8e04..633ef7e 100644 --- a/src/web/web_server.cpp +++ b/src/web/web_server.cpp @@ -81,6 +81,43 @@ static void handle_keymap_reset() { s_server.send(200, "application/json", "{\"status\":\"reset_ok\"}"); } +static void handle_ble_scan() { + String json = ble_remote_scan_devices_json(); + s_server.send(200, "application/json", json); +} + +static void handle_ble_connect() { + 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 mac = doc["mac"] | ""; + if (mac.length() == 0) { + s_server.send(400, "application/json", "{\"error\":\"empty_mac\"}"); + return; + } + + bool ok = ble_remote_connect_mac(mac); + s_server.send(200, "application/json", ok ? "{\"status\":\"connected\"}" : "{\"status\":\"failed\"}"); +} + +static void handle_ble_unpair() { + ble_remote_unpair(); + s_server.send(200, "application/json", "{\"status\":\"unpaired\"}"); +} + +static void handle_ble_info() { + String json = ble_remote_get_connected_info(); + s_server.send(200, "application/json", json); +} + static void handle_ble_reconnect() { app_log("BLE", "Triggered manual reconnect scan via Web API"); ble_remote_trigger_reconnect(); @@ -113,6 +150,10 @@ void web_server_init(void) { 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/scan", HTTP_GET, handle_ble_scan); + s_server.on("/api/ble/connect", HTTP_POST, handle_ble_connect); + s_server.on("/api/ble/unpair", HTTP_POST, handle_ble_unpair); + s_server.on("/api/ble/info", HTTP_GET, handle_ble_info); s_server.on("/api/ble/reconnect", HTTP_POST, handle_ble_reconnect); s_server.on("/api/system/restart", HTTP_POST, handle_system_restart); diff --git a/src/web/web_ui.h b/src/web/web_ui.h index 0ce72ec..3a6c721 100644 --- a/src/web/web_ui.h +++ b/src/web/web_ui.h @@ -49,7 +49,7 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( /* Grid & Cards */ .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 20px; } - .card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius); padding: 16px; } + .card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius); padding: 16px; margin-bottom: 16px; } .card-title { font-size: 15px; color: var(--text-bright); font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; } .stat-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); } .stat-row:last-child { border-bottom: none; } @@ -74,7 +74,10 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( .log-box { background: #010409; border: 1px solid var(--card-border); border-radius: var(--radius); height: 360px; overflow-y: auto; padding: 12px; font-family: Consolas, monospace; font-size: 12px; line-height: 1.6; color: #7ee787; } .log-line { margin-bottom: 2px; word-break: break-all; } - /* Keymap list */ + /* List items */ + .dev-item { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: #0d1117; border: 1px solid var(--card-border); border-radius: var(--radius); margin-bottom: 8px; } + .dev-name { font-weight: 600; color: var(--text-bright); } + .dev-mac { font-family: monospace; font-size: 12px; color: var(--text-muted); } .key-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: #0d1117; border: 1px solid var(--card-border); border-radius: var(--radius); margin-bottom: 8px; } .key-badge { background: #21262d; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; color: var(--accent); } .key-desc { font-size: 12px; color: var(--text-muted); } @@ -91,7 +94,7 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
-
BLE: 已连接
+
BLE: 搜索中
Wi-Fi: AP+STA
IP: 192.168.4.1
@@ -99,8 +102,8 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
+ -
@@ -110,12 +113,14 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
🎙️ BLE 遥控器与音频流
-
连接状态Connected
+
连接状态Scanning...
+
已绑定遥控器未绑定
音频规格16kHz 16-bit Mono (UAC 1.0)
已解码音频帧0 帧
已推流采样点0 点
- + +
@@ -139,11 +144,36 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
- + +
+
+
+ 🔗 当前绑定设备 + +
+
设备名称未连接
+
MAC 地址--
+
+ +
+
+ 📡 扫描周围蓝牙设备 (手动配对) + +
+

+ 提示:请先长按遥控器 主页键 + 菜单键 约 3 秒(指示灯闪烁),然后点击上方扫描按钮,在下方列表中点击“连接”。 +

+
+

暂未扫描,请点击上方按钮扫描周围蓝牙遥控器...

+
+
+
+ +
- 🎮 默认物理按键映射规则 + 🎮 物理按键映射规则
@@ -177,19 +207,6 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
- -
-
-
- 📡 扫描周围蓝牙遥控器 - -
-
-

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

-
-
-
-
@@ -238,13 +255,15 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( event.target.classList.add('active'); document.getElementById('tab-' + id).classList.add('active'); if (id === 'logs') refreshLogs(); + if (id === 'ble') fetchBleInfo(); } async function fetchStatus() { try { const res = await fetch('/api/status'); const data = await res.json(); - document.getElementById('stat-ble-state').innerText = data.ble_state === 3 ? 'Connected (已连接)' : (data.ble_state === 4 ? 'Talking (语音推流中)' : 'Scanning / Disconnected'); + const stateText = data.ble_state === 3 ? 'Connected (已连接)' : (data.ble_state === 4 ? 'Talking (语音推流中)' : 'Scanning / Disconnected'); + document.getElementById('stat-ble-state').innerText = stateText; document.getElementById('pill-ble').innerText = 'BLE: ' + (data.ble_state >= 3 ? '已连接' : '未连接'); document.getElementById('pill-ble').className = 'pill ' + (data.ble_state >= 3 ? 'green' : 'yellow'); document.getElementById('stat-frames').innerText = (data.frames_decoded || 0) + ' 帧'; @@ -257,6 +276,79 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( } catch (e) {} } + async function fetchBleInfo() { + try { + const res = await fetch('/api/ble/info'); + const data = await res.json(); + document.getElementById('ble-info-name').innerText = (data.name || '未连接') + (data.connected ? ' (在线)' : ''); + document.getElementById('ble-info-mac').innerText = data.mac || (data.bound_mac ? data.bound_mac + ' (已保存)' : '--'); + document.getElementById('stat-bound-remote').innerText = data.name ? (data.name + ' (' + data.mac + ')') : '未绑定'; + } catch (e) {} + } + + async function scanBle() { + const btn = document.getElementById('btn-scan-ble'); + const box = document.getElementById('ble-list'); + btn.innerText = '⏳ 正在扫描中...'; + btn.disabled = true; + box.innerHTML = '

正在扫描周围蓝牙设备 (4秒),请确保遥控器处于配对闪烁状态...

'; + + try { + const res = await fetch('/api/ble/scan'); + const data = await res.json(); + box.innerHTML = ''; + if (!data.devices || data.devices.length === 0) { + box.innerHTML = '

未发现蓝牙设备,请长按遥控器 主页+菜单 键后重试!

'; + } else { + data.devices.forEach(dev => { + const isMi = dev.name.includes('小米') || dev.name.includes('MI') || dev.name.includes('Xiaomi') || dev.name.includes('Remote'); + const div = document.createElement('div'); + div.className = 'dev-item'; + div.innerHTML = ` +
+
${dev.name} ${isMi ? '⭐' : ''}
+
${dev.mac} | 信号: ${dev.rssi} dBm
+
+ + `; + box.appendChild(div); + }); + } + } catch (e) { + box.innerHTML = '

扫描出错,请重试!

'; + } finally { + btn.innerText = '🔍 开始扫描 (4秒)'; + btn.disabled = false; + } + } + + async function connectBle(mac) { + if (confirm('确定要连接并绑定 MAC: ' + mac + ' 吗?')) { + const res = await fetch('/api/ble/connect', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mac }) + }); + const data = await res.json(); + if (data.status === 'connected') { + alert('连接成功并已保存绑定!'); + fetchBleInfo(); + fetchStatus(); + } else { + alert('连接失败,请确保遥控器在旁边且处于配对状态!'); + } + } + } + + async function unpairBle() { + if (confirm('确定解除当前遥控器绑定?')) { + await fetch('/api/ble/unpair', { method: 'POST' }); + alert('已清除绑定!'); + fetchBleInfo(); + fetchStatus(); + } + } + async function refreshLogs() { try { const res = await fetch('/api/logs'); @@ -273,6 +365,11 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( } catch (e) {} } + async function clearLogs() { + await fetch('/api/logs/clear', { method: 'POST' }); + refreshLogs(); + } + async function apiAction(url) { if (confirm('确定要执行此操作吗?')) { await fetch(url, { method: 'POST' }); @@ -319,6 +416,7 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( setInterval(fetchStatus, 2000); fetchStatus(); + fetchBleInfo(); diff --git a/tools/monitor_forever.py b/tools/monitor_forever.py new file mode 100644 index 0000000..77e1bb8 --- /dev/null +++ b/tools/monitor_forever.py @@ -0,0 +1,36 @@ +import serial +import time +import sys + +def main(): + try: + ser = serial.Serial('COM10', 115200, timeout=0.1) + except Exception as e: + print(f"Failed to open COM10: {e}") + return + + print("==================================================") + print(" RemoteMapper ESP32-S3 Live Monitor Started") + print("==================================================") + + last_status_time = 0 + try: + while True: + # Poll status every 5 seconds if idle + now = time.time() + if now - last_status_time > 5: + ser.write(b'status\n') + last_status_time = now + + line = ser.readline().decode('utf-8', errors='ignore').strip() + if line: + # Print any log or status + print(line) + time.sleep(0.02) + except KeyboardInterrupt: + pass + finally: + ser.close() + +if __name__ == "__main__": + main() diff --git a/tools/read_log.py b/tools/read_log.py new file mode 100644 index 0000000..2bb45f9 --- /dev/null +++ b/tools/read_log.py @@ -0,0 +1,39 @@ +import serial +import serial.tools.list_ports +import time + +def find_esp32_port(): + for p in serial.tools.list_ports.comports(): + if "303A" in p.hwid.upper(): + return p.device + return None + +def monitor(): + print("Waiting for ESP32-S3 port to appear...") + port = None + for _ in range(30): + port = find_esp32_port() + if port: + break + time.sleep(0.5) + + if not port: + print("No ESP32 port found.") + return + + print(f"Found ESP32 on {port}, connecting at 115200 baud...") + try: + ser = serial.Serial(port, 115200, timeout=0.2) + print("Connected! Listening for logs (Press Ctrl+C to exit):") + print("=" * 60) + t_end = time.time() + 15 + while time.time() < t_end: + line = ser.readline().decode('utf-8', errors='ignore').strip() + if line: + print(line) + ser.close() + except Exception as e: + print(f"Serial error: {e}") + +if __name__ == "__main__": + monitor() diff --git a/tools/stream_live.py b/tools/stream_live.py new file mode 100644 index 0000000..021942e --- /dev/null +++ b/tools/stream_live.py @@ -0,0 +1,20 @@ +import serial +import time +import sys + +def stream(): + ser = serial.Serial('COM10', 115200, timeout=0.1) + print("Listening to RemoteMapper ESP32-S3 (COM10)...") + ser.write(b'status\n') + t0 = time.time() + try: + while time.time() - t0 < 20: + line = ser.readline().decode('utf-8', errors='ignore').strip() + if line: + print(line) + time.sleep(0.01) + finally: + ser.close() + +if __name__ == "__main__": + stream()