由于项目需要,要在QT上编写一个蓝牙控制器的Android APP,之前写的应该是传统蓝牙的链接方式,所以无法链接BLE设备,需要重新改写设备连接部分的函数绝大部分程序。


1.搜索设备

discoveryAgent =new QBluetoothDeviceDiscoveryAgent();//创建搜索服务
    discoveryAgent->setLowEnergyDiscoveryTimeout(5000);//设置BLE的搜索时间
    connect(discoveryAgent,SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)),this,SLOT(addBlueToothDevicesToList(QBluetoothDeviceInfo)));//找到设备之后添加到列表显示出来
    connect(discoveryAgent, SIGNAL(finished()), this, SLOT(scanFinished()));
    connect(discoveryAgent, SIGNAL(canceled()), this, SLOT());

2.过滤传统蓝牙设备

只显示BLE设备,并将搜到的设备存入QList<QBluetoothdeviceInfo> devicesList方便后面选择

void bluetoothSetting::addBlueToothDevicesToList(const QBluetoothDeviceInfo &info)
{
    if (info.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {//判断是否是BLE设备
        QString label = QString("%1 %2").arg(info.address().toString()).arg(info.name());//按顺序显示地址和设备名称

            QList<QListWidgetItem *> items = ui->deviceList->findItems(label, Qt::MatchExactly);//检查设备是否已存在,避免重复添加

            if (items.empty()) {//不存在则添加至设备列表
                QListWidgetItem *item = new QListWidgetItem(label);
                ui->deviceList->addItem(item);
                devicesList.append(info);
            }
    }
}

3.连接与断开设备

获取到BLE从设备的地址之后,就可以通过新建一个QLowEnergyController类来连接设备了。这里通过槽函数将BLE从设备的地址发送到主窗口后再进行连接。

暂时没想到比较高效的获取设备方法,为了省事就写了一个for循环来对比地址

void bluetoothSetting::on_pushButton_3_clicked()//BTN_CHOOSE
{
    if(ui->deviceList->currentItem()->text().isEmpty()){}//确认选取有效
    else {
        QString bltAddress = ui->deviceList->currentItem()->text().left(17);//获取选择的地址
        for (int i = 0; i<devicesList.count(); i++) {
            if(devicesList.at(i).address().toString().left(17) == bltAddress)//地址对比
            {
                QBluetoothDeviceInfo choosenDevice = devicesList.at(i);
                emit returnAddress(choosenDevice);//发送设备信息
                discoveryAgent->stop();//停止搜索服务
                this->close();//关闭窗口,返回主页面
            }
        }
    }
}

主窗口接收到选择的QBluetoothdeviceInfo之后,创建centercontroller并建立连接,然后是各种connect,

以下函数直接从官方文档中复制过来然后稍作了些许修改

void MainWindow::on_pushButton_connect_clicked()
{
   m_control = QLowEnergyController::createCentral(choosenDeviceInfo, this);//创建中央控制器
   connect(m_control, &QLowEnergyController::serviceDiscovered,
           this, &MainWindow::serviceDiscovered);
   connect(m_control, &QLowEnergyController::discoveryFinished,
           this, &MainWindow::serviceScanDone);

   connect(m_control, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
           this, [this](QLowEnergyController::Error error) {
       Q_UNUSED(error);
       QMessageBox::information(this,tr("Info"),tr("Cannot connect to remote device."));
   });
   connect(m_control, &QLowEnergyController::connected, this, [this]() {
       QMessageBox::information(this,tr("Info"),tr("Controller connected. Search services..."));
       m_control->discoverServices();
   });
   connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
       QMessageBox::information(this,tr("Info"),tr("LowEnergy controller disconnected"));
   });
   m_control->connectToDevice();//建立连接
}

通过以下方式断开链接

if(!(m_control->state() == QLowEnergyController::UnconnectedState))
        m_control->disconnectFromDevice();//从设备断开链接

4.服务相关

与BLE的通讯都是通过service来进行,这里涉及到以下几个概念:profile、service、characteristic、descriptor、uuid。

profile:规范,也称为配置文件,可以理解为一个完整的功能比如蓝牙心率,它规定了这套完整功能中不同身份的角色,比如蓝牙心率中的心率测量设备和中央数据接收显示设备(如手机),以及不同角色所需要具备的service(服务)。每一个角色所需要具备的服务没有数量限制,可以没有,只有一个或有很多个。

