QT开发笔记(USB Bluetooth)

USB Bluetooth

Qt 官方提供了蓝牙的相关类和 API 函数,也提供了相关的例程给我们参考。笔者根据 Qt

官方的例程编写出适合我们 Ubuntu 和正点原子 I.MX6U 开发板的例程。注意 Windows 上不能
使用 Qt 的蓝牙例程,因为底层需要有 BlueZ 协议栈,而 Windows 没有。Windows 可能需要去
移植。笔者就不去探究了。确保我们正点原子 I.MX6U 开发板与 Ubuntu 可用即可,所以大家还
是老实的用 Ubuntu 来开发吧!

资源简介

在正点原子 IMX6U 开发板上虽然没有带板载蓝牙,但是可以外接免驱 USB 蓝牙,直接在

USB 接口插上一个 USB 蓝牙模块就可以进行本章节的实验了。详细请看【正点原子】I.MX6U

用户快速体验 V1.x.pdf 的第 3.29 小节蓝牙测试,先了解蓝牙是如何在 Linux 上如何使用的,切
记先看正点原子快速体验文档,了解用哪种蓝牙芯片,和怎么测试蓝牙的。本 Qt 教程就不再介
绍了。

应用实例

项目简介:Qt 蓝牙聊天。将蓝牙设置成一个服务器,或者用做客户端,连接手机即可通信。

例 06_bluetooth_chat,Qt 蓝牙聊天(难度:难)。项目路径为 Qt/3/06_bluetooth_chat。
Qt 使用蓝牙,需要在项目文件加上相应的蓝牙模块。添加的代码如下红色加粗部分。

06_bluetooth_chat.pro 文件代码如下。

1 QT += core gui bluetooth 

2 

3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 

4 

5 CONFIG += c++11 

6 

7 # The following define makes your compiler emit warnings if you use 

8 # any Qt feature that has been marked deprecated (the exact warnings 

9 # depend on your compiler). Please consult the documentation of the 

10 # deprecated API in order to know how to port your code away from it. 

11 DEFINES += QT_DEPRECATED_WARNINGS 

12 

13 # You can also make your code fail to compile if it uses deprecated APIs. 

14 # In order to do so, uncomment the following line. 

15 # You can also select to disable deprecated APIs only up to a certain 
version of Qt. 

16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the 
APIs deprecated before Qt 6.0.0 

17 

18 SOURCES += \ 

19 chatclient.cpp \ 

20 chatserver.cpp \ 

21 main.cpp \ 

22 mainwindow.cpp \ 

23 remoteselector.cpp 

24 

25 HEADERS += \ 

26 chatclient.h \ 

27 chatserver.h \ 

28 mainwindow.h \

29 remoteselector.h 

30 

31 # Default rules for deployment. 

32 qnx: target.path = /tmp/$${TARGET}/bin 

33 else: unix:!android: target.path = /opt/$${TARGET}/bin 

34 !isEmpty(target.path): INSTALLS += target 

第 18~29 行,可以看到我们的项目组成文件。一个客户端,一个服务端,一个主界面和一
个远程选择蓝牙的文件。总的看起来有四大部分,下面就介绍这四大部分的文件。
chatclient.h 的代码如下。

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief chatclient.h 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/ 

1 #ifndef CHATCLIENT_H 

2 #define CHATCLIENT_H 

3 

4 #include <qbluetoothserviceinfo.h> 

5 #include <QBluetoothSocket> 

6 #include <QtCore/QObject> 

7 

8 QT_FORWARD_DECLARE_CLASS(QBluetoothSocket) 

9 

10 class ChatClient : public QObject 

11 { 

12 Q_OBJECT 

13 

14 public: 

15 explicit ChatClient(QObject *parent = nullptr); 

16 ~ChatClient(); 

17 

18 /* 开启客户端 */ 

19 void startClient(const QBluetoothServiceInfo &remoteService); 

20 

21 /* 停止客户端 */ 

22 void stopClient(); 

23 

24 public slots: 

25 /* 发送消息 */ 

26 void sendMessage(const QString &message); 

27 

28 /* 主动断开连接 */ 

29 void disconnect(); 

30 

31 signals: 

32 /* 接收到消息信号 */ 

33 void messageReceived(const QString &sender, const QString &message); 

34 

35 /* 连接信号 */ 

36 void connected(const QString &name); 

37 

38 /* 断开连接信号 */ 

39 void disconnected(); 

40 

41 private slots: 

42 /* 从 socket 里读取消息 */ 

43 void readSocket(); 

44 

45 /* 连接 */ 

46 void connected(); 

47 

48 private: 

49 /* socket 通信 */ 

50 QBluetoothSocket *socket; 

51 }; 

52 

53 #endif // CHATCLIENT_H 

 chatclient.h 文件主要是客户端的头文件,其中写一些接口,比如开启客户端,关闭客户端,
接收信号与关闭信号等等。 
chatclient.cpp 的代码如下。 

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief chatclient.cpp 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/
 1 #include "chatclient.h" 

2 #include <qbluetoothsocket.h> 

3 

4 ChatClient::ChatClient(QObject *parent) 

5 : QObject(parent), socket(0) 

6 { 

7 } 

8 

9 ChatClient::~ChatClient() 

10 { 

11 stopClient(); 

12 } 

13 

14 /* 开启客户端 */ 

15 void ChatClient::startClient(const QBluetoothServiceInfo 

&remoteService) 

16 { 

17 if (socket) 

18 return; 

19 

20 // Connect to service 

21 socket = new 
QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); 

22 qDebug() << "Create socket"; 

23 socket->connectToService(remoteService); 

24 qDebug() << "ConnectToService done"; 

25 

26 connect(socket, SIGNAL(readyRead()), 

27 this, SLOT(readSocket())); 

28 connect(socket, SIGNAL(connected()), 

29 this, SLOT(connected())); 

