文章目錄
- 前言
- 一、應用案例演示
- 二、開發環境搭建
- 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切紙)