蓝牙体脂秤实战:Body Composition Service、阻抗测量、数据校准、用户管理
体脂秤,说白了就是给体重秤加了个「电信号扫描」功能。你站上去,微弱电流从一只脚流到另一只脚,通过阻抗算出脂肪率、肌肉量、骨量等等。这个功能在蓝牙规范里叫 Body Composition Service (BCS),UUID 是 0x181B。
我最早做体脂秤项目时,踩了不少坑。尤其是阻抗测量那部分,数据忽高忽低,差点让我怀疑人生。后来才发现,问题出在电极接触和校准算法上。今天我把这些经验都整理出来,希望能帮你少走弯路。
1. Body Composition Service 核心结构
BCS 服务里最重要的特征值是 Body Composition Measurement,UUID 是 0x2A2B。它用 16 字节的固定格式上报数据,结构如下:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0-1 | Flags | 指示哪些字段有效 |
| 2-3 | Body Fat Percentage | 体脂率,单位 0.1% |
| 4-7 | Impedance | 阻抗值,单位 0.1Ω |
| 8-9 | Weight | 体重,单位 0.1kg |
| 10-15 | Reserved / Other | 保留或扩展字段 |
解析代码其实不复杂,但要注意字节序。我习惯用 ByteBuffer 配合 LittleEndian 来读:
fun parseBodyComposition(data: ByteArray): BodyCompositionData {
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
val flags = buffer.short
val bodyFat = if (flags and 0x01 != 0) buffer.short / 10.0f else null
val impedance = if (flags and 0x02 != 0) buffer.int / 10.0f else null
val weight = if (flags and 0x04 != 0) buffer.short / 10.0f else null
return BodyCompositionData(bodyFat, impedance, weight)
}
2. 阻抗测量:核心原理与实现
体脂秤的阻抗测量,用的是 生物电阻抗分析法 (BIA)。简单说,就是通过电极向人体注入一个微弱的交流电流(通常 50kHz,几百微安),然后测量电压降,算出阻抗。
脂肪组织导电性差,阻抗高;肌肉组织含水多,导电性好,阻抗低。所以阻抗值直接反映了脂肪比例。
在 Android 端,我们通常不直接控制硬件测量阻抗,而是从 BCS 特征值里读取设备算好的结果。但有些高端体脂秤会提供原始阻抗数据,让你自己算体脂率。
我记得有一次,设备上报的阻抗值一直在 400Ω 到 600Ω 之间跳变。排查了半天,发现是用户脚底出汗,导致电极接触电阻不稳定。嗯,这个问题其实很常见。
3. 数据校准:从原始值到准确结果
原始阻抗值不能直接用。你需要做两步校准:
- 硬件校准:体脂秤内部会用已知电阻(比如 500Ω)做自校准,消除电路漂移。
- 软件校准:根据用户的身高、年龄、性别等参数,用回归模型修正体脂率。
常见的体脂率计算公式是 Jackson-Pollock 模型 的变体:
fun calculateBodyFat(
impedance: Float,
weight: Float,
height: Float,
age: Int,
isMale: Boolean
): Float {
val bmi = weight / ((height / 100) * (height / 100))
val base = if (isMale) {
1.20f * bmi + 0.23f * age - 16.2f
} else {
1.20f * bmi + 0.23f * age - 5.4f
}
// 阻抗修正项
val impedanceFactor = (impedance / 500f) * 0.1f
return base + impedanceFactor
}
这个公式是我从几个开源体脂秤项目里总结出来的。实际产品中,每个厂商都有自己的校准系数,甚至需要做大量临床实验来拟合。
4. 用户管理:多用户与数据隔离
体脂秤通常是家庭共用。所以用户管理是必须的。我建议用以下方案:
- 本地用户表:用 Room 数据库存储用户信息,包括身高、年龄、性别、体重基线。
- 自动识别:通过体重值匹配最近用户。如果体重接近,自动关联。
- 手动切换:提供用户选择界面,支持添加、删除、编辑用户。
用户表结构大概这样:
@Entity
data class User(
@PrimaryKey val id: Long,
val name: String,
val heightCm: Float,
val age: Int,
val isMale: Boolean,
val lastWeightKg: Float? = null
)
测量完成后,把数据关联到用户:
data class Measurement(
val userId: Long,
val timestamp: Long,
val weightKg: Float,
val bodyFatPercent: Float,
val impedanceOhm: Float
)
我遇到过一个问题:用户 A 和用户 B 体重只差 0.5kg,自动识别经常出错。后来我加了「最近 3 次体重均值」做匹配,准确率提升了不少。
5. 完整数据流与架构图
下面这张图展示了从 BLE 连接、数据接收、解析、校准到用户关联的完整流程:
这个流程看起来简单,但每一步都有细节。比如 BLE 连接后,要订阅 BCS 特征的 Notification,否则设备不会主动上报数据。再比如用户识别,如果匹配不到,应该提示用户手动选择,而不是直接报错。
6. 避坑指南
最后,分享几个我踩过的坑:
- 阻抗值单位搞错:有些设备用 0.1Ω,有些用 1Ω。解析前一定要看文档或实测确认。
- 体脂率范围异常:如果算出来是 0% 或 100%,大概率是公式参数错了。先检查身高体重单位。
- 多用户数据混淆:如果 Room 数据库没有加外键约束,可能出现测量数据找不到用户的情况。我后来加了
ForeignKey和级联删除。 - 校准系数不通用:不同品牌的体脂秤,校准公式差异很大。不要硬套开源代码,最好找厂商要 SDK 或算法文档。
好了,这一章的内容就到这里。希望你能从我的经验里找到有用的东西。如果你在开发中遇到什么奇怪的问题,欢迎交流。