Cleer Arc5耳机Battery Level电量特征值读取
你有没有试过打开手机蓝牙,连上耳机,却只能看到“已连接”四个字,完全不知道左耳还剩多少电?充电盒还有没有电?右耳是不是正在偷偷充电?😅
这事儿吧,说小不小——毕竟谁也不想关键时刻掉链子;说大也不大,毕竟厂商APP一般都能搞定。但如果你是个开发者,想做个自己的耳机状态面板,或者搞点自动化联动(比如耳机电量低了就让灯闪一下),那问题就来了: 这些数据到底藏在哪?怎么拿?
今天咱们就来“开盒”Cleer Arc5这款热门开放式耳机,看看它的BLE世界里,电池信息到底是怎么传输的。重点不是“能不能读”,而是“ 它怎么设计的?为什么有些工具读不全?我们该怎么正确获取? ”
先说结论:
✅ Cleer Arc5 确实用了标准的 BLE
Battery Level 特征值(
0x2A19
)
⚠️ 但它只代表
主耳(通常是右耳)的电量
❌ 想知道左耳和充电盒?得去翻它的
私有服务
——别指望一个标准特征值包打天下。
这就引出了一个很典型的现实: 标准化 ≠ 完整性 。很多设备表面上遵循规范,实际上关键信息都藏在自家的“黑盒服务”里。而搞清楚这一点,才是逆向分析或第三方集成的关键突破口。
蓝牙里的“电量上报”其实有一套官方标准,来自蓝牙SIG组织定义的
GATT 协议
。其中有个叫
Battery Service
的服务,UUID 是
0x180F
,下面挂一个叫
Battery Level
的特征值,UUID 是
0x2A19
,数据类型是 uint8,取值范围 0–100,单位是百分比。
听起来挺简单的对吧?那为啥实际用起来总感觉“差点意思”?
因为真实世界太复杂了。TWS 耳机可不是单一设备,它至少包含三个电源单元:左耳、右耳、充电盒。而标准 Battery Service 设计初衷是给“单体设备”用的,比如手环、鼠标、键盘。所以当你面对一个多部件系统时,厂商就得自己想办法扩展。
Cleer Arc5 的做法很典型:
👉 只在
主耳(右耳)
上暴露标准
0x180F
服务
👉 左耳和充电盒的信息不通过标准路径传递
👉 所有电量状态汇总在一个
厂商自定义服务
中统一下发
也就是说,你在 nRF Connect 这类通用工具里看到的那个
0x2A19
返回的数值,只是右耳自己的电量。你以为看到了全部?天真了 😏
我用 Wireshark + BLE sniffer 抓了一波包,确认了 Cleer Arc5 固件 V1.2.3 的行为:
-
广播包里确实包含了
Complete Local Name: Cleer Arc5 -
连接后服务发现阶段,能明确看到
0000180f-0000-1000-8000-00805f9b34fb这个服务存在 -
其下的
00002a19-0000-1000-8000-00805f9b34fb特征值支持 Read 和 Notify -
读取返回的是一个 byte,例如
75→ 表示右耳 75% -
同时存在一个私有服务,UUID 前缀为
f6d70f5c-...,里面有个特征值会返回 4 字节结构化数据:
struct {
uint8_t right_battery; // 右耳电量 %
uint8_t left_battery; // 左耳电量 %
uint8_t case_battery; // 充电盒电量 %
uint8_t charging_status; // 位字段标识各部分是否在充电
};
这才是真正的“全家福”电量报告!👏
所以如果你想开发一个真正完整的耳机电量监控应用,光读
0x2A19
是远远不够的。必须同时监听这个私有特征值,并解析它的二进制格式。
那代码该怎么写呢?下面是一个 Android Kotlin 示例,展示如何安全地读取标准 Battery Level:
class BatteryLevelReader(private val bluetoothGatt: BluetoothGatt) {
fun readBatteryLevel() {
val batteryService = bluetoothGatt.getService(UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb"))
?: run {
Log.w("BLE", "Battery Service not found")
return
}
val batteryLevelChar = batteryService.getCharacteristic(UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb"))
?: run {
Log.w("BLE", "Battery Level characteristic not found")
return
}
if (!bluetoothGatt.readCharacteristic(batteryLevelChar)) {
Log.e("BLE", "Failed to initiate read")
} else {
Log.d("BLE", "Reading main ear battery...")
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS && characteristic.uuid.toString().endsWith("2A19")) {
val level = characteristic.value[0].toInt() and 0xFF
Log.i("Battery", "Main Ear Battery: $level%") // e.g., 78%
}
}
}
}
💡 小贴士:Java 中 byte 是有符号的,直接转 int 会出问题(比如 255 变成 -1)。所以要用
and 0xFF转成无符号整数。
如果你想让它自动推送更新(而不是手动轮询),记得开启通知:
// 启用通知
bluetoothGatt.setCharacteristicNotification(batteryLevelChar, true)
// 获取 CCCD 描述符并写入启用值
val cccd = batteryLevelChar.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
bluetoothGatt.writeDescriptor(cccd)
一旦设置成功,只要耳机电量变化(比如插上充电),手机就会收到一条 GATT 通知,无需频繁查询,省电又实时⚡️。
那么问题来了:为什么有些第三方 APP 显示不了完整电量?比如 Tasker、Home Assistant 插件、甚至某些开源 BLE 工具?
答案很简单:它们大多只识别标准服务。看到
0x180F
就高兴地读
0x2A19
,以为万事大吉。结果拿到的只是一个局部数据,根本不知道还有个私有服务藏着全家桶信息。
要解决这个问题,你需要:
- 抓包分析 :用 nRF Sniffer 或 Wireshark 记录真实通信过程
-
定位私有服务
:找出那个非标准 UUID 的服务(比如
f6d70f5c-...) - 逆向数据结构 :观察特征值返回的数据模式,推测字段含义
- 编写定制逻辑 :在你的 APP 里主动订阅并解析该特征值
举个例子,假设你发现某个特征值总是返回类似这样的 hex 数据:
4B 3F 64 05
对应:
-
0x4B = 75
→ 右耳 75%
-
0x3F = 63
→ 左耳 63%
-
0x64 = 100
→ 充电盒满电
-
0x05 = b00000101
→ 右耳和充电盒正在充电
那你就可以写出对应的解析函数:
fun parseBatteryPacket(data: ByteArray): BatteryStatus? {
if (data.size < 4) return null
return BatteryStatus(
right = data[0].toInt() and 0xFF,
left = data[1].toInt() and 0xFF,
case = data[2].toInt() and 0xFF,
isRightCharging = (data[3] and 0x01) != 0,
isLeftCharging = (data[3] shr 1 and 0x01) != 0,
isCaseCharging = (data[3] shr 2 and 0x01) != 0
)
}
这才是真正意义上的“全状态监控”🎯。
从架构上看,Cleer Arc5 的 BLE 模型其实是典型的 星型拓扑 :
[智能手机]
│
↓ BLE
[主耳(右耳)]
├─→ [左耳] (专有无线协议或 I²C)
└─→ [充电盒](感应触点 + 私有通信)
主耳既是音频处理中心,也是 BLE 通信网关。所有外部交互都要经过它转发。这也解释了为什么左耳本身不广播任何服务——它压根就不对外说话。
这种设计的好处很明显:
- 减少多设备并发连接带来的干扰
- 统一电源管理和通信调度
- 节省功耗,延长续航
但也带来一个问题: 单点依赖 。一旦主耳断连,整个系统的状态反馈都会中断,哪怕左耳还在工作。
所以,在做第三方集成时,有几个最佳实践建议送给你:
| 项目 | 实践建议 |
|---|---|
| 连接稳定性 | 加入自动重连机制,避免短暂信号丢失导致监控失效 |
| 权限适配 |
Android 12+ 需声明
BLUETOOTH_CONNECT
权限,并动态申请
|
| 数据缓存 | 本地保存最后一次电量快照,离线也能显示历史值 |
| 用户体验 | 当电量低于 10% 时触发本地通知或震动提醒 |
| 多型号兼容 | 注意不同代际产品(如 Arc3/Arc5/Arc Spark)可能服务结构不同,需动态适配 |
最后聊聊更大的图景。现在越来越多的耳机不再只是“听音乐的工具”,而是逐渐演变为 个人健康传感器、语音助手入口、甚至是智能家居控制器 。
想象一下:
🎧 耳机检测到你心率异常 → 自动暂停运动播放列表
🔋 电量低于 20% → 触发智能插座给充电座供电
🗣️ 摘下耳机 → 房间灯光自动调亮
这些场景的背后,都是基于 BLE 提供的基础状态信息流。而电池状态,正是最基础也最关键的“健康指标”之一。
未来随着 LE Audio 和 Matter over Thread 的普及,耳机可能会作为边缘节点接入更广泛的 IoT 生态。理解像
Battery Level
这样的 GATT 基础组件,不仅是调试需要,更是构建下一代音频互联体验的起点。
技术从来不是孤立存在的。一个小小的
0x2A19
特征值背后,藏着协议规范、硬件设计、用户体验之间的微妙平衡。而我们的任务,就是拨开表象,看清数据是如何流动的,然后把它变成更有价值的东西。
下次当你看到“右耳电量 80%”这几个字时,不妨多问一句:那左耳呢?充电盒呢?它们是怎么知道的?又是谁在告诉它们?🤔💡
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
Cleer Arc5耳机电量特征值读取解析

被折叠的 条评论
为什么被折叠?



