Qtの基礎 - Classic Bluetooth

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
2024年11月17日 (日) 10:09時点におけるWiki (トーク | 投稿記録)による版
ナビゲーションに移動 検索に移動

概要

QtのBluetoothサポートは、Qt Bluetoothモジュールを通じて提供されている。
Qt Bluetoothモジュールは、クロスプラットフォームなBluetooth通信機能を実現するための包括的なAPIセットとなっている。

主要なコンポーネントとして、Classic BluetoothとBluetooth Low Energy (BLE) の両方をサポートしている。

  • Classic Bluetooth
    従来型の高帯域幅通信
  • BLE
    省電力デバイスとの通信


デバイスの検出と管理において、Qt Bluetoothモジュールは以下に示す機能を提供している。

  • デバイススキャンと検出
  • サービスディスカバリ
  • ペアリング管理
  • 接続の確立と維持


上記の機能は相互に関連しており、アプリケーションでは以下に示すような流れで使用される。

  1. まず、デバイススキャンを行い、周囲のデバイスを探索する。
  2. 目的のデバイスが存在する場合、サービスディスカバリでそのデバイスの機能を確認する。
  3. 次に、必要に応じてペアリングを実行する。
  4. 最後に、接続を確立してデータ通信を開始する。


Classic Bluetoothでの通信では、RFCOMMプロトコルを使用したシリアルポート型の通信が可能である。
これは QBluetoothSocketクラスを通じて実装されており、TCP/IPソケットに似た使い方ができる。

BLEについては、GATT (Generic Attribute Profile) プロトコルをベースとしたサービスとキャラクタリスティックの概念を使用する。
QLowEnergyControllerクラスがBLE通信の中心的な役割を果たしており、デバイスとの接続やデータのやり取りを管理する。

セキュリティ面においては、ペアリング、セキュアな接続の確立、暗号化等の機能が組み込まれている。
また、プラットフォーム固有のセキュリティ要件にも対応している。

実装する場合の注意点として、OSごとの権限設定や制限事項への対応が必要となる。
特に、モバイルプラットフォームでは適切な権限の設定が重要である。
また、Bluetooth機能の有無やステータスの確認も必要である。

エラーハンドリングについては、接続の切断、タイムアウト、デバイスが見つからない場合等、様々な状況に対応する必要がある。

Qt Bluetoothモジュールは、これらの状況を適切に検出して、シグナル / スロットメカニズムを通じて通知する仕組みを提供している。


Classic Bluetoothの特徴

Classic Bluetoothは、QBluetoothSocketクラスを使用した直接的なソケット通信である。

RFCOMMプロトコルベースの通信であり、主に、シリアル通信のような双方向データストリームに適している。
Bluetooth Low Energyと比較して、より大きなデータ転送が可能である。


デバイススキャンと検出

デバイススキャンは、周囲のBluetooth対応デバイスを探索するプロセスである。
このプロセスでは、アクティブに電波を送信して応答を待つアクティブスキャン、他のデバイスからのアドバタイズメントを受信するパッシブスキャンの2種類がある。

  • アクティブスキャン
  • パッシブスキャン


スキャン中は、各デバイスの基本情報 (デバイス名、MACアドレス、デバイスクラス、信号強度等) を取得できる。
また、スキャン時間や範囲を設定可能であり、バッテリー消費とスキャン精度のバランスを取ることができる。

