【QT】QtBluetooth 低功耗蓝牙BLE 笔记

发布于:2025-02-22 ⋅ 阅读:(9) ⋅ 点赞:(0)

前言, 最近发现笔记本的蓝牙可以被qt调用, 然后直接连接蓝牙模块, 不一定非要手机蓝牙app或是另一个蓝牙模块转usb.

1.环境要求

注意,因为电脑环境/版本等原因,你可能会遇到很多文本没提到的问题,不要慌,csdn查一下就好.我也会把配置过程中遇到的典型问题列出来.

  • PC电脑, 系统win10,
  • Qt Creadtor 4.10.0
  • Qt 5.13.1
  • 起初只有电脑只有 MinGw 7.3.0 32/64 要用 QtBluetooth 库必须使用 MSVC2017 32/64 (或其它 MSVC 版本);

2.安装MSVC

参考笔记: QT5.13.1-安装MSVC2017-Windows
参考笔记: 关于已经安装了QT再添加MSVC2017
参考笔记: QT5构建套件检测不到MSVC2017解决方法
下载 Visual Studio Installer 安装 MSVC : Windows 如何仅安装 MSVC 而不安装 Visual Studio
下载 Windows SDK 和模拟器存档 安装 CDB : 为Qt creator安装CDB调试器
关于解决Qt Creator CDB调试卡死这件事

  • 以上是参考笔记, 主要流程就是:

2.1 安装 MSVC 编译器

  • 注意需要使用 Visual Studio Installer , 在单个组件包内搜索 MSVC v140MSVC v141 安装框内的所有东西.注意是所有东西! 有一些笔记会说只需要安装部分即可,但是我没成功,折腾半天后才发现安装全部才可以.

在这里插入图片描述

  • 注意如果这一步安装成功后是可以在 Qt->选项->Kits 界面看到如下内容的, 点击添加也能看到 MSVC 选项.请确保也有如下内容且没有显示黄色感叹号才进行下一步.

在这里插入图片描述

在这里插入图片描述

  • 然后添加特定的编译链参数,确保一致 CC++ 都需要, 32位64位 都需要.
  • 确保参数一样,我也是跟着别人教程做的,这样最后能用,n_n

在这里插入图片描述
在这里插入图片描述

  • 添加完毕就可以在 构建套件(Kit) 中选择了; 就算完成一半了.

在这里插入图片描述

2.2.安装 SDK CDB 调试器

不正确的笔记: qtcreator下断点调试时卡住?
正确的笔记: 下载 Windows SDK 和模拟器存档 安装 CDB : 为Qt creator安装CDB调试器
关于解决Qt Creator CDB调试卡死这件事

  • 大部分教程都是教安装 Windows 10 SDK 版本 2104 (10.0.20348.0). 但是安装后我的工程不能单步调试, 我看有些是建议改电脑的语言格式, 效果不好.会把很多软件搞乱码.
  • 我又看到有人是建议使用 Windows 8.1 SDK 就可以解决问题了.确实如此.
  • 另外有些笔记是建议在 Visual Studio Installer 搜索安装, 我这边尝试没有效果, 最后还是得从网站下载 SDK 安装器.
  • 注意: 最后为了以防万一请到电脑的系统环境中添加路径.Path. 或者后面遇到问题时大多都是路径缺失导致的, 到时候再加.
  • 安装成功后能在Qt界面看到如下内容,

在这里插入图片描述
在这里插入图片描述

  • 至此就算完成环境的构建了.按理只需要设置这3个内容就可以正常使用; Compiler C , Compiler C++ , Debugger .

2.3.切换 MSVC 移植工程

参考笔记: QT编译出错:'rc’不是内部或外部命令,也不是可运行的程序 或批处理文件。
参考笔记: 【问题集锦·亲测有效!!】QT <5.14.2> mingw项目 转 MSVC编译报错 + 通过VS2022编译【一站式解决方案】
参考笔记: Qt报vcvarsall.bat x86_amd: The command “C:\WINDOWS\system32\cmd.exe“ could not be started.

  • MinGW 转为 MSVC 工程时, 大概率会出现编译错误; 因为某些编译规则不一样. 根据报错信息逐一修改即可.

3.了解蓝牙

在蓝牙低功耗(BLE)开发中,服务(Service)、特征(Characteristic)和 描述符(Descriptor) (由DS-R1生成)
Qt开发简易蓝牙调试助手(低功耗蓝牙)
QT开发低功耗蓝牙BLE连接ECB02模块进行数据收发
QT 低功耗蓝牙 PC端
Windows上用QT开发BLE(Bluetooth low energy)程序,及一个坑的填充
QBluetoothSocket
吐槽: 一开始不知道蓝牙区分低功耗蓝牙和经典蓝牙,还有部分是安卓api和linux的,虽然网上资料很多,但是注意区分.

  • 我要做的是用Qt编写一个win7/10/11平台的上位机软件, 使用电脑自带的蓝牙功能搜索蓝牙并
    连接, 进行数据交换.