service:服务,可以理解为将一个完整功能分解后的子功能。一般而言,每一个子功能之间是相互独立的。链接上一个BLE设备后一般需要做的第一件事就是查找对端设备支持的服务。服务又分为首要服务和子服务,首要服务一般为设备所具有的主要功能,比如温度采集器的首要服务就包括温度服务,而集成在电池内部,用来测量电池温度的温度计将不提供温度服务作为首要服务。

characteristic:特性,这是操作BLE设备时直接控制或监听的部分。在发现了设备的服务集之后,接下来的工作就是发现相关的特性集,同样,每一个服务对所具有的特性的数量没有要求。特性可以分为可读特性和可写特性,可写特性又包含WriteWithResponse、WriteWithoutResponse、WriteSigned三种写模式;可读特性中,具有通知或指示功能的特性,在使用这些功能之前,需要找到其descriptor。

descriptor:描述符,全称客户端特性配置描述符,在获取了具有通知或指示功能的可读特性之后向描述符写入正确的值,即可启用通知或指示。

uuid:通用唯一识别码,每一个规范、每一个服务、每一个特性、每一个描述符都有一个UUID,连接服务、使用特性都需要获取其唯一的UUID。一个UUID长128位,但是一般只需要使用其中16位即可,其他位置一般都是固定的,在BLE设备之间的传输过程中一般只传递16位。

本项目中,从机提供了三种服务:Generic Access(0x1800)、Generic Attribute(0x1801)、UART Service(0x0003)。其中,前两个服务是大部分设备默认的通用访问服务、通用属性服务,第三项是所需要使用的串口服务。

串口服务包含了两个特性:Tx Characteristic和Rx Characteristic,TX为可读特性,且具有通知功能,所以还具有一个Descriptor(0x2902),所以在启用TX的通知功能之前,需要向其描述符写入正确的值。RX为可写特性,具有WRITE和WRITE NO RESPINSE 两种写模式。因此,可以通过TX和RX两个特性来建立主机和从机之间的数据交流。

在上一节中,在创建控制器m_control之后,建立了连接成功的信号-槽:

connect(m_control, &QLowEnergyController::connected, this, [this]() {
       QMessageBox::information(this,tr("Info"),tr("Controller connected. Search services..."));
       m_control->discoverServices();
   });

在连接成功之后,首先需要寻找设备具有的服务: 

m_control->discoverServices();

找到每一个服务后会触发一次serviceDiscovered信号,在搜索完成之后会触发一次discoveryFinished信号,这里直接响应完成搜索的信号并链接到以下函数以链接服务。流程为:获取所有service、根据UUID匹配所需service、创建serviceObject、查找service所包含的characteristic。

void MainWindow::SL_discoveryFinished()
{
    serviceUUIDs = m_control->services();//获取所有找到的服务的UUID
    for (int i = 0;i<serviceUUIDs.count();i++) {
        //QMessageBox::information(this,tr("Info"),serviceUUIDs.at(i).toString());
        //在这些服务中,根据已知所需服务的UUID,来匹配
        if(serviceUUIDs.at(i).toString() == "{xxxx0001-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"){
            //找到所需要的服务之后,创建serviceObject
            m_Service = m_control->createServiceObject(serviceUUIDs.at(i));
            //如果找到的服务中有所需服务,并匹配、创建成功则进入if语句,链接相应的信号-槽
            if(m_Service){
               connect(m_Service, &QLowEnergyService::stateChanged, this, &MainWindow::SL_serviceStateChanged);//状态改变信号
               connect(m_Service, &QLowEnergyService::characteristicChanged, this, &MainWindow::SL_characteristicChanged);//特性改变信号
               connect(m_Service, &QLowEnergyService::descriptorWritten, this, &MainWindow::SL_descriptorWritten);//描述符写入信号
               m_Service->discoverDetails();//完成之后需要执行此行已发现服务所包含的特性
            }
            else{
               QMessageBox::information(this,tr("Info"),"Service not found.");
            }
        }
    }

}

链接service成功并查找完所有特性之后,还需要创建characteristic实例,这里需要在

5.串口透传

主从设备传递数据可以通过服务或串口两种方式,服务那块没看明白,所以暂且用串口透传的方式实现功能。