Classic Bluetoothでは、QBluetoothDeviceDiscoveryAgentを使用して、全てのBluetoothデバイスを検出する。

 #include <QObject>
 #include <QBluetoothDeviceDiscoveryAgent>
 #include <memory>
 #include <stdexcept>
 #include <QDebug>
 
 class BluetoothScanner : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QBluetoothDeviceDiscoveryAgent> discoveryAgent;
 
    // デバイスタイプを文字列に変換するヘルパー関数
    QString deviceTypeToString(QBluetoothDeviceInfo::MajorDeviceClass type)
    {
       switch (type) {
          case QBluetoothDeviceInfo::MiscellaneousDevice: return "その他";
          case QBluetoothDeviceInfo::ComputerDevice:      return "コンピュータ";
          case QBluetoothDeviceInfo::PhoneDevice:         return "電話";
          case QBluetoothDeviceInfo::AudioVideoDevice:    return "オーディオ/ビデオ";
          case QBluetoothDeviceInfo::NetworkDevice:       return "ネットワーク";
          case QBluetoothDeviceInfo::PeripheralDevice:    return "周辺機器";
          case QBluetoothDeviceInfo::ImagingDevice:       return "イメージング";
          case QBluetoothDeviceInfo::WearableDevice:      return "ウェアラブル";
          case QBluetoothDeviceInfo::ToyDevice:           return "おもちゃ";
          case QBluetoothDeviceInfo::HealthDevice:        return "ヘルスケア";
          default: return "不明";
        }
    }
 
    // エラーコードを文字列に変換するヘルパー関数
    QString errorToString(QBluetoothDeviceDiscoveryAgent::Error error)
    {
       switch (error) {
          case QBluetoothDeviceDiscoveryAgent::NoError:                      return "エラーなし";
          case QBluetoothDeviceDiscoveryAgent::InputOutputError:             return "I/Oエラー";
          case QBluetoothDeviceDiscoveryAgent::PoweredOffError:              return "Bluetoothがオフ";
          case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError: return "無効なアダプタ";
          case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError:     return "未対応のプラットフォーム";
          case QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod:   return "未対応の探索方法";
          default: return "不明なエラー";
        }
    }
 
 public:
    explicit BluetoothScanner(QObject* parent = nullptr) : QObject(parent)
    {
       try {
          // デバイス探索エージェントの作成
          discoveryAgent = std::make_unique<QBluetoothDeviceDiscoveryAgent>(this);
 
          // 各種シグナルとスロットの接続
          connect(discoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothScanner::onDeviceDiscovered);
          connect(discoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothScanner::onScanFinished);
          connect(discoveryAgent.get(), static_cast<void(QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(&QBluetoothDeviceDiscoveryAgent::error),
                  this, &BluetoothScanner::onError);
       }
       catch (const std::exception &e) {
          qDebug() << "初期化エラー: " << e.what();
          throw;
       }
    }
 
    // スキャン開始メソッド
    void startScan()
    {
       try {
          qDebug() << "デバイススキャンを開始...";
          discoveryAgent->start();
       }
       catch (const std::exception &e) {
          qDebug() << "スキャン開始エラー: " << e.what();
          throw;
       }
    }
 
    // スキャン停止メソッド
    void stopScan()
    {
       try {
          qDebug() << "デバイススキャンを停止...";
          discoveryAgent->stop();
       }
       catch (const std::exception &e) {
          qDebug() << "スキャン停止エラー: " << e.what();
          throw;
       }
    }
 
 private slots:
    // デバイスが存在する場合のスロット
    void onDeviceDiscovered(const QBluetoothDeviceInfo& device)
    {
       qDebug() << "デバイスの探索に成功:";
       qDebug() << "  名前: "         << device.name();
       qDebug() << "  アドレス: "      << device.address().toString();
       qDebug() << "  RSSI: "         << device.rssi();
       qDebug() << "  デバイスタイプ: " << deviceTypeToString(device.majorDeviceClass());
    }
 
    // スキャン完了時のスロット
    void onScanFinished()
    {
        qDebug() << "デバイススキャンが完了";
    }
 
    // エラー発生時のスロット
    void onError(QBluetoothDeviceDiscoveryAgent::Error error)
     {
        qDebug() << "エラーが発生: " << errorToString(error);
    }
 };



サービスディスカバリ

