因项目需要,涉及到了BLE通信,同时之前有BLE的设备端开发经验,又使用了一段时间的QT开发一些研发工具,如串口通信测试工具,tcp/ip测试工具,以及一些数据图形化工具等,那么就用QT进行上位机的开发BLE的研发工具吧。其中低功耗蓝牙的基础知识本处不细说了。
系统:win11
QT:5.15.2,使用qmake构建
注意:必须使用msvc,如果使用MingGW,会搜索不到设备,以及其它你不想见到的问题
1. 在工程文件 .pro 中添加对bluetooth的支持
QT += bluetooth
2.首先是扫描
void MainWindow::ble_dev_tool_start_scan(int scan_timeout)
{
// Clear the device infomation list before sverytime we start scan
m_devInfoList.clear();
//先清空列表
ui->lw_dev_info->clear();
// Add the title
QString devLabel = QString(" rssi ble address device name");
QListWidgetItem* devItemPtr = new QListWidgetItem(devLabel);
ui->lw_dev_info->addItem(devItemPtr);
m_deviceDiscoveryAgentPtr.setLowEnergyDiscoveryTimeout(scan_timeout);
connect(&m_deviceDiscoveryAgentPtr, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &MainWindow::ble_dev_tool_device_discovered_slot);
void (QBluetoothDeviceDiscoveryAgent:: *deviceDiscoveryErrorOccurred)(QBluetoothDeviceDiscoveryAgent::Error) = &QBluetoothDeviceDiscoveryAgent::error;
connect(&m_deviceDiscoveryAgentPtr, deviceDiscoveryErrorOccurred, this, &MainWindow::ble_dev_tool_device_discovery_error_slot);
connect(&m_deviceDiscoveryAgentPtr, &QBluetoothDeviceDiscoveryAgent::finished, this, &MainWindow::ble_dev_tool_device_discovery_finished_slot);
connect(&m_deviceDiscoveryAgentPtr, &QBluetoothDeviceDiscoveryAgent::canceled, this, &MainWindow::ble_dev_tool_device_discovery_finished_slot);
m_deviceDiscoveryAgentPtr.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
// Display the status in the statusbar
ble_dev_tool_status_bar_update("ble scanning......");
ui->pb_scan->setText("stop scan");
}
此处要注意的是扫描的时间,setLowEnergyDiscoveryTimeout方法的参数如果为0,则一直扫描直到调用停止扫描的方法,如果不为零,则扫描指定时间之后自动停止。
3.列出扫描到的设备
也就是slot槽函数ble_dev_tool_device_discovered_slot,将扫描到的设备列到一个QListWidget中。
void MainWindow::ble_dev_tool_device_discovered_slot(const QBluetoothDeviceInfo &devInfo)
{
// If the name is not empty and it is Bluetooth low power, consider adding it
if(devInfo.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration)
{
// 先判断是否是想要的设备
if(devInfo.name().contains(BLE_FILTER_NAME))
{
// 获取rssi,地址和名称
QString label = QString("%1 %2 %3").arg(QString::number(devInfo.rssi()) + " ", devInfo.address().toString() + " ", devInfo.name());
// Search the listwidget to see if the device already exists
QList itemPtrList = ui->lw_dev_info->findItems(label, Qt::MatchExactly);
// Prevent duplicate addition
if (itemPtrList.empty()) //If there is no information about this device
{
QListWidgetItem* itemPtr = new QListWidgetItem(label);
ui->lw_dev_info->addItem(itemPtr);//Add it to the listwidget
m_devInfoList.append(devInfo); //Add to devece information list
}
}
}
}
此处可以根据 if(devInfo.name().contains(BLE_FILTER_NAME)) 对扫描到的设备进行过滤
4.连接指定设备
在上述设备列表中,双击指定设备的slot槽函数on_lw_dev_info_itemDoubleClicked,可以发起连接操作
void MainWindow::on_lw_dev_info_itemDoubleClicked(QListWidgetItem *item)
{
if(ui->lw_dev_info->currentRow() == 0)
{
return;
}
// 正在扫描的话要先停止
if(scan_flag == true)
{
ble_dev_tool_stop_scan();
}
//设备被双击后需要先清空Uuid List,只保存当前选中设备的服务Uuid
m_uuidList.clear();
//先清空列表
ui->lw_dev_info_service->clear();
//创建蓝牙控制器; currentRow()-1是因为自己手动添加了一行标题
m_bleControllerPtr = QLowEnergyController::createCentral(m_devInfoList.at(ui->lw_dev_info->currentRow() - 1)); //central相当于是主机
if(m_bleControllerPtr == NULL)
{
QMessageBox::warning(this,"警告","创建控制器失败!");
}
else
{
//bleController的槽函数
connect(m_bleControllerPtr, &QLowEnergyController::connected, this, &MainWindow::ble_dev_tool_device_connected_slot); //设备连接成功
void (QLowEnergyController:: *bleDeviceConnectionErrorOccurred)(QLowEnergyController::Error) = &QLowEnergyController::error;//有重载
connect(m_bleControllerPtr, bleDeviceConnectionErrorOccurred, this, &MainWindow::ble_dev_tool_device_connection_error_slot); //设备连接出现错误
connect(m_bleControllerPtr, &QLowEnergyController::disconnected, this, &MainWindow::ble_dev_tool_device_disconnected_slot); //设备断开链接
connect(m_bleControllerPtr, &QLowEnergyController::serviceDiscovered, this, &MainWindow::ble_dev_tool_service_discovered_slot); //发现一个服务
connect(m_bleControllerPtr, &QLowEnergyController::discoveryFinished, this, &MainWindow::ble_dev_tool_service_discovery_finished_slot); //服务发现结束
//创建后控制器中对应的设备就是我们在列表中选中的设备
ble_dev_tool_status_bar_update("Connecting......");
QTimer::singleShot(BLE_SLOT_DELAY_CONN_mS, this, [this](){
ble_dev_tool_device_connecte();
});
}
}
此处做了一些简单的处理,如正在扫描则将扫描停止
并做了一个小延时再发起连接
void MainWindow::ble_dev_tool_device_connecte(void)
{
m_bleControllerPtr->connectToDevice();
}
5.发现服务
发现服务需要在连接成功之后进行,连接成功有一个slot槽函数ble_dev_tool_device_connected_slot
void MainWindow::ble_dev_tool_device_connected_slot()
{
ble_dev_tool_status_bar_update("Connected!");
QTimer::singleShot(BLE_SLOT_DELAY_DISC_SERVICE_mS, this, [this](){
ble_dev_tool_discover_service();
});
}
在该函数中做了个延时之后发起服务发现过程
void MainWindow::ble_dev_tool_discover_service(void)
{
m_bleControllerPtr->discoverServices();
}
6.对搜索到的服务进行处理,即slot槽函数ble_dev_tool_service_discovered_slot
void MainWindow::ble_dev_tool_service_discovered_slot(QBluetoothUuid serviceUuid)
{
//将搜索到的服务的Uuid存到一个label中
QString label = QString("%1").arg(serviceUuid.toString());
//在listwidget中搜索是否已经有这个服务
QList itemPtrList = ui->lw_dev_info_service->findItems(label, Qt::MatchExactly);
//防止重复
if (itemPtrList.empty()) //如果listwidget中没有搜索到的这个服务
{
QListWidgetItem* itemPtr = new QListWidgetItem(label);
ui->lw_dev_info_service->addItem(itemPtr);//将设备的信息添加到listwidget中
m_uuidList.append(serviceUuid); //设备信息添加到自己的列表中
if (label == BLE_FILTER_SERVICE)
{
service_find_flag = true;
ble_dev_tool_serv_index = m_uuidList.count();
}
}
}
7.对服务server发现之后就需要对characteristic进行detail获取
需要在服务发现完成slot槽函数ble_dev_tool_service_discovery_finished_slot中进行
void MainWindow::ble_dev_tool_service_discovery_finished_slot()
{
ui->te_ble_log->append("服务发现完成");
if(service_find_flag == true)
{
QTimer::singleShot(BLE_SLOT_DELAY_BEFOR_GET_DETAIL_mS, this, [this](){
ble_dev_tool_update_notify_get_details();
});
}
}
做了个延时触发
void MainWindow::ble_dev_tool_update_notify_get_details(void)
{
QBluetoothUuid serviceUuid = m_uuidList.at(ble_dev_tool_serv_index - 1); //当前选中的服务的Uuid
//创建服务
m_bleServicePtr = m_bleControllerPtr->createServiceObject(QBluetoothUuid(serviceUuid), this);
//判断创建服务是否出现错误
if(m_bleServicePtr == NULL)
{
QMessageBox::warning(this, "警告", "创建服务失败!");
}
else //创建服务成功;创建服务就相当于连接上了,执行完ServiceStateChangedSlot之后就可以正常通信了
{
ui->te_ble_log->append("开始获取服务细节并使能notify");
//监听服务状态变化
connect(m_bleServicePtr, &QLowEnergyService::stateChanged, this, &MainWindow::ble_dev_tool_service_state_changed_slot);
//服务的characteristic变化,有数据传来
connect(m_bleServicePtr, &QLowEnergyService::characteristicChanged, this, &MainWindow::ble_dev_tool_service_characteristic_changed_slot);
//错误处理
void (QLowEnergyService:: *bleServiceErrorOccurred)(QLowEnergyService::ServiceError) = &QLowEnergyService::error;//有重载
connect(m_bleServicePtr, bleServiceErrorOccurred, this, &MainWindow::ble_dev_tool_service_error_occurred_slot);
//描述符成功被写
connect(m_bleServicePtr, &QLowEnergyService::descriptorWritten, this, &MainWindow::ble_dev_tool_service_descriptor_written_slot);
//触发服务详情发现函数
QTimer::singleShot(BLE_SLOT_DELAY_GET_DETAIL_mS, this, [this](){
ble_dev_tool_discover_service_details();
});
}
}
函数
void MainWindow::ble_dev_tool_discover_service_details(void)
{
m_bleServicePtr->discoverDetails();
}
8.使能具有notify权限的characteristic
需要在slot槽函数ble_dev_tool_service_state_changed_slot中进行
void MainWindow::ble_dev_tool_service_state_changed_slot(QLowEnergyService::ServiceState state) //服务状态改变
{
QLowEnergyCharacteristic m_bleCharacteristic;
//发现服务
if(state == QLowEnergyService::ServiceDiscovered)
{
QList list = m_bleServicePtr->characteristics();
for(int i = 0; i < list.count(); i++)
{
//当前位置的bleCharacteritic
m_bleCharacteristic = list.at(i);
//如果当前characteristic有效
if(m_bleCharacteristic.isValid())
{
//描述符定义特征如何由特定客户端配置
m_notify_descriptor = m_bleCharacteristic.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
//如果descriptor有效
if(m_notify_descriptor.isValid())
{
m_bleServicePtr->writeDescriptor(m_notify_descriptor, QByteArray::fromHex("0100"));
}
if (m_bleCharacteristic.properties() & QLowEnergyCharacteristic::WriteNoResponse || m_bleCharacteristic.properties() & QLowEnergyCharacteristic::Write)
{
m_write_characteristic = m_bleCharacteristic;
}
}
}
}
}
9.notify数据接收
从机peripheral向主机central发送数据是通过notify方法,上述已经connect相应的signal信号,下面是具体处理
void MainWindow::ble_dev_tool_service_characteristic_changed_slot(QLowEnergyCharacteristic characteristic, QByteArray value)
{
ui->te_ble_log->append(QString(value));
qDebug() <<"receive notification data from peripheral";
}
10.发送数据
主机向从机发送数据是通过write进行的
oid MainWindow::ble_dev_tool_send_data(QByteArray value)
{
if(value.count() > 0)
{
m_bleServicePtr->writeCharacteristic(m_write_characteristic, value, QLowEnergyService::WriteWithResponse);
}
}
11.关于坑
由于BLE的一些方法不能直接在slot中操作,会造成运行时奔溃,并报错:Could not await service operation (A method was called at an unexpected time.因此,需要在slot中做一些延时,延时方法使用QTimer的singleShot,也就是上面几个函数中使用的 QTimer::singleShot(),这点很重要!!!