在这里插入图片描述

  • 蓝牙貌似分为经典蓝牙和低功耗蓝牙,我需要连接的是低功耗蓝牙 BLE . 代码也是针对 BLE 才有效的.
  • 在开始写代码之前, 还需要了解蓝牙的连接流程:
  1. 确定支持蓝牙功能, 并搜索蓝牙设备 (BLE);
  2. 连接蓝牙, 连接成功后发现支持的服务 (Service);
  3. 选择服务, 发现支持的特征 (Characteristic);
  4. 根据需要修改特征的描述符 (Descriptor);

4.创建蓝牙类

  • 方便打包调用, 第一件事就是创建 .c .h , 然后在 .h 头文件内一次性定义类的内容, 方便写 .c .
#ifndef BLUETOOTHPORT_H
#define BLUETOOTHPORT_H

#include <QObject>
#include <QString>
#include <QDebug>

#include <QLowEnergyController> //蓝牙控制器对象
#include <QLowEnergyService>
#include <QLowEnergyCharacteristic>
#include <QLowEnergyConnectionParameters>

#include <QtBluetooth/qtbluetoothglobal.h>
#include <QtBluetooth/qbluetoothlocaldevice.h>
#include <QtBluetooth/qbluetoothsocket.h>
#include <QtBluetooth/qbluetoothservicediscoveryagent.h>
#include <QtBluetooth/qbluetoothaddress.h>

// 类定义
class BluetoothPort : public QObject
{
    Q_OBJECT // 添加 Q_OBJECT 宏, 如果类中有自定义信号signals就必须要有, 添加后要重新构建编译
public : // 公开的 类里面 和 类外面可以直接访问

    BluetoothPort(QObject *parent = nullptr); // 构造函数
    ~BluetoothPort(void); // 析构函数

    bool deviceSupport(void); // 判断蓝牙是否可用
    void deviceDiscovery(int ms_timeout = 15000); // 开始搜索蓝牙
    QList<QList<QString>> deviceDiscoveryStop(void); // 停止搜索蓝牙

    QList<QString> deviceConnect(const QString &deviceAddStr, int ms_timeout = 5000); // 连接低功耗蓝牙对象 并 搜索支持的服务(Service)UUID
    void deviceDisconnect(void); // 断开连接低功耗蓝牙对象
    bool deviceState(void); // 获取控制器对象状态

    QList<QList<QString>> serviceConnect(QString serviceUUIDStr, int ms_timeout = 5000); // 连接服务(Service)UUID并搜索支持的特征(Characteristic)UUID
    void serviceDisconnect(void); // 断开服务对象
    bool serviceState(void); // 获取服务对象状态

    QByteArray rwCharacteristics(QByteArray arr = QByteArray(), // 修改写入特征值 -> 内含 更新读写 特征值(Characteristic) 和 读写 描述符(Descriptor)
                                 int ms_timeout = 0, // 等待读取特征值
                                 QString readCharacteristicStr = "", // 指定读取特征值
                                 QString writeCharacteristicStr = ""); // 指定写入特征值

private: // 私有的 只允许在类里面访问
    bool qDebug_ok; // 打印内容

    QBluetoothLocalDevice *localDevice; // 对本地设备进行操作,比如进行设备的打开,设备的关闭等等
    QBluetoothDeviceDiscoveryAgent *discoveryAgent; // 用来对周围蓝牙进行搜寻
    QLowEnergyController *lowEnergyController; // 低功耗蓝牙控制器对象
    QLowEnergyService *lowEnergyService; //低功耗蓝牙服务对象

    QList<QBluetoothDeviceInfo> listDeviceInfo; // 蓝牙对象列表
    QList<QBluetoothUuid> listServicesUuid; // 服务(Service)UUID列表
    QList<QLowEnergyCharacteristic> listLowEnergyCharacteristicRead; // 特征(Characteristic)UUID列表
    QList<QLowEnergyCharacteristic> listLowEnergyCharacteristicWrite; // 特征(Characteristic)UUID列表

    QByteArray readArray; // 读取数组
    QString lowEnergyCharacteristicRead; // 指定读取 特征(Characteristic)UUID

protected: // 保护的 只允许在本类 和 子类中访问

signals:
    void deviceDiscovered(QString name, QString address); // 发现设备时产生的信号
};

