Picooc(有品)体脂秤 BLE 通信协议分析
来源:反编译 Picooc App v4.13.4 (com.picooc),jadx 解包 486MB 源码 设备:Picooc Mini(广播协议)/ Latin-S(连接协议)/ Dual-SPP(经典蓝牙)
1. 型号矩阵与协议差异
| 通信方式 | 代表型号 | 特点 |
|---|---|---|
| Classic SPP | Dual-SPP | 经典蓝牙,非 BLE,独立协议栈(BTStandard.java) |
| BLE 连接型 | Latin-S / PICOOC-C1~CQ / PICOOC-14~25 / PICOOC-IS | BLE GATT 连接,走 0xFFF0 服务 |
| BLE 广播型 | PICOOC-L(Mini 等新秤) | 直读广播包,无需配对连接 |
Mini 的定位:走 PICOOC-L 广播协议,是 Picooc 最新方案。
2. BLE 服务与 UUID(连接型)
主服务
- Service:
0000fff0-0000-1000-8000-00805f9b34fb - Write (App→秤):
0000fff2-0000-1000-8000-00805f9b34fb - Notify (秤→App):
0000fff1-0000-1000-8000-00805f9b34fb
辅助服务
- Device Information:
0000180a-... - Quintic OTA:
0000fee8-... - Quintic QPP:
0000fee9-...
3. 通信协议数据包(连接型)
命令码总表
| 码 | 方向 | 含义 |
|---|---|---|
0x30(48) | 秤→App | 请求用户信息(性别/身高/年龄) |
0x32(50) | 秤→App | 体成分结果(含体脂率/水分/骨量等) |
0x35(53) | 秤→App | UTC 时间同步请求 |
0x36(54) | 秤→App | 历史记忆数据 |
0x37(55) | 秤→App | 历史数据发送完毕 |
0x39(57) | 秤→App | 实时体重+阻抗(主数据流) |
0x3A(58) | 秤→App | BLE 版本+BOM 信息+握手 |
0x3C(60) | 秤→App | 心率测量数据 |
0x3E(62) | 秤→App | 多频生物电阻抗数据 |
0x3F(63) | 秤→App | 平衡能力测试 |
0x51(81) | 秤→App | 握手(替代 0x3A) |
0x52(82) | 秤→App | 实时体重(替代 0x39) |
3B 03 01 | 秤→App | 低电量提醒 |
写命令前缀
- 拉丁系列:
0xF1(如F1 03 39确认数据) - 部分新型号:
0xA1(如A1 03 52确认数据)
0x39 实时体重数据格式
byte[0] = 0x39
byte[1] = length
byte[2-5] = Timestamp (Unix 秒, big-endian)
byte[6-7] = Weight (kg) = (b6<<8 | b7) / 20
byte[8-9] = Impedance = (b8<<8 | b9) / 10
byte[10-11] = WeightLB = (b10<<8 | b11) / 10
byte[12] = Unit (0=kg, 1=斤, 2=lb)
byte[13-14] = Phase50K (相位角) = (b13<<8 | b14) / 100
byte[15] = Flag (0=完成可上报)
0x32 体成分结果格式
byte[0] = 0x32
byte[1] = length
byte[2-3] = Weight (kg) /20
byte[4-5] = BodyFat (%) /10
byte[6-7] = Water (%) /10
byte[8-9] = (unknown)
byte[10-11] = BoneMass (kg) /10/2
byte[12-13] = (unknown)
byte[14] = VisceralFat (直接取值)
byte[15] = BodyAge (直接取值)
byte[18-19] = Impedance
连接握手流程
秤发 0x3A/0x51 (版本+时间请求)
→ App 回 F1 0A 3A [UTC-4B] [flag] [unit] [extra]
秤发 0x30 (请求用户信息)
→ App 回 F1 03 30 (ACK)
→ App 100ms 后回 31 06 01 [sex] [heightHex] [ageHex]
sex: 01=男, 02=女
height: (身高*10 - 1000) / 5
age: 十六进制(<18 取 18)
秤开始发 0x39 (实时数据)
→ App 回 F1 03 39 (ACK 每包)
测量完成 → 秤主动断开
BOM 信息解析(握手阶段 0x3A)
当 data[11] 高 4 位 = 1 时:
version = (data[11] & 0xF0) >> 4
bomVersion = data[11] & 0x0F
year = (data[12] & 0xF0) >> 4
month = data[12] & 0x0F
modelId = (data[13] << 4) + ((data[14] & 0xF0) >> 4)
shortBom = ((data[14] & 0x0F) << 10) + (data[15] << 2) + (data[16] >> 6)
shortFactoryId = data[16] & 0x3F
4. Picooc Mini 广播协议(PICOOC-L)
Mini 等新款秤使用广播协议,无需 GATT 连接,直接在 BLE ScanRecord 中携带数据。
广播名识别
- 设备名:
"PICOOC-L" - 或 AD Structure 偏移 2-9 匹配
"PICOOC-L" - 广播版本标志:
scanRecord[18]→0x3D(V2) /0x3E(新协议)
V2 数据解析
position = scanRecord[0] + 1 // AD 结构起始偏移
byte[pos+2 ~ pos+7] = MAC 地址 (6B hex)
byte[pos+13] = Weight MSB
byte[pos+14] = Weight LSB → kg = (b13<<8 | b14) / 20
byte[pos+15] = Impedance MSB
byte[pos+16] = Impedance LSB → imp = (b15<<8 | b16) / 10
byte[pos+17] = WeightLB MSB
byte[pos+18] = WeightLB LSB → lb = (b17<<8 | b18) / 10
byte[pos+19] = Unit (0=kg, 1=斤, 2=lb)
byte[pos+20] = Checksum (前 18B 数据校验和)
数据校验
makeCheckSum(18 字节数据 hex) == 最后一个字节 hex
5. 关键源文件(反编译参考)
| 文件 | 作用 |
|---|---|
BTBle.java | BLE 连接型协议核心实现(66KB) |
BluetoothLeService.java | BLE GATT 服务封装 |
PicoocBlueToothProfile.java | 数据解析 + BodyIndexEntity 构建 |
BTBleForBroadcasDevice.java | 广播协议核心实现(Mini 等) |
SampleGattAttributes.java | UUID 定义表 |
bleGlobalVariables.java | Quintic OTA 芯片 UUID |
BluetoothUtils.java | hex 转换工具 |
FatModelCaculate.java | 体成分算法 |
WeightMeasureController.java | 测量逻辑控制 |
6. Python 解析示例
连接型(0x39 实时数据)
def parse_picooc_39(data: bytes) -> dict:
if len(data) < 16 or data[0] != 0x39:
return None
ts = (data[2]<<24) | (data[3]<<16) | (data[4]<<8) | data[5]
weight = ((data[6]<<8) | data[7]) / 20.0
imp = ((data[8]<<8) | data[9]) / 10.0
flag = data[15] if len(data) >= 16 else 0
return {
'ts': ts, 'weight_kg': round(weight, 1),
'impedance': int(imp), 'complete': flag == 0
}广播型(PICOOC-L ScanRecord)
def parse_picooc_broadcast(scan_record: bytes) -> dict:
if len(scan_record) < 10: return None
if bytes(scan_record[2:10]) != b'PICOOC-L':
return None
pos = scan_record[0] + 1
mac = scan_record[pos+2:pos+8].hex()
dpos = pos + 13
weight = ((scan_record[dpos]<<8|scan_record[dpos+1])/20.0
if dpos+1 < len(scan_record) else 0)
imp = ((scan_record[dpos+2]<<8|scan_record[dpos+3])/10.0
if dpos+3 < len(scan_record) else 0)
return {'mac': mac, 'weight_kg': round(weight, 1), 'impedance': int(imp)}7. 备注
- App 使用 Flutter + 原生 Java 混合架构,BLE 通信在原生层
- 体成分(体脂率等)由 App 端 FatModelCaculate 算法计算,非秤端直接返回
- 体脂秤使用 Quintic 蓝牙芯片
- 反编译产物路径:
/opt/data/workspace/decompiled/picooc/ - 完整协议文档:
/opt/data/workspace/decompiled/picooc-ble-protocol.md