云计算百科
云计算领域专业知识百科平台

【BAS】规范精讲[1]: 一块电池一个实例?六种设备形态与电池热插拔上报全流程

先澄清一个容易混的点:这里的 BAS 是 Battery Service(电池服务),属于 GATT 体系下的标准服务。它的服务 UUID 是大家熟悉的 0x180F,1.1 版本在老版本只有一个电量百分比特征的基础上,把电池状态、能量、时间、健康、身份信息一整套特征补齐了。


目录

一、先立骨架:一个实例只伺候一块物理电池

二、六种部署形态:变的是物理边界,不变的是实例归属

2.1 形态一与形态二:各自独立的两套 Server

2.2 形态三:分布式设备共享一个 Server

2.3 形态四:可分离 + 分布式,热插拔问题的源头

2.4 形态五:后买的配件,实例按需开张

三、聚合组:多块电池怎么合成用户看到的一格电

四、Low 与 Critical:两级告警语义

五、读规范要先接受的几套规矩

5.1 情态动词就是法律效力

5.2 RFU 与 Prohibited:两种未知值,两种处理

5.3 需求表格与一致性

5.4 字节序:一律小端

六、电池拔插的两条完整消息流

6.1 场景一:拔下旧电池,插入一块不同的新电池

6.2 场景二:拔下的是同一块电池,又插了回来

6.3 三个工程上的坑

七、写在最后

八、测验


做过 BLE 设备的同学大概率都读过电量:搜服务、找 0x2A19、读一个 0 到 100 的字节。但真正做产品时会被一连串问题打脸:TWS 两只耳机在手机上为什么有时显示两个电池、有时三个(还带充电盒)?可拆卸笔记本的底座电池拔了,服务要不要销毁?后买的背夹电池怎么动态冒出来?这篇就围绕这些问题,把 Battery Service 的设计意图、六种典型部署形态、三条术语和热插拔消息流一次性串起来。


一、先立骨架:一个实例只伺候一块物理电池

规范对 Battery Service 的定义只有一句话,但信息密度很高:

The Battery Service exposes the Battery Level and other information for a battery (which may be composed of one or more cells) in the context of the battery's electrical connection to a device.

拆开看有三层意思。

第一,服务的描述对象是 battery,一块电池可以由一个或多个 cell(电芯)组成。也就是说,几节电芯串并联封装成一块电池包,对上只暴露一个服务实例,电芯不是单独建模的。

第二,所有信息都有一个大前提:in the context of the battery's electrical connection to a device,要结合电池与设备的电气连接关系来理解。电量是 0% 还是 100% 只是表层,这块电池此刻在不在位、谁在给谁供电、是充电还是放电。

第三,多电池就多实例,规范用词是 should:

If a device has several batteries to be monitored, then multiple instances of the Battery Service should be declared.

我个人的记忆模型是:把每个 BAS 实例想象成一块电池的专属档案袋,里面既有体检表(电量、电压、温度、循环次数),也有身份证(厂商、型号、序列号)。一块物理电池对应一个档案袋,设备上有几块电池,GATT 数据库里就挂几个档案袋。后面所有的部署图,本质上都是在回答档案袋挂在哪面墙上(哪个 GATT Server)、由谁来更新。

最简单的形态就是一部手机,一块电池,一个 GATT Server,一个服务实例,所有东西层层嵌套:

另外补两条服务声明层面的硬约束(在后面的服务定义章节给出,先记住):该服务没有任何其他 GATT 服务依赖,LE 和 BR/EDR 两种传输都可以跑;一个服务实例内部,每种特征最多只允许出现一个实例,想描述多块电池只能靠多开服务,不能在一个服务里堆两个 Battery Level。

二、六种部署形态:变的是物理边界,不变的是实例归属

规范用六张图把真实产品的形态枚举了一遍。建议按两个维度去读:物理上有几个设备、逻辑上有几个 GATT Server。

2.1 形态一与形态二:各自独立的两套 Server