30 connect(socket, SIGNAL(disconnected()), 

31 this, SIGNAL(disconnected())); 

32 } 

33 

34 /* 停止客户端 */ 

35 void ChatClient::stopClient() 

36 { 

37 delete socket; 

38 socket = 0; 

39 } 

40 

41 /* 从 Socket 读取消息 */ 
42 void ChatClient::readSocket() 

43 { 

44 if (!socket) 

45 return; 

46 

47 while (socket->canReadLine()) { 

48 QByteArray line = socket->readLine(); 

49 emit messageReceived(socket->peerName(), 

50 QString::fromUtf8(line.constData(), 

51 line.length())); 

52 } 

53 } 

54 

55 /* 发送的消息 */ 

56 void ChatClient::sendMessage(const QString &message) 

57 { 

58 qDebug()<<"Sending data in client: " + message; 

59 

60 QByteArray text = message.toUtf8() + '\n'; 

61 socket->write(text); 

62 } 

63 

64 /* 主动连接 */ 

65 void ChatClient::connected() 

66 { 

67 emit connected(socket->peerName()); 

68 } 

69 

70 /* 主动断开连接*/ 

71 void ChatClient::disconnect() { 

72 qDebug()<<"Going to disconnect in client"; 

73 if (socket) { 

74 qDebug()<<"diconnecting..."; 

75 socket->close(); 

76 } 

77 } 

chatclient.cpp 文件主要是客户端的 chatclient.h 头文件的实现。代码参考 Qt 官方 btchat 例子,
代码比较长,也有相应的注释了,大家自由查看。主要我们关注的是下面的代码。
第 15~32 行,我们需要开启客户端模式,那么我们需要将扫描服务器(手机蓝牙)的结果,
实例化一个蓝牙 socket,使用 socket 连接传入来的服务器信息,即可将本地蓝牙当作客户端,
实现了客户端创建。
chatserver.h 代码如下。

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief chatserver.h 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/ 
 

1 #ifndef CHATSERVER_H 

2 #define CHATSERVER_H 

3 

4 #include <qbluetoothserviceinfo.h> 

5 #include <qbluetoothaddress.h> 

6 #include <QtCore/QObject> 

7 #include <QtCore/QList> 

8 #include <QBluetoothServer> 

9 #include <QBluetoothSocket> 

10 

11 

12 class ChatServer : public QObject 

13 { 

14 Q_OBJECT 

15 

16 public: 

17 explicit ChatServer(QObject *parent = nullptr); 

18 ~ChatServer(); 

19 

20 /* 开启服务端 */ 

21 void startServer(const QBluetoothAddress &localAdapter = 
QBluetoothAddress()); 

22 

23 /* 停止服务端 */ 

24 void stopServer(); 

25 

26 public slots: 

27 /* 发送消息 */ 

28 void sendMessage(const QString &message); 

29 

30 /* 服务端主动断开连接 */ 

31 void disconnect(); 

32 

33 signals: 
34 /* 接收到消息信号 */ 

35 void messageReceived(const QString &sender, const QString &message); 

36 

37 /* 客户端连接信号 */ 

38 void clientConnected(const QString &name); 

39 

40 /* 客户端断开连接信号 */ 

41 void clientDisconnected(const QString &name); 

42 

43 private slots: 

44 

45 /* 客户端连接 */ 

46 void clientConnected(); 

47 

48 /* 客户端断开连接 */ 

49 void clientDisconnected(); 

50 

51 /* 读 socket */ 

52 void readSocket(); 

53 

54 private: 

55 /* 使用 rfcomm 协议 */ 

56 QBluetoothServer *rfcommServer; 

57 

58 /* 服务器蓝牙信息 */ 

59 QBluetoothServiceInfo serviceInfo; 

60 

61 /* 用于保存客户端 socket */ 

62 QList<QBluetoothSocket *> clientSockets; 

63 

64 /* 用于保存客户端的名字 */ 

65 QList<QString> socketsPeername; 

66 }; 

67 

68 #endif // CHATSERVER_H 

chatserver.h 文件主要是服务端的头文件,其中写一些接口,比如开启服务端,关闭服务端,
接收信号与关闭信号等等。
chatserver.cpp 代码如下。

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat
 * * @brief chatserver.cpp 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/ 
 

1 #include "chatserver.h" 

2 

3 #include <qbluetoothserver.h> 

4 #include <qbluetoothsocket.h> 

5 #include <qbluetoothlocaldevice.h> 

6 

7 static const QLatin1String 
serviceUuid("e8e10f95-1a70-4b27-9ccf-02010264e9c8"); 

8 ChatServer::ChatServer(QObject *parent) 

9 : QObject(parent), rfcommServer(0) 

10 { 

11 } 

12 

13 ChatServer::~ChatServer() 

14 { 

15 stopServer(); 

16 } 

17 

18 /* 开启服务端,设置服务端使用 rfcomm 协议与 serviceInfo 的一些属性 */ 

19 void ChatServer::startServer(const QBluetoothAddress& localAdapter) 

20 { 

21 if (rfcommServer) 

22 return; 

23 

24 rfcommServer = new 
QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this); 

25 connect(rfcommServer, SIGNAL(newConnection()), this, 
SLOT(clientConnected())); 

26 bool result = rfcommServer->listen(localAdapter); 

27 if (!result) { 

28 qWarning()<<"Cannot bind chat server 
to"<<localAdapter.toString(); 

29 return; 

30 } 

31 
32 

//serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceRecordHandle, 
(uint)0x00010010); 

33 

34 QBluetoothServiceInfo::Sequence classId; 

35 

36 
classId<<QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort)
); 

37 
serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescrip
torList, 

38 classId); 

39 

40 
classId.prepend(QVariant::fromValue(QBluetoothUuid(serviceUuid))); 

