3. GATT Profile 规范:标准 Profile 与自定义 Profile 的设计原则
GATT Profile 这个词,说白了就是蓝牙设备之间“怎么说话”的约定。你定义了 Service、Characteristic,还得告诉别人它们是什么、怎么用。这就是 Profile 干的事。
我刚开始做蓝牙开发时,总觉得 Profile 就是个文档,随便写写就行。直到有一次,我写了个自定义 Profile,结果手机端死活连不上——后来发现是 UUID 格式写错了。嗯,从那以后我再也不敢小看 Profile 了。
3.1 标准 Profile 概览
蓝牙 SIG 定义了一大堆标准 Profile。它们的好处是:开箱即用,手机、电脑、平板都认识。你不用自己写 App,系统自带支持。
常见的标准 Profile 有这些:
| Profile 名称 | 用途 | 核心 Service |
|---|---|---|
| HID over GATT (HOGP) | 键盘、鼠标、游戏手柄 | HID Service, Battery Service |
| Battery Service (BAS) | 电量上报 | Battery Service |
| Device Information (DIS) | 设备信息(型号、固件版本) | Device Information Service |
| Heart Rate Profile (HRP) | 心率监测 | Heart Rate Service, Device Information |
| Blood Pressure Profile (BLP) | 血压监测 | Blood Pressure Service |
| Environmental Sensing (ESP) | 温度、湿度、气压 | Environmental Sensing Service |
3.2 HID over GATT (HOGP) 深度解析
HOGP 是我用得最多的标准 Profile。为什么?因为现在无线键盘、鼠标、游戏手柄全是它。你想想看,一个蓝牙键盘要同时支持 Windows、macOS、Android、iOS,不用标准 Profile 根本不可能。
HOGP 的核心结构是这样的:
- HID Service (UUID: 0x1812) — 必须包含
- Battery Service (UUID: 0x180F) — 可选但强烈建议
- Device Information Service (UUID: 0x180A) — 可选
HID Service 里面有几个关键 Characteristic:
- HID Information (0x2A4A) — 版本号、国家码、标志位
- Report Map (0x2A4B) — HID 报告描述符,定义按键布局
- HID Control Point (0x2A4C) — 控制命令(挂起、恢复)
- Report (0x2A4D) — 输入/输出/功能报告,实际数据通道
- Protocol Mode (0x2A4E) — 协议模式(报告模式/启动模式)
避坑指南: 我曾经在 HID Report Map 里写错了一个字节,导致键盘在 macOS 上能正常用,但在 Windows 上按“A”键输出“B”。排查了整整两天,最后发现是 Report ID 的偏移量算错了。所以,HID 报告描述符一定要用工具验证,别手写。
3.3 Battery Service (BAS) 实现要点
Battery Service 看起来简单,就一个 Characteristic。但我在项目中踩过坑,分享给你。
BAS 的结构:
- Service UUID: 0x180F
- Battery Level (0x2A19): 0-100%,单位 1%
- 可选: Battery Power State (0x2A1A) — 充电状态、电源状态
实现时要注意几点:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 上报间隔 | ≥ 1 秒 | 太频繁会耗电,手机端也会拒绝 |
| 电量变化阈值 | ≥ 5% | 低于 5% 的变化不通知,避免频繁唤醒 |
| 初始值 | 50% 或实际值 | 不要默认 0%,否则手机显示没电 |
| Notification 配置 | 必须支持 CCCD | 客户端订阅后才发通知 |
3.4 自定义 Profile 设计原则
标准 Profile 不够用时,就得自己设计。但自定义 Profile 不是随便写几个 Characteristic 就完事了。我总结了几个原则:
3.4.1 UUID 分配
自定义 Service 和 Characteristic 必须使用 128-bit UUID。别用 16-bit UUID,那是 SIG 保留的。
UUID 生成建议:
- 用在线工具生成随机 UUID(比如 uuidgenerator.net)
- 固定一个 Base UUID,只改最后几位
- 例如:
0000XXXX-1234-5678-ABCD-123456789ABC - XXXX 从 0001 开始递增,方便管理
3.4.2 Service 粒度
一个 Service 只做一件事。别把温度、湿度、气压、加速度计全塞进一个 Service 里。
我习惯这样拆分:
- Temperature Service — 温度相关
- Humidity Service — 湿度相关
- Motion Service — 加速度计、陀螺仪
为什么?因为客户端可能只关心温度,它不需要订阅湿度或加速度的数据。Service 拆得细,功耗和带宽都能优化。
3.4.3 Characteristic 属性设计
每个 Characteristic 的属性要仔细想清楚:
| 属性 | 适用场景 | 我的建议 |
|---|---|---|
| Read | 静态数据(设备信息、配置) | 必须支持,客户端需要轮询 |
| Write | 控制命令、配置写入 | 尽量用 Write Without Response,减少交互 |
| Notify | 周期性数据、事件触发 | 优先使用,比 Indicate 省电 |
| Indicate | 重要事件、需要确认 | 少用,每次都要等 ACK,影响吞吐 |
3.4.4 数据格式设计
Characteristic 的数据格式要简单、可扩展。我推荐用 IEEE 标准格式:
- 温度:IEEE 11073-20601 格式,SFLOAT(16-bit)
- 电量:uint8,0-100%
- 时间:Unix 时间戳,uint32
- 字符串:UTF-8,长度前缀
举个例子,一个自定义温度 Service 的 Characteristic 定义:
// Temperature Measurement Characteristic
// UUID: 00000001-1234-5678-ABCD-123456789ABC
// 属性: Notify, Read
// 格式:
// Byte 0: 标志位 (bit0: 温度单位, 0=摄氏, 1=华氏)
// Byte 1-2: 温度值 (IEEE 11073 SFLOAT)
// Byte 3-6: 时间戳 (Unix 时间戳, 可选)
typedef struct {
uint8_t flags; // 标志位
int16_t temperature; // SFLOAT 格式
uint32_t timestamp; // 可选
} temp_measurement_t;
3.5 标准 Profile 与自定义 Profile 的选择策略
我画了一张决策流程图,帮你快速判断该用哪种:
我个人的选择策略很简单:
- 先查 SIG 官网 — 看有没有现成的标准 Profile
- 能复用就复用 — 哪怕只复用一部分,也比全自定义好
- 自定义时参考标准 — 数据格式、属性设计都模仿标准 Profile
- 写文档 — 自定义 Profile 一定要写清楚每个 Characteristic 的用途、格式、取值范围
核心原则: 标准 Profile 是经过 SIG 认证的,兼容性有保障。自定义 Profile 虽然灵活,但每个客户端都要单独适配。除非你的产品有特殊需求,否则别给自己找麻烦。
3.6 实战:一个自定义温湿度 Profile
最后,我分享一个实际项目中的例子。一个温湿度传感器,需要上报温度和湿度,同时支持配置上报间隔。
Profile 结构:
Service: Environmental Sensing (0x181A)
├── Temperature (0x2A6E) — Notify, Read
│ └── 格式: IEEE 11073 SFLOAT
├── Humidity (0x2A6F) — Notify, Read
│ └── 格式: IEEE 11073 SFLOAT
└── Measurement Interval (0x2A21) — Write, Read
└── 格式: uint16, 单位秒
这里我复用了标准 Environmental Sensing Service,但加了一个自定义的 Measurement Interval Characteristic。为什么?因为标准 Service 没有定义配置上报间隔的 Characteristic,我得自己加。
但注意,我加的是 Optional Characteristic,不是改标准 Service 的定义。客户端如果不知道这个 Characteristic,它仍然可以正常读取温度和湿度。这就是“兼容优先”的设计思路。