把小米遥控器2 Pro(Mi Remote 2 Pro / RC003)变成 Windows 的“无线键盘 + 快捷动作板”:
- 方向键 / OK 直接当键盘用(开箱即用)
- 任意信号可达按键可重映射为:键盘快捷键(如 Win+D 回桌面、Ctrl+Shift+T)、系统动作(Alt+Tab 切窗口、开始菜单)、打开程序 / 文件 / 网址、执行命令、输入一段文本
- 语音 (F5) 键可接管,避免原生 F5 刷新行为;返回 / 音量± / 电源 / TV 因 Windows 驱动丢弃其报文而暂不可达(详见「按键信号可达性」,规划走 WUDFHost 通道)
- 实时报文侦测:遥控器发来的每个 HID 报文都能看到,未收录按键也能现场学习并分配动作
技术栈:Electron + React + TypeScript(electron-vite 构建,node-hid 读设备,koffi 调 Win32 API)。
npm install # 若 Electron 二进制下载失败见下方「镜像」一节
npm run dev # 启动开发版(热更新)- 先在 Windows 蓝牙设置里配对小米遥控器2 Pro
- 启动本应用,状态栏应显示「已连接」(自动识别名称含 Mi Remote / 小米遥控器的设备)
- 到「按键映射」页给各按键分配动作;「测试」按钮可立刻试效果
npm run dist:dir # 免安装目录版 → release/win-unpacked/
npm run dist # NSIS 安装包npm run test # 单元测试(HID 报文解码)
npm run typecheck # TS 类型检查
npm run smoke # 无头冒烟:枚举/解码/钩子/注入/渲染进程挂载 16 项检查遥控器 ──蓝牙HID──► Windows HID 栈
├──► node-hid 直接读集合(可打开的集合)
└──► 键盘集合被系统独占 → Raw Input (WM_INPUT) 兜底
│
▼
ReportDecoder / vkToButtonId
(统一产出 buttonId 事件流)
│
▼
MappingEngine 查映射表
│
┌─────────────┼─────────────┐
▼ ▼ ▼
透传(放行) 执行动作(SendInput) 禁用(吞掉)
快捷键/媒体键/开程序/命令/文本
| 项目 | 实测值 |
|---|---|
| HID 层名称 | RC003 / manufacturer MIOM(不是蓝牙显示名「小米蓝牙语音遥控器」) |
| VID/PID | 0x2717 / 0x32b8(node-hid 视角) |
| 暴露的集合 | 仅 1 个:usagePage 0x01 + usage 0x06(Generic Desktop/Keyboard),input 报文长 121 字节(含厂商 usage 位域) |
| 直接读取 | 不可行 —— 路径结尾 \KBD 被系统键盘驱动独占,ReadFile 返回 ACCESS_DENIED(5)(HidD_GetPreparsedData 等 IOCTL 查询可用,见 tools/ri-descriptor-probe.mjs) |
| Raw Input 设备名 | \\?\HID#{00001812-...}_Dev_VID&012717_PID&32b8_...(注意其中 VID 是蓝牙 DID 值 0x12717,与 HID 层 0x2717 不同!按 PID/序列号匹配更可靠) |
因此键盘事件走 Raw Input:注册键盘 + 消费者两个 usage(RIDEV_INPUTSINK,后台可收),
用 RAWINPUTHEADER.hDevice 只放行目标遥控器(hDevice=0 是注入事件,天然防回环),
VK 经反向表映射回按钮 ID(如 0x74→语音键 F5、0x24→主页)。
注意 Raw Input 只能收到 kbdhid 翻译成功的 VK:方向/OK/主页/菜单/语音 8 个键可达;
返回/音量±/电源/TV 是键盘页厂商 usage,在驱动层就被丢弃(见下方「按键信号可达性」)。
可打开的集合(消费者/厂商,其它设备)仍走 node-hid 报文解码,两条路径产出一致的 buttonId。
踩坑记录:
RIDI_DEVICENAME的正确值是0x20000007(见 SDK WinUser.h), 网上常传的0x20000000会一直返回ERROR_INVALID_PARAMETER。
RC003 全部按键都在唯一的键盘集合里、走 report ID 1,报文格式:
01 00 00 + 3 个 16-bit 小端键盘页 (0x07) usage 槽位(如音量+ 按下 = 01 00 00 80 00 00 00 00 00)。
| 遥控器按键 | usage(键盘页 0x07) | Windows 原生行为 |
|---|---|---|
| 方向 ▲▼◀▶ | 0x52 / 0x51 / 0x50 / 0x4F | 方向键 ✅ |
| OK | 0x28 | Enter ✅ |
| 返回 (‹) | 0xF1(厂商 usage) | 无——kbdhid 不翻译,直接丢弃 |
| 主页 (Home) | 0x4A | Home 键 ✅ |
| 菜单 (Menu) | 0x65 | 应用菜单键 ✅ |
| 语音键 | 0x3E | F5 ✅(会刷新浏览器,建议改映射或禁用) |
| 音量 +/− | 0x80 / 0x81(键盘页保留位,厂商挪用) | 无——kbdhid 丢弃 |
| 静音(固件保留) | 0x7F(键盘页保留位) | 无(遥控器无独立静音实体键) |
| 电源 | 0x66(厂商 usage) | 无 |
| TV | 0x35(厂商 usage) | 无 |
历史版本曾把音量±记为消费者页 0xE9/0xEA、返回记为键盘 0x29(Esc)——都是错的: RC003 没有 0x29 这个键,音量也不走消费者页。以上 usage 表与 remote-bridge-hub(Windows 实测)一致。
固件差异可能引入别的 usage:打开「报文侦测」页按下按键即可看到原始报文, 未收录按键会自动出现在映射页的「发现的其它按键」,一样可以分配动作。
被改映射的按键若在 Windows 上有原生效果(如语音键=F5、主页=Home),用低级键盘钩子拦下。 关键机制:LL 钩子返回 1 会把事件从整条输入流(含 Raw Input)中抹掉——直接拦截会让引擎 「看不到」按键,映射动作永远不触发(v2 版曾因此回归)。因此钩子吞掉原键后,立即把原 VK 编码到 F13-F24 私有通道并用第二魔数重新注入:
遥控器 Home ──► LL 钩子(吞) ──► 替身 F13+魔数2 ──► Raw Input ──► 翻译回 Home ──► 映射引擎 ──► Win+D
- 替身带魔数可穿过钩子(防递归),F13-F24 无实体键、无原生效果
- 引擎自己的动作注入(Win+D 等)带第一魔数,Raw Input 忽略,天然防回环
- 代价:遥控器连接期间,被改映射的键在物理键盘上同样被接管(如物理 F5 暂时无效)。 按设备区分需要 WUDFHost 通道(在驱动宿主内按设备过滤,规划中)
实体布局:电源 / 语音麦克风(顶部)· 五维导航盘(方向×4 + OK)· 返回 + 音量+/− · 主页 · 菜单 + TV
| 键 | 到达 Windows | 途径 |
|---|---|---|
| 方向×4 / OK / 主页 / 菜单 / 语音(F5) | ✅(8 个可达键) | 键盘集合标准 usage → kbdhid 翻译 → Raw Input |
| 返回(‹)、音量 +/−、电源、TV | ❌ | 键盘页厂商 usage(0xF1/0x80/0x81/0x66/0x35),kbdhid 不翻译、直接丢弃 |
不可达键的通道调研结论(2026-08 两轮实测,证据见 tools/gatt-*.mjs、tools/gatt-vendor-koffi.mjs):
① BLE GATT 订阅 —— 对按键报文不可行(双保险实测):
- HID 服务(0x1812)接口
CreateFile返回 GLE=31,被 Windows HID 栈独占; 厂商服务 ab5e0001 也独占(GLE=32) - 对照实验:电池/设备信息/厂商服务(0x180F/0x180A/0xFE59/0x1BF/0x8a7a0001)全部可正常打开—— 证明调用方式正确,是系统策略专门锁 HID 服务
- 厂商服务也不转发按键:订阅了全部 8 个可打开的 notifiable 特征值 (0x2A19、0x2BED、8a7a0102/0301/1201、8ec90001、0x0003、0xFF03), 实测按 音量±/返回/电源/TV → 0 条通知。按键只走 HID 服务内的报告
② Raw Input —— 同样收不到:注册键盘+消费者+系统控制三类 usage 实测,
这些键按下时遥控器句柄的 WM_INPUT 为 0 条(tools/remote-listen.log)——
kbdhid 在驱动层就把非标准 usage 丢了,Raw Input 拿到的是翻译后的 VK,自然也是空。
因此剩余路线只有一条:注入 WUDFHost(系统蓝牙 HID 驱动宿主,remote-bridge-hub 的做法:
附加只读探针、截取 HidOverGatt 的 IOCTL 输出缓冲 01 00 00 + 3×16bit usage,
需要 UAC 管理员确认),或接受 8 个可达键的现状(方向×4/OK/主页/菜单/语音)。
shared/ 主/渲染进程共享契约(类型、VK 表、按键目录、默认映射)
src/main/ 主进程
hid/decode.ts HID 报文 → 按键事件(纯逻辑,可测)
hid/watcher.ts 设备发现/连接/重连 + Raw Input 兜底切换
input/rawinput.ts Raw Input (WM_INPUT):窗口 + INPUTSINK 注册 + 设备过滤
input/inject.ts SendInput 注入 + 打开程序/命令/文本
input/suppress.ts 低级键盘钩子
input/engine.ts 映射引擎
src/preload/ contextBridge 安全桥
src/renderer/ React 界面(映射 / 设备 / 报文侦测 / 设置)
tests/ vitest 单元测试
tools/ 排障脚本(HID 枚举、Raw Input 链路自测等)
- 配置:
%APPDATA%\mi-remote-mapper\config.json(界面里可打开目录) - 日志:
%APPDATA%\mi-remote-mapper\logs\(按天滚动)
Electron 二进制下载失败(fetch failed / 证书不匹配):
本机若把 electron-mirror 指到已失效的 npm.taobao.org,会出现证书报错。用环境变量覆盖为
现行镜像(不必改全局 .npmrc):
$env:npm_config_electron_mirror = "https://npmmirror.com/mirrors/electron/"
$env:ELECTRON_MIRROR = "https://npmmirror.com/mirrors/electron/"
npx install-electron仍失败时手动放置(见上文版本号对应):
https://npmmirror.com/mirrors/electron/<版本>/electron-v<版本>-win32-x64.zip
→ 解压到 node_modules\electron\dist\
→ node_modules\electron\path.txt 写入 electron.exe
打包(electron-builder)同理加:
$env:npm_config_electron_builder_binaries_mirror = "https://npmmirror.com/mirrors/electron-builder-binaries/"识别不到遥控器:确认已在系统蓝牙里配对;到「设备」页手动指定 VID/PID。
语音键按了没反应:语音键原生是 F5 且默认「禁用」,编辑映射改成想要的动作即可。 (本工具不做语音听写,只做按键映射。)
占用冲突:不要同时运行其它接管同一遥控器的工具(如 Voice VibeCoding 等)。
MIT