41 

42 serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, 
classId); 

43 
serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescrip
torList,classId); 

44 

45 serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceName, 
tr("Bt Chat Server")); 

46 
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceDescription, 

47 tr("Example bluetooth chat server")); 

48 serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceProvider, 
tr("qt-project.org")); 

49 

50 serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid)); 

51 

52 QBluetoothServiceInfo::Sequence publicBrowse; 

53 publicBrowse<< 
QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::PublicBrowseGroup)); 

54 serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList, 

55 publicBrowse); 

56 

57 QBluetoothServiceInfo::Sequence protocolDescriptorList; 

58 QBluetoothServiceInfo::Sequence protocol; 

59 protocol<< 
QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::L2cap)); 

60 protocolDescriptorList.append(QVariant::fromValue(protocol)); 

61 protocol.clear(); 

62 protocol<< 
QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Rfcomm)) 

63 << 
QVariant::fromValue(quint8(rfcommServer->serverPort())); 

64 protocolDescriptorList.append(QVariant::fromValue(protocol)); 

65 
serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, 

66 protocolDescriptorList); 

67 

68 serviceInfo.registerService(localAdapter); 

69 } 

70 

71 /* 停止服务端 */ 

72 void ChatServer::stopServer() 

73 { 

74 // Unregister service 

75 serviceInfo.unregisterService(); 

76 

77 // Close sockets 

78 qDeleteAll(clientSockets); 

79 

80 // Close server 

81 delete rfcommServer; 

82 rfcommServer = 0; 

83 } 

84 

85 /* 主动断开连接 */ 

86 void ChatServer::disconnect() 

87 { 

88 qDebug()<<"Going to disconnect in server"; 

89 

90 foreach (QBluetoothSocket *socket, clientSockets) { 

91 qDebug()<<"sending data in server!"; 

92 socket->close(); 

93 } 

94 } 

95 

96 /* 发送消息 */ 

97 void ChatServer::sendMessage(const QString &message) 

98 { 

99 qDebug()<<"Going to send message in server: " << message; 

100 QByteArray text = message.toUtf8() + '\n'; 

101 
102 foreach (QBluetoothSocket *socket, clientSockets) { 

103 qDebug()<<"sending data in server!"; 

104 socket->write(text); 

105 } 

106 qDebug()<<"server sending done!"; 

107 } 

108 

109 /* 客户端连接 */ 

110 void ChatServer::clientConnected() 

111 { 

112 qDebug()<<"clientConnected"; 

113 

114 QBluetoothSocket *socket = rfcommServer->nextPendingConnection(); 

115 if (!socket) 

116 return; 

117 

118 connect(socket, SIGNAL(readyRead()), this, SLOT(readSocket())); 

119 connect(socket, SIGNAL(disconnected()), this, 
SLOT(clientDisconnected())); 

120 clientSockets.append(socket); 

121 socketsPeername.append(socket->peerName()); 

122 emit clientConnected(socket->peerName()); 

123 } 

124 

125 /* 客户端断开连接 */ 

126 void ChatServer::clientDisconnected() 

127 { 

128 QBluetoothSocket *socket = qobject_cast<QBluetoothSocket 

*>(sender()); 

129 if (!socket) 

130 return; 

131 

132 if (clientSockets.count() != 0) { 

133 QString peerName; 

134 

135 if (socket->peerName().isEmpty()) 

136 peerName = 
socketsPeername.at(clientSockets.indexOf(socket)); 

137 else 

138 peerName = socket->peerName(); 

139 

140 emit clientDisconnected(peerName); 

141 

142 clientSockets.removeOne(socket); 
143 socketsPeername.removeOne(peerName); 

144 } 

145 

146 socket->deleteLater(); 

147 

148 } 

149 

150 /* 从 Socket 里读取数据 */ 

151 void ChatServer::readSocket() 

152 { 

153 QBluetoothSocket *socket = qobject_cast<QBluetoothSocket 

*>(sender()); 

154 if (!socket) 

155 return; 

156 

157 while (socket->bytesAvailable()) { 

158 QByteArray line = socket->readLine().trimmed(); 

159 qDebug()<<QString::fromUtf8(line.constData(), 
line.length())<<endl; 

160 emit messageReceived(socket->peerName(), 

161 QString::fromUtf8(line.constData(), 
line.length())); 

162 qDebug()<<QString::fromUtf8(line.constData(), 
line.length())<<endl; 

163 } 

164 } 

chatserver.cpp 文件主要是服务端的 chatserver.h 头文件的实现。代码也是参考 Qt 官方 btchat

例子,代码比较长,也有相应的注释了,大家自由查看。主要我们关注的是下面的代码。
第 19~69 行,我们需要开启服务端模式,那么我们需要将本地的蓝牙 localAdapter 的地址
传入,创建一个 QBluetoothServer 对象 rfcommServer。在 19 至 69 行代码很长,其中使用了

serviceInfo.setAttribute()设置了许多参数,这个流程是官方给出的流程,我们只需要了解下就可
以了。大体流程:使用了 QBluetoothServiceInfo 类允许访问服务端蓝牙服务的属性,其中有设
置蓝牙的 UUID 为文件开头定义的 serviceUuid,设置 serviceUuid 的目的是为了区分其他蓝牙,
用于搜索此类型蓝牙,但是作用并不是很大,因为我们的手机并不一定开启了这个 uuid 标识。
最后必须用 registerService()启动蓝牙。

第 36 行,转换串行端口(SerialPort),转换成 classId,然后再设置串行端口服务。通信原
理就是串行端口连接到 RFCOMM server channel。(PS:蓝牙使用的协议多且复杂,本教程并不
能清晰解释这种原理,如果有错误,欢迎指出)。
remoteselector.h 代码如下。

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief remoteselector.h 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/ 
 

