feat(milestone): stable BLE pairing, HOGP key extraction, USB HID injection and web dashboard
This commit is contained in:
1 parent
2d5112eca6
commit
1f2cb49511
6 files changed
+157
-28
No files matched your search
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user