下面左图是物理上完全分离的两个设备,典型代表是一对各自独立工作的左右耳塞;下图是两个可贴合(attachable)的设备,比如平板和键盘。注意它们的共同点:无论物理上贴不贴合,每个设备都保留自己的电池、自己的 BAS 实例和自己的 GATT Server。

对 Client 侧的含义是:你要分别建立两条 GATT 连接、做两次服务发现,谁也不代理谁。贴合动作不会把两台设备合并成一个 Server。

2.2 形态三:分布式设备共享一个 Server

同样是两只耳塞,换一种产品设计就完全不同:两只物理耳塞对外呈现为同一个 headset(distributed device),电池和实例仍然各有一个,但 GATT Server 只有一个,由其中一只耳塞(或充电盒/主耳)代管。

对比前一种形态可以体会到规范的分寸:Server 的边界不是由物理外壳决定的,而是由谁对外维护 GATT 数据库决定的。Client 只连一次,却能在一个 Server 里发现两个 BAS 实例。图中两只耳塞分别被标成 Aggregation Group 1 和 Group 2,这点先留个印象,第四节展开。

2.3 形态四:可分离 + 分布式,热插拔问题的源头

这是六张图里信息量最大的一张。一台可拆卸笔记本:显示屏部分有一块不可拆卸电池(Battery 1),底座部分塞了两块电池(Battery 2、3)。整机共享一个 GATT Server,但只有显示屏带蓝牙射频,所以底座那两块电池的状态是先通过设备内部链路汇总到显示屏,再由显示屏对外暴露的。

关键设计在这里:底座被拔下来之后,对应 Battery 2、3 的服务和特征并不会从 GATT 数据库里消失,而是继续保留,只是特征值会表明电池已被移除。规范在这里直接点名了:

When the base is detached, the services and characteristics for those batteries remain in the GATT database but their values will show the batteries as being removed. See Appendix A for an example message sequence illustrating the removal and insertion of a battery.

为什么不动态删服务?工程上很好理解:服务句柄是 Client 做发现、订阅、缓存的基础。如果热插拔一次就销毁重建一次句柄,Client 就得反复做服务发现,CCCD 订阅状态也全部丢失。让档案袋留在墙上、只改档案袋里的内容,是代价最小的方案。SPEC给的就是这个方案的标准时序,第六节细讲。

2.4 形态五:后买的配件,实例按需开张

最后一种形态是手机加一块后购买的背夹电池(phone battery sleeve)。手机出厂时只有一个实例;背夹接上后,手机通过一条内部的电池信息链路识别到它,再动态新建一个 BAS 实例代表背夹,图中用绿色虚线框表示这个事后开张的实例。

注意它和形态四的差别:形态四是电池物理移除但实例常驻(电池可拆卸、实例不销毁);形态五是整个配件一开始不存在,实例随配件接入而新增。这也是规范留给实现者的弹性:可拆卸电池走值表达,外挂配件走新增实例。

六张图浓缩成一张表:

形态

物理设备

GATT Server

BAS 实例

典型产品

电池不在位时

基础设备

1

1

1

手机

不涉及

物理分离

2

2

2

独立双耳塞

各管各

可贴合

2

2

2

平板+键盘

各管各

分布式

2

1

2

呈现为一个headset的双耳

值表达

可分离分布式

多部件

1

3

可拆卸笔记本

实例保留,值显示removed

外挂配件

2

1(代管)

初始1,接入后2

手机+背夹电池

配件级新增实例

Client 侧的工程结论就一句话:永远不要假设对端只有一个 0x180F,发现服务时要枚举全部实例并按句柄范围分别订阅。

三、聚合组:多块电池怎么合成用户看到的一格电

引入一个概念,Battery Aggregation Group:

A Battery Aggregation Group represents a collection of one or more batteries that provide power to a device concurrently and in parallel.