// 全局变量声明
extern BluetoothPort *bluetooth_pc;


#endif // BLUETOOTHPORT_H

5.构造函数和析构函数

  • 使用指针的方式创建实例, 供其他地方调用.
  • 构造函数中需要初始化几个变量指针, localDevicediscoveryAgent是不需要变更的,所以一开始就创建.lowEnergyControllerlowEnergyService是根据所选蓝牙和服务创建的,初始状态要给空指针.否则后面程序删除指针时会卡死.
  • 析构函数貌似不写也没关系,会指针自己删除.
#include "bluetoothport.h"

#include <QTime>
#include <QTimer>

#include <QCoreApplication>
#include <QElapsedTimer>

BluetoothPort *bluetooth_pc = new BluetoothPort();
#define BLE_SLOT_DELAY_CONN_MS (500) // 局部宏定义, 限定延时启动部分蓝牙功能

// ====================================================================================== //

// 构造函数
BluetoothPort::BluetoothPort(QObject *parent) : QObject(parent)
{
    localDevice = new QBluetoothLocalDevice(); // 本地操作
    discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); // 用来发现扫描周围设备
    lowEnergyController = nullptr; // 用来连接蓝牙
    lowEnergyService = nullptr; // 创建服务


    qDebug_ok = false;
    if (qDebug_ok) qDebug() << "执行 BluetoothPort 构造函数";
}

// 析构函数
BluetoothPort::~BluetoothPort(void)
{
    // 顺序不能乱, 必须是先控制器对象再是服务对象
    if (lowEnergyController)
        delete lowEnergyController; // 低功耗蓝牙控制器对象
    if (lowEnergyService)
        delete lowEnergyService; //低功耗蓝牙服务对象

    // lowEnergyController = nullptr;
    // lowEnergyService = nullptr;

    listDeviceInfo.clear(); // 蓝牙对象列表
    listServicesUuid.clear(); // 服务(Service)UUID列表
    listLowEnergyCharacteristicRead.clear(); // 特征(Characteristic)UUID列表
    listLowEnergyCharacteristicWrite.clear(); // 特征(Characteristic)UUID列表
    readArray.clear(); // 读取数组

    if (qDebug_ok) qDebug() << "执行 BluetoothPort 析构函数";
}

5.搜索&连接蓝牙

  • 判断蓝牙是否可用的代码貌似没有用, 即使电脑没有打开蓝牙依旧会判断通过执行代码,并且会弹出报错窗口.
  • 搜索蓝牙的代码貌似只是查询电脑已经找到的蓝牙. 比如:在win11的电脑上搜索很慢甚至没有,可以主动点击win11的蓝牙搜索,这时程序才陆续查找到设备.
  • 注意找到蓝牙后需要筛选, 排除非BLE的蓝牙和没有名字的蓝牙;发现蓝牙后会调用信号deviceDiscovered,供外部使用.
  • 因为搜索蓝牙是一个比较长时间的过程, 所以还打包了一个手动停止搜索的函数.同时一次性返回搜索到的蓝牙名字和地址的字符串列表.
