ブルートゥースチャット

RFCOMM プロトコルを使用した Bluetooth による通信を示します。

Bluetooth Chat のサンプルでは、Qt BluetoothAPI を使用して Bluetooth RFCOMM プロトコルを使用してリモートデバイス上の別のアプリケーションと通信する方法を示します。

Bluetooth Chat の例では、複数の当事者間で簡単なチャットプログラムを実装しています。このアプリケーションは常にサーバとクライアントの両方の役割を果たすため、誰が誰に接続するかを決定する必要はありません。

サンプルを実行する

Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細については、Building and Running an Example を参照してください。

チャットサーバー

チャットサーバーはChatServer クラスで実装されています。ChatServer クラスは次のように宣言されています:

class ChatServer : public QObject
{
    Q_OBJECT

public:
    explicit ChatServer(QObject *parent = nullptr);
    ~ChatServer();

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

public slots:
    void sendMessage(const QString &message);

signals:
    void messageReceived(const QString &sender, const QString &message);
    void clientConnected(const QString &name);
    void clientDisconnected(const QString &name);

private slots:
    void clientConnected();
    void clientDisconnected();
    void readSocket();

private:
    QBluetoothServer *rfcommServer = nullptr;
    QBluetoothServiceInfo serviceInfo;
    QList<QBluetoothSocket *> clientSockets;
    QMap<QBluetoothSocket *, QString> clientNames;
};

チャット・サーバーが最初に行う必要があるのは、着信Bluetooth接続をリッスンするためにQBluetoothServer のインスタンスを作成することです。clientConnected() スロットは、新しい接続が作成されるたびに呼び出されます。

rfcommServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
connect(rfcommServer, &QBluetoothServer::newConnection,
        this, QOverload<>::of(&ChatServer::clientConnected));
bool result = rfcommServer->listen(localAdapter);
if (!result) {
    qWarning() << "Cannot bind chat server to" << localAdapter.toString();
    return;
}

チャットサーバーは、他のデバイスがその存在を知っている場合にのみ役に立ちます。他のデバイスが発見できるようにするには、システムのSDP(Service Discovery Protocol)データベースでサービスを説明するレコードを公開する必要があります。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"));

BluetoothはUUIDを一意の識別子として使用します。チャット・サービスはランダムに生成されたUUIDを使用します。

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

Bluetoothサービスは、PublicBrowseGroup にある場合のみ発見可能です。

const auto 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() シグナルに接続される。このシグナルは、新しいクライアントが接続したことを他のクライアントに通知する。

void ChatServer::clientConnected()
{
    QBluetoothSocket *socket = rfcommServer->nextPendingConnection();
    if (!socket)
        return;

    connect(socket, &QBluetoothSocket::readyRead, this, &ChatServer::readSocket);
    connect(socket, &QBluetoothSocket::disconnected,
            this, QOverload<>::of(&ChatServer::clientDisconnected));
    clientSockets.append(socket);
    clientNames[socket] = socket->peerName();
    emit clientConnected(socket->peerName());
}

readSocket() スロットは、クライアントソケットからデータを読み込む準備ができたときに呼び出される。このスロットはソケットから個々の行を読み込み、UTF-8から変換し、messageReceived() シグナルを出力する。

void ChatServer::readSocket()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine().trimmed();
        emit messageReceived(clientNames[socket],
                             QString::fromUtf8(line.constData(), line.length()));
    }
}

clientDisconnected() スロットは、クライアントがサービスを切断するたびに呼び出される。このスロットは、クライアントが接続を切断したことを通知するシグナルを発し、ソケットを削除する。

void ChatServer::clientDisconnected()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    emit clientDisconnected(clientNames[socket]);

    clientSockets.removeOne(socket);
    clientNames.remove(socket);

    socket->deleteLater();
}

sendMessage() スロットは、接続しているすべてのクライアントにメッセージを送信するために使用されます。メッセージはUTF-8に変換され、すべてのクライアントに送信される前に改行が追加されます。

void ChatServer::sendMessage(const QString &message)
{
    QByteArray text = message.toUtf8() + '\n';

    for (QBluetoothSocket *socket : std::as_const(clientSockets))
        socket->write(text);
}

チャットサーバーが停止すると、システムのSDPデータベースからサービスレコード が削除され、接続されているすべてのクライアントソケットが削除され、rfcommServer インスタンスが削除される。

void ChatServer::stopServer()
{
    // Unregister service
    serviceInfo.unregisterService();

    // Close sockets
    qDeleteAll(clientSockets);
    clientNames.clear();

    // Close server
    delete rfcommServer;
    rfcommServer = nullptr;
}

サービスの検出