关键词是 concurrently and in parallel,同一时刻并联供电的一组电池才能进同一个聚合组。它解决的是 UI 呈现问题:系统有三块电池,用户不想看三个百分比,只想看整机还剩多少电。规范给了两个非常具体的聚合算法示例。

能量口径的算法是把同组实例的可用能量求和、可用容量也求和,再相除:

剩余百分比 = Σ Available Energy / Σ Available Battery Capacity

续航时间口径则是取同组各实例 Time until Discharged 字段的最大值,作为整机预估剩余时间——并联供电时系统能撑多久取决于最能扛的那一块,而不是平均值。

用代码表达大概是这样:

/* 同一聚合组内多个 BAS 实例的汇总结果 */
struct bat_inst {
uint16_t avail_energy; /* Available Energy, 单位 mWh */
uint16_t avail_capacity; /* Available Battery Capacity, 单位 mAh */
uint32_t time_to_discharge; /* Time until Discharged, 单位 s */
};

uint8_t agg_group_percent(const struct bat_inst *g, size_t n)
{
uint32_t sum_e = 0, sum_c = 0;
for (size_t i = 0; i < n; i++) {
sum_e += g[i].avail_energy;
sum_c += g[i].avail_capacity;
}
if (sum_c == 0)
return 0;
/* 先乘 100 再除,避免整数截断把结果吃成 0 */
return (uint8_t)((sum_e * 100u) / sum_c);
}

uint32_t agg_group_remaining_seconds(const struct bat_inst *g, size_t n)
{
uint32_t max_t = 0;
for (size_t i = 0; i < n; i++)
if (g[i].time_to_discharge > max_t)
max_t = g[i].time_to_discharge;
return max_t;
}

回看形态四那张图,底座的两块电池同属 Aggregation Group 2,因为它们同时给底座供电;显示屏的电池单独占 Group 1。两只耳塞虽然共享 Server,却分到两个组——它们不并联,各供各的。聚合关系本身会通过 Battery Information 特征里的 Battery Aggregation Group 字段告诉 Client,UI 怎么分组、要不要加总,由上层应用自己决定,规范只提供素材。

四、Low 与 Critical:两级告警语义

规范对两个词给了严格定义,做低电策略时不能混:

Low: the device will still function normally, but immediate recharge or replacement of the battery is recommended.

Critical: a load powered by the battery might not function fully or correctly.

打个比方:Low 相当于油表亮灯,车还能正常开,但规范建议你立刻去加油或换电池;Critical 则是发动机已经开始喘振,由电池供电的负载可能无法完整或正确地工作,该保存数据、降频、通知用户了。一个是功能完好的提醒态,一个是功能即将受损的失效预警,两者在 Battery Critical Status、Battery Level Status 等特征里都有对应比特位,SPEC 1.1 版本正是为了把这种语义标准化,而不是让每家厂商自定义一个低电阈值。

五、读规范要先接受的几套规矩

5.1 情态动词就是法律效力

蓝牙规范用 shall、must、will、should、may、can 这几个词表达不同强度的要求,读的时候必须逐字较真:

用词

含义

落地理解

shall

is required to

强制要求,不合规就是不合格

must

强制要求的必然推论,或不争的事实

没有商量余地

will

it is true that

事实陈述

should

is recommended that

推荐,不做要有理由

may

is permitted to

允许的可选项

can

is able to

能力/因果描述

前面那句多电池 should 多实例就是推荐而非强制;而字节序、服务 UUID 用的是 shall,没有例外。另外 note 只是提示,不能夹带新要求,当 note 的澄清和正文要求冲突时,以要求为准。规范还明确:图和正文不一致时,文字优先,图只是 visual aid,图里画的某一种实现方式不构成对实现的限制。

5.2 RFU 与 Prohibited:两种未知值,两种处理

预留字段 RFU(Reserved for Future Use,这个缩写和全称等价)的处理分三种情形,非常容易在面试里被绕:

1. 结构体里的 RFU 字段:发送方填 0;接收方必须忽略它,尤其不能因为它非零就拒绝整个结构。

