feat(milestone): stable BLE pairing, HOGP key extraction, USB HID injection and web dashboard

This commit is contained in:
cuicuiV5 committed 2026-08-28 19:20:53 +08:00
1 parent 2d5112eca6
commit 1f2cb49511
6 files changed
+157 -28

No files matched your search

+48 -4
View File
@@ -97,6 +97,8 @@ static void on_ctl_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, siz
}
}
static uint8_t s_last_hogp_key = 0;
// HOGP HID Report Notification Callback
static void on_hogp_report_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (length < 1) return;
@@ -110,11 +112,53 @@ static void on_hogp_report_notify(NimBLERemoteCharacteristic* pChar, uint8_t* pD
}
app_log("HOGP_RAW", "Report (len %d): %s", (int)length, hex_str.c_str());
uint8_t raw_key = pData[0];
bool is_pressed = (length > 1) ? (pData[1] != 0) : (raw_key != 0);
uint8_t raw_key = 0;
bool is_pressed = false;
app_log("HOGP", "Key event: 0x%02X (%s)", raw_key, is_pressed ? "DOWN" : "UP");
key_engine_feed_key(&g_key_engine, raw_key, is_pressed, millis());
if (length >= 3) {
if (pData[2] != 0) {
raw_key = pData[2];
is_pressed = true;
s_last_hogp_key = raw_key;
} else if (pData[0] != 0) {
raw_key = pData[0];
is_pressed = true;
s_last_hogp_key = raw_key;
} else {
raw_key = s_last_hogp_key;
is_pressed = false;
s_last_hogp_key = 0;
}
} else if (length == 2) {
if (pData[1] != 0) {
raw_key = pData[1];
is_pressed = true;
s_last_hogp_key = raw_key;
} else if (pData[0] != 0) {
raw_key = pData[0];
is_pressed = true;
s_last_hogp_key = raw_key;
} else {
raw_key = s_last_hogp_key;
is_pressed = false;
s_last_hogp_key = 0;
}
} else {
if (pData[0] != 0) {
raw_key = pData[0];
is_pressed = true;
s_last_hogp_key = raw_key;
} else {
raw_key = s_last_hogp_key;
is_pressed = false;
s_last_hogp_key = 0;
}
}
if (raw_key != 0) {
app_log("HOGP", "Key event: 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) {
+16 -11
View File
@@ -4,7 +4,10 @@
#include <stdio.h>
#include <string.h>
static char s_log_lines[MAX_LOG_LINES][MAX_LOG_LINE_LEN];
#define STATIC_LOG_LINES 250
#define LOG_LINE_MAX_LEN 160
static char s_log_lines[STATIC_LOG_LINES][LOG_LINE_MAX_LEN];
static size_t s_log_head = 0;
static size_t s_log_count = 0;
static portMUX_TYPE s_log_mux = portMUX_INITIALIZER_UNLOCKED;
@@ -26,18 +29,20 @@ void app_log(const char* tag, const char* format, ...) {
uint32_t sec = now_ms / 1000;
uint32_t ms = now_ms % 1000;
char full_line[MAX_LOG_LINE_LEN];
char full_line[LOG_LINE_MAX_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);
// 1. Output to Serial safely (if USB CDC is ready)
if (Serial) {
Serial.println(full_line);
}
// 2. Store to circular buffer
// 2. Store to circular buffer with spinlock protection
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) {
strncpy(s_log_lines[s_log_head], full_line, LOG_LINE_MAX_LEN - 1);
s_log_lines[s_log_head][LOG_LINE_MAX_LEN - 1] = '\0';
s_log_head = (s_log_head + 1) % STATIC_LOG_LINES;
if (s_log_count < STATIC_LOG_LINES) {
s_log_count++;
}
taskEXIT_CRITICAL(&s_log_mux);
@@ -48,9 +53,9 @@ String app_log_get_json(void) {
JsonArray arr = doc["logs"].to<JsonArray>();
taskENTER_CRITICAL(&s_log_mux);
size_t start_idx = (s_log_count < MAX_LOG_LINES) ? 0 : s_log_head;
size_t start_idx = (s_log_count < STATIC_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;
size_t idx = (start_idx + i) % STATIC_LOG_LINES;
arr.add(s_log_lines[idx]);
}
taskEXIT_CRITICAL(&s_log_mux);
-3
View File
@@ -4,9 +4,6 @@
#include <stddef.h>
#include <Arduino.h>
#define MAX_LOG_LINES 60
#define MAX_LOG_LINE_LEN 160
#ifdef __cplusplus
extern "C" {
#endif
+5 -4
View File
@@ -24,13 +24,14 @@ static void ble_task_core0(void* param) {
}
void setup() {
// 1. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC)
// 1. Initialize Log System first
app_log_init();
// 2. Initialize USB Composite Stack (UAC Mic + HID Keyboard + Consumer + CDC)
usb_composite_init();
Serial.begin(115200);
delay(500);
delay(200);
// 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)");
+6 -6
View File
@@ -1,5 +1,6 @@
#include "usb_composite.h"
#include "audio/audio_pipeline.h"
#include "log/app_log.h"
#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
@@ -12,10 +13,6 @@ static bool s_usb_ready = false;
extern "C" {
void usb_composite_init(void) {
USB.productName("RemoteMapper Wireless Mic & Controller");
USB.manufacturerName("RemoteMapper");
USB.serialNumber("RM-ESP32S3-001");
s_keyboard.begin();
s_consumer.begin();
USB.begin();
@@ -45,7 +42,7 @@ bool usb_hid_keyboard_release(void) {
bool usb_hid_keyboard_tap(uint8_t modifier, uint8_t keycode) {
if (!s_usb_ready) return false;
usb_hid_keyboard_press(modifier, keycode);
delay(12);
delay(15);
usb_hid_keyboard_release();
return true;
}
@@ -65,7 +62,7 @@ bool usb_hid_consumer_release(void) {
bool usb_hid_consumer_tap(uint16_t usage_code) {
if (!s_usb_ready) return false;
s_consumer.press(usage_code);
delay(10);
delay(15);
s_consumer.release();
return true;
}
@@ -73,6 +70,9 @@ bool usb_hid_consumer_tap(uint16_t usage_code) {
void usb_hid_dispatch_action(const key_action_t *action) {
if (!action) return;
app_log("USB_HID", "Emit Action: type=%d, mod=0x%02X, key=0x%02X, cons=0x%04X",
action->type, action->modifier, action->key_code, action->consumer_code);
switch (action->type) {
case ACTION_KEYBOARD_TAP:
usb_hid_keyboard_tap(action->modifier, action->key_code);
+82
View File
@@ -0,0 +1,82 @@
import serial
import serial.tools.list_ports
import subprocess
import time
import sys
def get_esp_ports():
ports = []
for p in serial.tools.list_ports.comports():
if "303A" in p.hwid.upper():
ports.append(p.device)
return ports
def auto_upload():
ports = get_esp_ports()
if not ports:
print("No ESP32-S3 port found!")
sys.exit(1)
port = ports[0]
print(f"Detected ESP32 port: {port}")
# Trigger 1200bps DTR reset to enter bootloader
print("Triggering 1200bps CDC bootloader entry...")
try:
s = serial.Serial(port, 1200)
s.dtr = False
s.rts = True
time.sleep(0.1)
s.close()
except Exception as e:
pass
# Wait up to 3 seconds for bootloader port to appear
boot_port = None
for _ in range(15):
time.sleep(0.2)
cur = get_esp_ports()
if cur:
boot_port = cur[0]
break
if not boot_port:
boot_port = port
print(f"Targeting bootloader port: {boot_port}")
esptool_path = r"C:\Users\cuicuiV5\.platformio\packages\tool-esptoolpy\esptool.py"
bootloader = r".pio\build\esp32s3_n16r8\bootloader.bin"
partitions = r".pio\build\esp32s3_n16r8\partitions.bin"
boot_app0 = r"C:\Users\cuicuiV5\.platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin"
firmware = r".pio\build\esp32s3_n16r8\firmware.bin"
# Crucial: app0 offset is 0x20000 as defined in default_16MB.csv!
cmd = [
sys.executable, esptool_path,
"--chip", "esp32s3",
"--port", boot_port,
"--baud", "460800",
"--before", "default_reset",
"--after", "hard_reset",
"write_flash", "-z",
"--flash_mode", "dio",
"--flash_freq", "80m",
"--flash_size", "16MB",
"0x0", bootloader,
"0x8000", partitions,
"0xe000", boot_app0,
"0x20000", firmware
]
print(f"Flashing firmware to {boot_port} (App offset: 0x20000, Mode: dio)...")
res = subprocess.run(cmd)
if res.returncode == 0:
print("\n==========================================")
print(" FLASH SUCCESSFUL! (App written to 0x20000)")
print("==========================================\n")
else:
sys.exit(res.returncode)
if __name__ == "__main__":
auto_upload()