サーバーに接続する前に、クライアントは近くのデバイスをスキャンして、チャッ トサービスを宣伝しているデバイスを検索する必要がある。これは、RemoteSelector クラスによって行われます。

サービス検索を開始するために、RemoteSelectorQBluetoothServiceDiscoveryAgent のインスタンスを作成し、そのシグナルに接続する。

    m_discoveryAgent = new QBluetoothServiceDiscoveryAgent(localAdapter);

    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered,
            this, &RemoteSelector::serviceDiscovered);
    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::finished,
            this, &RemoteSelector::discoveryFinished);
    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::canceled,
            this, &RemoteSelector::discoveryFinished);

UUIDフィルターが設定され、サービス・ディスカバリーは、必要なサービスを広告しているデバイスのみを表示する。その後、FullDiscovery

    m_discoveryAgent->setUuidFilter(uuid);
    m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);

一致するサービスが発見されると、QBluetoothServiceInfo のインスタンスをパラメータとしてserviceDiscovered() シグナルが発信される。このサービス情報は、デバイス名とサービス名を抽出し、検出されたリモート・デバイスのリストに新しいエントリを追加するために使用される:

    QString remoteName;
    if (serviceInfo.device().name().isEmpty())
        remoteName = address.toString();
    else
        remoteName = serviceInfo.device().name();

    QListWidgetItem *item =
        new QListWidgetItem(QString::fromLatin1("%1 %2").arg(remoteName,
                                                             serviceInfo.serviceName()));

    m_discoveredServices.insert(item, serviceInfo);
    ui->remoteDevices->addItem(item);

後でユーザーは、リストから1つのデバイスを選択し、接続を試みることができます。

チャットクライアント

チャット・クライアントは、ChatClient クラスによって実装されています。ChatClient

class ChatClient : public QObject
{
    Q_OBJECT

public:
    explicit ChatClient(QObject *parent = nullptr);
    ~ChatClient();

    void startClient(const QBluetoothServiceInfo &remoteService);
    void stopClient();

public slots:
    void sendMessage(const QString &message);

signals:
    void messageReceived(const QString &sender, const QString &message);
    void connected(const QString &name);
    void disconnected();
    void socketErrorOccurred(const QString &errorString);

private slots:
    void readSocket();
    void connected();
    void onSocketErrorOccurred(QBluetoothSocket::SocketError);

private:
    QBluetoothSocket *socket = nullptr;
};

クライアントは新しいQBluetoothSocket を作成し、remoteService パラメータで指定されたリモートサービスに接続します。スロットはソケットのreadyRead(),connected(),disconnected() シグナルに接続される。

void ChatClient::startClient(const QBluetoothServiceInfo &remoteService)
{
    if (socket)
        return;

    // Connect to service
    socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
    qDebug() << "Create socket";
    socket->connectToService(remoteService);
    qDebug() << "ConnectToService done";

    connect(socket, &QBluetoothSocket::readyRead, this, &ChatClient::readSocket);
    connect(socket, &QBluetoothSocket::connected, this, QOverload<>::of(&ChatClient::connected));
    connect(socket, &QBluetoothSocket::disconnected, this, &ChatClient::disconnected);
    connect(socket, &QBluetoothSocket::errorOccurred, this, &ChatClient::onSocketErrorOccurred);
}

ソケット接続に成功すると、他のユーザーに通知するためのシグナルを発する。

void ChatClient::connected()
{
    emit connected(socket->peerName());
}

チャットサーバーと同様に、readSocket() スロットはソケットからデータが利用可能になると呼び出される。行は個別に読み込まれ、UTF-8から変換される。messageReceived() シグナルが発信される。

void ChatClient::readSocket()
{
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine().trimmed();
        emit messageReceived(socket->peerName(),
                             QString::fromUtf8(line.constData(), line.length()));
    }
}

sendMessage() スロットはリモートデバイスにメッセージを送信するために使用される。メッセージはUTF-8に変換され、改行が追加される。

void ChatClient::sendMessage(const QString &message)
{
    QByteArray text = message.toUtf8() + '\n';
    socket->write(text);
}

リモートチャットサービスから切断するには、QBluetoothSocket インスタンスを削除します。

void ChatClient::stopClient()
{
    delete socket;
    socket = nullptr;
}

チャットダイアログ

この例のメインウィンドウは、Chat クラスで実装されたチャットダイアログです。このクラスは、単一のChatServer と0個以上のChatClientの間のチャット・セッションを表示します。Chat クラスは、次のように宣言されています:

class Chat : public QDialog
{
    Q_OBJECT

public:
    explicit Chat(QWidget *parent = nullptr);
    ~Chat();

signals:
    void sendMessage(const QString &message);

private slots:
    void connectClicked();
    void sendClicked();

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