特定のBluetoothデバイスが提供するサービスを探索するプロセスである。
各Bluetoothデバイスは複数のサービスを提供できるが、
サービスディスカバリによってそのデバイスがどのようなサービス (シリアル通信、オーディオ転送、ファイル転送等) を提供しているかを特定できる。

各サービスはUUIDで識別されており、標準的なサービスには予め定義されたUUIDが割り当てられている。
また、カスタムサービスの場合は独自のUUIDを使用する。

Classic Bluetoothでは、QBluetoothServiceInfoクラスを使用して、単純なサービス情報を管理する。

 #include <QBluetoothServiceDiscoveryAgent>
 #include <QBluetoothAddress>
 #include <QDebug>
 #include <memory>
 
 class ServiceDiscovery : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QBluetoothServiceDiscoveryAgent> discoveryAgent;
 
    // プロトコルタイプを文字列に変換するヘルパー関数
    QString protocolToString(int protocol)
    {
       switch (protocol) {
          case 0:  return "不明なプロトコル";
          case 1:  return "SDP";
          case 3:  return "RFCOMM";
          case 15: return "L2CAP";
          default: return QString("その他のプロトコル (%1)").arg(protocol);
       }
    }
 
    // エラーコードを文字列に変換するヘルパー関数
    QString errorToString(QBluetoothServiceDiscoveryAgent::Error error)
    {
       switch (error) {
          case QBluetoothServiceDiscoveryAgent::NoError:                      return "エラーなし";
          case QBluetoothServiceDiscoveryAgent::InputOutputError:             return "I/Oエラー";
          case QBluetoothServiceDiscoveryAgent::PoweredOffError:              return "Bluetoothがオフ";
          case QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError: return "無効なアダプタ";
          case QBluetoothServiceDiscoveryAgent::UnknownError:                 return "不明なエラー";
          default: return "予期せぬエラー";
       }
    }
 
 public:
    explicit ServiceDiscovery(QObject* parent = nullptr) : QObject(parent)
    {
       try {
          // サービス探索エージェントの作成
          discoveryAgent = std::make_unique<QBluetoothServiceDiscoveryAgent>(this);
 
          // 各種シグナルとスロットの接続
          connect(discoveryAgent.get(), &QBluetoothServiceDiscoveryAgent::serviceDiscovered, this, &ServiceDiscovery::onServiceDiscovered);
          connect(discoveryAgent.get(), &QBluetoothServiceDiscoveryAgent::finished, this, &ServiceDiscovery::onScanFinished);
          connect(discoveryAgent.get(), static_cast<void(QBluetoothServiceDiscoveryAgent::*)(QBluetoothServiceDiscoveryAgent::Error)>(&QBluetoothServiceDiscoveryAgent::error),
                  this, &ServiceDiscovery::onError);
       }
       catch (const std::exception &e) {
          qDebug() << "初期化エラー: " << e.what();
          throw;
       }
    }
 
    // 特定のデバイスのサービス探索を開始
    void startDiscovery(const QBluetoothAddress& address)
    {
       try {
          qDebug() << "サービス探索を開始...";
          discoveryAgent->setRemoteAddress(address);
          discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
       }
       catch (const std::exception &e) {
          qDebug() << "探索開始エラー:" << e.what();
          throw;
       }
    }
 
    // 探索を停止
    void stopDiscovery()
    {
       try {
          qDebug() << "サービス探索を停止...";
          discoveryAgent->stop();
       }
       catch (const std::exception &e) {
          qDebug() << "探索停止エラー:" << e.what();
          throw;
       }
    }
 
 private slots:
    // サービスの探索に成功した場合のスロット
    void onServiceDiscovered(const QBluetoothServiceInfo& service)
    {
       qDebug() << "サービスの探索に成功:";
       qDebug() << "  サービス名: "   << service.serviceName();
       qDebug() << "  サービス説明: " << service.serviceDescription();
       qDebug() << "  プロトコル: "   << protocolToString(service.protocolServiceMultiplexer());
 
       // サービスの詳細情報を表示
       if (service.serviceUuid().isNull()) {
          qDebug() << "  UUID: カスタムUUID";
       }
       else {
          qDebug() << "  UUID: " << service.serviceUuid().toString();
       }
 
       // サービスクラスを表示
       QList<QBluetoothUuid> serviceClasses = service.serviceClassUuids();
       if (!serviceClasses.isEmpty()) {
          qDebug() << "  サービスクラス:";
          for (const QBluetoothUuid& uuid : serviceClasses) {
             qDebug() << "    -" << uuid.toString();
          }
       }
    }
 
    // 探索完了時のスロット
    void onScanFinished()
    {
       qDebug() << "サービス探索が完了";
    }
 
    // エラー発生時のスロット
    void onError(QBluetoothServiceDiscoveryAgent::Error error)
    {
       qDebug() << "エラーが発生: " << errorToString(error);
    }
 };