1 #ifndef REMOTESELECTOR_H 

2 #define REMOTESELECTOR_H 

3 

4 #include <qbluetoothuuid.h> 

5 #include <qbluetoothserviceinfo.h> 

6 #include <qbluetoothservicediscoveryagent.h> 

7 #include <QListWidgetItem> 

8 

9 /* 声明一个蓝牙适配器类 */ 

10 class RemoteSelector : public QObject 

11 { 

12 Q_OBJECT 

13 

14 public: 

15 explicit RemoteSelector(QBluetoothAddress&, 

16 QObject *parent = nullptr); 

17 ~RemoteSelector(); 

18 

19 /* 开启发现蓝牙 */ 

20 void startDiscovery(const QBluetoothUuid &uuid); 

21 

22 /* 停止发现蓝牙 */ 

23 void stopDiscovery(); 

24 

25 /* 蓝牙服务 */ 

26 QBluetoothServiceInfo service() const; 

27 

28 signals: 

29 /* 找到新服务 */ 

30 void newServiceFound(QListWidgetItem*); 

31 

32 /* 完成 */ 

33 void finished(); 

34 

35 private: 

36 /* 蓝牙服务代理,用于发现蓝牙服务 */ 

37 QBluetoothServiceDiscoveryAgent *m_discoveryAgent; 

38 

39 /* 服务信息 */ 

40 QBluetoothServiceInfo m_serviceInfo; 

41 

42 private slots: 

43 /* 服务发现完成 */ 

44 void serviceDiscovered(const QBluetoothServiceInfo &serviceInfo); 

45 

46 /* 蓝牙发现完成 */ 

47 void discoveryFinished(); 

48 

49 public: 

50 /* 键值类容器 */ 

51 QMap<QString, QBluetoothServiceInfo> m_discoveredServices; 

52 }; 

53 

54 #endif // REMOTESELECTOR_H 

55 

remoteselector.h 翻译成远程选择器,代码也是参考 Qt 官方 btchat 例子,这个头文件定义了
开启蓝牙发现模式,蓝牙关闭发现模式,还有服务完成等等,代码有注释,请自由查看。
remoteselector.cpp 代码如下。

 /****************************************************************** 
 Copyright (C) 2015 The Qt Company Ltd. 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief remoteselector.cpp 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-20 
 *******************************************************************/ 

1 #include "remoteselector.h" 

2 

3 /* 初始化本地蓝牙 */ 

4 RemoteSelector::RemoteSelector(QBluetoothAddress &localAdapter, 
QObject *parent) 

5 : QObject(parent) 

6 { 

7 m_discoveryAgent = new 
QBluetoothServiceDiscoveryAgent(localAdapter); 

8 

9 connect(m_discoveryAgent, 
SIGNAL(serviceDiscovered(QBluetoothServiceInfo)), 

10 this, SLOT(serviceDiscovered(QBluetoothServiceInfo))); 

11 connect(m_discoveryAgent, SIGNAL(finished()), this, 
SLOT(discoveryFinished())); 

12 connect(m_discoveryAgent, SIGNAL(canceled()), this, 
SLOT(discoveryFinished())); 

13 } 

14 

15 RemoteSelector::~RemoteSelector() 

16 { 

17 delete m_discoveryAgent; 

18 } 

19 

20 /* 开启发现模式,这里无需设置过滤 uuid,否则搜索不到手机 
21 * uuid 会过滤符合条件的 uuid 服务都会返回相应的蓝牙设备 
22 */ 

23 void RemoteSelector::startDiscovery(const QBluetoothUuid &uuid) 

24 { 

25 Q_UNUSED(uuid); 

26 qDebug()<<"startDiscovery"; 

27 if (m_discoveryAgent->isActive()) { 

28 qDebug()<<"stop the searching first"; 

29 m_discoveryAgent->stop(); 

30 } 

31 

32 //m_discoveryAgent->setUuidFilter(uuid); 

33 
m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery)
; 

34 } 

35 

36 /* 停止发现 */ 

37 void RemoteSelector::stopDiscovery() 

38 { 

39 qDebug()<<"stopDiscovery"; 

40 if (m_discoveryAgent){ 

41 m_discoveryAgent->stop(); 

42 } 

43 } 

44 

45 QBluetoothServiceInfo RemoteSelector::service() const 

46 {

47 return m_serviceInfo; 

48 } 

49 

50 /* 扫描蓝牙服务信息 */ 

51 void RemoteSelector::serviceDiscovered(const QBluetoothServiceInfo 

&serviceInfo) 

52 { 

53 #if 0 

54 qDebug() << "Discovered service on" 

55 << serviceInfo.device().name() << 
serviceInfo.device().address().toString(); 

56 qDebug() << "\tService name:" << serviceInfo.serviceName(); 

57 qDebug() << "\tDescription:" 

58 << 
serviceInfo.attribute(QBluetoothServiceInfo::ServiceDescription).toStri
ng(); 

59 qDebug() << "\tProvider:" 

60 << 
serviceInfo.attribute(QBluetoothServiceInfo::ServiceProvider).toString(
); 

61 qDebug() << "\tL2CAP protocol service multiplexer:" 

62 << serviceInfo.protocolServiceMultiplexer(); 

63 qDebug() << "\tRFCOMM server channel:" << 
serviceInfo.serverChannel(); 

64 #endif 

65 

66 QMapIterator<QString, QBluetoothServiceInfo> 
i(m_discoveredServices); 

67 while (i.hasNext()){ 

68 i.next(); 

69 if (serviceInfo.device().address() == 
i.value().device().address()){ 

70 return; 

71 } 

72 } 

73 

74 QString remoteName; 

75 if (serviceInfo.device().name().isEmpty()) 

76 remoteName = serviceInfo.device().address().toString(); 

77 else 

78 remoteName = serviceInfo.device().name(); 

79 

80 qDebug()<<"adding to the list...."; 

81 qDebug()<<"remoteName: "<< remoteName; 

82 QListWidgetItem *item = 

83 new QListWidgetItem(QString::fromLatin1("%1%2") 

84 .arg(remoteName, 
serviceInfo.serviceName())); 

85 m_discoveredServices.insert(remoteName, serviceInfo); 

86 emit newServiceFound(item); 

87 } 