2. 取值范围里被划为 RFU 的枚举值:发送方不得使用;接收方收到应当连同所在数据结构一起拒绝,除非规范明确说该场景下要忽略无法识别的值。

3. RFU 比特位:未分配比特发 0;收到为 1 的消息时按 0 继续处理(另有规定除外)。

Prohibited 只用于枚举型字段里那些未分配的值,原文要求三连:

These values shall never be used by an implementation, and any message received that includes a Prohibited value shall be ignored and shall not be processed and shall not be responded to.

收到带 Prohibited 值的消息:忽略、不处理、不回复,注意是不响应,连错误应答都不要回。而且 Prohibited 这个词从来不缩写。一句话区分:RFU 比特收到当 0、协议尽量向前兼容;Prohibited 值收到当垃圾、整包静默丢弃。

5.3 需求表格与一致性

后面所有特性表格用 M(强制)、O(可选)、X(排除)、N/A(不适用)、C.n(条件强制)标注,条件语句紧跟在表格下方。一致性原则叫 process-mandatory:只要你宣称符合本规范,所有标 M 的能力必须按规定方式支持;你自己选了要支持的 O 和 C 项,同样必须完整做到。不能只实现一半然后宣称支持。

5.4 字节序:一律小端

All characteristics used with this service shall be transmitted with the least significant octet first (i.e., little endian).

所有特征值一律最低有效字节在前。BAS 1.1 新增的字段大量是 uint16、uint24(电压、能量、容量、时间、温度),解析时不能想当然按网络字节序处理。一段典型的解析代码:

/* 特征值缓冲区中按小端取出多字节整数 */
static uint16_t rd_u16_le(const uint8_t *p)
{
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}

static uint32_t rd_u24_le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16);
}

/* 例:解析 Battery Energy Status 中的电压与可用能量(字段前还有 flags) */
void parse_energy_status(const uint8_t *v)
{
uint16_t present_voltage_mv = rd_u16_le(v + 2); /* Present Voltage, 分辨率 1mV */
uint16_t avail_energy_mwh = rd_u16_le(v + 5); /* Available Energy, 分辨率 1mWh */
/* … */
(void)present_voltage_mv; (void)avail_energy_mwh;
}

六、电池拔插的两条完整消息流

形态四埋的钩子在这里收。规范用两张消息序列图(MSC)回答同一个问题:电池被物理拔出、再插入,空口到底发生了什么。前提设计是特征常驻 GATT 数据库,靠特征值本身表达电池在不在位:

…the characteristics remain in the GATT database but their values tell if the battery is present or not.

6.1 场景一:拔下旧电池,插入一块不同的新电池

逐步走一遍:

  • 步骤 1 到 3,Client 分别给 Battery Information、Serial Number String、Battery Energy Status 三个特征的 CCCD(Client Characteristic Configuration Descriptor)写值,使能 indication。这是订阅动作,本质是对 CCCD 句柄写入 0x0002(小端两个字节):

/* ATT Write Request: 使能某个特征的 Indication */
uint8_t att_op = 0x12; /* Write Request */
uint16_t cccd_handle = 0x0142;/* 目标特征 CCCD 的句柄,按实际发现结果填入 */
uint8_t value[2] = { 0x02, 0x00 }; /* 0x0002 = Indication,小端 */
/* notification 对应 0x0001;indication 带确认,适合电池这种关键状态 */

  • 步骤 4 是 Server 本地事件:电池被拔出,没有空口消息。

  • 步骤 5,Server 主动上报 Battery Energy Status 的变化,其中 Battery Present 位为 No,宣告电池不在位。

– 步骤 6,本地事件:一块新的电池插入。

  • 步骤 7、8,因为换了电池,身份证信息全部翻新,Battery Information 和 Serial Number String 各自上报一次变化。

  • 步骤 9,再次上报 Battery Energy Status,Battery Present 位变回 Yes。

6.2 场景二:拔下的是同一块电池,又插了回来