ペアリング管理

ペアリングは、2つのBluetoothデバイス間で安全な通信を確立するための認証プロセスである。

このプロセスおける重要な要素を以下に示す。

  • 初回のペアリング時において、PINコードや数値の確認による認証を行う。
  • ペアリング情報は両デバイスに保存されて、再接続時に使用する。
  • セキュリティレベルの設定が可能であり、必要に応じて暗号化強度を変更できる。
  • ペアリング状態の管理 (新規ペアリング、ペアリング解除、ペアリング状態の確認等) が含まれる。
  • 1度ペアリングされたデバイスは、特別な設定がない限り、自動的に再接続可能になる。


 #include <QBluetoothLocalDevice>
 #include <QDebug>
 #include <memory>
 
 class PairingManager : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QBluetoothLocalDevice> localDevice;
 
    // ペアリング状態を文字列に変換するヘルパー関数
    QString pairingStatusToString(QBluetoothLocalDevice::Pairing status)
    {
       switch (status) {
          case QBluetoothLocalDevice::Unpaired:         return "未ペアリング";
          case QBluetoothLocalDevice::Paired:           return "ペアリング済み";
          case QBluetoothLocalDevice::AuthorizedPaired: return "認証済みペアリング";
          default:                                      return "不明な状態";
       }
    }
 
 public:
    explicit PairingManager(QObject* parent = nullptr) : QObject(parent)
    {
       try {
          // ローカルBluetoothデバイスの初期化
          localDevice = std::make_unique<QBluetoothLocalDevice>(this);
 
          // 各種シグナルとスロットの接続
          connect(localDevice.get(), &QBluetoothLocalDevice::pairingFinished, this, &PairingManager::onPairingFinished);
          connect(localDevice.get(), &QBluetoothLocalDevice::error, this, &PairingManager::onError);
          connect(localDevice.get(), &QBluetoothLocalDevice::pairingDisplayConfirmation, this, &PairingManager::onPairingConfirmationRequest);
          connect(localDevice.get(), &QBluetoothLocalDevice::pairingDisplayPinCode, this, &PairingManager::onPairingDisplayPinCode);
       }
       catch (const std::exception &e) {
          qDebug() << "初期化エラー:" << e.what();
          throw;
       }
    }
 
    // ペアリングを開始
    void requestPairing(const QBluetoothAddress& address)
    {
       try {
          qDebug() << "ペアリングを開始..." << address.toString();
          localDevice->requestPairing(address, QBluetoothLocalDevice::Paired);
       }
       catch (const std::exception &e) {
          qDebug() << "ペアリング開始エラー: " << e.what();
          throw;
       }
    }
 
    // ペアリングの解除
    void removePairing(const QBluetoothAddress& address)
    {
       try {
          qDebug() << "ペアリングを解除..." << address.toString();
          localDevice->requestPairing(address, QBluetoothLocalDevice::Unpaired);
       }
       catch (const std::exception &e) {
          qDebug() << "ペアリング解除エラー:" << e.what();
          throw;
       }
    }
 
    // ペアリング状態の確認
    QBluetoothLocalDevice::Pairing getPairingStatus(const QBluetoothAddress &address)
    {
       try {
          return localDevice->pairingStatus(address);
       }
       catch (const std::exception &e) {
          qDebug() << "ペアリング状態確認エラー:" << e.what();
          throw;
       }
    }
 
    // ペアリング済みデバイスの一覧を取得
    QList<QBluetoothAddress> getPairedDevices()
    {
       try {
          return localDevice->connectedDevices();
       }
       catch (const std::exception &e) {
          qDebug() << "ペアリング済みデバイス取得エラー: " << e.what();
          throw;
       }
    }
 
 private slots:
    // ペアリング完了時のスロット
    void onPairingFinished(const QBluetoothAddress& address, QBluetoothLocalDevice::Pairing status)
    {
       qDebug() << "ペアリング処理が完了:";
       qDebug() << "  デバイス:" << address.toString();
       qDebug() << "  状態:" << pairingStatusToString(status);
    }
 
    // エラー発生時のスロット
    void onError()
    {
        qDebug() << "ペアリング処理でエラーが発生";
    }
 
    // ペアリング確認要求時のスロット
    void onPairingConfirmationRequest(const QBluetoothAddress& address, QString pin)
    {
       qDebug() << "ペアリング確認要求を受信:";
       qDebug() << "  デバイス:" << address.toString();
       qDebug() << "  確認コード:" << pin;
 
       // ここに、ユーザに確認を求めるUIを表示する
       // 以下の例では、自動的に確認している
       localDevice->pairingConfirmation(true);
    }
 
    // PINコード表示要求時のスロット
    void onPairingDisplayPinCode(const QBluetoothAddress &address, QString pin)
    {
       qDebug() << "PINコード表示要求を受信:";
       qDebug() << "  デバイス:" << address.toString();
       qDebug() << "  PINコード:" << pin;
    }
 };



