低功耗蓝牙(BLE)开发——Qt

背景知识

低功耗蓝牙比经典蓝牙复杂些,需要了解一些协议的基础知识。

此部分参考博客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个字节) + 属性权限
低功耗蓝牙(BLE)开发——Qt_第1张图片

  • 属性句柄(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),工作流程如下:

  1. 搜寻附近全部的蓝牙设备(GAP)
  2. 根据搜寻出的蓝牙设备信息,筛选出要连接的蓝牙设备进行连接(此处包括以下都是GATT)
  3. 建立连接后,去获取该蓝牙设备等services列表,根据约定好的服务uuid筛选出自己需要的服务
  4. 发现对应的服务后,根据约定好的服务下characteristic特性uuid,创建特征对象,并监听特征对象内容的变化(读Rx);或者向“写特征配置对象”写入特征生效消息(写Tx)。
  5. 关闭连接,释放资源。

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::of(&QBluetoothDeviceDiscoveryAgent::error),
            this, &BluetoothDevice::onDiscoverFinished);


//开始查找BLE设备
m_pDevDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);


  •  筛选出要连接的蓝牙设备进行连接:通过上面的查找可以获取到需要连接的外围设备的MAC地址等信息,创建QLowEnergyController进行连接;连接成功后,查找外围设备中包含的服务。
        

//因为我做的连接逻辑是只要没连接上,就不停的尝试连接,
//注意的是即使没连接上也是需要关闭连接,释放内存的。
close();

 //获取本地设备地址信息,创建低功耗控制器需要
QList 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);
#else
        m_BLEController = QLowEnergyController::createCentral(QBluetoothAddress(strAddr), localAddr);
#endif

connect(m_BLEController, &QLowEnergyController::connected, m_BLEController, &QLowEnergyController::discoverServices);
connect(m_BLEController, QOverload::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))
    {//write
        m_BLETxService = m_BLEController->createServiceObject(serviceUUID,this);
        connect(m_BLETxService, &QLowEnergyService::stateChanged, this, &BluetoothConnection::BLEC_onServiceDetailDiscovered);
        m_BLETxService->discoverDetails();
    }
    else if(serviceUUID == QBluetoothUuid(RxServerUUID))
    {//notify
        m_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(sender());
    if(newState == QLowEnergyService::ServiceDiscovered)
    {
        deleteService = true;
        const QList chars = service->characteristics();
        // delete unused service
        if(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)
            {
                // Rx
                connect(m_BLERxService, QOverload::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"));
                // Tx
                connect(m_BLETxService, QOverload::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(sender());
        QLowEnergyService::ServiceError error;
        error = service->error();
        // service->errorString() doesn't exist
        qDebug() << "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().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查找外围设备,是查找不到此外围设备的。 

 

你可能感兴趣的:(qt,开发语言)