88 

89 /* 发现完成 */ 

90 void RemoteSelector::discoveryFinished() 

91 { 

92 qDebug()<<"discoveryFinished"; 

93 emit finished(); 

94 } 

remoteselector .cpp 是 remoteselector.h 的实现代码。主要看以下几点。
第 4~13 行,初始化本地蓝牙,实例化对象 discoveryAgent(代理对象),蓝牙主要通过本
地代理对象去扫描其他蓝牙。
第 32 行,这里官方 Qt 代码设计是过滤 uuid。只有符合对应的 uuid 的蓝牙,才会返回结果。
因为我们要扫描我们的手机,所以这里我们要把它注释掉。手机的uuid没有设置成设定的uuid,
如果设置了 uuid 过滤,手机就扫描不出了。(uuid 指的是唯一标识,手机蓝牙有很多 uuid,不
同的 uuid 有不同的作用,指示着不同的服务)。
其他代码都是一些逻辑性的代码,比较简单,请自由查看。
mainwindow.h 代码如下。

 /****************************************************************** 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief mainwindow.h 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-19 
 *******************************************************************/ 

1 #ifndef MAINWINDOW_H 

2 #define MAINWINDOW_H 

3 

4 #include <QMainWindow> 

5 #include <qbluetoothserviceinfo.h> 

6 #include <qbluetoothsocket.h> 

7 #include <qbluetoothhostinfo.h> 

8 #include <QDebug> 

9 #include <QTabWidget> 

10 #include <QHBoxLayout>

11 #include <QVBoxLayout> 

12 #include <QPushButton> 

13 #include <QListWidget> 

14 #include <QTextBrowser> 

15 #include <QLineEdit> 

16 

17 class ChatServer; 

18 class ChatClient; 

19 class RemoteSelector; 

20 

21 class MainWindow : public QMainWindow 

22 { 

23 Q_OBJECT 

24 

25 public: 

26 MainWindow(QWidget *parent = nullptr); 

27 ~MainWindow(); 

28 

29 public: 

30 /* 暴露的接口,主动连接设备*/ 

31 Q_INVOKABLE void connectToDevice(); 

32 

33 signals: 

34 /* 发送消息信号 */ 

35 void sendMessage(const QString &message); 

36 

37 /* 连接断开信号 */ 

38 void disconnect(); 

39 

40 /* 发现完成信号 */ 

41 void discoveryFinished(); 

42 

43 /* 找到新服务信号 */ 

44 void newServicesFound(const QStringList &list); 

45 

46 public slots: 

47 /* 停止搜索 */ 

48 void searchForDevices(); 

49 

50 /* 开始搜索 */ 

51 void stopSearch(); 

52 

53 /* 找到新服务 */ 

54 void newServiceFound(QListWidgetItem*); 

55 

56 /* 已连接 */ 

57 void connected(const QString &name); 

58 

59 /* 显示消息 */ 

60 void showMessage(const QString &sender, const QString &message); 

61 

62 /* 发送消息 */ 

63 void sendMessage(); 

64 

65 /* 作为客户端断开连接 */ 

66 void clientDisconnected(); 

67 

68 /* 主动断开连接 */ 

69 void toDisconnected(); 

70 

71 /* 作为服务端时,客户端断开连接 */ 

72 void disconnected(const QString &name); 

73 

74 private: 

75 /* 选择本地蓝牙 */ 

76 int adapterFromUserSelection() const; 

77 

78 /* 本地蓝牙的 Index */ 

79 int currentAdapterIndex; 

80 

81 /* 蓝牙本地适配器初始化 */ 

82 void localAdapterInit(); 

83 

84 /* 布局初始化 */ 

85 void layoutInit(); 

86 

87 /* 服务端*/ 

88 ChatServer *server; 

89 

90 /* 多个客户端 */ 

91 QList<ChatClient *> clients; 

92 

93 /* 远程选择器,使用本地蓝牙去搜索蓝牙,可过滤蓝牙等 */ 

94 RemoteSelector *remoteSelector; 

95 

96 /* 本地蓝牙 */ 

97 QList<QBluetoothHostInfo> localAdapters; 

98 

99 /* 本地蓝牙名称 */ 

100 QString localName; 

101 

102 /* tabWidget 视图,用于切换页面 */ 

103 QTabWidget *tabWidget; 

104 

105 /* 3 个按钮,扫描按钮,连接按钮,发送按钮 */ 

106 QPushButton *pushButton[5]; 

107 

108 /* 2 个垂直布局,一个用于页面一,另一个用于页面二 */ 

109 QVBoxLayout *vBoxLayout[2]; 

110 

111 /* 2 个水平布局,一个用于页面一,另一个用于页面二 */ 

112 QHBoxLayout *hBoxLayout[2]; 

113 

114 /* 页面一和页面二容器 */ 

115 QWidget *pageWidget[2]; 

116 

117 /* 用于布局, pageWidget 包含 subWidget */ 

118 QWidget *subWidget[2]; 

119 

120 /* 蓝牙列表 */ 

121 QListWidget *listWidget; 

122 

123 /* 显示对话的内容 */ 

124 QTextBrowser *textBrowser; 

125 

126 /* 发送消息输入框 */ 

127 QLineEdit *lineEdit; 

128 

129 }; 

130 #endif // MAINWINDOW_H 