接続の確立と維持

実際のデータ通信を行うための接続管理プロセスである。

  • 接続の確立
    ペアリング済みデバイスとの接続を開始する。
  • 接続状態の監視
    接続品質、切断検知、エラー検出等を常時監視する。
  • 再接続管理
    予期せぬ切断が発生した場合の自動再接続処理を行う。
  • データ転送の管理
    送受信バッファの管理、フロー制御、エラー訂正等を実施する。
  • 接続パラメータの最適化
    電力消費、通信速度、接続の安定性等のバランスを取る。


Classic Bluetoothでは、QBluetoothSocketクラスを使用して、
connectメソッドでソケット接続、writeメソッド / readメソッドでデータの送受信を行う。

 #include <QBluetoothSocket>
 #include <QDebug>
 #include <memory>
 
 class BluetoothConnection : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QBluetoothSocket> socket;
 
    // エラーコードを文字列に変換するヘルパー関数
    QString errorToString(QBluetoothSocket::SocketError error)
    {
       switch (error) {
          case QBluetoothSocket::NoSocketError:        return "エラーなし";
          case QBluetoothSocket::UnknownSocketError:   return "不明なエラー";
          case QBluetoothSocket::HostNotFoundError:    return "ホストが見つかりません";
          case QBluetoothSocket::ServiceNotFoundError: return "サービスが見つかりません";
          case QBluetoothSocket::NetworkError:         return "ネットワークエラー";
          case QBluetoothSocket::OperationError:       return "操作エラー";
          default:                                     return "予期せぬエラー";
       }
    }
 
    // 接続状態を文字列に変換するヘルパー関数
    QString stateToString(QBluetoothSocket::SocketState state)
    {
       switch (state) {
          case QBluetoothSocket::UnconnectedState:   return "未接続";
          case QBluetoothSocket::ConnectingState:    return "接続中";
          case QBluetoothSocket::ConnectedState:     return "接続済み";
          case QBluetoothSocket::BoundState:         return "バインド済み";
          case QBluetoothSocket::ClosingState:       return "切断中";
          case QBluetoothSocket::ServiceLookupState: return "サービス検索中";
          default:                                   return "不明な状態";
       }
    }
 
 public:
    explicit BluetoothConnection(QObject* parent = nullptr) : QObject(parent)
    {
       try {
          // RFCOMMソケットの作成
          socket = std::make_unique<QBluetoothSocket>(QBluetoothServiceInfo::RfcommProtocol, this);
 
          // 各種シグナルとスロットの接続
          connect(socket.get(), &QBluetoothSocket::connected, this, &BluetoothConnection::onConnected);
          connect(socket.get(), &QBluetoothSocket::disconnected, this, &BluetoothConnection::onDisconnected);
          connect(socket.get(), &QBluetoothSocket::errorOccurred, this, &BluetoothConnection::onError);
          connect(socket.get(), &QBluetoothSocket::readyRead, this, &BluetoothConnection::onDataReceived);
          connect(socket.get(), &QBluetoothSocket::stateChanged, this, &BluetoothConnection::onStateChanged);
       }
       catch (const std::exception &e) {
          qDebug() << "初期化エラー: " << e.what();
          throw;
       }
    }
 
    // デバイスに接続
    void connectToDevice(const QBluetoothAddress &address, quint16 port)
    {
       try {
          if (socket->state() == QBluetoothSocket::ConnectedState) {
             qDebug() << "既に接続済み";
             return;
          }
 
          qDebug() << "デバイスに接続します: " << address.toString();
          qDebug() << "ポート: " << port;
          socket->connectToService(address, port);
       }
       catch (const std::exception &e) {
          qDebug() << "接続エラー: " << e.what();
          throw;
       }
    }
 
    // 接続を切断
    void disconnect()
    {
       try {
          if (socket->state() != QBluetoothSocket::UnconnectedState) {
             qDebug() << "接続を切断...";
             socket->disconnectFromService();
          }
       }
       catch (const std::exception &e) {
          qDebug() << "切断エラー: " << e.what();
          throw;
       }
    }
 
    // データを送信
    bool sendData(const QByteArray &data)
    {
       try {
          if (socket->state() != QBluetoothSocket::ConnectedState) {
             qDebug() << "送信エラー: 接続されていません";
             return false;
          }
 
          qint64 bytesWritten = socket->write(data);
          if (bytesWritten == -1) {
             qDebug() << "送信エラー: データの書き込みに失敗";
             return false;
          }
 
          qDebug() << bytesWritten << "バイトデータの送信完了";
          return true;
       }
       catch (const std::exception &e) {
          qDebug() << "送信エラー: " << e.what();
          throw;
       }
    }
 
 private slots:
    // 接続確立時のスロット
    void onConnected()
    {
       qDebug() << "接続が確立";
       qDebug() << "  ローカルアドレス:" << socket->localAddress().toString();
       qDebug() << "  リモートアドレス:" << socket->peerAddress().toString();
    }
 
    // 切断時のスロット
    void onDisconnected()
    {
       qDebug() << "接続が切断";
    }
 
    // エラー発生時のスロット
    void onError(QBluetoothSocket::SocketError error)
    {
       qDebug() << "エラーが発生: " << errorToString(error);
    }
 
    // データ受信時のスロット
    void onDataReceived()
    {
       QByteArray data = socket->readAll();
       qDebug() << "データを受信: " << data.size() << "バイト";
 
       // ここでデータを処理する
       // ...略
    }
 
    // 接続状態変更時のスロット
    void onStateChanged(QBluetoothSocket::SocketState state)
    {
       qDebug() << "接続状態が変更: " << stateToString(state);
    }
 };