本文最后更新于 2026年11月6日。
https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-5
https://gadgetbridge.org/gadgets/wearables/xiaomi/#mi-band-5
针对 小米手环 5,我建议不要尝试开发手环固件,也不要一开始就逆向全部蓝牙协议,而是开发一个独立的 Android App,通过 BLE(低功耗蓝牙)直接连接手环。
这个方向是可行的,而且有成熟的开源项目可以借鉴。
我建议项目定位为:
MiBandTool:小米手环 5 开源蓝牙控制中心
目标是不依赖小米运动健康 App、不依赖小米云端,尽可能在本地完成设备连接、通知、震动控制和数据管理。
一、先明确小米手环 5 能实现什么
Produtos com até 15 OFF no PIX KaBuM
Обзор фитнес-браслета Xiaomi Mi Band 5 и сравнение с Mi Band 4
Xiaomi Mi Band 5 Review: $45 Fitness Tracker Packs More Features Than Rivals For Half The Price
7
小米手环 5 有 BLE 蓝牙通信能力,可以围绕以下功能开发:
| 功能 | 可实现性 | 说明 |
|---|---|---|
| 蓝牙连接、断开 | 可行 | BLE GATT |
| 读取设备信息 | 可行 | 固件、电量等 |
| 控制震动 | 可行 | 预设震动或通知 |
| 手机通知同步 | 可行 | 自己实现通知监听 |
| 来电提醒 | 可行 | 手机侧监听来电 |
| 心率测量 | 可行 | 需要正确处理测量协议 |
| 步数读取 | 可行 | 需要研究对应特征值 |
| 自定义表盘 | 有条件 | 图片格式、资源和协议限制 |
| 屏幕任意绘制 | 不建议 | 不是开放式显示设备 |
| 安装 APK 到手环 | 不支持常规方式 | 不是 Wear OS |
这里最值得做的是:把手环作为手机的一个可编程震动与信息输出设备。
二、优先研究的开源项目
我筛选了几个真正有参考价值的项目。这里有一个好消息:小米手环 5 已经有相当成熟的第三方协议实现,不需要从零逆向。
- Gadgetbridge
首选参考
AGPL-3.0 · Android · 开源替代官方运动健康 App
GitHub: Freeyourgadget/Gadgetbridge
这是最重要的参考项目。
已支持小米手环 5 的主要功能:
-
蓝牙配对和认证
-
通知和震动
-
心率、步数、睡眠等数据
-
闹钟、勿扰模式
-
音乐控制
-
设备设置
官方设备支持文档将 Mi Band 5 列为高度支持设备。
Gadgetbridge+1
我的建议:先阅读它的 MiBand5Support、HuamiSupport 和 BLE 通信实现。
- miband5-ble
协议研究首选
MIT · Python · 专门针对小米手环 5
GitHub: Erencv/miband5-ble
这是一个专门针对 Mi Band 5 的独立 BLE 协议实现。
已验证的功能包括:
-
AES-128-ECB 认证
-
实时心率
-
步数、距离、卡路里
-
电池电量
-
历史活动数据
-
震动和寻找设备
-
部分设备参数设置
特别有价值的是它提供 PROTOCOL.md,可以直接研究底层数据包。
GitHub+1
建议:用它作为你的协议实验和功能验证工具。
- huami-token
GitHub: argrento/huami-token
用于获取已经绑定设备的认证密钥。
小米手环 5 的 BLE 认证不是简单连接即可完成,通常需要此前官方 App 绑定设备时建立的 auth key。
这个密钥是你自己的手环专属凭证,不是所有同型号手环共用的密钥。
- BreakMi
GitHub: Skiti/BreakMi
适合研究小米手环旧版认证和通信机制。
但需要注意,部分 Mi Band 5 固件已经更新过认证协议,旧版实验方案不一定适用。它更适合作为协议研究资料,而不是直接作为你的 App 基础。
Skiti/BreakMi · GitHub+1
开源项目怎么选?
| 项目 | 用途 | 我的建议 |
|---|---|---|
| Gadgetbridge | Android 完整实现 | 重点研究 |
| miband5-ble | 5 代协议实验 | 重点使用 |
| huami-token | 获取认证密钥 | 辅助工具 |
| BreakMi | 研究旧协议 | 了解即可 |
我更倾向于借鉴 Gadgetbridge 的协议代码,独立开发自己的 Android 界面,而不是直接 fork 一个庞大的项目。
注意:Gadgetbridge 是 AGPL-3.0。如果直接复制它的代码并发布衍生程序,需要遵守相应的开源义务。miband5-ble 是 MIT 许可,适合参考和移植,但仍要检查具体依赖的许可证。
三、最关键的问题:蓝牙认证
小米手环 5 有一个开发门槛:需要获取 auth key。
官方 App 首次绑定设备时,会通过服务端完成认证密钥的配置。之后第三方 App 才能使用这个密钥进行本地 BLE 认证。
基本过程:
-
官方 App 绑定手环
使用 Zepp Life(原小米运动)完成首次绑定。
-
获取自己的 auth key
使用 Gadgetbridge 官方文档介绍的方式,或 huami-token 工具获取。
-
保存认证密钥
放在 Android Keystore 保护的本地存储中,不上传服务器。
-
Android App 执行 BLE 认证
连接手环,交换 challenge,完成 AES-128 认证。
-
读写手环特征值
开始读取数据、控制震动、发送通知。
参考:
重要提醒:
-
auth key 相当于设备控制密码,不要上传 GitHub。
-
获取密钥后不需要每次连接都访问小米服务器。
-
不要随意恢复手环出厂设置,否则可能需要重新配对和获取密钥。
-
调试时避免让 Zepp Life 和自制 App 同时占用手环的 BLE 连接。
四、我建议的 App 技术架构
不要引入 Go 后端,也不需要 NAS 参与第一阶段。手环和手机直接通信即可。
MiBandTool 技术架构
Android App(Kotlin)
Material 3 / Jetpack Compose 或简单原生界面
BLE Manager
扫描、连接、断线重连
Auth Manager
认证和密钥管理
Data Manager
心率、步数、电量
Command Manager
震动、提醒、设备控制
Room / SQLite
本地数据存储、历史记录、设置
Bluetooth Low Energy / GATT
Xiaomi Mi Band 5
BLE 服务和特征值
推荐开发环境
| 项目 | 选择 |
|---|---|
| 语言 | Kotlin |
| IDE | Android Studio |
| UI | Jetpack Compose |
| 蓝牙 | Android BluetoothLeScanner / BluetoothGatt |
| 数据库 | Room / SQLite |
| 加密 | Android Keystore + AES |
| 最低系统 | Android 8.0 起步即可 |
| 运行模式 | 前台服务(需要持续连接时) |
Android 12 及以上要处理 BLUETOOTH_SCAN、BLUETOOTH_CONNECT 等运行时权限;后台长期运行还需要考虑前台服务限制和厂商省电策略。
五、分四个阶段开发
我建议不要一次性做所有功能。先验证最关键的蓝牙链路。
第一阶段:蓝牙连接实验
首先完成
Your First Bluetooth Low Energy App with Flutter
-
扫描附近的小米手环 5
-
显示设备名称和 MAC(系统允许时)
-
建立 GATT 连接
-
枚举服务 UUID 和特征值
-
读取设备型号、固件版本、电量
验收标准:能够稳定连接手环,并读取设备信息。
第二阶段:认证和控制
-
导入 auth key
-
实现 AES-128 认证
-
发送震动指令
-
实现查找手环
-
自定义震动次数和时长
验收标准:点击手机按钮,手环成功震动。
第三阶段:数据读取
-
获取实时心率
-
获取实时步数
-
获取电池信息
-
获取历史活动数据
-
保存到 SQLite
验收标准:可以查看当天数据和历史曲线。
第四阶段:创意功能
-
手机通知转发
-
NAS 和 VPS 告警
-
自定义震动规则
-
远程智能家居控制
-
自动化任务联动
验收标准:手环能够成为一个可靠的个人通知终端。
六、我认为最值得做的特色功能
这部分才是自己的 App 相比 Gadgetbridge 最有意思的地方。
| 功能 | 具体玩法 |
|---|---|
| NAS 告警 | Docker 容器停止,手环震动三次 |
| VPS 告警 | SSH 服务不可用,手环发出特殊震动 |
| 家庭控制 | 手机快捷操作触发手环震动反馈 |
| 专属闹钟 | 不同时间使用不同震动模式 |
| 专注模式 | 工作 50 分钟,手环震动提醒休息 |
| 智能提醒 | 重要联系人消息使用特殊震动 |
| 心率记录 | 自己决定采样频率并保存数据 |
| 防丢提醒 | 手机与手环断开时提醒 |
其中我最喜欢的是自定义震动编码。
例如:
自定义震动编码示例
NAS 异常
短、短、短
VPS 宕机
长、长
重要通知
长、短、长
休息提醒
短、长
示意编码。实际震动时序和强度取决于手环支持的震动指令,未必能精确控制每个脉冲。
这样你甚至可以不看手机,通过震动节奏判断服务器出了什么问题。
七、第一步实际怎么开始?
我建议先做一个独立的最小实验,不要马上搭建完整 App。
先用 Python 验证小米手环 5 的协议。
因为 miband5-ble 已经实现了认证、连接、读取电量和震动,我们可以先复现:
async with await MiBand5.open() as band: print(await band.battery()) await band.vibrate(1.5)
然后再把 Python 验证成功的协议移植到 Kotlin。
这样可以把问题拆开:
-
手环本身是否支持该指令?
-
auth key 是否正确?
-
BLE 连接是否稳定?
-
Kotlin 实现是否正确?
避免同时调试蓝牙、认证和 UI。
一个需要提前确定的事情
小米手环 5 的不同固件版本可能存在协议差异。上面这个 Python 项目明确验证的是 XMSH10HM,固件 V1.0.2.66,不能保证你的固件完全一致。
GitHub+1
八、我的最终推荐
我建议把项目分成两个版本:
-
MiBandTool Lite:纯 Android、Kotlin、BLE、SQLite,不依赖云服务。
-
MiBandTool Plus:增加 NAS、Go 服务、MQTT、智能家居和服务器告警。
先把 Lite 做好,Plus 只需要在后续增加通信模块。
为了确定第一阶段的开发参数,我还想知道你的手环当前状态:
- 手环固件版本
不知道
V1.0.2.66
其他版本
- 目前是否绑定过官方 App?
已经绑定过 Zepp Life
已经绑定过小米运动健康
从未绑定
- 对官方 App 的依赖程度
完全独立,不再依赖官方 App
可以首次借助官方 App,之后独立使用
允许与官方 App 配合使用
确定开发方案
我建议最终做成一个真正轻量、无广告、无账号、无云端依赖的个人手环控制 App。
如果你愿意,还可以持续跟踪相关开源协议项目的更新:
⌚每周跟踪手环开源项目更新Tap to set up
根据你的情况,我建议把 MiBandTool Lite 做成一个真正独立的 Android 应用:
-
手环:小米手环 5(XMSH10HM)。
-
手机:Android,直接通过 BLE 连接。
-
认证:使用自己的手环 Auth Key。
-
数据:全部保存在手机本地 SQLite。
-
网络:正常使用不需要互联网。
-
官方 App:只在获取认证密钥的准备阶段使用,之后不再依赖它。
最重要的一点:先获取 Auth Key,再考虑解除官方 App 绑定。
你目前已经用小米运动健康绑定过手环,这是一个很好的起点。但不要先恢复出厂设置、解除绑定或重新配对。否则可能增加获取原有密钥的难度。
我建议第一版只实现四个功能:
-
扫描并连接小米手环 5。
-
使用 Auth Key 完成认证。
-
读取电池信息和设备信息。
-
点击按钮让手环震动。
这四项成功,就证明我们已经掌握了独立控制手环的基本链路。
一、先解决 Auth Key:针对你的小米运动健康
这里需要纠正一个容易混淆的地方:
小米手环 5 属于较早的 Huami 平台设备。Gadgetbridge 将它列为高度支持,但要求 Huami Auth Key。小米运动健康(Mi Fitness)与 Zepp Life 使用的认证后端并不完全相同。
Gadgetbridge+1
建议按下面顺序获取。
方案 A:从手机 App 日志提取
这是我建议优先尝试的方法。
如果你安装的是新版小米运动健康,包名通常需要确认是否为:
com.xiaomi.wearable
用 ADB 查询:
adb shell pm list packages | findstr /i "wearable hm.health"
如果返回:
package:com.xiaomi.wearable
可以检查:
/sdcard/Android/data/com.xiaomi.wearable/files/log/
如果是 Zepp Life,则检查:
/sdcard/Android/data/com.xiaomi.hm.health/files/log/
Gadgetbridge 文档记录了可能包含认证密钥的日志字段:
encryptKey
token
authKey
huamiAuthKey
可以尝试将日志目录复制到电脑,然后在本地搜索这些字段。这个方法不需要 Root,但日志是否包含密钥取决于 App 版本。
Gadgetbridge
方案 B:Xiaomi Cloud Tokens Extractor
项目:
它可以从小米云端获取已绑定设备的相关信息和认证密钥。Gadgetbridge 官方配对文档也列出了这个工具。
Gadgetbridge+1
获取成功后
你应该得到类似这样的 32 位十六进制字符串:
0123456789abcdef0123456789abcdef
这只是格式示例,不是你的真实密钥。
安全提醒
-
不要把 Auth Key 发给我或其他人。
-
不要上传到 GitHub。
-
不要在日志中打印完整密钥。
-
不要先解绑或恢复出厂设置。
-
拿到密钥后再进行独立连接测试。
还有一个重要区别:获得 Auth Key 不等于已经完成独立配对。 我们还需要验证手环是否接受该密钥,以及是否存在其他手机正在占用 BLE 连接。
二、MiBandTool Lite 项目目录
第一版使用 Kotlin + Jetpack Compose,不依赖第三方蓝牙框架。
建议项目名:
MiBandTool
包名:
com.personal.mibandtool
目录如下:
MiBandTool/
├── app/
│ ├── src/main/
│ │ ├── AndroidManifest.xml
│ │ └── java/com/personal/mibandtool/
│ │ ├── MainActivity.kt
│ │ ├── ble/
│ │ │ ├── BleScanner.kt
│ │ │ ├── BleConnection.kt
│ │ │ ├── GattExplorer.kt
│ │ │ └── BandProtocol.kt
│ │ ├── auth/
│ │ │ ├── AuthManager.kt
│ │ │ └── KeyStoreManager.kt
│ │ ├── data/
│ │ │ ├── BandInfo.kt
│ │ │ └── BandRepository.kt
│ │ └── ui/
│ │ ├── HomeScreen.kt
│ │ └── Theme.kt
│ └── build.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
各模块职责:
| 模块 | 负责内容 |
|---|---|
| BleScanner | 扫描附近设备 |
| BleConnection | BLE 连接和断开 |
| GattExplorer | 枚举服务和特征值 |
| AuthManager | AES 认证 |
| KeyStoreManager | 安全保存 Auth Key |
| BandProtocol | 小米手环 5 专用协议 |
| BandRepository | 设备信息和数据管理 |
不要把认证、扫描、UI 全部写进 MainActivity。后续协议调试会很痛苦。
三、第一版代码:BLE 扫描和连接
第一版先不发送任何未经验证的控制指令。
它能够:
-
扫描附近 BLE 设备。
-
展示设备名称和地址。
-
选择小米手环 5。
-
建立 GATT 连接。
-
枚举所有 BLE 服务。
-
读取标准设备信息和电池特征(如果设备开放)。
这部分可以直接创建 Android Studio 项目运行。
1. Gradle 配置
创建 Empty Activity 项目,选择 Kotlin 和 Jetpack Compose。
app/build.gradle.kts:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.personal.mibandtool"
compileSdk = 35
defaultConfig {
applicationId = "com.personal.mibandtool"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
}
}
如果使用 Android Studio 自动生成的版本目录,保留它已有的 Compose 插件配置即可。
2. AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT" />
<application
android:allowBackup="false"
android:label="MiBandTool"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
3. MainActivity.kt
下面是第一版完整的扫描、连接和 GATT 枚举代码。
package com.personal.mibandtool
import android.Manifest
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import java.util.UUID
data class BandDevice(
val name: String,
val address: String,
val device: BluetoothDevice
)
class MainActivity : ComponentActivity() {
private var gatt: BluetoothGatt? = null
private val bluetoothManager by lazy {
getSystemService(Context.BLUETOOTH_SERVICE)
as BluetoothManager
}
private val scanner by lazy {
bluetoothManager.adapter.bluetoothLeScanner
}
private var scanCallback: ScanCallback? = null
private val _devices = mutableStateListOf<BandDevice>()
private val _logs = mutableStateListOf<String>()
private var scanning by mutableStateOf(false)
private fun log(message: String) {
runOnUiThread {
_logs.add(0, message)
}
}
private fun hasPermissions(): Boolean {
val permissions = if (Build.VERSION.SDK_INT >= 31) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
return permissions.all {
ContextCompat.checkSelfPermission(
this, it
) == PackageManager.PERMISSION_GRANTED
}
}
fun startScan() {
if (!hasPermissions()) {
log("请先授予蓝牙权限")
return
}
stopScan()
_devices.clear()
val callback = object : ScanCallback() {
override fun onScanResult(
callbackType: Int,
result: ScanResult
) {
val device = result.device
val name = result.scanRecord?.deviceName
?: device.name
?: "Unknown"
val address = try {
device.address
} catch (e: SecurityException) {
"Unknown"
}
if (_devices.none {
it.address == address
}) {
_devices.add(
BandDevice(name, address, device)
)
log("发现设备: $name")
}
}
override fun onScanFailed(errorCode: Int) {
scanning = false
log("扫描失败: $errorCode")
}
}
scanCallback = callback
try {
scanner.startScan(callback)
scanning = true
log("开始扫描 BLE 设备")
} catch (e: Exception) {
log("启动扫描失败: ${e.message}")
}
}
fun stopScan() {
try {
scanCallback?.let {
scanner.stopScan(it)
}
} catch (_: Exception) {
}
scanCallback = null
scanning = false
}
fun connect(device: BluetoothDevice) {
stopScan()
gatt?.close()
gatt = null
log("正在连接设备...")
try {
gatt = device.connectGatt(
this,
false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
g: BluetoothGatt,
status: Int,
newState: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) {
log("连接失败,状态码: $status")
g.close()
return
}
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
log("BLE 已连接")
g.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
log("BLE 已断开")
}
}
}
override fun onServicesDiscovered(
g: BluetoothGatt,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) {
log("服务发现失败: $status")
return
}
log("发现 ${g.services.size} 个服务")
g.services.forEach { service ->
log("服务 UUID: ${service.uuid}")
service.characteristics.forEach { char ->
log(
" 特征: ${char.uuid} " +
"属性=${char.properties}"
)
if (
service.uuid == BATTERY_SERVICE &&
char.uuid == BATTERY_LEVEL
) {
if (
char.properties and
BluetoothGattCharacteristic.PROPERTY_READ != 0
) {
g.readCharacteristic(char)
}
}
if (
service.uuid == DEVICE_INFO_SERVICE &&
(
char.uuid == MODEL_NUMBER ||
char.uuid == FIRMWARE_REVISION
)
) {
if (
char.properties and
BluetoothGattCharacteristic.PROPERTY_READ != 0
) {
g.readCharacteristic(char)
}
}
}
}
}
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val result = if (
characteristic.uuid == BATTERY_LEVEL
) {
value.firstOrNull()?.toInt()?.and(0xff)
?.let { "$it%" }
?: "空"
} else {
value.toString(Charsets.UTF_8)
}
log("${characteristic.uuid}: $result")
}
}
@Suppress("DEPRECATION")
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (Build.VERSION.SDK_INT < 33 &&
status == BluetoothGatt.GATT_SUCCESS
) {
val value = characteristic.value ?: return
val result = value.joinToString(" ") {
"%02X".format(it)
}
log("${characteristic.uuid}: $result")
}
}
},
BluetoothDevice.TRANSPORT_LE
)
} catch (e: Exception) {
log("连接异常: ${e.message}")
}
}
companion object {
val BATTERY_SERVICE: UUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb")
val BATTERY_LEVEL: UUID =
UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")
val DEVICE_INFO_SERVICE: UUID =
UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb")
val MODEL_NUMBER: UUID =
UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb")
val FIRMWARE_REVISION: UUID =
UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
}
override fun onDestroy() {
stopScan()
gatt?.close()
gatt = null
super.onDestroy()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
val permissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) {
if (hasPermissions()) startScan()
else log("蓝牙权限未授予")
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
"MiBandTool Lite",
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
if (hasPermissions()) {
startScan()
} else {
val permissions =
if (Build.VERSION.SDK_INT >= 31) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
permissionLauncher.launch(permissions)
}
}) {
Text("扫描设备")
}
OutlinedButton(onClick = {
stopScan()
}) {
Text("停止扫描")
}
}
Spacer(Modifier.height(12.dp))
Text("发现的设备")
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(_devices) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(item.name)
Text(
item.address,
style = MaterialTheme.typography.bodySmall
)
Button(onClick = {
connect(item.device)
}) {
Text("连接")
}
}
}
}
}
Text("调试日志")
LazyColumn(
modifier = Modifier.height(190.dp)
) {
items(_logs) {
Text(
it,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}
}
代码说明:
-
这是第一版 BLE 连接测试器,不是完整认证客户端。
-
标准电池服务和设备信息服务可能不存在于手环公开的 GATT 服务中。
-
手环的 Huami 私有服务需要后续结合实际枚举结果和协议文档解析。
-
Android BLE 的 GATT 操作应当串行化;这版只用于初步发现和测试,不适合直接当作长期运行的连接管理器。
四、针对小米手环 5 的真正协议 UUID
这里有一个重要细节:不能完全照搬普通 BLE 电池读取代码。
miband5-ble 的逆向协议文档给出了 Mi Band 5 的实际 GATT 映射。它使用 Huami 私有 UUID,而不只是标准 BLE 服务。
GitHub
| 功能 | UUID |
|---|---|
| 设备信息服务 | 180A |
| 软件版本 | 2A28 |
| 硬件版本 | 2A27 |
| 序列号 | 2A25 |
| 心率服务 | 180D |
| 心率数据 | 2A37 |
| 寻找设备/震动 | 1802 / 2A06 |
| Huami 主服务 | FEE0 |
| 电池信息 | 00000006-0000-3512-2118-0009AF100700 |
| 实时步数 | 00000007-0000-3512-2118-0009AF100700 |
| Huami 认证服务 | FEE1 |
| 认证特征值 | 00000009-0000-3512-2118-0009AF100700 |
因此,上面的第一版代码需要针对手环增加以下两个常量:
val HUAMI_BATTERY_INFO = UUID.fromString(
"00000006-0000-3512-2118-0009AF100700"
)
val HUAMI_AUTH = UUID.fromString(
"00000009-0000-3512-2118-0009AF100700"
)
读取电池时,不能按照标准电池特征值的第一个字节解析。
Mi Band 5 的电池数据格式中:
-
byte[1]:电量百分比
-
byte[2]:充电状态,
00正常,01充电中
例如:
0F 3F 00 B2 07 ...
表示电量 63%,正常状态。
建议第一版先完整打印 GATT UUID 和属性,不要急着读取全部特征值。 后续增加操作队列,保证一次只执行一个 GATT 操作,避免 Android 返回 GATT_BUSY。
五、第二阶段:认证代码设计
等你拿到 Auth Key 后,再实现 AuthManager.kt。
小米手环 5 使用 AES-128-ECB Challenge-Response 认证。
认证流程已经在开源项目中验证:
GitHub+1
BLE 认证过程
-
App 向认证特征值写入:
02 00 -
手环返回随机挑战值:
10 02 01 + 16字节随机数 -
App 使用 Auth Key 加密随机数:
AES-128-ECB -
App 写入:
03 00 + 16字节加密结果 -
手环返回:
10 03 01认证成功
Kotlin 加密部分可以这样实现:
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
fun encryptChallenge(
authKey: ByteArray,
challenge: ByteArray
): ByteArray {
require(authKey.size == 16)
require(challenge.size == 16)
val cipher = Cipher.getInstance("AES/ECB/NoPadding")
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(authKey, "AES")
)
return cipher.doFinal(challenge)
}
注意:这只是 AES 加密部分。完整 AuthManager 还要处理通知订阅、认证状态机、超时、错误响应和 GATT 写入队列。
六、第三阶段:实现震动控制
这是我建议的第一个真正可见的功能。
认证成功后,使用标准 Immediate Alert 特征值:
Service: 1802
Characteristic: 2A06
发送:
03
停止:
00
这个指令已经在 Mi Band 5 的实机协议实验中验证可以触发震动。
GitHub
最终 UI 可以是:
MiBandTool Lite
UI 原型
Xiaomi Mi Band 5 / Sports Modes / Heart Rate / Sleep Tracker
Mi Smart Band 5
XMSH10HM
已连接(示意)
63%
电池电量(示意)
—
心率
震动测试(认证后开放)读取设备信息这是未来完成认证后的界面示意,不代表当前已经连接了你的手环。
七、实际开发顺序和验收标准
| 阶段 | 任务 | 验收结果 |
|---|---|---|
| 0 | 获取 Auth Key | 得到本地 16 字节密钥 |
| 1 | 运行第一版 APK | 能扫描并连接手环 |
| 2 | GATT 枚举 | 能识别 FEE0、FEE1 等服务 |
| 3 | BLE 认证 | 收到 10 03 01 |
| 4 | 电池读取 | 能解析实际电量 |
| 5 | 震动控制 | 手机按钮能控制手环 |
| 6 | 数据读取 | 获取步数和心率 |
开发时建议以 miband5-ble 的 MIT 实现作为协议实验参考,以 Gadgetbridge 的 GPL-3.0 实现作为协议交叉验证资料。不要直接复制 Gadgetbridge 代码到闭源项目。
参考资料:
八、我们现在最应该做的事
我建议暂时不要继续增加代码,而是先完成两个准备工作:
-
确认你手机上小米运动健康的实际包名。
-
获取手环固件版本和 Auth Key。
这样后续我就可以按照你的真实固件版本编写协议代码,而不是盲目套用别人测试的版本。
请先运行这两条 ADB 命令:
adb shell pm list packages | findstr /i "wearable hm.health"
然后:
adb shell dumpsys bluetooth_manager
第一条确认 App 类型,第二条辅助确认蓝牙状态(输出可能较长,不必全部发送)。
另外,第一版 APK 运行后,请把扫描结果中手环的设备名称,以及 GATT 服务 UUID 列表发给我。地址和序列号可以打码。
下一步我可以据此继续完成 AuthManager.kt + KeyStoreManager.kt + 震动控制,让 MiBandTool 从一个 BLE 扫描器真正变成可以独立控制小米手环 5 的 App。