mainwindow.h 是整个代码重要的文件,这里使用了客户端类,服务端类和远程服务端类。
前面介绍的客户端类,服务端类和远程服务端类都是为 mainwindow.h 服务的。我们在编程的时
候可以不用改动客户端类,服务端类和远程服务端类了,直接像 mainwindow.h 一样使用它们的
接口就可以编程了。

其中笔者还在 mainwindow.h 使用了很多控件,这些控件都是界面组成的重要元素。并不复
杂,如果看不懂界面布局,或者理解不了界面布局,请回到本教程的第七章学习基础,本教程
不再一一说明这种简单的布局了。
mainwindow.cpp 代码如下。

 /****************************************************************** 
 Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved. 
 * @projectName 06_bluetooth_chat 
 * @brief mainwindow.cpp 
 * @author Deng Zhimao 
 * @email 1252699831@qq.com 
 * @net www.openedv.com 
 * @date 2021-03-19 
 *******************************************************************/ 

1 #include "mainwindow.h" 

2 #include "remoteselector.h" 

3 #include "chatserver.h" 

4 #include "chatclient.h" 

5 #include <qbluetoothuuid.h> 

6 #include <qbluetoothserver.h> 

7 #include <qbluetoothservicediscoveryagent.h> 

8 #include <qbluetoothdeviceinfo.h> 

9 #include <qbluetoothlocaldevice.h> 

10 #include <QGuiApplication> 

11 #include <QScreen> 

12 #include <QRect> 

13 #include <QTimer> 

14 #include <QDebug> 

15 #include <QTabBar> 

16 #include <QHeaderView> 

17 #include <QTableView> 

18 

19 

20 static const QLatin1String 

21 serviceUuid("e8e10f95-1a70-4b27-9ccf-02010264e9c8"); 

22 

23 MainWindow::MainWindow(QWidget *parent) 

24 : QMainWindow(parent) 

25 { 

26 /* 本地蓝牙初始化 */ 

27 localAdapterInit(); 

28 

29 /* 界面布局初始化 */ 

30 layoutInit(); 

31 } 

32 

33 MainWindow::~MainWindow() 

34 { 

35 qDeleteAll(clients); 

36 delete server; 

37 } 

38 

39 /* 初始化本地蓝牙,作为服务端 */ 

40 void MainWindow::localAdapterInit() 

41 { 

42 /* 查找本地蓝牙的个数 */ 

43 localAdapters = QBluetoothLocalDevice::allDevices(); 

44 qDebug() << "localAdapter: " << localAdapters.count(); 

45 

46 QBluetoothLocalDevice localDevice; 

47 
localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable); 

48 

49 QBluetoothAddress adapter = QBluetoothAddress(); 

50 remoteSelector = new RemoteSelector(adapter, this); 

51 connect(remoteSelector, 

52 SIGNAL(newServiceFound(QListWidgetItem*)), 

53 this, SLOT(newServiceFound(QListWidgetItem*))); 

54 

55 /* 初始化服务端 */ 

56 server = new ChatServer(this); 

57 

58 connect(server, SIGNAL(clientConnected(QString)), 

59 this, SLOT(connected(QString))); 

60 

61 connect(server, SIGNAL(clientDisconnected(QString)), 

62 this, SLOT(disconnected(QString))); 

63 

64 connect(server, SIGNAL(messageReceived(QString, QString)), 

65 this, SLOT(showMessage(QString, QString))); 

66 

67 connect(this, SIGNAL(sendMessage(QString)), 

68 server, SLOT(sendMessage(QString))); 

69 

70 connect(this, SIGNAL(disconnect()), 

71 server, SLOT(disconnect())); 

72 

73 server->startServer(); 

74 

75 /* 获取本地蓝牙的名称 */ 

76 localName = QBluetoothLocalDevice().name(); 

77 } 

78 

79 void MainWindow::layoutInit() 

