Skip to content

taowen/BluetoothDemo

Repository files navigation

VirtualBT (WinUI 3 Port)

Virtual Bluetooth Keyboard & Mouse -- 把 PC 变成 BLE HID 键盘/鼠标外设,连接手机后可以将 PC 输入转发到手机。

基于 Microsoft BluetoothLEExplorer (UWP) 移植到 WinUI 3 / .NET 8。

技术栈

层面 技术
UI 框架 WinUI 3 (Windows App SDK 1.5)
运行时 .NET 8 (net8.0-windows10.0.19041.0)
MVVM CommunityToolkit.Mvvm 8.2
蓝牙 Windows.Devices.Bluetooth (WinRT BLE GATT Server)
打包 MSIX(必须,BLE GATT Server 广播需要 Package Identity)
序列化 Newtonsoft.Json 13.0

项目结构

VirtualBT.sln
├── VirtualBT/              # 主项目(WinUI 3 应用)
│   ├── Models/             # 蓝牙设备模型、VirtualKeyboard / VirtualMouse HID 实现
│   ├── ViewModels/         # MVVM ViewModel
│   ├── Views/              # XAML 页面
│   ├── Services/           # 转换器、设置、Toast、蓝牙服务辅助
│   ├── CustomControls/     # 自定义 GATT 特征控件
│   ├── Assets/             # 图标资源
│   ├── Package.appxmanifest
│   └── VirtualBT.csproj
├── GattHelper/             # GATT 辅助库
├── GattServicesLibrary/    # 预定义 GATT 服务(心率、电池等)
└── SortedObservableCollection/  # 排序的 ObservableCollection

环境要求

  • Visual Studio 2022 (17.8+)
    • 工作负载:".NET Desktop Development" + "Windows App SDK C# Templates"
  • .NET 8 SDK
  • Windows 10 19041+
  • 蓝牙适配器需支持 Peripheral Role(大部分 Intel 无线网卡自带的蓝牙都支持)

编译

方法 1:Visual Studio(推荐)

  1. 用 VS 2022 打开 VirtualBT.sln
  2. 工具栏选择 x64 / Debug(不要选 Any CPU 或 x86)
  3. Ctrl+Shift+B 编译

方法 2:命令行

# 用 VS 的 MSBuild(不要用 dotnet build,XAML 编译器有兼容问题)
& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe" `
    VirtualBT.sln /p:Configuration=Debug /p:Platform=x64 /restore /t:Build

运行

MSIX 打包应用需要先注册再运行:

# 注册应用(首次或重新编译后)
Add-AppxPackage -Register "VirtualBT\bin\x64\Debug\net8.0-windows10.0.19041.0\AppxManifest.xml"

# 启动
Start-Process "shell:AppsFolder\VirtualBT_qhx848t84fegm!App"

# 如需重新注册(更新代码后)
Get-AppxPackage -Name "VirtualBT" | Remove-AppxPackage
Add-AppxPackage -Register "VirtualBT\bin\x64\Debug\net8.0-windows10.0.19041.0\AppxManifest.xml"

也可以在 VS 中直接 F5 调试运行。

使用方法