// 判断蓝牙是否可用
bool BluetoothPort::deviceSupport(void)
{
    // 保护措施
    if (!localDevice) {
        if (qDebug_ok) qDebug() << "localDevice == nullptr";
        return false;
    }

    // 清除所有信号
    disconnect(localDevice, nullptr, nullptr, nullptr);

    // 创建并连接到其信号
    connect(localDevice, &QBluetoothLocalDevice::deviceConnected, this, [this](const QBluetoothAddress &address){
        if (qDebug_ok) qDebug() << "localDevice:deviceConnected:当本地设备与具有地址的远程设备建立连接时:" << address; });
    connect(localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, [this](const QBluetoothAddress &address){
        if (qDebug_ok) qDebug() << "localDevice:deviceDisconnected:当本地设备与具有地址的远程蓝牙设备断开连接时:" << address; });
    connect(localDevice, &QBluetoothLocalDevice::error, this, [this](QBluetoothLocalDevice::Error error){
        if (qDebug_ok) qDebug() << "localDevice:error:配对时出现异常错误时发出信号:" << error; });
    connect(localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, [this](QBluetoothLocalDevice::HostMode state){
        if (qDebug_ok) qDebug() << "localDevice:hostModeStateChanged:主机的状态已转换为不同的HostMode:" << state; });
    connect(localDevice, &QBluetoothLocalDevice::pairingDisplayConfirmation, this, [this](const QBluetoothAddress &address, QString pin){
        if (qDebug_ok) qDebug() << "localDevice:pairingDisplayConfirmation:在收到 pairingDisplayConfirmation() 后调用:" << address << pin; });
    connect(localDevice, &QBluetoothLocalDevice::pairingDisplayPinCode, this, [this](const QBluetoothAddress &address, QString pin){
        if (qDebug_ok) qDebug() << "localDevice:pairingDisplayPinCode:通过调用 requestPairing() 进行配对请求时发出:" << address << pin; });
    connect(localDevice, &QBluetoothLocalDevice::pairingFinished, this, [this](const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing){
        if (qDebug_ok) qDebug() << "localDevice:pairingFinished:已完成与地址的配对或取消配对:" << address << pairing; });

    // 如果 QBluetoothLocalDevice 表示可用的本地蓝牙设备, 则返回true, 否则返回false,
    if (!(localDevice->isValid())) {
        if (qDebug_ok) qDebug() << "蓝牙在此设备上不可用";
        return false; // 返回失败
    }

    // 尝试打开蓝牙
    localDevice->powerOn();

    // 读取本地设备名称 (可能为空或未设置)
    if (localDevice->name().size() != 0) {
        if (qDebug_ok) qDebug() << "本地设备名称:" << localDevice->name();
    }

    // 尝试将设备设置为可发现模式 // 可选地,连接到主机ModeChanged信号,以便在模式更改时收到通知
    if (localDevice->hostMode() != QBluetoothLocalDevice::HostDiscoverable) {
        localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable);
        if (qDebug_ok) qDebug() << "设备处于可发现模式";
    }

    // 获取已连接的设备
    QList<QBluetoothAddress> remotes = localDevice->connectedDevices();
    if (remotes.size()) {
        if (qDebug_ok) qDebug() << "已连接设备: " << remotes.size();
        for (const QBluetoothAddress &address : remotes) {
            if (qDebug_ok) qDebug() << " - " << address.toString();
        }
    }

    if (qDebug_ok) qDebug() << "蓝牙在此设备上已开启";
    return true; // 返回成功
}

// 开始搜索蓝牙
void BluetoothPort::deviceDiscovery(int ms_timeout)
{
    // 保护措施
    if (!discoveryAgent) {
        if (qDebug_ok) qDebug() << "discoveryAgent == nullptr";
        return;
    }

    // 停止发现
    discoveryAgent->stop();

    // 清空列表
    listDeviceInfo.clear();

    // 关闭所有信号
    disconnect(discoveryAgent, nullptr, nullptr, nullptr);

    // 创建并连接到其信号
    connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, [this](const QBluetoothDeviceInfo &info){
        if (qDebug_ok) qDebug() << "discoveryAgent:deviceDiscovered:找到新设备:" << info.name()
                                << "; 地址:" << info.address().toString()
                                << "; 设备类型:" << info.coreConfigurations();
        if (info.name().toUpper().contains(info.address().toString()) == false) {
            listDeviceInfo.append(info); // 只获取有效ID的设备
            emit deviceDiscovered(info.name(), info.address().toString()); // 发送信号量
        }
    });
    connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, this, [this](const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields){
        if (qDebug_ok) qDebug() << "discoveryAgent:deviceUpdated:信息已更新:" << info.name()
                                << "; 地址:" << info.address().toString()
                                << "; 更新数据:" << updatedFields
                                << "; rssi:" << info.rssi()
                                << "; 制造商数据:" << info.manufacturerData();
    });
    connect(discoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error), [this](QBluetoothDeviceDiscoveryAgent::Error error){
        if (qDebug_ok) qDebug() << "discoveryAgent:error:发生错误:" << error; });
    connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, [this](void){
        if (qDebug_ok) qDebug() << "discoveryAgent:finished:蓝牙设备发现完成"; });
    connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, [this](void){
        if (qDebug_ok) qDebug() << "discoveryAgent:QBluetoothDeviceDiscoveryAgent:中止设备发现"; });

    // 设置低功耗设备发现超时时间
    if (qDebug_ok) qDebug() << "开始搜索蓝牙, 超时" << ms_timeout << "秒.";
    discoveryAgent->setLowEnergyDiscoveryTimeout(ms_timeout); // 将蓝牙低功耗设备搜索的最大搜索时间设置为超时 (毫秒). 如果超时为0, 则查找将运行,  直到调用 stop()

    // 开始发现低功耗蓝牙设备
    discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); // 仅搜索低功耗蓝牙设备 QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods
}