80 { 

81 /* 获取屏幕的分辨率,Qt 官方建议使用这 
82 * 种方法获取屏幕分辨率,防上多屏设备导致对应不上 
83 * 注意,这是获取整个桌面系统的分辨率 
84 */ 

85 QList <QScreen *> list_screen = QGuiApplication::screens(); 

86 

87 /* 如果是 ARM 平台,直接设置大小为屏幕的大小 */ 

88 #if __arm__ 

89 /* 重设大小 */ 

90 this->resize(list_screen.at(0)->geometry().width(), 

91 list_screen.at(0)->geometry().height()); 

92 #else 

93 /* 否则则设置主窗体大小为 800x480 */ 

94 this->resize(800, 480); 

95 #endif 

96 

97 /* 主视图 */ 

98 tabWidget = new QTabWidget(this); 

99 

100 /* 设置主窗口居中视图为 tabWidget */ 

101 setCentralWidget(tabWidget); 

102 

103 /* 页面一对象实例化 */ 

104 vBoxLayout[0] = new QVBoxLayout(); 

105 hBoxLayout[0] = new QHBoxLayout(); 

106 pageWidget[0] = new QWidget(); 

107 subWidget[0] = new QWidget(); 

108 listWidget = new QListWidget(); 

109 /* 0 为扫描按钮,1 为连接按钮 */ 

110 pushButton[0] = new QPushButton(); 

111 pushButton[1] = new QPushButton(); 

112 pushButton[2] = new QPushButton(); 

113 pushButton[3] = new QPushButton(); 

114 pushButton[4] = new QPushButton(); 

115 

116 /* 页面二对象实例化 */ 

117 hBoxLayout[1] = new QHBoxLayout(); 

118 vBoxLayout[1] = new QVBoxLayout(); 

119 subWidget[1] = new QWidget(); 

120 textBrowser = new QTextBrowser(); 

121 lineEdit = new QLineEdit(); 

122 pushButton[2] = new QPushButton(); 

123 pageWidget[1] = new QWidget(); 

124 

125 

126 tabWidget->addTab(pageWidget[1], "蓝牙聊天"); 

127 tabWidget->addTab(pageWidget[0], "蓝牙列表"); 

128 

129 /* 页面一 */ 

130 vBoxLayout[0]->addWidget(pushButton[0]); 

131 vBoxLayout[0]->addWidget(pushButton[1]); 

132 vBoxLayout[0]->addWidget(pushButton[2]); 

133 vBoxLayout[0]->addWidget(pushButton[3]); 

134 subWidget[0]->setLayout(vBoxLayout[0]); 

135 hBoxLayout[0]->addWidget(listWidget); 

136 hBoxLayout[0]->addWidget(subWidget[0]); 

137 pageWidget[0]->setLayout(hBoxLayout[0]); 

138 pushButton[0]->setMinimumSize(120, 40); 

139 pushButton[1]->setMinimumSize(120, 40); 

140 pushButton[2]->setMinimumSize(120, 40); 

141 pushButton[3]->setMinimumSize(120, 40); 

142 pushButton[0]->setText("开始扫描"); 

143 pushButton[1]->setText("停止扫描"); 

144 pushButton[2]->setText("连接"); 

145 pushButton[3]->setText("断开"); 

146 

147 /* 页面二 */ 

148 hBoxLayout[1]->addWidget(lineEdit); 

149 hBoxLayout[1]->addWidget(pushButton[4]); 

150 subWidget[1]->setLayout(hBoxLayout[1]); 

151 vBoxLayout[1]->addWidget(textBrowser); 

152 vBoxLayout[1]->addWidget(subWidget[1]); 

153 pageWidget[1]->setLayout(vBoxLayout[1]); 

154 pushButton[4]->setMinimumSize(120, 40); 

155 pushButton[4]->setText("发送"); 

156 lineEdit->setMinimumHeight(40); 

157 lineEdit->setText("正点原子论坛网址 www.openedv.com"); 

158 
159 /* 设置表头的大小 */ 

160 QString str = tr("QTabBar::tab {height:40; width:%1};") 

161 .arg(this->width()/2); 

162 tabWidget->setStyleSheet(str); 

163 

164 /* 开始搜寻蓝牙 */ 

165 connect(pushButton[0], SIGNAL(clicked()), 

166 this, SLOT(searchForDevices())); 

167 

168 /* 停止搜寻蓝牙 */ 

169 connect(pushButton[1], SIGNAL(clicked()), 

170 this, SLOT(stopSearch())); 

171 

172 /* 点击连接按钮,本地蓝牙作为客户端去连接外界的服务端 */ 

173 connect(pushButton[2], SIGNAL(clicked()), 

174 this, SLOT(connectToDevice())); 

175 

176 /* 点击断开连接按钮,断开连接 */ 

177 connect(pushButton[3], SIGNAL(clicked()), 

178 this, SLOT(toDisconnected())); 

179 

180 /* 发送消息 */ 

181 connect(pushButton[4], SIGNAL(clicked()), 

182 this, SLOT(sendMessage())); 

183 } 

184 

185 /* 作为客户端去连接 */ 

186 void MainWindow::connectToDevice() 

187 { 

188 if (listWidget->currentRow() == -1) 

189 return; 

190 

191 QString name = listWidget->currentItem()->text(); 

192 qDebug() << "Connecting to " << name; 

193 

194 // Trying to get the service 

195 QBluetoothServiceInfo service; 

196 QMapIterator<QString,QBluetoothServiceInfo> 

197 i(remoteSelector->m_discoveredServices); 

198 bool found = false; 

199 while (i.hasNext()){ 

200 i.next(); 

201202 QString key = i.key(); 

203 

204 /* 判断连接的蓝牙名称是否在发现的设备里 */ 

205 if (key == name) { 

206 qDebug() << "The device is found"; 

207 service = i.value(); 

208 qDebug() << "value: " << i.value().device().address(); 

209 found = true; 

210 break; 

211 } 

212 } 

213 

214 /* 如果找到,则连接设备 */ 

215 if (found) { 

216 qDebug() << "Going to create client"; 

217 ChatClient *client = new ChatClient(this); 

218 qDebug() << "Connecting..."; 

219 

220 connect(client, SIGNAL(messageReceived(QString,QString)), 

221 this, SLOT(showMessage(QString,QString))); 

222 connect(client, SIGNAL(disconnected()), 

223 this, SLOT(clientDisconnected()));; 

224 connect(client, SIGNAL(connected(QString)), 

225 this, SLOT(connected(QString))); 

226 connect(this, SIGNAL(sendMessage(QString)), 

227 client, SLOT(sendMessage(QString))); 

228 connect(this, SIGNAL(disconnect()), 

229 client, SLOT(disconnect())); 

230 

231 qDebug() << "Start client"; 

232 client->startClient(service); 

233 

234 clients.append(client); 

235 } 

236 } 

237 

238 /* 本地蓝牙选择,默认使用第一个蓝牙 */ 

239 int MainWindow::adapterFromUserSelection() const 

240 { 

241 int result = 0; 

242 QBluetoothAddress newAdapter = localAdapters.at(0).address(); 

243 return result; 

244 } 

245 

246 /* 开始搜索 */ 

247 void MainWindow::searchForDevices() 

248 { 

249 /* 先清空 */ 

250 listWidget->clear(); 

251 qDebug() << "search for devices!"; 

252 if (remoteSelector) { 

253 delete remoteSelector; 

254 remoteSelector = NULL; 

255 } 

256 

257 QBluetoothAddress adapter = QBluetoothAddress(); 

258 remoteSelector = new RemoteSelector(adapter, this); 

259 

260 connect(remoteSelector, 

261 SIGNAL(newServiceFound(QListWidgetItem*)), 

262 this, SLOT(newServiceFound(QListWidgetItem*))); 

263 

264 remoteSelector->m_discoveredServices.clear(); 

265 remoteSelector->startDiscovery(QBluetoothUuid(serviceUuid)); 

266 connect(remoteSelector, SIGNAL(finished()), 

267 this, SIGNAL(discoveryFinished())); 

268 } 