虚拟键盘

  1. 启动应用后,点左侧栏 键盘图标 进入 Virtual Keyboard 页面
  2. 打开 "Discoverable and Connectable" 开关
  3. 在手机蓝牙设置中搜索并配对你的电脑名(如 DESKTOP-XXXXXX
  4. 配对后,在 VirtualBT 窗口中按键,输入会转发到手机

虚拟鼠标

  1. 点左侧栏 鼠标图标 进入 Virtual Mouse 页面
  2. 打开 "Discoverable and Connectable" 开关
  3. 在手机蓝牙设置中搜索并配对你的电脑名
  4. 配对成功后,页面状态会显示已连接客户端数量
  5. 在触摸板区域拖拽控制光标移动
  6. 点击 Left / Middle / Right 按钮发送鼠标点击
  7. 点击 Scroll Up / Scroll Down 按钮滚动

注意:鼠标和键盘各自独立广播,手机上会看到两个配对请求。如果连接后立即断开,请在手机上删除旧的配对记录后重新搜索配对。

从 UWP 移植的关键差异

问题 UWP 原版 WinUI 3 移植方案
Package Identity 自带(AppContainerExe) 必须用 MSIX 打包,否则 BLE GATT Server 广播静默失败
键盘捕获 CoreWindow.GetForCurrentThread().KeyDown Window.Content 上挂 KeyDown/KeyUp 事件
ToggleSwitch 双向绑定 x:Bind TwoWay 直接工作 需要额外的 Toggled 事件处理器显式设值
UI 线程调度 CoreDispatcher DispatcherQueue
导航 Template10 NavigationService 自定义 NavigationHelper 静态委托
MVVM 基类 Template10 ViewModelBase CommunityToolkit.Mvvm.ObservableObject

已知问题

  • "List of subscribed clients" UI 列表可能不显示已连接的设备(功能不影响,键盘转发正常工作)
  • XAML 编译器 Pass2 在 dotnet build 下可能崩溃,需用 VS MSBuild 编译

设备兼容性

手机型号 蓝牙键盘 蓝牙鼠标 备注
小米 13 (xiaomi13) 正常 正常
一加 8T (KB2000, Android 11) 正常 正常
一加 13 (PJZ110, Android 15) 需验证 需验证 已实现 BR/EDR 抑制修复,见下方说明

一加 13 兼容性修复(BR/EDR 抑制)

问题:一加 13 (Android 15) 的 HidHostService 发现 PC 为 DUAL 模式设备(BR/EDR + LE)时,优先通过经典蓝牙 HID 连接(transport=0),而非 BLE HOGP(transport=2)。VirtualBT 只实现了 BLE GATT HID,经典 HID 连接直接超时,且不会 fallback 到 HOGP。

修复方案:在 BLE HID 广播期间,通过 Win32 API(BluetoothEnableDiscovery / BluetoothEnableIncomingConnections)临时关闭 BR/EDR 的 Inquiry Scan 和 Page Scan,使 PC 对手机呈现为 LE-only 设备,迫使 Android 走 BLE HOGP 路径。

实现细节

  • BrEdrHelper.cs:P/Invoke BluetoothAPIs.dll,引用计数管理(键盘和鼠标独立调用)
  • 开启广播时调用 BrEdrHelper.Suppress(),停止广播时调用 BrEdrHelper.Restore()
  • 应用退出 / 崩溃时通过 ForceRestore() 确保 BR/EDR 恢复原状
  • 不需要管理员权限(WinUI 3 desktop 是 full-trust 进程)

副作用:广播期间 PC 的经典蓝牙功能暂时不可用(蓝牙音箱、文件传输等),停止广播后自动恢复。

诊断过程(通过 adb shell dumpsys bluetooth_manager 获取):

  1. 一加 13 将 PC 识别为 DUAL 模式设备

    44:7C:3F:C4:24:6D => 9C:B1:50:71:24:BE [ DUAL ][ 0x000500 ] DESKTOP-6MH3QMG
    

    CoD 0x000500 = Peripheral 设备类型。Windows 在注册 BLE GATT HID 服务时会自动将 Class of Device 设为 Peripheral。

  2. HidHostService 选择了错误的传输通道

    HidHostService:
      XX:XX:XX:XX:24:6D : Selected transport=0  HID connection state=0  HOGP connection state=0
    

    transport=0 表示 AUTO(实际走了经典蓝牙 HID),而非 transport=2(BLE HOGP)。

  3. 一加 13 通过 BR/EDR SDP 发现服务,而非 BLE GATT

    SDP Discovery completed: xx:xx:xx:xx:24:be Result:SDP_SUCCESS services_found:0xc10c4048
    
  4. 经典 HID 连接超时(VirtualBT 只实现了 BLE GATT HID,没有经典蓝牙 HID 服务):

    CachedBluetoothDevice: Connect to profile : 2 timeout, show error message !
    

对比一加 8T(正常工作)

一加 8T (Android 11) 一加 13 (Android 15)
设备识别 纯 LE DUAL (BR/EDR + LE)
HID transport transport=2 (LE → HOGP) transport=0 (AUTO → 经典 HID)
服务发现 BLE GATT Discovery BR/EDR SDP Discovery
结果 HOGP 订阅成功 经典 HID 超时,HOGP 从未尝试

结论:一加 13 的 Android 15 蓝牙栈在发现对端为 DUAL 模式且 CoD 包含 Peripheral 时,优先通过经典蓝牙 HID profile 连接,不会 fallback 到 BLE HOGP。VirtualBT 现在通过在广播期间抑制 BR/EDR 来绕过此问题。

如果你的手机配对连接后按键无响应,先用小米等已验证的手机排除 PC 端问题,再判断是否为手机兼容性问题。

故障排除

手机配对后立即断开

症状:手机能搜到电脑蓝牙、配对成功,但随后连接立刻断开,反复重连均失败。

原因:手机或 PC 端残留了旧的配对密钥(LTK),与当前 GATT 服务的加密上下文不匹配,Windows 蓝牙栈在链路层直接拒绝连接。

解决:在 两端 都删除对方的配对记录后重新配对:

  1. 手机端:蓝牙设置 → 找到电脑名称 → 取消配对 / 忘记设备
  2. PC 端:设置 → 蓝牙和其他设备 → 找到手机名称 → 删除设备
  3. 重启应用,开启广播,用手机重新搜索配对

手机配对成功、连接正常,但按键/鼠标无响应

症状:手机显示已连接,但在 VirtualBT 窗口按键后手机端无任何反应。日志中持续出现 No clients are currently subscribed

原因:应用以 unpackaged 模式(直接运行 exe)启动。WinUI 3 桌面应用的 BLE GATT Server 必须拥有 Package Identity 才能正常工作。直接运行 exe 时,GattServiceProvider 的广播和服务创建在 API 层面返回成功,但 Windows 蓝牙栈实际上不会将 GATT 服务注册到蓝牙控制器中。手机连上后做 GATT Service Discovery 时找不到 HID 服务,因此永远不会订阅 Report 特征。

解决:必须以 packaged 方式运行,有两种方法:

# 方法 1:命令行注册后启动
Add-AppxPackage -Register "VirtualBT\bin\x64\Debug\net8.0-windows10.0.19041.0\AppxManifest.xml"
Start-Process "shell:AppsFolder\VirtualBT_qhx848t84fegm!App"

# 方法 2:在 Visual Studio 中 F5 调试运行(VS 会自动注册包)

绝对不要 直接双击 bin\...\VirtualBT.exe 启动,这样蓝牙功能不会正常工作。

日志

运行时日志写入 %TEMP% 目录,用于排查连接问题:

  • VirtualBT_log.txt — 键盘日志
  • VirtualBT_mouse_log.txt — 鼠标日志

正常蓝牙报文流程(OnePlus 8T 实测)

以下是蓝牙鼠标功能正常工作时,从配对到数据传输的完整 BLE 报文序列。基于 OnePlus 8T (Android 11) + DESKTOP-6MH3QMG (Windows, Intel BLE) 实测记录。

1. 连接建立

手机(Central)                              PC(Peripheral)
    |                                           |
    |--- LE Create Connection ----------------->|
    |<-- LE Enhanced Connection Complete --------|  status=0, role=Peripheral
    |                                           |
    |<-- LE Read Remote Features ---------------|
    |<-- LE Data Length Change -----------------|  协商数据包长度
  • 手机作为 Central 发起 BLE 连接
  • PC 的 BLE 随机地址(如 6b:29:52:88:f0:dc)作为广播地址
  • PC 的公有地址(如 9c:b1:50:71:24:be)也参与连接管理

2. 加密(SMP 配对)

手机(Central)                              PC(Peripheral)
    |                                           |
    |<========= Encryption Change ==============>|  enabled=1
    |<-- LE Data Length Change -----------------|  加密后重新协商 DLE
  • 首次连接时通过 SMP 配对交换密钥
  • 后续重连使用已存储的 LTK 直接加密
  • HID 服务要求 EncryptionRequired,加密完成前不可读写特征

3. GATT 服务发现

手机在加密完成后执行 GATT Service Discovery:

手机(Central)                              PC(Peripheral)
    |                                           |
    |--- ATT Read By Group Type Request ------->|  UUID=0x2800 (Primary Service)
    |<-- ATT Read By Group Type Response -------|  发现以下服务:
    |                                           |    0x1800 Generic Access
    |                                           |    0x1801 Generic Attribute
    |                                           |    0x180A Device Information
    |                                           |    0x1812 HID Service  ← 关键
    |                                           |
    |--- ATT Read By Type Request ------------->|  UUID=0x2803 (Characteristic)
    |<-- ATT Read By Type Response -------------|  发现 HID 服务的特征:
    |                                           |    0x2A4D Report (Input, Notify)
    |                                           |    0x2A4B Report Map (Read)
    |                                           |    0x2A4A HID Information (Read)
    |                                           |    0x2A4C HID Control Point (Write)
    |                                           |    0x2A4E Protocol Mode (Read/Write)
    |                                           |
    |--- ATT Find Information Request --------->|  查询 Report 特征的描述符
    |<-- ATT Find Information Response ---------|    0x2902 CCCD
    |                                           |    0x2908 Report Reference
    |                                           |
    |--- ATT Read Request --------------------->|  读取 Report Reference
    |<-- ATT Read Response ---------------------|  [0x02, 0x01] = Report ID 2, Input
    |                                           |
    |--- ATT Read Request --------------------->|  读取 Report Map
    |<-- ATT Read Response ---------------------|  (HID Report Descriptor, 见下方)
    |                                           |
    |--- ATT Read Request --------------------->|  读取 HID Information
    |<-- ATT Read Response ---------------------|  [0x11, 0x01, 0x00, 0x01]
    |                                           |    HID Version 1.1.1, RemoteWake

4. 订阅 HID Report(CCCD 写入)

手机(Central)                              PC(Peripheral)
    |                                           |
    |--- ATT Write Request -------------------->|  handle=CCCD, value=0x0001
    |<-- ATT Write Response --------------------|  (Notifications enabled)
    |                                           |
    |  PC 日志: "Mouse subscribed clients changed. Count: 1"
    |  PC 日志: "NEW GattSession tracked: MaxPduSize=517"
  • 写入 CCCD = 0x0001 表示启用 Notification
  • 此时 PC 端 SubscribedClients.Count 从 0 变为 1
  • MaxPduSize 协商到 517(BLE 5.x 最大值)

5. 鼠标数据传输(ATT Notification)

手机(Central)                              PC(Peripheral)
    |                                           |
    |<-- ATT Handle Value Notification ---------|  鼠标移动
    |<-- ATT Handle Value Notification ---------|  ...
    |<-- ATT Handle Value Notification ---------|  按键点击

鼠标 HID Report 格式(4 字节,Report ID 2)

字节 含义 取值范围
Byte 0 Buttons bit0=左键, bit1=右键, bit2=中键
Byte 1 X 位移 -127 ~ 127(有符号,相对移动)
Byte 2 Y 位移 -127 ~ 127(有符号,相对移动)
Byte 3 滚轮 -127 ~ 127(有符号)

实测报文示例

# 鼠标右移(dx=9, dy=0)
Mouse report: buttons=0x00 dx=9 dy=0 wheel=0    → Notification: [00 09 00 00]

# 鼠标左上移动(dx=-15, dy=-8)
Mouse report: buttons=0x00 dx=-15 dy=-8 wheel=0 → Notification: [00 F1 F8 00]

# 左键按下
Mouse report: buttons=0x01 dx=0 dy=0 wheel=0    → Notification: [01 00 00 00]

# 左键释放
Mouse report: buttons=0x00 dx=0 dy=0 wheel=0    → Notification: [00 00 00 00]

鼠标 HID Report Descriptor

05 01        USAGE_PAGE (Generic Desktop)
09 02        USAGE (Mouse)
A1 01        COLLECTION (Application)
  09 01        USAGE (Pointer)
  A1 00        COLLECTION (Physical)
    85 02        REPORT_ID (2)
    05 09        USAGE_PAGE (Button)
    19 01        USAGE_MINIMUM (Button 1 – Left)
    29 03        USAGE_MAXIMUM (Button 3 – Middle)
    15 00        LOGICAL_MINIMUM (0)
    25 01        LOGICAL_MAXIMUM (1)
    95 03        REPORT_COUNT (3)
    75 01        REPORT_SIZE (1)
    81 02        INPUT (Data,Var,Abs)       ← 3 个按键位
    95 01        REPORT_COUNT (1)
    75 05        REPORT_SIZE (5)
    81 03        INPUT (Cnst,Var,Abs)       ← 5 bit 填充
    05 01        USAGE_PAGE (Generic Desktop)
    09 30        USAGE (X)
    09 31        USAGE (Y)
    15 81        LOGICAL_MINIMUM (-127)
    25 7F        LOGICAL_MAXIMUM (127)
    75 08        REPORT_SIZE (8)
    95 02        REPORT_COUNT (2)
    81 06        INPUT (Data,Var,Rel)       ← X, Y 相对位移
    09 38        USAGE (Wheel)
    15 81        LOGICAL_MINIMUM (-127)
    25 7F        LOGICAL_MAXIMUM (127)
    75 08        REPORT_SIZE (8)
    95 01        REPORT_COUNT (1)
    81 06        INPUT (Data,Var,Rel)       ← 滚轮
  C0           END_COLLECTION
C0           END_COLLECTION

6. 连接生命周期

PC 日志时序(正常鼠标会话):

[10:46:28] Mouse advertising started. Status: Aborted → Started
[10:46:33] Mouse subscribed clients changed. Count: 1
[10:46:33] NEW GattSession tracked: MaxPduSize=517
[10:46:33] SESSION STATUS CHANGED: Closed (短暂断开)
[10:46:36] SESSION STATUS CHANGED: Active (自动重连)
[10:46:41] Session MaxPduSize changed: 517 (DLE 重协商)
[10:46:43] Mouse report: buttons=0x00 dx=1 dy=0 wheel=0  ← 数据开始传输
  ...
[10:56:00] SESSION STATUS CHANGED: Closed (手机休眠/超时)
[10:56:10] SESSION STATUS CHANGED: Active (自动重连)
  • 首次连接后可能出现短暂的 Closed→Active 循环,属于正常的连接参数协商
  • 手机息屏后连接可能断开,亮屏后自动重连
  • 重连时不需要重新配对,使用已存储的 LTK

键盘 vs 鼠标 HID Report 对比

键盘 鼠标
Report ID 1 2
报告长度 8 字节 4 字节
格式 [modifier, key1, key2, ..., key7] [buttons, dx, dy, wheel]
示例(按 A) 00 04 00 00 00 00 00 00
示例(左键) 01 00 00 00
示例(释放) 00 00 00 00 00 00 00 00 00 00 00 00

调试:BLE HCI 抓包分析

当遇到手机连接后不工作(日志中持续出现 No clients are currently subscribed)时,需要在 BLE 协议层抓包,分析手机在 GATT 服务发现阶段的具体行为。

复现步骤

  1. 在 PC 端编译并以 packaged 方式启动应用(见上方"运行"章节)
  2. 进入键盘页面,打开 "Discoverable and Connectable" 开关
  3. 在手机蓝牙设置中搜索并配对电脑
  4. 配对成功后在 VirtualBT 窗口按几个键
  5. 检查 %TEMP%\VirtualBT_log.txt
    • 如果出现 Sending keyboard report → 正常工作
    • 如果只有 No clients are currently subscribed → 手机未订阅 HID 服务,需要抓包分析

方法一:Android 端 HCI 日志(推荐)

从手机端抓包可以看到手机发出和收到的所有 BLE 报文,包括 GATT 服务发现请求、ATT 错误响应等。

1. 启用 HCI 日志

# 通过 adb 启用(需要 root 或开发者选项已开启)
adb shell settings put secure bluetooth_hci_log 1

# 或者手动操作:
# 设置 → 系统 → 开发者选项 → 启用蓝牙 HCI 信息收集日志 (Enable Bluetooth HCI snoop log)

启用后重启蓝牙(关闭再打开),让日志开始记录。

2. 复现问题

按上面的复现步骤操作一遍。

3. 提取日志

# root 手机直接拉取
adb pull /data/misc/bluetooth/logs/btsnoop_hci.log

# 如果上面的路径不存在,试试这些(不同厂商/系统版本路径不同)
adb pull /data/log/bt/btsnoop_hci.log
adb pull /sdcard/btsnoop_hci.log
adb pull /data/misc/bluedroid/btsnoop_hci.log

# 如果权限不够,通过 bug report 导出(不需要 root)
adb bugreport bugreport.zip
# 解压后在 FS/data/misc/bluetooth/logs/ 下找 btsnoop_hci.log

4. 用 Wireshark 分析

# 安装 Wireshark 后直接打开 btsnoop_hci.log
wireshark btsnoop_hci.log

重点关注以下过滤器和报文:

# 只看 ATT 协议(GATT 服务发现)
btatt

# 只看 ATT 错误响应
btatt.opcode == 0x01

# 只看 GATT 服务发现请求
btatt.opcode == 0x10 || btatt.opcode == 0x08

# 只看 HID 服务相关(UUID 0x1812)
btatt.uuid16 == 0x1812

# 只看 SMP 配对相关
btsmp

正常工作的手机应该能看到以下报文序列:

  1. LE Create Connection → 连接建立
  2. SMP Pairing Request/Response → 配对
  3. ATT Read By Group Type Request (UUID=0x2800) → 发现所有主服务
  4. ATT Read By Type Request (UUID=0x2803) → 发现 HID 服务的特征
  5. ATT Write Request (handle=Report CCCD, value=0x0001) → 订阅 HID Report 通知
  6. ATT Handle Value Notification → 收到键盘报告

不工作的手机可能在第 3~5 步缺失或出错。

方法二:Windows 端 BTSnoop 日志

从 PC 端抓包,适合没有 root 手机或无法在手机端操作的场景。

1. 开启 BTSnoop(管理员 PowerShell)

reg add "HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters" /v "DebugFlags" /t REG_DWORD /d 0x1 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters" /v "SniffLogPath" /t REG_SZ /d "C:\btsnoop.log" /f

2. 重启蓝牙适配器

在设备管理器中禁用再启用蓝牙适配器,或者重启电脑。

3. 复现问题后,用 Wireshark 打开 C:\btsnoop.log 分析

4. 用完后关闭(避免持续写日志)

reg delete "HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters" /v "DebugFlags" /f
reg delete "HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters" /v "SniffLogPath" /f
# 重启蓝牙适配器

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages