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
我的习惯: 只要产品功能匹配标准 Profile,我绝不自己造轮子。标准 Profile 的兼容性测试成本低太多了。

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:

  1. HID Information (0x2A4A) — 版本号、国家码、标志位
  2. Report Map (0x2A4B) — HID 报告描述符,定义按键布局
  3. HID Control Point (0x2A4C) — 控制命令(挂起、恢复)
  4. Report (0x2A4D) — 输入/输出/功能报告,实际数据通道
  5. 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 客户端订阅后才发通知
注意: Battery Service 的 Notification 不能和 HID Report 的 Notification 共用同一个连接事件。否则电量上报会阻塞键盘输入,造成卡顿。我建议把 BAS 的 Notification 间隔设得长一些,比如 5 秒一次。

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,影响吞吐
我的经验: 数据上报用 Notify,控制命令用 Write Without Response。只有像“固件升级完成”这种关键事件才用 Indicate。你想想看,一个温度传感器每秒上报一次数据,如果用 Indicate,每次都要等手机确认,延迟和功耗都上去了。

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 的选择策略

我画了一张决策流程图,帮你快速判断该用哪种:

Profile 选择决策流程 开始设计 Profile 功能是否匹配标准 Profile? 是 否 使用标准 Profile 设计自定义 Profile 查找 SIG 规范文档 实现所有 Mandatory 特征 分配 128-bit UUID 设计 Service 粒度与属性 标准 Profile 优先,减少兼容性问题 自定义 Profile 要写详细文档

我个人的选择策略很简单:

  1. 先查 SIG 官网 — 看有没有现成的标准 Profile
  2. 能复用就复用 — 哪怕只复用一部分,也比全自定义好
  3. 自定义时参考标准 — 数据格式、属性设计都模仿标准 Profile
  4. 写文档 — 自定义 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,它仍然可以正常读取温度和湿度。这就是“兼容优先”的设计思路。

我的建议: 自定义 Profile 时,尽量在标准 Service 里加 Optional Characteristic,而不是新建一个 Service。这样客户端即使不认识你的自定义 Characteristic,也能正常使用标准功能。