// 停止搜索蓝牙
QList<QList<QString>> BluetoothPort::deviceDiscoveryStop(void)
{
    discoveryAgent->stop();

    QList<QList<QString>> ret;
    for (QBluetoothDeviceInfo &info : listDeviceInfo)
    {
        QList<QString> temp;
        temp.append(info.name());
        temp.append(info.address().toString());
        ret.append(temp);
    }
    return ret;
}

6.连接蓝牙 并 发现服务

  • 因为连接蓝牙和发现服务是一个比较快的过程,所以打包在一起并且使用非阻塞的等待延时,最后成功的话只需要返回包含服务UUID的字符串列表即可.
  • 注意,部分环境可能会出现连接蓝牙失败或搜索服务失败的情况, 根据别人分享的经验, 是不能太快调用部分函数,可以另外打包一个函数,分两次先后调用, 或是使用 QTimer::singleShot 延时调用.可以自行修改宏定义 BLE_SLOT_DELAY_CONN_MS 尝试0或是500看看.
// 连接低功耗蓝牙对象并搜索支持的服务(Service)UUID
QList<QString> BluetoothPort::deviceConnect(const QString &deviceAddStr, int ms_timeout)
{
    QList<QString> ret;

    // 根据地址码获取蓝牙句柄
    const QBluetoothDeviceInfo *deviceInfo = nullptr;
    for (int i=0; i<listDeviceInfo.size(); i++) {
        if (listDeviceInfo.at(i).address().toString() == deviceAddStr) {
            deviceInfo = &listDeviceInfo.at(i); // 遍历储存的内容查找符合的内容
            break;
        }
    }
    if (deviceInfo == nullptr) // 确保有效
        return ret;
    if (deviceInfo->coreConfigurations() != QBluetoothDeviceInfo::LowEnergyCoreConfiguration) // 确保低功耗设备
        return ret;

    // 停止发现
    discoveryAgent->stop();

    // 清空列表
    listServicesUuid.clear();

    // 删除上一个指针
    if (lowEnergyController)
        delete lowEnergyController;

    // 创建低功耗蓝牙控制器对象
    lowEnergyController = QLowEnergyController::createCentral(deviceInfo[0]);
    if (lowEnergyController == nullptr) {
        if (qDebug_ok) qDebug() << ("创建 lowEnergyController 失败");
        return ret; // 返回失败
    }

    // 关闭所有信号
    disconnect(lowEnergyController, nullptr, nullptr, nullptr);

    // 创建并连接到其信号
    connect(lowEnergyController, &QLowEnergyController::serviceDiscovered, this, [this](const QBluetoothUuid &newService){
        listServicesUuid.append(newService); // 保存找到的服务
        if (qDebug_ok) qDebug() << "lowEnergyController:serviceDiscovered:发现新服务:" << newService.toString();
    });
    connect(lowEnergyController, &QLowEnergyController::connected, this, [this](void){
        QTimer::singleShot(BLE_SLOT_DELAY_CONN_MS, this, [this](void) {
            if (qDebug_ok) qDebug() << "lowEnergyController:connected:延迟后开始搜索";
            lowEnergyController->discoverServices(); // 成功连接到设备后开始搜索服务
        });
        if (qDebug_ok) qDebug() << "lowEnergyController:connected:成功连接";
    });
    connect(lowEnergyController, &QLowEnergyController::disconnected, this, [this](void){
        // 出现断开后程序奔溃的情况,直接清空所有内容
        if (lowEnergyController)
            disconnect(lowEnergyController, nullptr, nullptr, nullptr);
        if (lowEnergyService)
            disconnect(lowEnergyService, nullptr, nullptr, nullptr);
        if (qDebug_ok) qDebug() << "lowEnergyController:disconnected:控制器与远程低功耗设备断开连接";
    });
    connect(lowEnergyController, &QLowEnergyController::connectionUpdated, this, [this](const QLowEnergyConnectionParameters &newParameters){
        if (qDebug_ok) qDebug() << "lowEnergyController:connectionUpdated:连接参数发生变化:" << newParameters.latency(); });
    connect(lowEnergyController, &QLowEnergyController::discoveryFinished, this, [this](void){
        if (qDebug_ok) qDebug() << "lowEnergyController:discoveryFinished:正在运行的服务发现完成"; });
    connect(lowEnergyController, QOverload<QLowEnergyController::Error>::of(&QLowEnergyController::error), [this](QLowEnergyController::Error newError){
        if (qDebug_ok) qDebug() << "lowEnergyController:error:发生错误:" << newError; });
    connect(lowEnergyController, &QLowEnergyController::stateChanged, this, [this](QLowEnergyController::ControllerState state) {
        if (qDebug_ok) qDebug() << "lowEnergyController:stateChanged:控制器的状态改变:" << state; });

    // 连接到远程低功耗蓝牙设备
    if (lowEnergyController->state() != QLowEnergyController::UnconnectedState) {
        if (qDebug_ok) qDebug() << "已占用,不可连接到远程低功耗蓝牙设备";
        return ret;
    }

    QTimer::singleShot(BLE_SLOT_DELAY_CONN_MS, this, [this](void) {
        if (qDebug_ok) qDebug() << "开始连接 (设备) 并扫描 (服务)"; // 经过一定延时后才执行
        lowEnergyController->connectToDevice(); // 连接到远程低功耗蓝牙设备 // 未连接时才有效
    });
    ms_timeout += BLE_SLOT_DELAY_CONN_MS*2; // 累加上

    // 等待扫描完成, 不为空才等待
    if (ms_timeout != 0) {
        QElapsedTimer timer;
        timer.start();
        while (timer.elapsed() < ms_timeout) {
            QCoreApplication::processEvents(QEventLoop::AllEvents, 50); // 它允许事件循环在调用它的位置暂时接管,处理所有挂起的事件,然后立即返回
            //QThread::yieldCurrentThread(); // 可以使用此函数来让出CPU时间片,但不推荐在长时间阻塞循环中使用

            if (deviceState() == true) {
                for (QBluetoothUuid &uuid : listServicesUuid) {
                    ret.append(uuid.toString());
                }
                return ret; // 连接成功才执行
            }
        }
    }

    deviceDisconnect(); // 上面没有成功, 这里就删除对象
    return ret; // 返回失败
}

// 断开连接低功耗蓝牙对象
void BluetoothPort::deviceDisconnect(void)
{
    // 删除上一个指针
    if (lowEnergyController) {
        lowEnergyController->disconnectFromDevice(); // 不断开直接删除貌似也没啥问题?
        disconnect(lowEnergyController, nullptr, nullptr, nullptr);
        delete lowEnergyController;
        lowEnergyController = nullptr;
    }
}

// 获取控制器对象状态
bool BluetoothPort::deviceState(void)
{
    if (lowEnergyController == nullptr)
        return false;
    if (lowEnergyController->state() != QLowEnergyController::DiscoveredState) // 连接成功才执行
        return false;

    return true;
}

7.选择服务 并 发现特征

  • 服务不同设备, 没有断开的api调用, 而且除非重新连接设备, 否则服务的特征只能发现一次, 之后即使删除服务指针重新初始化, 也不会第二次触发 stateChanged .
  • 所以推断服务对象应该用一个列表存储,但有点麻烦, 为了保持和上面的一致性, 我还是写了断开服务的函数, 不过是没有效果的.各位可以自己尝试一下, 就能理解了.
// 连接服务(Service)UUID并搜索支持的特征(Characteristic)UUID
QList<QList<QString>> BluetoothPort::serviceConnect(QString serviceUUIDStr, int ms_timeout)
{
    QList<QList<QString>> ret;

    // 遍历储存的内容查找符合的内容
    const QBluetoothUuid *serviceUUID = nullptr;
    for (int i=0; i<listServicesUuid.size(); i++) {
        if (listServicesUuid.at(i).toString() == serviceUUIDStr) {
            serviceUUID = &listServicesUuid.at(i);
            break;
        }
    }

    // 保护措施
    if (serviceUUID == nullptr)
        return ret;
    if (deviceState() == false)
        return ret;

    // 清空列表, 下面从新添加
    readArray.clear();
    lowEnergyCharacteristicRead = "";
    listLowEnergyCharacteristicRead.clear();
    listLowEnergyCharacteristicWrite.clear();

    // 删除上一个指针
    if (lowEnergyService)
        delete lowEnergyService;

    //创建服务
    lowEnergyService = lowEnergyController->createServiceObject(serviceUUID[0], this); // 扫描完uuid后才有效
    if (lowEnergyService == nullptr) {
        if (qDebug_ok) qDebug() << ("创建 lowEnergyService 失败");
        return ret; // 返回失败
    }

    // 关闭所有信号
    disconnect(lowEnergyService, nullptr, nullptr, nullptr);

    // 创建并连接到其信号
    connect(lowEnergyService, &QLowEnergyService::characteristicChanged, this, [this](const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue){
        if (qDebug_ok) qDebug() << "lowEnergyService:characteristicChanged:BLE设备:" << characteristic.uuid().toString()
                                << "; 类型:" << characteristic.properties()
                                << "; 特性值发生变化:" << newValue.toHex();
        if (characteristic.uuid().toString() == lowEnergyCharacteristicRead) // 符合特指值, 累加数据
            readArray.append(newValue);
    });
    connect(lowEnergyService, &QLowEnergyService::characteristicRead, this, [this](const QLowEnergyCharacteristic &characteristic, const QByteArray &value){
        if (qDebug_ok) qDebug() << "lowEnergyService:characteristicRead:BLE设备:" << characteristic.uuid().toString()
                                << "; 类型:" << characteristic.properties()
                                << "; 特性值读取到的值:" << value.toHex();
    });
    connect(lowEnergyService, &QLowEnergyService::characteristicWritten, this, [this](const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue){
        if (qDebug_ok) qDebug() << "lowEnergyService:characteristicWritten:BLE设备:" << characteristic.uuid().toString()
                                << "; 类型:" << characteristic.properties()
                                << "; 特性值成功写入值:" << newValue.toHex();
    });
    connect(lowEnergyService, &QLowEnergyService::descriptorRead, this, [this](const QLowEnergyDescriptor &descriptor, const QByteArray &value){
        if (qDebug_ok) qDebug() << "lowEnergyService:descriptorRead:BLE设备:" << descriptor.name()
                                << "; 描述符成功返回其值:" << value.toHex();
    });
    connect(lowEnergyService, &QLowEnergyService::descriptorWritten, this, [this](const QLowEnergyDescriptor &descriptor, const QByteArray &newValue){
        if (qDebug_ok) qDebug() << "lowEnergyService:descriptorWritten:BLE设备:" << descriptor.name()
                                << "; 描述符成功写入值:" <<  newValue.toHex();
    });
    connect(lowEnergyService, QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error), [this](QLowEnergyService::ServiceError newError){
        if (qDebug_ok) qDebug() << "lowEnergyService:error:发生错误:" << newError; });
    connect(lowEnergyService, &QLowEnergyService::stateChanged, this, [this](QLowEnergyService::ServiceState newState){
        if (qDebug_ok) qDebug() << "lowEnergyService:stateChanged:服务的状态发生变化:" << newState;
        if (serviceState() == true) {
            QTimer::singleShot(BLE_SLOT_DELAY_CONN_MS, this, [this](void) {
                if (qDebug_ok) qDebug() << "lowEnergyService:stateChanged:稍后延迟后查询:" << rwCharacteristics();
            });
        }
    });

    // 启动服务, 特征和服务所包含的描述符的发现. 发现过程通过 stateChanged() 信号指示.
    QTimer::singleShot(BLE_SLOT_DELAY_CONN_MS, this, [this](void) {
        if (qDebug_ok) qDebug() << ("开始连接 (服务) 并扫描 (特征)");
        lowEnergyService->discoverDetails();
    });
    ms_timeout += BLE_SLOT_DELAY_CONN_MS*2; // 累加上延迟的时间

    // 等待扫描完成, 不为空才等待
    if (ms_timeout != 0) {
        QElapsedTimer timer;
        timer.start();
        while (timer.elapsed() < ms_timeout) {
            QCoreApplication::processEvents(QEventLoop::AllEvents, 50); // 它允许事件循环在调用它的位置暂时接管,处理所有挂起的事件,然后立即返回
            //QThread::yieldCurrentThread(); // 可以使用此函数来让出CPU时间片,但不推荐在长时间阻塞循环中使用

            if (serviceState() == true && // 没有扫描完成标记位, 为了不误判, 需要确保列表不为空
                (listLowEnergyCharacteristicRead.size() != 0 ||
                listLowEnergyCharacteristicWrite.size() != 0)) {
                QList<QString> tempRead;
                QList<QString> tempWrite;

                for (QLowEnergyCharacteristic &ch : listLowEnergyCharacteristicRead)
                    tempRead.append(ch.uuid().toString());
                for (QLowEnergyCharacteristic &ch : listLowEnergyCharacteristicWrite)
                    tempWrite.append(ch.uuid().toString());

                ret.append(tempRead);
                ret.append(tempWrite);
                return ret; // 连接成功才执行
            }
        }
    }

    serviceDisconnect(); // 上面没有成功, 这里就删除对象
    return ret; // 返回失败
}