    void clientConnected(const QString &name);
    void clientDisconnected(const QString &name);
    void clientDisconnected();
    void connected(const QString &name);
    void reactOnSocketError(const QString &error);

    void newAdapterSelected();

    void initBluetooth();

    void updateIcons(Qt::ColorScheme scheme);

private:
    int adapterFromUserSelection() const;
    int currentAdapterIndex = 0;
    Ui::Chat *ui;

    ChatServer *server = nullptr;
    QList<ChatClient *> clients;
    QList<QBluetoothHostInfo> localAdapters;

    QString localName;
};

まず、ユーザー・インターフェースを構築します。

ui->setupUi(this);

connect(ui->connectButton, &QPushButton::clicked, this, &Chat::connectClicked);
connect(ui->sendButton, &QPushButton::clicked, this, &Chat::sendClicked);

ChatServer のインスタンスを作成し、clientConnected()clientDiconnected()messageReceived() シグナルに応答します。

server = new ChatServer(this);
connect(server, QOverload<const QString &>::of(&ChatServer::clientConnected),
        this, &Chat::clientConnected);
connect(server, QOverload<const QString &>::of(&ChatServer::clientDisconnected),
        this,  QOverload<const QString &>::of(&Chat::clientDisconnected));
connect(server, &ChatServer::messageReceived,
        this,  &Chat::showMessage);
connect(this, &Chat::sendMessage, server, &ChatServer::sendMessage);
server->startServer();

ChatServerclientConnected()clientDisconnected() シグナルに応答して、典型的な "X has joined chat. "と "Y has left. "のメッセージをチャット・セッションに表示する。

void Chat::clientConnected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("%1 has joined chat.\n").arg(name));
}

void Chat::clientDisconnected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("%1 has left.\n").arg(name));
}

ChatServer に接続されたクライアントからの着信メッセージは、showMessage() スロットで処理される。リモートデバイス名でタグ付けされたメッセージテキストがチャットセッションに表示されます。

void Chat::showMessage(const QString &sender, const QString &message)
{
    ui->chat->moveCursor(QTextCursor::End);
    ui->chat->insertPlainText(QString::fromLatin1("%1: %2\n").arg(sender, message));
    ui->chat->ensureCursorVisible();
}

接続ボタンがクリックされると、アプリケーションはサービス検出を開始し、リモートデバイス上で検出されたチャットサービスのリストを表示します。サービスのChatClient がユーザーによって選択されます。

void Chat::connectClicked()
{
    ui->connectButton->setEnabled(false);

    // scan for services
    const QBluetoothAddress 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) {
        QBluetoothServiceInfo service = remoteSelector.service();

        qDebug() << "Connecting to service" << service.serviceName()
                 << "on" << service.device().name();

        // Create client
        ChatClient *client = new ChatClient(this);

        connect(client, &ChatClient::messageReceived,
                this, &Chat::showMessage);
        connect(client, &ChatClient::disconnected,
                this, QOverload<>::of(&Chat::clientDisconnected));
        connect(client, QOverload<const QString &>::of(&ChatClient::connected),
                this, &Chat::connected);
        connect(client, &ChatClient::socketErrorOccurred,
                this, &Chat::reactOnSocketError);
        connect(this, &Chat::sendMessage, client, &ChatClient::sendMessage);
        client->startClient(service);

        clients.append(client);
    }

    ui->connectButton->setEnabled(true);
}

ChatClient からのconnected() シグナルに応答して、"Joined chat with X." というメッセージをチャットセッションに表示します。

void Chat::connected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("Joined chat with %1.\n").arg(name));
}

メッセージは、sendMessage() シグナルを発することで、ChatServerChatClient インスタンス経由ですべてのリモート・デバイスに送信される。

void Chat::sendClicked()
{
    ui->sendButton->setEnabled(false);
    ui->sendText->setEnabled(false);

    showMessage(localName, ui->sendText->text());
    emit sendMessage(ui->sendText->text());

    ui->sendText->clear();

    ui->sendText->setEnabled(true);
    ui->sendButton->setEnabled(true);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
    // avoid keyboard automatically popping up again on mobile devices
    ui->sendButton->setFocus();
#else
    ui->sendText->setFocus();
#endif
}

リモート・デバイスが強制的に切断した場合、ChatClient インスタンスをクリーンアップする必要があります。

void Chat::clientDisconnected()
{
    ChatClient *client = qobject_cast<ChatClient *>(sender());
    if (client) {
        clients.removeOne(client);
        client->deleteLater();
    }
}

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。