背景知识
低功耗蓝牙比经典蓝牙复杂些,需要了解一些协议的基础知识。
此部分参考博客GATT Profile 简介-CSDN博客
GATT详细介绍-CSDN博客
Introduction | Introduction to Bluetooth Low Energy | Adafruit Learning System
蓝牙 (四) GATT profile-CSDN博客
关于低功耗蓝牙
简介
低功耗蓝牙(BLE),有时被称为“智能蓝牙”,是经典蓝牙的轻量级子集,作为蓝牙4.0核心规范的一部分引入。虽然与传统蓝牙有一些重叠,但BLE实际上有一个完全不同的血统,在被蓝牙技术联盟采用之前,它是由诺基亚作为一个内部项目开始的,名为“Wibree”。
支持的平台
支持蓝牙4.0和蓝牙低功耗(这是BT 4.0的一个子集)在大多数主要平台上可用,如下所列的版本:
- iOS5+ (iOS7+ preferred)
- Android 4.3+ (numerous bug fixes in 4.4+)
- Apple OS X 10.6+
- Windows 8 (XP, Vista and 7 only support Bluetooth 2.1)
- GNU/Linux Vanilla BlueZ 4.93+
关于GAP
说明
GAP(Generic Access Profile),它在用来控制设备连接和广播。外围设备发出广播,中心设备扫描接收到附近外围设备发出的广播,可选择连接。外围设备和中心设备是GAP定义的角色,一个外围设备只能连接一个中心设备,但是一个中心设备可连接多个外围设备。
设备角色及职能
GAP 给设备定义了若干角色,其中主要的两个是:外围设备(Peripheral)和中心设备(Central)。
外围设备(Peripheral):这一般就是非常小或者简单的低功耗设备,用来提供数据,并连接到一个更加相对强大的中心设备。例如小米手环。
中心设备(Central):中心设备相对比较强大,用来连接其他外围设备。例如手机等。
关于GATT
说明
GATT 的全名是 Generic Attribute Profile(通用属性协议,基本属性协议, ATT 指Attribute-属性),它定义了 两个 BLE 设备互相传输数据进行通信的方法,也就是说中心设备和外围设备通过GAP创建连接后,然后通过GATT协议进行通信。该方法用了两个概念, Service 和 Characteristic 。GATT 使用 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic以及相关的数据存储在一个简单的有索引的表中,这个索引表使用 16 位的 ID 作为表中每一项条目的索引。
通信事务
GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 Server 发起请求。需要注意的是,所有的通信事件,都是由客户端(也叫主设备,Master)发起,并且接收服务端(也叫从设备,Slave)的响应。
GATT 结构
GATT 事务是建立在嵌套的Profiles, Services 和 Characteristics之上的的,如下图所示:
Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率Profile(Heart Rate Profile)就是结合了 Heart Rate Service 和 Device Information Service。所有官方通过 GATT Profile 的列表可以从这里找到。
Service 是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。 UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,这个就可以自己随便设置。
Characteristic(特征)在 GATT 事务中的最低界别的是 Characteristic,Characteristic 是最小的逻辑数据单元,与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。你可以免费使用 Bluetooth SIG 官方定义的标准 Characteristic,使用官方定义的,可以确保 BLE 的软件和硬件能相互理解。当然,你可以自定义 Characteristic,这样的话,就只有你自己的软件和外设能够相互理解。
Characteristic(特征)其实是个集合,包含以下子元素:
- 特征声明(Characteristic Declaration)
- 特征值声明(Characteristic Value Declaration)
- 特征描述符声明(Characteristic Descriptor Declaration)
其中描述符是可选项,可能包含一个或多个描述符,也可能不包含描述符。
Characteristic(特征)是BLE 设备与外界通信的接口,当手机与BLE 设备通信,其实都是与某个具体的特征进行读写。其中读写的就是Characteristic Value(特征值)。
Attribute - 属性
规定数据按照一定规则存放,这个规则就是属性。
服务(Services)、特性(Characteristics)和描述符(Descriptors)都是属性类别,因此也就有了通用属性配置文件(Generic Attribute Profile)、属性表(Attribute Table)和属性协议(Attribute Protocol)等。具体是哪一个类别的属性,由“通用唯一标识符(Universally Unique Identifier,简称UUID)”来定义。
服务(Service)、特性(Characteristics)和描述符(Descriptors)也有层级之分:服务包括一项或多项特性;一项特性可能没有、拥有一个或拥有多个描述符
Atrribute规则结构:
属性句柄(2个字节) + 属性类型(2个字节) + 属性值(0-512个字节) + 属性权限
- 属性句柄(Attribute Handle):通过他可以找到对应属性,并用于区分不同服务中的相同属性。我理解类似是数组下标。
- 属性类型(Attribute Type):是对某个东西取一个数字代号(用uuid来代号),比如心率计,SIG就是用0x180D这个uuid来表示这个这条属性是和心率计有关SIG将uuid进行了范围规定,下面这些uuid都来标识属性类型。
0x1800~0x26FF 用于服务类 UUID
0x2700~0x27FF 用于标识计量单位
0x2800~0x28FF 用于区分属性类型
0x2900~0x29FF 用于特性描述
0x2A00~0x7FFF 用于区分特性类型
- 属性值(Attribute Value):属性值是一个 0~512 字节的数据,属性值是给上层应用层使用的,是用户“真正”要使用的数据,属性值可以有一下几类
服务通用唯一识别码(UUID) // 1800
单位
属性类型
特性描述符
特性类型
工作流程
在了解完以上的知识后,根据需求(在安卓系统平板上开发,收发数据),我需要开发的角色是中心设备(Central),工作流程如下:
- 搜寻附近全部的蓝牙设备(GAP)
- 根据搜寻出的蓝牙设备信息,筛选出要连接的蓝牙设备进行连接(此处包括以下都是GATT)
- 建立连接后,去获取该蓝牙设备等services列表,根据约定好的服务uuid筛选出自己需要的服务
- 发现对应的服务后,根据约定好的服务下characteristic特性uuid,创建特征对象,并监听特征对象内容的变化(读Rx);或者向“写特征配置对象”写入特征生效消息(写Tx)。
- 关闭连接,释放资源。
BLE中心设备实现代码
- 查找附近的BLE设备(外围设备),和经典蓝牙一样都是通过类QBluetoothDeviceDiscoveryAgent实现
m_pDevDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);connect(m_pDevDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDevice::onDeviceDiscovered);
connect(m_pDevDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothDevice::onDiscoverFinished);connect(m_pDevDiscoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error),this, &BluetoothDevice::onDiscoverFinished);//开始查找BLE设备
m_pDevDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
- 筛选出要连接的蓝牙设备进行连接:通过上面的查找可以获取到需要连接的外围设备的MAC地址等信息,创建QLowEnergyController进行连接;连接成功后,查找外围设备中包含的服务。
//因为我做的连接逻辑是只要没连接上,就不停的尝试连接,
//注意的是即使没连接上也是需要关闭连接,释放内存的。
close();//获取本地设备地址信息,创建低功耗控制器需要
QList<QBluetoothHostInfo> localdevices = QBluetoothLocalDevice::allDevices();
if(localdevices.size() == 0)
{return;
}
QBluetoothAddress localAddr = localdevices[0].address();//初始化 strAddr为需要连接的外围设备的MAC地址
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)m_BLEController = new QLowEnergyController(QBluetoothAddress(strAddr), localAddr);
#elsem_BLEController = QLowEnergyController::createCentral(QBluetoothAddress(strAddr), localAddr);
#endifconnect(m_BLEController, &QLowEnergyController::connected, m_BLEController, &QLowEnergyController::discoverServices);
connect(m_BLEController, QOverload<QLowEnergyController::Error>::of(&QLowEnergyController::error), this, &BluetoothConnection::onErrorOccurred);
connect(m_BLEController, &QLowEnergyController::serviceDiscovered, this, &BluetoothConnection::BLEC_onServiceDiscovered);//尝试连接
m_BLEController->connectToDevice();
- 从查找出的服务中根据约定好的服务uuid筛选出自己需要的服务 。TxServerUUID和RxServerUUID分别是我定义的写服务uuid宏 和 读服务uuid宏,在使用控制器创建服务对象(createServiceObject)后,还需要查询此服务的详情(discoverDetails()),也就是此服务下的特性(Characteristics)等信息。
void BluetoothConnection::BLEC_onServiceDiscovered(const QBluetoothUuid &serviceUUID)
{if(serviceUUID == QBluetoothUuid(TxServerUUID)){//writem_BLETxService = m_BLEController->createServiceObject(serviceUUID,this);connect(m_BLETxService, &QLowEnergyService::stateChanged, this, &BluetoothConnection::BLEC_onServiceDetailDiscovered);m_BLETxService->discoverDetails();}else if(serviceUUID == QBluetoothUuid(RxServerUUID)){//notifym_BLERxService = m_BLEController->createServiceObject(serviceUUID,this);connect(m_BLERxService, &QLowEnergyService::stateChanged, this, &BluetoothConnection::BLEC_onServiceDetailDiscovered);m_BLERxService->discoverDetails();}
}
- 服务在查找到详情后,可调用characteristics()获取本服务下的特征数组,通过uuid对比及属性判断后,得到合法的读特征和写特征;
读特征需要监听数据、状态改变和错误信息(中心设备监听外围设备发来的数据);
写特征需要保存下指针,便于后续的写操作(中心设备将数据发送给外围设备),
在以上的操作执行完成后,才算连接成功了。
void BluetoothConnection::BLEC_onServiceDetailDiscovered(QLowEnergyService::ServiceState newState)
{bool deleteService = true;auto service = qobject_cast<QLowEnergyService*>(sender());if(newState == QLowEnergyService::ServiceDiscovered){deleteService = true;const QList<QLowEnergyCharacteristic> chars = service->characteristics();// delete unused serviceif(service != m_BLERxService && service != m_BLETxService){deleteService = true;}else if(service == m_BLERxService){for(auto it = chars.cbegin(); it != chars.cend(); ++it){if(!m_BLERxCharacteristicValid && it->uuid() == QBluetoothUuid(RxBLECharacteristicUUID)&& it->properties().testFlag(QLowEnergyCharacteristic::Notify)){m_BLERxCharacteristicValid = true;deleteService = false;}}}else if(service == m_BLETxService){for(auto it = chars.cbegin(); it != chars.cend(); ++it){if(!m_BLETxCharacteristicValid && it->uuid() == QBluetoothUuid(TxBLECharacteristicUUID)&& it->properties().testFlag(QLowEnergyCharacteristic::Write)){m_BLETxCharacteristicValid = true;deleteService = false;}}}if(!deleteService){if(m_BLERxCharacteristicValid && m_BLETxCharacteristicValid){// Rxconnect(m_BLERxService, QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error), this, &BluetoothConnection::onErrorOccurred);connect(m_BLERxService, &QLowEnergyService::characteristicChanged, this, &BluetoothConnection::BLEC_onDataArrived);connect(m_BLERxService, &QLowEnergyService::characteristicRead, this, &BluetoothConnection::BLEC_onDataArrived);QLowEnergyDescriptor desc = m_BLERxService->characteristic(QBluetoothUuid(RxBLECharacteristicUUID)).descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);m_BLERxService->writeDescriptor(desc, QByteArray::fromHex("0100"));// Txconnect(m_BLETxService, QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error), this, &BluetoothConnection::onErrorOccurred);m_BLETxCharacteristic = m_BLETxService->characteristic(QBluetoothUuid(TxBLECharacteristicUUID));onConnected();}}else{if(service == m_BLERxService) // characteristic not found{m_BLERxService = nullptr;onDisconnect();}else if(service == m_BLETxService) // characteristic not found{m_BLETxService = nullptr;onDisconnect();}service->deleteLater();}}
}
读:
QByteArray m_buf ;void BluetoothConnection::onReadyRead()
{qDebug()<<"BluetoothConnection::onReadyRead";m_buf += m_pSocket->readAll();emit readyRead();}
写:
qint64 BluetoothConnection::write(const char *data, qint64 len)
{if(m_BLETxService == nullptr){return 0;}m_BLETxService->writeCharacteristic(m_BLETxCharacteristic, QByteArray::fromRawData(data, len));return len;
}
报错后的处理:
void BluetoothConnection::onErrorOccurred()
{if(sender() == m_BLEController){QLowEnergyController::Error error;error = m_BLEController->error();qDebug() << "BLE Central Controller Error:" << error << m_BLEController->errorString();qDebug() << "State:" << m_BLEController->state();if(error == QLowEnergyController::NoError);else{if(m_BLEController->state() == QLowEnergyController::ConnectingState){emit connectFailed(tr("Controller Error: ")+m_BLEController->errorString());}close();}}else if(sender() == m_BLERxService || sender() == m_BLETxService){QLowEnergyService* service = qobject_cast<QLowEnergyService*>(sender());QLowEnergyService::ServiceError error;error = service->error();// service->errorString() doesn't existqDebug() << "BLE Central Service Error:" << error;qDebug() << "State:" << service->state();if(error == QLowEnergyService::NoError);else if(error == QLowEnergyService::CharacteristicReadError || error == QLowEnergyService::CharacteristicWriteError || error == QLowEnergyService::DescriptorReadError || error == QLowEnergyService::DescriptorWriteError);else{if(service->state() == QLowEnergyService::DiscoveringServices)emit connectFailed(tr("Service Error: ")+ QString::fromUtf8(QMetaEnum::fromType<QLowEnergyService::ServiceError>().valueToKey(error)));close();}}
}
- 关闭释放内存:
void BluetoothConnection::close()
{if(m_BLEController == nullptr)return;if(m_BLEController->state() == QLowEnergyController::UnconnectedState){return;}m_BLERxCharacteristicValid = false;m_BLETxCharacteristicValid = false;if(m_BLERxService != nullptr){QLowEnergyDescriptor desc = m_BLERxService->characteristic(QBluetoothUuid(RxBLECharacteristicUUID)).descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);m_BLERxService->writeDescriptor(desc, QByteArray::fromHex("0000"));m_BLERxService->deleteLater();m_BLERxService = nullptr;}if(m_BLETxService != nullptr){m_BLETxService->deleteLater();m_BLETxService = nullptr;}if(m_BLEController != nullptr){m_BLEController->disconnectFromDevice();m_BLEController->deleteLater();m_BLEController = nullptr;}onDisconnect();
}
在连接成功或者关闭后,向外发送信号
void BluetoothConnection::onConnected()
{emit connected();
}void BluetoothConnection::onDisconnect()
{emit disconnected();
}
注意事项
- 记得关闭释放内存,即使连接失败:我这边的连接逻辑是自动连接的,只要没连接上就一直尝试连接。我之前在尝试连接前,没对上一次的连接进行关闭释放内存,导致报错:gatt status=133,这个就是因为连接个数是有限的,超额就会报错。
Android BLE 开发,GATT报错 status 133全面解析_gatt 133-CSD博客
- 在连接成功后,外围设备不再定时发送信息供中心设备识别,也就是在连接成功后,使用QBluetoothDeviceDiscoveryAgent查找外围设备,是查找不到此外围设备的。