// 断开服务对象
void BluetoothPort::serviceDisconnect(void)
{
    // 已知问题: 蓝牙中 服务貌似是可以共存的, 而且服务对象创建并发现特征后, 即使删除对象也没法再次发现特征.
    // 意味着正确的使用方法, 应该是连接蓝牙, 成功后就一次性获取所有服务和包含的所有特征. 然后再挑选

    // 删除上一个指针
    if (lowEnergyService) {
        disconnect(lowEnergyService, nullptr, nullptr, nullptr);
        delete lowEnergyService;
        lowEnergyService = nullptr;
    }
}

// 获取服务对象状态
bool BluetoothPort::serviceState(void)
{
    if (lowEnergyService == nullptr)
        return false;
    if (lowEnergyService->state() != QLowEnergyService::ServiceDiscovered) // 连接成功才执行
        return false;

    return true;
}

8.读写数据

  • 发现特征后就可以直接写描述符了.读取数据其实就是读取特征值的变化, 特征值的变化会产生信号被捕捉到.前提是需要写入描述符打开通知功能才会产生信号.
  • 写入数据其实也是往可写特征值内写数据.
  • 不同蓝牙的服务,特征值,描述符可能不一样!!!
// 内含 更新读写 特征值(Characteristic) 和 读写 描述符(Descriptor)
QByteArray BluetoothPort::rwCharacteristics(QByteArray writeArray, int ms_timeout,
                                            QString readCharacteristicStr, QString writeCharacteristicStr)
{
    readArray.clear();
    lowEnergyCharacteristicRead = "";

    // 保护措施
    if (deviceState() == false)
        return readArray;

    // 前提条件:服务详情已经被发现
    if (serviceState() == false)
        return readArray;

    listLowEnergyCharacteristicRead.clear(); // 清空列表, 下面从新添加
    listLowEnergyCharacteristicWrite.clear();

    // 更新特征值列表
    QList<QLowEnergyCharacteristic> chars = lowEnergyService->characteristics();
    for (const QLowEnergyCharacteristic &ch : chars) {
        if (qDebug_ok) qDebug() << "特性类型:" << ch.properties() << "; uuid:" <<ch.uuid().toString();
        if (ch.isValid()) {
            if (ch.properties() & QLowEnergyCharacteristic::Notify) {
                QLowEnergyDescriptor cccd = ch.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
                if (cccd.isValid()) {
                    listLowEnergyCharacteristicRead.append(ch); // 保存可读特征值
                    lowEnergyService->writeDescriptor(cccd, QByteArray::fromHex("0100")); // 修改描述符 打开通知
                }
            }
            if (ch.properties() & QLowEnergyCharacteristic::WriteNoResponse ||  ch.properties() & QLowEnergyCharacteristic::Write) {
                listLowEnergyCharacteristicWrite.append(ch); // 保存可写特征值
            }
            if (ch.properties() & QLowEnergyCharacteristic::Read) {
                lowEnergyService->readCharacteristic(ch); // 读取特性值 // 还没遇到过
            }
        }
    }

    // 写入特征值 // 不为空才写入
    if (writeArray.size() != 0 && writeCharacteristicStr != "") {
        for (const QLowEnergyCharacteristic &ch : listLowEnergyCharacteristicWrite) {
            if (ch.uuid().toString() == writeCharacteristicStr) {
                QLowEnergyService::WriteMode mode = (ch.properties() & QLowEnergyCharacteristic::WriteNoResponse) ? \
                            (QLowEnergyService::WriteWithoutResponse) : (QLowEnergyService::WriteWithResponse); // 判断写入类型 // 符合才执行
                lowEnergyService->writeCharacteristic(ch, writeArray, mode); // 写入特性值
                break;
            }
        }
    }

    // 读取特征值 // 不为空才等待
    lowEnergyCharacteristicRead = readCharacteristicStr; // 拷贝 uuid 字符串
    QElapsedTimer timer;
    int readArrayLen = 0;
    while (ms_timeout != 0 && readCharacteristicStr != "") {
        timer.start(); // 注意, 这里的超时时间指两次数据收到的间隔, 不是总时间
        while (timer.elapsed() < ms_timeout) {
            QCoreApplication::processEvents(QEventLoop::AllEvents, 50); // 它允许事件循环在调用它的位置暂时接管,处理所有挂起的事件,然后立即返回
            //QThread::yieldCurrentThread(); // 可以使用此函数来让出CPU时间片,但不推荐在长时间阻塞循环中使用
        }
        if (readArrayLen != readArray.size())
            readArrayLen = readArray.size(); // 如果长度没变就是没收到
        else
            break;
    }

    return readArray;
}

9. 总结

我手上有3款蓝牙,它们的服务和特征值居然都不一样,而且有其中一个一直不能写入特征值和描述符.好巧不巧的是我一开始就是拿着这个不能用的蓝牙测试,导致我一度怀疑人生,代码怎么都跑不通.后来换了两个蓝牙后就成功了.不过我用手机小程序和手机app都是能连上的,所以可能程序还是有点问题.目前代码至少是能用了,

  • 暂时告一段落了.