蓝牙体脂秤实战: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)
}
我的习惯:解析 BCS 数据时,先打印原始 hex 字符串,对照协议文档逐字节核对。这样能快速发现字节序或位掩码的错误。

2. 阻抗测量:核心原理与实现

体脂秤的阻抗测量,用的是 生物电阻抗分析法 (BIA)。简单说,就是通过电极向人体注入一个微弱的交流电流(通常 50kHz,几百微安),然后测量电压降,算出阻抗。

脂肪组织导电性差,阻抗高;肌肉组织含水多,导电性好,阻抗低。所以阻抗值直接反映了脂肪比例。

在 Android 端,我们通常不直接控制硬件测量阻抗,而是从 BCS 特征值里读取设备算好的结果。但有些高端体脂秤会提供原始阻抗数据,让你自己算体脂率。

我记得有一次,设备上报的阻抗值一直在 400Ω 到 600Ω 之间跳变。排查了半天,发现是用户脚底出汗,导致电极接触电阻不稳定。嗯,这个问题其实很常见。

注意:阻抗测量对电极接触质量非常敏感。如果数据异常,先检查电极是否清洁、用户双脚是否干燥、是否站正。我曾经因为用户脚上涂了润肤露,数据直接飘到 1000Ω 以上。

3. 数据校准:从原始值到准确结果

原始阻抗值不能直接用。你需要做两步校准:

  1. 硬件校准:体脂秤内部会用已知电阻(比如 500Ω)做自校准,消除电路漂移。
  2. 软件校准:根据用户的身高、年龄、性别等参数,用回归模型修正体脂率。

常见的体脂率计算公式是 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 数据 解析阻抗/体重 数据校准 用户识别 关联用户 存储到 Room 显示体脂/肌肉/骨量

这个流程看起来简单,但每一步都有细节。比如 BLE 连接后,要订阅 BCS 特征的 Notification,否则设备不会主动上报数据。再比如用户识别,如果匹配不到,应该提示用户手动选择,而不是直接报错。

6. 避坑指南

最后,分享几个我踩过的坑:

  • 阻抗值单位搞错:有些设备用 0.1Ω,有些用 1Ω。解析前一定要看文档或实测确认。
  • 体脂率范围异常:如果算出来是 0% 或 100%,大概率是公式参数错了。先检查身高体重单位。
  • 多用户数据混淆:如果 Room 数据库没有加外键约束,可能出现测量数据找不到用户的情况。我后来加了 ForeignKey 和级联删除。
  • 校准系数不通用:不同品牌的体脂秤,校准公式差异很大。不要硬套开源代码,最好找厂商要 SDK 或算法文档。
总结一下:体脂秤开发的核心是 BCS 数据解析、阻抗校准、用户管理。这三块做好了,产品就稳了。剩下的 UI 和图表展示,都是锦上添花。

好了,这一章的内容就到这里。希望你能从我的经验里找到有用的东西。如果你在开发中遇到什么奇怪的问题,欢迎交流。

公众号:蓝海资料掘金营,微信 deep3321