时序几乎一样,唯一区别在步骤 7、8 被 alt / optional 组合框包住:插回来的还是同一块电池,身份信息没变,厂商、型号、序列号都一样,这两条 indication 就可以不发;但步骤 9 不能省,在位状态从 No 翻回 Yes 必须通知 Client。

这两张图放在一起,规范的意图非常清楚:上报内容按状态差异裁剪,身份没变就不重复报身份证,在位状态永远照报。

6.3 三个工程上的坑

第一,indication 是有确认的(Server 发完要等 Client 的 Confirmation Handle Value Indication),和不需要确认的 notification 不同。电池在位状态属于关键事件,用 indication 可以保证 Client 不丢事件;但如果固件里没处理好确认超时重传,连续快速拔插可能把指示队列堵死。

第二,热插拔期间不要销毁服务、不要改句柄。Client 不需要重新做服务发现,等指示即可;如果实现成动态删服务,不仅违背附录的示例模型,还会让订阅过的 CCCD 全部失效。

第三,电池不在位时,除在位标志外的其他字段是否可读、读出什么值,要以各特征自身行为定义为准,Client 端不要把 0 电量直接显示成空电——先判 Battery Present,再看数值,否则拔出电池的瞬间 UI 可能闪出一格 0%。

七、写在最后

用一条主线把全文串起来:一块物理电池对应一个 BAS 实例(电芯不单独建模),实例挂在哪个 GATT Server 取决于设备形态,电池物理不在位时实例常驻、用值表达,身份变化才触发身份类特征上报。再配上小端字节序、RFU/Prohibited 的处理纪律和聚合组的两条求和/取最大值算法,后面十二个具体特征的行为定义读起来就只是查字典了。


八、测验

Q1:一台 BLE 设备上最多/最少有几个 Battery Service?TWS 耳机在手机上可能看到几个电池服务?

参考答案:

没有数量上限的强制规定,一块需要监控的电池对应一个实例,多电池应声明多个实例(规范用词 should),单电池设备就是一个。TWS 耳机取决于产品形态:如果左右耳各自是独立 GATT Server(各自独立连接),手机通过两条链路各看到一个;如果双耳对外呈现为一个分布式设备、共享一个 GATT Server,则在一条链路的同一个 Server 下发现两个实例;若充电盒也纳入并由同一 Server 代管,还可能出现三个实例(左、右、盒)。每台 Server 内部每种特征只允许一个实例,扩展靠多开服务而不是重复特征。

Q2:可拆卸设备的电池被拔出时,GATT 数据库中的 Battery Service 及其特征是否应当删除?Client 如何感知拔插?

参考答案:

不删除。服务和特征常驻 GATT 数据库,由特征值表达电池是否在位。Client 事先通过写 CCCD 使能 Battery Energy Status 等特征的 indication;电池拔出后 Server 指示 Battery Energy Status 变化(Battery Present: No);插入新电池时依次指示 Battery Information、Serial Number String(身份更新)和 Battery Energy Status(Present: Yes);如果插回的是同一块电池,前两条身份类指示为可选,在位状态指示必发。Client 全程无需重新发现服务。

Q3:收到字段中 RFU 比特为 1、或枚举字段取到 Prohibited 值,分别怎么处理?BAS 特征的多字节字段按什么字节序传输?

参考答案:

RFU 发送方必须置 0;接收方对 RFU 字段应忽略、不得因此拒绝结构,对 RFU 比特按 0 继续解析;若收到的是取值范围内被划为 RFU 的枚举值,则应将该对象及所在数据结构判错拒绝(规范另有忽略规定的除外)。Prohibited 值任何实现不得发送,收到后整条消息忽略、不处理、不回复,且 Prohibited 无缩写形式。BAS 所有特征值一律小端传输(least significant octet first),多字节字段按小端解析。


赞(0)
未经允许不得转载:网硕互联帮助中心 » 【BAS】规范精讲[1]: 一块电池一个实例?六种设备形态与电池热插拔上报全流程
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!