269 

270 /* 停止搜索 */ 

271 void MainWindow::stopSearch() 

272 { 

273 qDebug() << "Going to stop discovery..."; 

274 if (remoteSelector) { 

275 remoteSelector->stopDiscovery(); 

276 } 

277 } 

278 

279 /* 找到蓝牙服务 */ 

280 void MainWindow::newServiceFound(QListWidgetItem *item) 

281 { 

282 /* 设置项的大小 */ 

283 item->setSizeHint(QSize(listWidget->width(), 50)); 

284 

285 /* 添加项 */ 

286 listWidget->addItem(item); 

287 

288 /* 设置当前项 */ 

289 listWidget->setCurrentRow(listWidget->count() - 1); 

290 

291 qDebug() << "newServiceFound"; 

292 

293 // get all of the found devices 

294 QStringList list; 

295 

296 QMapIterator<QString, QBluetoothServiceInfo> 

297 i(remoteSelector->m_discoveredServices); 

298 while (i.hasNext()){ 

299 i.next(); 

300 qDebug() << "key: " << i.key(); 

301 qDebug() << "value: " << i.value().device().address(); 

302 list << i.key(); 

303 } 

304 

305 qDebug() << "list count: " << list.count(); 

306 

307 emit newServicesFound(list); 

308 } 

309 

310 /* 已经连接 */ 

311 void MainWindow::connected(const QString &name) 

312 { 

313 textBrowser->insertPlainText(tr("%1:已连接\n").arg(name)); 

314 tabWidget->setCurrentIndex(0); 

315 textBrowser->moveCursor(QTextCursor::End); 

316 } 

317 

318 /* 接收消息 */ 

319 void MainWindow::showMessage(const QString &sender, 

320 const QString &message) 

321 { 

322 textBrowser->insertPlainText(QString::fromLatin1("%1: %2\n") 

323 .arg(sender, message)); 

324 tabWidget->setCurrentIndex(0); 

325 textBrowser->moveCursor(QTextCursor::End); 

326 } 

327 

328 /* 发送消息 */ 

329 void MainWindow::sendMessage() 

330 { 

331 showMessage(localName, lineEdit->text()); 

332 emit sendMessage(lineEdit->text()); 

333 } 

334 

335 /* 作为客户端断开连接 */ 

336 void MainWindow::clientDisconnected() 

337 { 

338 ChatClient *client = qobject_cast<ChatClient *>(sender()); 

339 if (client) { 

340 clients.removeOne(client); 

341 client->deleteLater(); 

342 } 

343 

344 tabWidget->setCurrentIndex(0); 

345 textBrowser->moveCursor(QTextCursor::End); 

346 } 

347 

348 /* 主动断开连接 */ 

349 void MainWindow::toDisconnected() 

350 { 

351 emit disconnect(); 

352 textBrowser->moveCursor(QTextCursor::End); 

353 tabWidget->setCurrentIndex(0); 

354 } 

355 

356 /* 作为服务端时,客户端断开连接 */ 

357 void MainWindow::disconnected(const QString &name) 

358 { 

359 textBrowser->insertPlainText(tr("%1:已断开\n").arg(name)); 

360 tabWidget->setCurrentIndex(0); 

361 textBrowser->moveCursor(QTextCursor::End); 

362 } 

mainwindow.cpp 则是整个项目的核心文件,包括处理界面点击的事件,客户端连接,服务
端连接,扫描蓝牙,断开蓝牙和连接蓝牙等。设计这样的一个逻辑界面并不难,只要我们前面
第七章 Qt 控件打下了基础。上面的代码注释详细,请自由查看。

程序运行效果

本例程运行后,默认开启蓝牙的服务端模式,可以用手机安装蓝牙调试软件(安卓手机如
蓝牙调试宝、蓝牙串口助手)。当我们点击蓝牙列表页面时,点击扫描后请等待扫描的结果,选
中需要连接的蓝牙再点击连接。

下面程序效果是 Ubuntu 虚拟机上连接 USB 蓝牙模块,用手机连接后运行的蓝牙聊天第一
页效果图。

在这里插入图片描述
下面程序效果是 Ubuntu 虚拟机上连接 USB 蓝牙模块运行的蓝牙聊天第二页效果图。在这里插入图片描述
安卓手机可以用蓝牙调试宝等软件进行配对连接。IOS 手机请下载某些蓝牙调试软件测试
即可。手机接收到的消息如下。在这里插入图片描述
在笔者测试的过程中,发现在 Ubuntu 上运行蓝牙聊天程序不太好用,需要开启扫描后,才
能连接得上,而且接收的消息反应比较慢,有可能是虚拟机的原因吧。不过在正点原子 I.MX6U

开发板上运行没有问题。先按照详细请看【正点原子】I.MX6U 用户快速体验 V1.x.pdf 的第 3.29

小节蓝牙测试开启蓝牙,启用蓝牙被扫描后,先进行配对,手机用蓝牙调试软件就可以连接上
进行聊天了。
注意:本程序需要在确保蓝牙能正常使用的情况下才能运行,默认使用第一个蓝牙,如果

Ubuntu 上查看有两个蓝牙,请不要插着 USB 蓝牙启动电脑,先等 Ubuntu 启动后再插蓝牙模块。
连接前应先配对,连接不上的原因可能或者蓝牙质量问题,或者系统里的软件没有开启蓝牙,
或者使用的手机蓝牙调试软件不支持 SPP(串行端口)蓝牙调试等,请退出重试等。程序仅供
学习与参考。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ManGo CHEN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值