警告

本节包含自动从 C++ 翻译到 Python 的代码片段,可能包含错误。

蓝牙聊天#

展示通过 RFCOMM 协议使用蓝牙进行通信。

蓝牙聊天示例展示了如何使用 Qt Bluetooth API 通过蓝牙 RFCOMM 协议与远程设备上的另一个应用程序进行通信。

../_images/btchat-example.png

蓝牙聊天示例实现了一个简单的多人聊天程序。应用程序始终作为服务器和客户端,无需确定谁应该连接到谁。

运行示例#

要从 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、发现性属性和连接参数的服务记录。

服务的文本描述存储在 ServiceNameServiceDescriptionServiceProvider 属性中。

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()

响应ChatServerclientConnected()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)

响应来自ChatClientconnected()信号时,我们在聊天会话中显示“已加入与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()

示例项目 @ code.qt.io