文章目录
- 前言
- 一、应用案例演示
- 二、开发环境搭建
- 2.1 硬件准备
- 2.2 软件配置
- 三、蓝牙通信原理剖析
- 3.1 实现原理
- 3.2 通信流程
- 3.3 流程详解
- 3.4 关键技术点
- 四、Qt蓝牙核心类深度解析
- 4.1 QBluetoothDeviceDiscoveryAgent
- 4.2 QBluetoothDeviceInfo
- 4.3 QBluetoothSocket
- 五、功能实现关键步骤
- 5.1 设备扫描与发现
- 5.2 设备连接与状态管理
- 5.3 打印数据封装与发送
前言
本文基于Qt5的蓝牙模块,详细讲解了Linux 下如何实现蓝牙设备扫描、连接、数据通信与打印功能的开发。文章内容涵盖核心类的解析、关键接口设计及讲解,助你快速掌握嵌入式蓝牙应用开发。
一、应用案例演示
演示视频之基于Qt5的蓝牙打印开发实战:从扫描到小票打印
二、开发环境搭建
2.1 硬件准备
- Orange Pi开发板(RK3566芯片)
- 支持SPP协议的蓝牙打印机
我使用的是香橙派的CM4开发板,您可以根据实际需求选择合适的开发板即可,系统信息如下所示:
root@orangepicm4:~# uname -a
Linux orangepicm4 5.10.160-rockchip-rk356x #1.0.6 SMP Mon May 27 17:03:18 CST 2024 aarch64 GNU/Linux
root@orangepicm4:~# cat /etc/issue
Orange Pi 1.0.6 Bullseye \l
而打印机方面,我选择的是这款便携式的热敏打印机:
2.2 软件配置
安装依赖:
sudo apt-get install libbluetooth-dev qtconnectivity5-dev
CMakeList.txt 配置:
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Bluetooth REQUIRED)
target_link_libraries(BluetoothPrinterDemo PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Bluetooth)
三、蓝牙通信原理剖析
3.1 实现原理
蓝牙打印功能基于经典蓝牙(BR/EDR)的SPP协议(Serial Port Profile),核心流程如下:
1. 设备发现: 扫描周围蓝牙设备,筛选支持SPP协议的设备。
2. 建立连接: 通过设备的MAC地址和服务UUID(00001101-0000-1000-8000-00805F9B34FB)创建Socket连接。
3. 数据通信: 向打印机发送符合ESC/POS标准的指令集(文本、格式控制、切纸等)。
4. 资源释放: 断开连接并释放蓝牙资源。
3.2 通信流程
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
│ 启动扫描 │────>│ 发现蓝牙设备 │────>│ 显示设备列表 │
└─────────────┘ └───────────────┘ └──────────────┘│▼
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
│ 用户选择设备 │────>│ 建立Socket连接 │───┬>│ 连接成功 │
└─────────────┘ └───────────────┘ │ └──────────────┘│ │▼ │
┌─────────────┐ ┌───────────────┐ │ ┌──────────────┐
│ 发送打印数据 │<────│ 生成打印指令 │ └─┤ 连接失败/超时 │
└─────────────┘ └───────────────┘ └──────────────┘│▼
┌─────────────┐ ┌───────────────┐
│ 断开连接 │<────│ 完成打印任务 │
└─────────────┘ └───────────────┘
3.3 流程详解
设备发现阶段:
- 调用QBluetoothDeviceDiscoveryAgent.start()启动扫描。
- 过滤设备类型(仅保留经典蓝牙设备)。
- 将设备信息(名称、MAC地址)显示在列表中。
连接阶段:
- 用户选择设备后,通过QBluetoothSocket连接设备的SPP服务。
- 设置超时监控(10秒未连接成功则自动取消)。
打印阶段:
- 数据封装:组合文本内容与ESC/POS指令(如\x1B\x40初始化打印机)。
- 编码处理:中文需转换为GBK编码(兼容大多数国产打印机)。
- 数据发送:通过QBluetoothSocket.write()发送字节流。
断开连接:
- 主动调用disconnectFromService()断开Socket。
- 在析构函数中自动释放资源,防止内存泄漏。
3.4 关键技术点
步骤 | 技术实现 | 对应代码类/方法 |
---|---|---|
设备扫描 | QBluetoothDeviceDiscoveryAgent | start()/deviceDiscovered() |
连接管理 | QBluetoothSocket + 服务UUID | connectToService() |
数据封装 | ESC/POS指令集 + 编码转换 | QByteArray/QTextCodec |
错误处理 | 监听errorOccurred信号 | handleSocketError() |
四、Qt蓝牙核心类深度解析
类名 | 功能说明 |
---|---|
QBluetoothDeviceDiscoveryAgent | 蓝牙设备扫描器,支持经典/低功耗双模式 |
QBluetoothDeviceInfo | 存储设备MAC地址、名称、信号强度等信息 |
QBluetoothSocket | 实现数据读写的核心通信通道 |
4.1 QBluetoothDeviceDiscoveryAgent
作用:
蓝牙设备扫描的核心控制器,负责发现周边可见的经典蓝牙设备(非BLE)。
关键方法:
方法 | 作用 | 代码示例 |
---|---|---|
start() | 启动设备扫描 | m_discoveryAgent->start() |
stop() | 停止扫描 | m_discoveryAgent->stop() |
isActive() | 检查是否正在扫描 | if(m_discoveryAgent->isActive()) |
信号:
// 设备发现时触发
void deviceDiscovered(const QBluetoothDeviceInfo &info);// 扫描完成时触发
void finished();
在代码中的应用:
// 初始化扫描器
m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);// 绑定设备发现信号
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,this, &BluetoothWindow::deviceDiscovered);// 启动扫描(代码截取自startScan())
m_discoveryAgent->start();
m_statusLabel->setText("正在扫描设备...");
关键实现细节:
- 设备过滤: 通过coreConfigurations()筛选经典蓝牙设备
if(device.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) {// 只显示传统蓝牙设备
}
4.2 QBluetoothDeviceInfo
作用:
存储蓝牙设备的完整信息,包括名称、MAC地址、支持的服务等。
关键属性获取方法:
方法 | 返回值 | 代码示例 |
---|---|---|
name() | 设备名称(如"Printer-01") | device.name() |
address() | MAC地址(QBluetoothAddress类型) | device.address().toString() |
serviceUuids() | 设备支持的服务UUID列表 | device.serviceUuids().contains(QBluetoothUuid::SerialPort) |
在代码中的应用:
// 存储设备信息到列表项(deviceDiscovered()中)
QListWidgetItem *item = new QListWidgetItem(QString("%1 [%2]").arg(device.name()).arg(device.address().toString()));
item->setData(Qt::UserRole, QVariant::fromValue(device)); // 原始设备数据存储// 连接时获取设备信息(connectDevice()中)
m_currentDevice = item->data(Qt::UserRole).value<QBluetoothDeviceInfo>();
设计亮点:
- 数据持久化:通过Qt::UserRole直接存储设备对象,避免后续从字符串重新解析MAC地址
- 服务验证:连接前检查设备是否支持串口服务
if(!m_currentDevice.serviceUuids().contains(QBluetoothUuid::SerialPort)) {QMessageBox::warning(this, "错误", "设备不支持打印服务");
}
4.3 QBluetoothSocket
作用:
实现蓝牙协议栈的数据传输,支持RFCOMM(经典蓝牙)和L2CAP协议。
关键方法:
方法 | 作用 | 代码示例 |
---|---|---|
connectToService() | 连接到指定服务 | socket->connectToService(addr, uuid) |
disconnectFromService() | 断开连接 | socket->disconnectFromService() |
write() | 发送数据 | socket->write(data) |
重要信号:
void stateChanged(QBluetoothSocket::SocketState state); // 连接状态变化
void errorOccurred(QBluetoothSocket::SocketError error); // 错误发生时
void bytesWritten(qint64 bytes); // 数据成功写入时
在代码中的应用:
// 创建Socket对象(connectDevice()中)
m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);// 连接状态处理
connect(m_socket, &QBluetoothSocket::stateChanged,this, &BluetoothWindow::socketStateChanged);// 错误处理
connect(m_socket, QOverload<QBluetoothSocket::SocketError>::of(&QBluetoothSocket::error),this, &BluetoothWindow::handleSocketError);// 发起连接(使用SerialPort服务UUID)
m_socket->connectToService(m_currentDevice.address(), QBluetoothUuid(QBluetoothUuid::SerialPort));
状态机详解:
状态值 | 含义 | 代码处理逻辑 |
---|---|---|
QBluetoothSocket::UnconnectedState | 未连接 | 显示"未连接"状态 |
QBluetoothSocket::ConnectingState | 正在连接 | 显示"连接中…" |
QBluetoothSocket::ConnectedState | 已连接 | 启用打印按钮 |
QBluetoothSocket::ClosingState | 正在断开 | 显示"断开中…" |
五、功能实现关键步骤
5.1 设备扫描与发现
// BluetoothWindow.cpp
void BluetoothWindow::startScan() {m_deviceList->clear();m_discoveryAgent->start(); // 启动扫描m_statusLabel->setText("正在扫描设备...");// 扫描完成处理connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, [this]() {m_statusLabel->setText(QString("找到%1个设备").arg(m_deviceList->count()));});
}void BluetoothWindow::deviceDiscovered(const QBluetoothDeviceInfo &device) {if (device.coreConfigurations() & QBluetoothDeviceInfo::BaseRateCoreConfiguration) {QListWidgetItem *item = new QListWidgetItem(QString("%1 [%2]").arg(device.name()).arg(device.address().toString()));item->setData(Qt::UserRole, QVariant::fromValue(device)); // 存储原始设备数据m_deviceList->addItem(item);}
}
关键点:
- 通过QBluetoothDeviceDiscoveryAgent实现非阻塞设备扫描
- 使用Qt::UserRole存储设备原始数据,避免后续连接时重复解析字符串
- 过滤仅显示经典蓝牙设备(BaseRateCoreConfiguration)
5.2 设备连接与状态管理
void BluetoothWindow::connectDevice() {QListWidgetItem *item = m_deviceList->currentItem();if (!item) return;// 从Item中直接获取设备信息m_currentDevice = item->data(Qt::UserRole).value<QBluetoothDeviceInfo>();if (m_socket) m_socket->deleteLater();m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);// 连接状态信号绑定connect(m_socket, &QBluetoothSocket::stateChanged, this, &BluetoothWindow::socketStateChanged);// 连接超时处理(10秒)m_connectionTimer->start(10000);m_socket->connectToService(m_currentDevice.address(), QBluetoothUuid(QBluetoothUuid::SerialPort));
}void BluetoothWindow::socketStateChanged(QBluetoothSocket::SocketState state) {switch (state) {case QBluetoothSocket::ConnectedState:m_statusLabel->setText("已连接:" + m_currentDevice.name());enableControls(true);break;case QBluetoothSocket::UnconnectedState:enableControls(false);break;}
}
关键点:
- 通过QBluetoothUuid::SerialPort指定串口协议(SPP)
- 使用QTimer实现连接超时保护
- 状态机管理连接流程(UI状态同步)
5.3 打印数据封装与发送
QByteArray BluetoothWindow::generatePrintData(CustomerInfo info) const
{// 获取当前日期QString currentDate = QDate::currentDate().toString("yyyy/MM/dd");const QString printData = QString("ID: %1\n""姓名: %2 性别: %3\n\n""OD(右眼): DS %4\n"" DC %5 \n"" AX %6° \n"" SE %7 \n\n""OD(左眼): DS %8\n"" DC %9 \n"" AX %10° \n"" SE %11 \n""瞳孔大小: (%12mm OD,%13mm OS)\n""瞳距: (%14mm)\n""结果: %15\n""日期: %16 (C) %17\n\n").arg(info.IdentityID)//1.arg(info.Name)//2.arg(info.Gender)//3.arg(info.reportData.RightEyeBallMirror) // 4 右眼 DS.arg(info.reportData.RightOphthlmoscope) // 5 右眼 DC.arg(info.reportData.RightEyeAxialPosition) // 6 右眼 AX.arg(info.reportData.RightEyeBallMirror + (info.reportData.RightOphthlmoscope/2)) // 7 右眼 SE.arg(info.reportData.LeftEyeBallMirror) // 8 左眼 DS.arg(info.reportData.LeftOphthlmoscope) // 9 左眼 DC.arg(info.reportData.LeftEyeAxialPosition) // 10 左眼 AX.arg(info.reportData.LeftEyeBallMirror + (info.reportData.LeftOphthlmoscope/2)) // 11 左眼 SE.arg(info.reportData.RightEyePupilSize) // 12 右眼瞳孔大小.arg(info.reportData.LeftEyePupilSize) // 13 左眼瞳孔大小.arg(info.reportData.PupillaryDistance) // 14 瞳距.arg(info.Result) // 15 结果.arg(currentDate) // 16 使用当天的日期.arg(info.hospital); // 17 医院// 添加中文支持检查和更完整的打印指令QByteArray data;data.append("\x1B\x40"); // 初始化// data.append("\x1C\x2E"); // 中文模式// data.append("\x1B\x21\x10"); // 设置字体大小// 使用更安全的编码检测if(QTextCodec::codecForName("GBK")) {QTextCodec *gbkCodec = QTextCodec::codecForName("GBK");data.append(gbkCodec->fromUnicode(printData));} else {data.append(printData.toLocal8Bit()); // 回退到本地编码}data.append("\n\n\x1D\x56\x41\x02"); // 更标准的切纸指令return data;
}
关键点:
- 兼容GBK编码与本地编码回退机制
- 使用ESC/POS标准指令集(\x1B\x40初始化,\x1D\x56\x41\x02切纸)