蓝牙通信基础与GATT协议

各位同学好,我是你们的老朋友。今天咱们正式开始《美容仪APP联调与指令对接实战》的第一章。

说实话,做蓝牙开发这么多年,我见过太多人一上来就急着写代码,结果连设备都搜不到,或者连上了发指令没反应。为什么?说白了,就是没搞懂底层的通信机制。今天这一章,咱们就把BLE蓝牙低功耗协议栈的底裤扒开,重点讲讲GATT协议——这个在美容仪开发中你天天都要打交道的东西。

1.1 BLE协议栈长什么样?

先看整体。BLE协议栈分两层:Controller和Host。中间有个HCI接口隔开。

Controller负责物理层和链路层,说白了就是管怎么发无线电波、怎么组包、怎么重传。这部分一般由芯片厂家搞定,咱们APP开发者基本不用碰。

Host层才是咱们的主战场。它包含L2CAP、ATT、GATT、SM、GAP等模块。嗯,这里要特别注意GATT和GAP的区别——GAP管的是设备怎么被发现、怎么连接;GATT管的是连接之后怎么交换数据。

我个人习惯把GATT比作一个「文件系统」。你想想看,手机连上美容仪之后,总得知道怎么读写数据吧?GATT就是定义了这套读写规则。

1.2 GATT的核心结构

GATT的结构其实很简单,就三层:

  • Service(服务):相当于一个文件夹
  • Characteristic(特征):相当于文件夹里的文件
  • UUID(通用唯一标识符):相当于文件名

我在项目中遇到过不少新手,一上来就问「老师,我该用哪个UUID?」。其实UUID就是身份证号,每个Service和Characteristic都有一个唯一的UUID来标识。

重要概念:一个Service包含多个Characteristic,每个Characteristic包含一个Value和若干个Descriptor。Value就是实际的数据,Descriptor描述这个Value的属性(比如能不能读、能不能写、有没有通知)。

1.3 美容仪中的典型GATT结构

拿咱们的美容仪举例。假设设备有温度控制、射频强度和电量显示三个功能。那GATT结构大概长这样:

Service UUID Service名称 Characteristic UUID 功能 属性
0xFFE0 温度服务 0xFFE1 读取当前温度 Read / Notify
0xFFE2 射频服务 0xFFE3 设置射频强度 Write
0xFFE4 电量服务 0xFFE5 读取电量百分比 Read

你看,每个功能对应一个Service,每个Service里放一个Characteristic。APP要读温度,就找到0xFFE1这个Characteristic,然后读它的Value。

我的经验:实际项目中,很多厂商会使用自定义的16位UUID(比如0xFFE0),而不是标准的128位UUID。这样做的好处是省流量、好记忆。但要注意,自定义UUID不能和标准服务冲突。

1.4 Characteristic的四种操作模式

Characteristic有四种基本操作,咱们必须搞清楚:

  • Read:APP主动读取设备数据。比如查电量。
  • Write:APP向设备写数据。比如设置温度。
  • Notify:设备主动通知APP。比如温度变化时,设备推送给APP。
  • Indicate:类似Notify,但需要APP确认收到。

我曾经踩过一个坑:美容仪的温度变化很快,如果用Read模式,APP需要不断轮询,既费电又容易丢数据。后来改成Notify模式,设备一有变化就主动推,体验好多了。

注意:Notify和Indicate的区别在于确认机制。Notify不保证送达,Indicate保证送达但速度慢。实时性要求高的场景(比如温度监控)用Notify;关键指令(比如关机)用Indicate。

1.5 实战:如何发现Service和Characteristic

APP连上设备后,第一步就是发现Service和Characteristic。流程是这样的:

  1. 调用 discoverServices() 发现所有Service
  2. 遍历每个Service,调用 discoverCharacteristics() 发现所有Characteristic
  3. 找到目标Characteristic后,设置通知或开始读写

代码示例(Android端):

// 发现服务
bluetoothGatt.discoverServices();

// 在回调中处理
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        for (BluetoothGattService service : gatt.getServices()) {
            // 找到温度服务
            if (service.getUuid().toString().equals("0000ffe0-0000-1000-8000-00805f9b34fb")) {
                BluetoothGattCharacteristic tempChar = 
                    service.getCharacteristic(UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"));
                // 开启通知
                gatt.setCharacteristicNotification(tempChar, true);
            }
        }
    }
}

这段代码我几乎每个项目都会用到。你想想看,不管是什么蓝牙设备,只要连上了,第一步永远是发现服务。这是铁律。

1.6 避坑指南

最后分享几个我踩过的坑:

  • UUID大小写问题:Android和iOS对UUID的大小写处理不一样。我建议统一用小写,省得排查半天。
  • MTU限制:默认MTU只有23字节,实际数据只有20字节。如果要发长数据,记得先协商MTU。
  • 通知开关:开启Notify后,别忘了写CCCD(客户端特征配置描述符)。很多新手漏了这一步,导致收不到数据。

好了,第一章就讲到这里。GATT协议是蓝牙开发的基石,搞懂了它,后面写指令对接就是水到渠成的事。下一章咱们聊聊「如何设计美容仪的指令协议」,到时候我会拿出一个真实项目的协议文档给大家拆解。

记住:蓝牙开发,七分靠协议,三分靠代码。协议搞懂了,代码怎么写都顺。