Warning

This section contains snippets that were automatically translated from C++ to Python and may contain errors.

Bluetooth Chat#

Shows communication through Bluetooth using RFCOMM protocol.

The Bluetooth Chat example shows how to use the Qt Bluetooth API to communicate with another application on a remote device using Bluetooth RFCOMM protocol.

../_images/btchat-example.png

The Bluetooth Chat example implements a simple chat program between multiple parties. The application always acts as both a server and a client eliminating the need to determine who should connect to whom.

Running the Example#

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.

Chat Server#

The chat server is implemented by the ChatServer class. The ChatServer class is declared as:

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

The first thing the chat server needs to do is create an instance of QBluetoothServer to listen for incoming Bluetooth connections. The clientConnected() slot will be called whenever a new connection is created.

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

The chat server is only useful if others know that it is there. To enable other devices to discover it, a record describing the service needs to be published in the system’s SDP (Service Discovery Protocol) database. The QBluetoothServiceInfo class encapsulates a service record.

We will publish a service record that contains some textual descriptions of the services, a UUID that uniquely identifies the service, the discoverability attribute, and connection parameters.

The textual description of the service is stored in the ServiceName, ServiceDescription, and ServiceProvider attributes.

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

Bluetooth uses UUIDs as unique identifiers. The chat service uses a randomly generated UUID.

staticexpr auto serviceUuid = "e8e10f95-1a70-4b27-9ccf-02010264e9c8"
serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid))

A Bluetooth service is only discoverable if it is in the PublicBrowseGroup .

groupUuid = QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.PublicBrowseGroup)
QBluetoothServiceInfo.Sequence publicBrowse
publicBrowse << QVariant.fromValue(groupUuid)
serviceInfo.setAttribute(QBluetoothServiceInfo.BrowseGroupList, publicBrowse)

The ProtocolDescriptorList attribute is used to publish the connection parameters that the remote device requires to connect to our service. Here we specify that the Rfcomm protocol is used and set the port number to the port that our rfcommServer instance is listening to.

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)

Finally, we register the service record with the system.

serviceInfo.registerService(localAdapter)

As mentioned earlier, incoming connections are handled in the clientConnected() slot where pending connections are connected to the readyRead() and disconnected() signals. The signals notify others that a new client has connected.

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

The readSocket() slot is called whenever data is ready to be read from a client socket. The slot reads individual lines from the socket, converts them from UTF-8, and emits the messageReceived() signal.

def readSocket(self):

    socket = QBluetoothSocket(sender())
    if not socket:
        return
    while socket.canReadLine():
        line = socket.readLine().trimmed()

                             QString.fromUtf8(line.constData(), line.length()))

The clientDisconnected() slot is called whenever a client disconnects from the service. The slot emits a signal to notify others that a client has disconnected, and deletes the socket.

def clientDisconnected(self):

    socket = QBluetoothSocket(sender())
    if not socket:
        return
    clientDisconnected.emit(clientNames[socket])
    clientSockets.removeOne(socket)
    clientNames.remove(socket)
    socket.deleteLater()

The sendMessage() slot is used to send a message to all connected clients. The message is converted into UTF-8 and appended with a newline before being sent to all clients.

def sendMessage(self, message):

    text = message.toUtf8() + '\n'
    for socket in clientSockets:
        socket.write(text)

When the chat server is stopped, the service record is removed from the system SDP database, all connected client sockets are deleted, and the rfcommServer instance is deleted.

def stopServer(self):

    # Unregister service
    serviceInfo.unregisterService()
    # Close sockets
    qDeleteAll(clientSockets)
    clientNames.clear()
    # Close server
    del rfcommServer
    rfcommServer = None

Service Discovery#

Before connecting to the server, the client needs to scan the nearby devices and search for the device that is advertising the chat service. This is done by the RemoteSelector class.

To start service lookup, the RemoteSelector creates an instance of QBluetoothServiceDiscoveryAgent and connects to its signals.

m_discoveryAgent = QBluetoothServiceDiscoveryAgent(localAdapter)
m_discoveryAgent.serviceDiscovered.connect(
        self.serviceDiscovered)
m_discoveryAgent.finished.connect(
        self.discoveryFinished)
m_discoveryAgent.canceled.connect(
        self.discoveryFinished)

An UUID filter is set, so that the service discovery only shows the devices that advertise the needed service. After that a FullDiscovery is started:

m_discoveryAgent.setUuidFilter(uuid)
m_discoveryAgent.start(QBluetoothServiceDiscoveryAgent.FullDiscovery)

When a matching service is discovered, a serviceDiscovered() signal is emitted with an instance of QBluetoothServiceInfo as a parameter. This service info is used to extract the device name and the service name, and add a new entry to the list of discovered remote devices:

    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)

Later the user can select one of the devices from the list and try to connect to it.

Chat Client#

The chat client is implemented by the ChatClient class. The ChatClient class is declared as:

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

The client creates a new QBluetoothSocket and connects to the remote service described by the remoteService parameter. Slots are connected to the socket’s readyRead(), connected() , and disconnected() signals.

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)

On successful socket connection we emit a signal to notify other users.

def connected(self):

    connected.emit(socket.peerName())

Similarly to the chat server, the readSocket() slot is called when data is available from the socket. Lines are read individually and converted from UTF-8. The messageReceived() signal is emitted.

def readSocket(self):

    if not socket:
        return
    while socket.canReadLine():
        line = socket.readLine().trimmed()
        messageReceived.emit(socket.peerName()
                             QString.fromUtf8(line.constData(), line.length()))

The sendMessage() slot is used to send a message to the remote device. The message is converted to UTF-8 and a newline is appended.

def sendMessage(self, message):

    text = message.toUtf8() + '\n'
    socket.write(text)

To disconnect from the remote chat service, the QBluetoothSocket instance is deleted.

def stopClient(self):

    del socket
    socket = None

Chat Dialog#

The main window of this example is the chat dialog, implemented in the Chat class. This class displays a chat session between a single ChatServer and zero or more ChatClients. The Chat class is declared as:

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

First we construct the user interface

ui.setupUi(self)
ui.connectButton.clicked.connect(self.connectClicked)
ui.sendButton.clicked.connect(self.sendClicked)

We create an instance of the ChatServer and respond to its clientConnected(), clientDiconnected(), and messageReceived() signals.

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

In response to the clientConnected() and clientDisconnected() signals of the ChatServer, we display the typical “X has joined chat.” and “Y has left.” messages in the chat session.

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

Incoming messages from clients connected to the ChatServer are handled in the showMessage() slot. The message text tagged with the remote device name is displayed in the chat session.

def showMessage(self, sender, message):

    ui.chat.moveCursor(QTextCursor.End)
    ui.chat.insertPlainText("%1: %2\n".arg(sender, message))
    ui.chat.ensureCursorVisible()

In response to the connect button being clicked, the application starts service discovery and presents a list of discovered chat services on remote devices. A ChatClient for the service is selected by the user.

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)

In reponse to the connected() signals from ChatClient, we display the “Joined chat with X.” message in the chat session.

def connected(self, name):

    ui.chat.insertPlainText("Joined chat with %1.\n".arg(name))

Messages are sent to all remote devices via the ChatServer and ChatClient instances by emitting the sendMessage() signal.

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

We need to clean up ChatClient instances when the remote device forces a disconnect.

def clientDisconnected(self):

    client = ChatClient(sender())
    if client:
        clients.removeOne(client)
        client.deleteLater()

Example project @ code.qt.io