警告
本节包含自动从 C++ 翻译到 Python 的代码片段,可能包含错误。
蓝牙聊天#
展示通过 RFCOMM 协议使用蓝牙进行通信。
蓝牙聊天示例展示了如何使用 Qt Bluetooth API 通过蓝牙 RFCOMM 协议与远程设备上的另一个应用程序进行通信。
蓝牙聊天示例实现了一个简单的多人聊天程序。应用程序始终作为服务器和客户端,无需确定谁应该连接到谁。
运行示例#
要从 Qt Creator 运行此示例,请打开欢迎模式并从示例中选择示例。有关更多信息,请访问构建和运行示例。
聊天服务器#
聊天服务器通过 ChatServer
类实现。类 ChatServer
被声明为
class ChatServer(QObject): Q_OBJECT # public ChatServer = explicit(QObject parent = None) ~ChatServer() def startServer(QBluetoothAddress()): def stopServer(): # public slots def sendMessage(message): # signals def messageReceived(sender, message): def clientConnected(name): def clientDisconnected(name): # private slots def clientConnected(): def clientDisconnected(): def readSocket(): # private rfcommServer = None serviceInfo = QBluetoothServiceInfo() *> = QList<QBluetoothSocket() *, = QMap<QBluetoothSocket()
聊天服务器需要做的第一件事是创建一个 QBluetoothServer
实例以监听传入的蓝牙连接。每当创建新的连接时,都会调用 clientConnected()
插槽。
rfcommServer = QBluetoothServer(QBluetoothServiceInfo.RfcommProtocol, self) rfcommServer.newConnection.connect( self, QOverload<>.of(ChatServer.clientConnected)) result = rfcommServer.listen(localAdapter) if not result: qWarning() << "Cannot bind chat server to" << localAdapter.toString() return
聊天服务器只有当其他设备知道它的存在时才有用。为了使其他设备能够发现它,需要在系统 SDP(服务发现协议)数据库中发布描述该服务的记录。类 QBluetoothServiceInfo
封装了一个服务记录。
我们将发布一个包含一些服务文本描述、唯一标识服务的 UUID、发现性属性和连接参数的服务记录。
服务的文本描述存储在 ServiceName
、ServiceDescription
和 ServiceProvider
属性中。
serviceInfo.setAttribute(QBluetoothServiceInfo.ServiceName, tr("Bt Chat Server")) serviceInfo.setAttribute(QBluetoothServiceInfo.ServiceDescription, tr("Example bluetooth chat server")) serviceInfo.setAttribute(QBluetoothServiceInfo.ServiceProvider, tr("qt-project.org"))
蓝牙使用 UUID 作为唯一标识符。聊天服务使用随机生成的 UUID。
staticexpr auto serviceUuid = "e8e10f95-1a70-4b27-9ccf-02010264e9c8" serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid))
蓝牙服务只有在代码 PublicBrowseGroup
中时才是可发现的。
groupUuid = QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.PublicBrowseGroup) QBluetoothServiceInfo.Sequence publicBrowse publicBrowse << QVariant.fromValue(groupUuid) serviceInfo.setAttribute(QBluetoothServiceInfo.BrowseGroupList, publicBrowse)
使用 ProtocolDescriptorList
属性发布远程设备连接到我们的服务所需的连接参数。在此处,我们指定使用 Rfcomm
协议并将端口号设置为我们的 rfcommServer
实例正在监听的端口号。
QBluetoothServiceInfo.Sequence protocolDescriptorList QBluetoothServiceInfo.Sequence protocol protocol << QVariant.fromValue(QBluetoothUuid(QBluetoothUuid.ProtocolUuid.L2cap)) protocolDescriptorList.append(QVariant.fromValue(protocol)) protocol.clear() protocol << QVariant.fromValue(QBluetoothUuid(QBluetoothUuid.ProtocolUuid.Rfcomm)) << QVariant.fromValue(quint8(rfcommServer.serverPort())) protocolDescriptorList.append(QVariant.fromValue(protocol)) serviceInfo.setAttribute(QBluetoothServiceInfo.ProtocolDescriptorList, protocolDescriptorList)
最后,我们将服务记录注册到系统中。
serviceInfo.registerService(localAdapter)
如前所述,传入连接由clientConnected()
槽处理,这里有挂起的连接与readyRead()
和disconnected()
信号连接。这些信号通知其他人有新客户端已连接。
def clientConnected(self): socket = rfcommServer.nextPendingConnection() if not socket: return socket.readyRead.connect(self.readSocket) socket.disconnected.connect( self, QOverload<>.of(ChatServer.clientDisconnected)) clientSockets.append(socket) clientNames[socket] = socket.peerName() clientConnected.emit(socket.peerName())
每当从客户端套接字中有数据可以读取时,都会调用readSocket()
槽。该槽从套接字读取单独的行,将它们从UTF-8转换,并发射messageReceived()
信号。
def readSocket(self): socket = QBluetoothSocket(sender()) if not socket: return while socket.canReadLine(): line = socket.readLine().trimmed() QString.fromUtf8(line.constData(), line.length()))
每当客户端从服务断开连接时,都会调用clientDisconnected()
槽。该槽发射一个信号以通知其他人客户端已断开连接,并删除套接字。
def clientDisconnected(self): socket = QBluetoothSocket(sender()) if not socket: return clientDisconnected.emit(clientNames[socket]) clientSockets.removeOne(socket) clientNames.remove(socket) socket.deleteLater()
使用sendMessage()
槽向所有连接的客户端发送消息。消息被转换为UTF-8,并在发送给所有客户端之前附加一个换行符。
def sendMessage(self, message): text = message.toUtf8() + '\n' for socket in clientSockets: socket.write(text)
当聊天服务器停止时,将从系统SDP数据库中删除服务记录,删除所有连接的客户端套接字,并删除rfcommServer
实例。
def stopServer(self): # Unregister service serviceInfo.unregisterService() # Close sockets qDeleteAll(clientSockets) clientNames.clear() # Close server del rfcommServer rfcommServer = None
服务发现#
在连接到服务器之前,客户端需要扫描附近的设备并搜索发布聊天服务的设备。这通过RemoteSelector
类来完成。
为了开始服务查找,RemoteSelector
创建一个QBluetoothServiceDiscoveryAgent
实例并将其信号连接。
m_discoveryAgent = QBluetoothServiceDiscoveryAgent(localAdapter) m_discoveryAgent.serviceDiscovered.connect( self.serviceDiscovered) m_discoveryAgent.finished.connect( self.discoveryFinished) m_discoveryAgent.canceled.connect( self.discoveryFinished)
设置一个UUID过滤器,以便服务发现只显示广告所需服务的设备。之后启动FullDiscovery
。
m_discoveryAgent.setUuidFilter(uuid) m_discoveryAgent.start(QBluetoothServiceDiscoveryAgent.FullDiscovery)
当检测到匹配的服务时,会发射一个带有QBluetoothServiceInfo
实例的serviceDiscovered()
信号。这个服务信息用于提取设备名称和服务名称,并将新条目添加到已发现远程设备列表中。
remoteName = QString() if serviceInfo.device().name().isEmpty(): remoteName = address.toString() else: remoteName = serviceInfo.device().name() item = QListWidgetItem("%1 %2".arg(remoteName, serviceInfo.serviceName())) m_discoveredServices.insert(item, serviceInfo) ui.remoteDevices.addItem(item)
之后,用户可以从列表中选择一个设备并尝试连接。
聊天客户端#
聊天客户端是通过ChatClient
类实现的。该ChatClient
类声明为
class ChatClient(QObject): Q_OBJECT # public ChatClient = explicit(QObject parent = None) ~ChatClient() def startClient(remoteService): def stopClient(): # public slots def sendMessage(message): # signals def messageReceived(sender, message): def connected(name): def disconnected(): def socketErrorOccurred(errorString): # private slots def readSocket(): def connected(): def onSocketErrorOccurred(QBluetoothSocket.SocketError): # private socket = None
客户端创建一个新的QBluetoothSocket
并连接到由参数remoteService
描述的远程服务。将槽(slots)连接到socket的readyRead()、connected()
和disconnected()
信号。
def startClient(self, remoteService): if socket: return # Connect to service socket = QBluetoothSocket(QBluetoothServiceInfo.RfcommProtocol) print("Create socket") socket.connectToService(remoteService) print("ConnectToService done") socket.readyRead.connect(self.readSocket) socket.connected.connect(this, QOverload<>::of(&ChatClient::connected)) socket.disconnected.connect(self.disconnected) socket.errorOccurred.connect(self.onSocketErrorOccurred)
成功连接socket后,将发出一个信号通知其他用户。
def connected(self): connected.emit(socket.peerName())
与聊天服务器类似,当从socket可用数据时,会调用readSocket()
槽。逐行读取并转换为UTF-8。发出messageReceived()
信号。
def readSocket(self): if not socket: return while socket.canReadLine(): line = socket.readLine().trimmed() messageReceived.emit(socket.peerName() QString.fromUtf8(line.constData(), line.length()))
使用sendMessage()
槽向远程设备发送消息。消息转换为UTF-8并追加换行符。
def sendMessage(self, message): text = message.toUtf8() + '\n' socket.write(text)
要断开与远程聊天服务的连接,将QBluetoothSocket
实例删除。
def stopClient(self): del socket socket = None
聊天对话框#
本示例的主窗口是聊天对话框,在Chat
类中实现。这个类显示单个ChatServer
和零个或多个ChatClient
之间的聊天会话。Chat
类声明如下:
class Chat(QDialog): Q_OBJECT # public Chat = explicit(QWidget parent = None) ~Chat() # signals def sendMessage(message): # private slots def connectClicked(): def sendClicked(): def showMessage(sender, message): def clientConnected(name): def clientDisconnected(name): def clientDisconnected(): def connected(name): def reactOnSocketError(error): def newAdapterSelected(): def initBluetooth(): def updateIcons(scheme): # private adapterFromUserSelection = int() currentAdapterIndex = 0 Ui.Chat ui server = None *> = QList<ChatClient() localAdapters = QList() localName = QString()
首先我们构建用户界面
ui.setupUi(self) ui.connectButton.clicked.connect(self.connectClicked) ui.sendButton.clicked.connect(self.sendClicked)
我们创建一个ChatServer
实例,并响应其clientConnected()
、clientDiconnected()
和messageReceived()
信号。
server = ChatServer(self) connect(server, QOverload<QString >.of(ChatServer.clientConnected), self.clientConnected) connect(server, QOverload<QString >.of(ChatServer.clientDisconnected), self, QOverload<QString >.of(Chat.clientDisconnected)) server.messageReceived.connect( self.showMessage) self.sendMessage.connect(server.sendMessage) server.startServer()
响应ChatServer
的clientConnected()
和clientDisconnected()
信号,我们在聊天会话中显示典型的“X已加入聊天。”和“Y已离开。”消息。
def clientConnected(self, name): ui.chat.insertPlainText("%1 has joined chat.\n".arg(name)) def clientDisconnected(self, name): ui.chat.insertPlainText("%1 has left.\n".arg(name))
来自连接到ChatServer
的客户端的传入消息在showMessage()
槽中进行处理。带有远程设备名称标记的消息文本在聊天会话中显示。
def showMessage(self, sender, message): ui.chat.moveCursor(QTextCursor.End) ui.chat.insertPlainText("%1: %2\n".arg(sender, message)) ui.chat.ensureCursorVisible()
响应连接按钮被点击,应用程序开始服务发现,并在远程设备上显示发现的聊天服务列表。用户选择要使用的服务对应的ChatClient
。
def connectClicked(self): ui.connectButton.setEnabled(False) # scan for services adapter = localAdapters.isEmpty() ? QBluetoothAddress() : localAdapters.at(currentAdapterIndex).address() remoteSelector = RemoteSelector(adapter) #ifdef Q_OS_ANDROID # QTBUG-61392 Q_UNUSED(serviceUuid) remoteSelector.startDiscovery(QBluetoothUuid(reverseUuid)) #else remoteSelector.startDiscovery(QBluetoothUuid(serviceUuid)) #endif if remoteSelector.exec() == QDialog.Accepted: service = remoteSelector.service() print("Connecting to service", service.serviceName()) << "on" << service.device().name() # Create client client = ChatClient(self) client.messageReceived.connect( self.showMessage) client.disconnected.connect( self, QOverload<>.of(Chat.clientDisconnected)) connect(client, QOverload<QString >.of(ChatClient.connected), self.connected) client.socketErrorOccurred.connect( self.reactOnSocketError) self.sendMessage.connect(client.sendMessage) client.startClient(service) clients.append(client) ui.connectButton.setEnabled(True)
响应来自ChatClient
的connected()
信号时,我们在聊天会话中显示“已加入与X的聊天。”消息。
def connected(self, name): ui.chat.insertPlainText("Joined chat with %1.\n".arg(name))
通过发出sendMessage()
信号,将消息发送到所有远程设备。
def sendClicked(self): ui.sendButton.setEnabled(False) ui.sendText.setEnabled(False) showMessage(localName, ui.sendText.text()) sendMessage.emit(ui.sendText.text()) ui.sendText.clear() ui.sendText.setEnabled(True) ui.sendButton.setEnabled(True) #if defined(Q_OS_ANDROID) or defined(Q_OS_IOS) # avoid keyboard automatically popping up again on mobile devices ui.sendButton.setFocus() #else ui.sendText.setFocus() #endif
当远程设备强制断开连接时,我们需要清理ChatClient
实例。
def clientDisconnected(self): client = ChatClient(sender()) if client: clients.removeOne(client) client.deleteLater()