Qtの基礎 - Bluetooth Low Energy

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

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

※Bluetooth Low Energyを使用する場合の注意

  • サービスUUIDおよびキャラクタリスティックUUIDの理解が重要である。
  • データサイズの制限を考慮する必要がある。(一般的に20[byte]程度)
  • 非同期処理が基本となるため、シグナル/スロットの適切な設計が重要である。


ソフトウェアの要件 (データ量、通信頻度、電力消費等) に応じて適切な方式を選択する必要がある。
また、多くのモダンなBluetoothデバイスはBLEを採用する傾向にあり、特に省電力性が重要な場合はBLEが推奨される。

※開発時の注意
BLEはClassic Bluetoothと基本的な手順は同じであるが、BLEは独自のアーキテクチャと概念を持っており、より細かな制御と状態管理が必要となる。
特に、省電力性を重視する場合は、以下に示す特徴を理解して実装することが重要となる。

  • 状態管理
    BLEは状態遷移が多いため、適切な状態管理が重要となる。
    各操作のタイミングと順序に注意が必要である。
  • エラーハンドリング
    • 接続切断
    • タイムアウト
    • 権限の問題 (特に、モバイル環境)
  • パフォーマンス考慮
    • データサイズの制限 (MTU)
    • 通信頻度の最適化
    • バッテリー消費の管理
  • プラットフォーム固有の考慮
    • OSごとの権限設定
    • バックグラウンド動作の制限
    • スキャンフィルタの設定



Bluetooth Low Energy (BLE) の特徴

BLEは、QLowEnergyControllerクラスを使用した通信である。

GATT (Generic Attribute Profile) プロトコルを使用しており、サービス、キャラクタリスティック、ディスクリプタという階層構造を持つ。

BLEは、Classic Bluetoothとは異なるアーキテクチャと通信モデルを採用しているため、別のAPIとクラスを使用する必要がある。
BLEで使用するQtクラスを、以下に示す。

  • QLowEnergyControllerクラス
    BLEデバイスとの接続を管理する。
  • QLowEnergyServiceクラス
    BLEサービスを表現する。
  • QLowEnergyCharacteristicクラス
    データの読み書きを行う最小単位


BLEでは、短いデータパケットの断続的な送受信する。
また、Notify / Indicateによる省電力なデータ更新を行う。


デバイススキャンと検出

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

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


BLEデバイスのスキャン中は、
BLEペリフェラル (アドバタイザ) デバイスがアドバタイズメントと呼ばれる特別なブロードキャストパケットを定期的に発信して、セントラル (スキャナ) デバイスがそれをスキャンする。
スキャンを行うのはセントラル側、アドバタイズメントパケットを発信するのはペリフェラル側である。

Qt BLE 1.png


このアドバタイジングパケットには、以下に示す情報が含まれる。

  • デバイス名
  • メーカー固有データ
  • 提供するサービスのUUID
  • MACアドレス (デバイスアドレス)
  • その他のカスタムデータ


また、スキャン時間や範囲を設定可能であり、バッテリー消費とスキャン精度のバランスを取ることができる。
アドバタイジング間隔は、20ミリ秒~10.24秒の範囲で設定可能である。

BLEでは、同様のQBluetoothDeviceDiscoveryAgentクラスを使用するが、LowEnergyDiscoveryTimeoutの設定が必要となる。
また、フィルタリングでBLEデバイスのみを検出する。

アドバタイジングパケットの種類

  • ADV_IND
    一般的な接続可能なアドバタイジング
  • ADV_DIRECT_IND
    特定のデバイスのみに向けたアドバタイジング
  • ADV_NONCONN_IND
    接続不可の通知専用
  • ADV_SCAN_IND
    スキャン応答が可能な通知専用
  • ADV_EXT_IND
    拡張アドバタイジング (Bluetooth 5.0以降)


データ構造

  • PDU Header (2バイト)
    • アドバタイジングタイプ
    • TxAddressタイプ
    • RxAddressタイプ
  • アドバタイジングアドレス (6バイト)
  • アドバタイジングデータ (最大31バイト)


Qt BLE 2.png


アドバタイジングデータの主要要素

  • Length (1バイト)
  • AD Type (1バイト)
    • 0x01
      Flags
    • 0x09
      Complete Local Name
    • 0xFF
      Manufacturer Specific Data
    • 0x03
      Complete List of 16-bit Service UUIDs
  • AD Data (可変長)


Qt BLE 3.png


その他 (通信特性 / セキュリティ等)

  • 通信特性
    • アドバタイジング間隔
      20ミリ秒~10.24秒
    • チャンネル
      37, 38, 39の3チャンネル
    • 送信電力
      -20[dBm]~+4[dBm] (地域規制による)


  • セキュリティ
    • プライバシー保護のためのランダムアドレス
    • アドバタイジングデータの暗号化オプション
    • ホワイトリストによるフィルタリング機能


  • スキャン応答
    • SCAN_REQ
      スキャナからの追加情報要求
    • SCAN_RSP
      アドバタイザーからの応答 (最大31バイト)


使用例

Qt Bluetoothモジュールを使用したBLEスキャンの処理を以下に示す。

  • QBluetoothDeviceDiscoveryAgentクラス
  • QBluetoothDeviceInfoクラス


デバイス探索エージェントの作成

QBluetoothDeviceDiscoveryAgentのインスタンスを生成する。

 #include <QBluetoothDeviceDiscoveryAgent>
 
 QBluetoothDeviceDiscoveryAgent discoveryAgent;


シグナル / スロット接続
  • QBluetoothDeviceDiscoveryAgent::deviceDiscoveredシグナル
    デバイスを発見した時に送信される。
  • QBluetoothDeviceDiscoveryAgent::finished
    スキャンが完了した時に送信される。
  • QBluetoothDeviceDiscoveryAgent::error
    エラーが発生した時に送信される。


 // デバイス発見時
 connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &MyClass::onDeviceDiscovered);
 
 // スキャン完了時
 connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &MyClass::onScanFinished);
 
 // エラー発生時
 // エラーハンドリングは必ず実装する
 connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::error, this, &MyClass::onError);


スキャン開始

QBluetoothDeviceDiscoveryAgent::LowEnergyMethodメソッドを指定して、QBluetoothDeviceDiscoveryAgent::startメソッドを実行する。
これによりBLEデバイスのスキャンが開始する。

 discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);


デバイス発見時

QBluetoothDeviceDiscoveryAgent::deviceDiscoveredシグナルで通知する。
QBluetoothDeviceInfoクラスを使用して、以下に示す情報を取得することが可能である。

  • デバイス名
  • アドレス
  • 信号強度 (RSSI値)
  • サービスUUID
  • マニファクチャラーデータ


ただし、これらの情報を取得する前は、必ず存在確認を行う。

 void onDeviceDiscovered(const QBluetoothDeviceInfo &device)
 {
    // デバイス名の取得
    QString name = device.name();
 
    // アドレスの取得
    QString address = device.address().toString();
 
    // RSSI値の取得
    qint16 rssi = device.rssi();
 
    // サービスUUIDの取得
    QList<QBluetoothUuid> services = device.serviceUuids();
 
    // マニファクチャラーデータの取得
    QMap<quint16, QByteArray> manufacturerData = device.manufacturerData();
 }


スキャン完了時

QBluetoothDeviceDiscoveryAgent::finishedシグナルで送信する。

スキャン完了時の処理を記述する。
また、必要に応じて再スキャンを開始する。

 void onScanFinished()
 {
    // 必要に応じて再スキャン
    discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
 }


エラー発生時

リソースの解放は適切に行う。

 void onError(QBluetoothDeviceDiscoveryAgent::Error error)
 {
    switch (error) {
       case QBluetoothDeviceDiscoveryAgent::NoError:
            break;
       case QBluetoothDeviceDiscoveryAgent::InputOutputError:
            break;
       case QBluetoothDeviceDiscoveryAgent::PoweredOffError:
            break;
       case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError:
            break;
       case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError:
            break;
       case QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod:
            break;
       default:
            break;
    }
 }


スキャン停止
 discoveryAgent->stop();


組み合わせ
 // BLEデバイスのスキャンを管理するクラス
 // BLEデバイスの検出と監視, 継続的なスキャンモード, デバイス情報の重複排除, 自動リカバリを行う
 
 #include <QObject>
 #include <QBluetoothDeviceDiscoveryAgent>
 #include <QBluetoothDeviceInfo>
 #include <QTimer>
 #include <QDebug>
 
 class BLEDeviceScanner : public QObject
 {
    Q_OBJECT
 
 private:
    QBluetoothDeviceDiscoveryAgent discoveryAgent;  // BLEデバイス探索用エージェント
    QTimer rescanTimer;                             // 継続的スキャン用タイマ
    bool   isContinuousScan = false;                // 継続的スキャンモードのフラグ
    QMap<QBluetoothAddress, QBluetoothDeviceInfo> knownDevices;  // 既知デバイスのキャッシュ
 
    // デバイス探索エージェントとタイマーのシグナルを適切なスロットに接続
    // デバイス発見時の処理, スキャン完了時の処理, エラー発生時の処理, 定期的な再スキャンの制御を行う
    void connectSignals()
    {
       // デバイス探索エージェントのシグナル接続
       connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BLEDeviceScanner::onDeviceDiscovered);
       connect(&discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BLEDeviceScanner::onScanFinished);
       connect(&discoveryAgent, static_cast<void(QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(&QBluetoothDeviceDiscoveryAgent::error),
               this, &BLEDeviceScanner::onError);
 
       // 再スキャンタイマのシグナル接続
       connect(&rescanTimer, &QTimer::timeout, this, [this]() {
          if (isContinuousScan) {
             discoveryAgent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
          }
       });
    }
 
    // エラーコード
    QString getErrorMessage(QBluetoothDeviceDiscoveryAgent::Error error)
    {
       switch (error) {
          case QBluetoothDeviceDiscoveryAgent::NoError:                      return "エラーなし";
          case QBluetoothDeviceDiscoveryAgent::InputOutputError:             return "Bluetooth IOエラー";
          case QBluetoothDeviceDiscoveryAgent::PoweredOffError:              return "Bluetoothがオフ";
          case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError: return "無効なBluetoothアダプタ";
          case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError:     return "プラットフォームがサポートされていない";
          case QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod:   return "未対応の探索方法";
          default:                                                           return "不明なエラー";
       }
    }
 
    // 発見されたデバイスの詳細情報をログに出力
    // デバイス名, MACアドレス, 電波強度 (RSSI), 提供サービスのUUID, メーカー固有データを表示
    void logDeviceInfo(const QBluetoothDeviceInfo& device)
    {
       qDebug() << "BLEデバイスを発見:";
       qDebug() << "  名前:" << device.name();
       qDebug() << "  アドレス:" << device.address().toString();
       qDebug() << "  RSSI:" << device.rssi();
 
       // サービスUUIDのログ出力
       const QList<QBluetoothUuid> serviceUuids = device.serviceUuids();
       if (!serviceUuids.isEmpty()) {
          qDebug() << "  提供サービス:";
          for (const QBluetoothUuid& uuid : serviceUuids) {
             qDebug() << "    -" << uuid.toString();
          }
       }
 
       // マニファクチャラーデータのログ出力
       const QMap<quint16, QByteArray> manufacturerData = device.manufacturerData();
       if (!manufacturerData.isEmpty()) {
          qDebug() << "  マニファクチャラーデータ:";
          QMap<quint16, QByteArray>::const_iterator i = manufacturerData.constBegin();
          while (i != manufacturerData.constEnd()) {
             qDebug() << "    ID: " << i.key() << "データ: " << i.value().toHex();
             i++;
          }
       }
    }
 
 public:
    explicit BLEDeviceScanner(QObject* parent = nullptr) : QObject(parent)
    {
       // BLEデバイスのみをスキャンするように設定
       discoveryAgent.setLowEnergyDiscoveryTimeout(10000);  // 10秒のタイムアウト
 
       // 自動再スキャンタイマの設定
       rescanTimer.setInterval(30000);  // 30秒間隔で再スキャン
 
       connectSignals();
    }
 
    // BLEデバイスのスキャンを開始
    void startScan(bool continuous = false)
    {
       try {
          qDebug() << "BLEデバイススキャンを開始...";
          isContinuousScan = continuous;
          discoveryAgent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
 
          if (continuous) rescanTimer.start();
 
          emit scanStarted();
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("スキャン開始エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // スキャンを停止
    void stopScan()
    {
       try {
          qDebug() << "BLEデバイススキャンを停止...";
          discoveryAgent.stop();
          rescanTimer.stop();
          isContinuousScan = false;
          emit scanStopped();
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("スキャン停止エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
 signals:
    void deviceDiscovered(const QBluetoothDeviceInfo& device);  // 新規デバイス発見時
    void scanStarted();    // スキャン開始時
    void scanStopped();    // スキャン停止時
    void scanFinished();   // スキャン完了時
    void errorOccurred(const QString& error);  // エラー発生時
 
 private slots:
    // デバイス発見時の処理
    // BLEデバイスのフィルタリング, 重複デバイスの検出と更新, デバイス情報のログ出力, 新規 / 更新デバイスの通知
    void onDeviceDiscovered(const QBluetoothDeviceInfo& device)
    {
       // BLEデバイスのみを処理
        if (!(device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration)) {
           return;
        }
 
        // デバイスの重複確認
        const QBluetoothAddress& address = device.address();
        if (!knownDevices.contains(address) || knownDevices[address].rssi() != device.rssi()) {
            knownDevices[address] = device;
 
            // デバイス情報のログ出力と通知
            logDeviceInfo(device);
            emit deviceDiscovered(device);
        }
    }
 
    // スキャン完了時の処理
    // 完了通知の発行, 継続的スキャンモードの場合は1秒後に再スキャン
    void onScanFinished()
    {
       qDebug() << "スキャンが完了";
       emit scanFinished();
 
       // 継続的スキャンの場合は再開
       if (isContinuousScan) {
          QTimer::singleShot(1000, this, [this]() {
             discoveryAgent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
          });
       }
    }
 
    // エラー発生時の処理 (エラーメッセージの生成とログ出力)
    // 継続的スキャンモードの場合は5秒後に自動再試行
    void onError(QBluetoothDeviceDiscoveryAgent::Error error)
    {
       QString errorMessage = getErrorMessage(error);
       qDebug() << "スキャンエラー: " << errorMessage;
       emit errorOccurred(errorMessage);
 
       // エラーからの自動復帰を試みる (5秒後にBLEデバイスの検出を開始)
       if (isContinuousScan) {
          QTimer::singleShot(5000, this, [this]() {
             discoveryAgent.start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
          });
       }
    }
 };



サービスディスカバリ

特定のBluetoothデバイスが提供するサービスを探索するプロセスである。

GATT階層構造

BLEでは、GATT (Generic Attribute Profile) という階層構造を持つ。
Classic Bluetoothとは異なり、キャラクタリスティックとディスクリプタの概念が追加されている。

  • サービス
    デバイスの機能をグループ化 (例: 心拍計測、温度センサ等)
    キャラクタリスティックは各サービスに1つ以上必要である。
  • キャラクタリスティック
    実際のデータ値や操作 (例: 心拍値、温度値、設定値等)
    キャラクタリスティックは、各サービスに1つ以上必要である。
  • ディスクリプタ
    キャラクタリスティックの追加情報 (単位や説明等)
    ディスクリプタは必須ではなく、オプショナル (0個以上) である。


# GATT階層構造

Profile
└── Service (1..n)       # Primary Service (メインとなる独立したサービス)
    │                    # デバイスの主要な機能を表現
    │                    # 他のサービスに依存しない
    │                    # 例: 心拍計測、温度センサ等
    ├── Include (0..n)   # Secondary Serviceへの参照
    ├── Characteristic (1..n)
    │   └── Descriptor (0..n)
    ├── Secondary Service 1 (0..n)  # バッテリー監視 (Primaryサービスをサポートする補助的なサービス)
    └── Secondary Service 2 (0..n)  # デバイス診断   (Primaryサービスをサポートする補助的なサービス)
                                    # Secondary Serviceは、単独では意味を持たない
                                    # 必ず1つ以上のPrimary Serviceに関連付けられる


サービスディスカバリでは、これらの階層構造を探索して、利用可能な機能を特定する。
標準的なサービスには決められたUUIDが割り当てられている。(例: 心拍サービス、電池残量サービス等)
UUIDは16-bit (標準) または 128-bit (カスタム) である。

BLEサービスディスカバリの手順

  1. GAP (Generic Access Profile) によるデバイススキャン
  2. デバイスへの接続
  3. GATTサービスの列挙
  4. 各サービス内のキャラクタリスティック探索
  5. 必要に応じてディスクリプタ探索


Qtでは、QLowEnergyServiceクラスを使用して、BLEの複雑なサービス階層を管理する。

Classic Bluetoothとの違いを以下に示す。

  • QLowEnergyController::connectToDeviceメソッドで接続する。
  • QLowEnergyController::discoveryFinishedシグナルで探索完了を検知する。
  • QLowEnergyController::createServiceObjectメソッドを使用して、サービスオブジェクトを生成する。


使用例

QLowEnergyControllerクラスのインスタンスの生成

まず、QBluetoothDeviceDiscoveryAgentクラスを使用して、スキャンを実行する。

次に、接続するデバイスのQBluetoothDeviceInfoクラスのデバイス情報を取得する。
取得したデバイス情報を元に、QLowEnergyControllerクラスのインスタンスを生成する。

コントローラのシグナル / スロット接続

最低限必要なシグナルを示す。

  • connected
    デバイスへの接続完了を通知
  • disconnected
    デバイスとの切断を通知
  • serviceDiscovered
    新しいサービスの発見を通知
  • discoveryFinished
    サービス探索の完了を通知


BLEデバイスの接続

connectToDeviceメソッドを実行してBLEデバイスに接続する。
connectedシグナルの受信を待つ。

サービスの探索

BLEデバイスへ接続後、discoverServicesメソッドを実行してサービスの探索を開始する。

  • serviceDiscoveredシグナルで個々のサービスが見つかる度に通知される。
  • discoveryFinishedシグナルで探索完了が通知される。


サービスの取得

createServiceObjectメソッドをを実行して、探索で発見したサービスのQLowEnergyServiceオブジェクトを生成する。
このオブジェクトを使用して、特性 (Characteristic) や ディスクリプタにアクセスできる。

組み合わせ
 // BLEのサービス探索を管理するクラス
 // BLEデバイスへの接続, サービスの探索と監視
 
 #include <QObject>
 #include <QLowEnergyController>
 #include <QLowEnergyService>
 #include <QTimer>
 #include <QDebug>
 
 class BLEServiceDiscovery : public QObject
 {
    Q_OBJECT
 
 private:
    QLowEnergyController controller;        // BLE接続とサービス探索を制御するコントローラ
    QTimer               discoveryTimeout;  // サービス探索のタイムアウトを管理するタイマ
 
    // コントローラの各種シグナルを接続
    // 接続 / 切断, サービス探索の進行状況, 状態変更, エラー
    void connectControllerSignals()
    {
       connect(&controller, &QLowEnergyController::connected, this, &BLEServiceDiscovery::onConnected);
       connect(&controller, &QLowEnergyController::disconnected, this, &BLEServiceDiscovery::onDisconnected);
       connect(&controller, &QLowEnergyController::serviceDiscovered, this, &BLEServiceDiscovery::onServiceDiscovered);
       connect(&controller, &QLowEnergyController::discoveryFinished, this, &BLEServiceDiscovery::onDiscoveryFinished);
       connect(&controller, static_cast<void(QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error), this, &BLEServiceDiscovery::onError);
       connect(&controller, &QLowEnergyController::stateChanged, this, &BLEServiceDiscovery::onStateChanged);
    }
 
    // 個別のBLEサービスに対するシグナル
    // 状態変更とエラーイベントを監視してログを出力する
    void connectServiceSignals(QLowEnergyService *service)
    {
       connect(service, &QLowEnergyService::stateChanged, this, [this, service](QLowEnergyService::ServiceState newState) {
          qDebug() << "サービス状態が変更: " << getServiceStateMessage(newState);
       });
 
       connect(service, static_cast<void(QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, [this](QLowEnergyService::ServiceError error) {
          qDebug() << "サービスエラー: " << getServiceErrorMessage(error);
       });
    }
 
    // エラーコードの変換
    QString getErrorMessage(QLowEnergyController::Error error)
    {
       switch (error) {
          case QLowEnergyController::NoError:                      return "エラーなし";
          case QLowEnergyController::UnknownError:                 return "不明なエラー";
          case QLowEnergyController::UnknownRemoteDeviceError:     return "リモートデバイスが見つからない";
          case QLowEnergyController::NetworkError:                 return "ネットワークエラー";
          case QLowEnergyController::InvalidBluetoothAdapterError: return "無効なBluetoothアダプタ";
          case QLowEnergyController::ConnectionError:              return "接続エラー";
          default:                                                 return "予期せぬエラー";
       }
    }
 
    QString getStateMessage(QLowEnergyController::ControllerState state)
    {
       switch (state) {
          case QLowEnergyController::UnconnectedState: return "未接続";
          case QLowEnergyController::ConnectingState:  return "接続中";
          case QLowEnergyController::ConnectedState:   return "接続済み";
          case QLowEnergyController::DiscoveringState: return "探索中";
          case QLowEnergyController::DiscoveredState:  return "探索完了";
          case QLowEnergyController::ClosingState:     return "切断中";
          case QLowEnergyController::AdvertisingState: return "アドバタイジング中";
          default:                                     return "不明な状態";
       }
    }
 
    QString getServiceStateMessage(QLowEnergyService::ServiceState state)
    {
       switch (state) {
          case QLowEnergyService::InvalidService:      return "無効なサービス";
          case QLowEnergyService::DiscoveryRequired:   return "探索が必要";
          case QLowEnergyService::DiscoveringServices: return "サービス探索中";
          case QLowEnergyService::ServiceDiscovered:   return "サービス探索完了";
          default:                                     return "不明なサービス状態";
       }
    }
 
    QString getServiceErrorMessage(QLowEnergyService::ServiceError error)
    {
       switch (error) {
          case QLowEnergyService::NoError:                  return "エラーなし";
          case QLowEnergyService::OperationError:           return "操作エラー";
          case QLowEnergyService::CharacteristicReadError:  return "キャラクタリスティック読み取りエラー";
          case QLowEnergyService::CharacteristicWriteError: return "キャラクタリスティック書き込みエラー";
          case QLowEnergyService::DescriptorReadError:      return "ディスクリプタ読み取りエラー";
          case QLowEnergyService::DescriptorWriteError:     return "ディスクリプタ書き込みエラー";
          case QLowEnergyService::UnknownError:             return "不明なエラー";
          default:                                          return "予期せぬサービスエラー";
       }
    }
 
 public:
    explicit BLEServiceDiscovery(QObject* parent = nullptr) : QObject(parent)
    {
       // タイムアウトタイマの初期化
       discoveryTimeout.setInterval(10000);  // 10秒のタイムアウト
       discoveryTimeout.setSingleShot(true);
 
       connect(&discoveryTimeout, &QTimer::timeout, this, &BLEServiceDiscovery::onDiscoveryTimeout);
    }
 
    // デバイスへの接続とサービス探索の開始
    // コントローラの初期化, シグナルの接続, デバイスへの接続開始, タイムアウトタイマの開始
    void startDiscovery(const QBluetoothDeviceInfo &device)
    {
       try {
          qDebug() << "サービス探索を開始: " << device.name();
 
          // コントローラの初期化
          connectControllerSignals();
 
          // 接続開始
          controller.connectToDevice();
          discoveryTimeout.start();
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("探索開始エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 探索を停止 (タイムアウトタイマも停止)
    void stopDiscovery()
    {
       try {
          if (controller) {
             controller.disconnectFromDevice();
             discoveryTimeout.stop();
          }
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("探索停止エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
 signals:
    void serviceDiscovered(QLowEnergyService *service);  // 新しいサービスが発見された時に発行
    void discoveryComplete();                            // 全てのサービス探索が完了した時に発行
    void errorOccurred(const QString &error);            // エラーが発生した時に発行
    void connectionStateChanged(QLowEnergyController::ControllerState state);  // 接続状態が変更された時に発行
 
 private slots:
    // デバイスへの接続が完了した時 (タイムアウトタイマを停止して、サービス探索を開始)
    void onConnected()
    {
       qDebug() << "デバイスに接続";
       discoveryTimeout.stop();
 
       // サービス探索の開始
       controller.discoverServices();
    }
 
    // デバイスから切断された時 (タイムアウトタイマを停止)
    void onDisconnected()
    {
       qDebug() << "デバイスから切断";
       discoveryTimeout.stop();
    }
 
    // 新しいサービスが発見された時
    void onServiceDiscovered(const QBluetoothUuid& uuid)
    {
       qDebug() << "サービスを発見:" << uuid.toString();
 
       // サービスオブジェクトの作成
       QLowEnergyService* service = controller.createServiceObject(uuid, this);
       if (service) {
          connectServiceSignals(service);
          emit serviceDiscovered(service);
       }
    }
 
    // サービス探索が完了した時
    void onDiscoveryFinished()
    {
       qDebug() << "サービス探索が完了";
       discoveryTimeout.stop();
       emit discoveryComplete();
    }
 
    // エラーが発生した時
    void onError(QLowEnergyController::Error error)
    {
       QString errorMessage = getErrorMessage(error);
       qDebug() << "エラーが発生: " << errorMessage;
       emit errorOccurred(errorMessage);
    }
 
    // タイムアウトが発生した時 (探索を停止して、エラーとして通知)
    void onDiscoveryTimeout()
    {
       qDebug() << "サービス探索がタイムアウト";
       stopDiscovery();
       emit errorOccurred("探索タイムアウト");
    }
 
    // 接続状態が変更された時 (状態の変更をログ出力して、通知)
    void onStateChanged(QLowEnergyController::ControllerState state)
    {
       qDebug() << "接続状態が変更: " << getStateMessage(state);
       emit connectionStateChanged(state);
    }
 };



ペアリング管理 (ボンディング)

セキュリティレベル

  • Just Works
    自動的にペアリングを実行する。
    Man-in-the-Middle攻撃に対して脆弱である。
    IoTセンサ等、機密性の低いデバイスに適用される。
    最も簡単であるが、セキュリティは低い。

  • Passkey Entry
    PINコードを使用する。
    • Fixed PIN
      デバイスに事前設定された固定PIN
    • Dynamic PIN
      接続時に動的に生成されるPIN
    キーボード付きデバイスやディスプレイ付きデバイスで使用される。

  • Out of Band
    NFC / QRコード等の別チャネルの通信によりでペアリング情報を交換する。
    高いセキュリティレベルを実現しているが、追加のハードウェアが必要となる。

  • Numeric Comparison
    両デバイスで6桁の数字を表示・確認する。
    そのため、ディスプレイ付きデバイス間で使用される。
    Bluetooth 4.2以降で利用可能である。


ボンディング管理の要素

  • LTK (Long Term Key)
    暗号化に使用する長期キー
    不揮発性メモリに保存
    デバイスペア固有の値

  • IRK (Identity Resolving Key)
    プライバシー保護用の識別子解決キー
    ランダムアドレスの解決に使用する。
    デバイスのプライバシー保護に重要である。

  • ボンディング情報の管理
    タイムアウト時の自動削除
    手動での削除機能
    最大ペアリング数の制限


Classic Bluetoothとの主な違い

  • 接続パラメータ
    接続間隔、レイテンシ、タイムアウト等
    Connection Interval: 7.5ミリ秒~4秒
    Slave Latencyが設定可能である。
    MTUサイズは、BLEは23バイト (デフォルト)

  • 省電力モード
    スリープモード制御
    接続パラメータの動的調整
    バッテリー状態の監視

  • セキュリティ面
    BLEは、SMPプロトコルを使用する。
    より安全なECDH鍵交換方式採用している。
    簡略化されたペアリングプロセス

  • 追加のセキュリティ機能
    • LEガーディアンタイムアウト
      接続維持の時間制限
      セキュリティリスク軽減
      設定可能な時間値


使用例

ローカルデバイスの初期化

BLEデバイスを利用するため、QBluetoothLocalDeviceクラスを使用して初期化する。
初期化時にBLEデバイスが有効かどうかの確認を行う。

  • QBluetoothLocalDeviceクラス
  • QBluetoothAddressクラス


 #include <QBluetoothLocalDevice>
 #include <QBluetoothAddress>
 
 QBluetoothLocalDevice localDevice;
 if (!localDevice.isValid()) {
    qDebug() << "Bluetoothデバイスが利用できません";
    return;
 }


ペアリング状態の変化を監視

ペアリングの結果を受信すため、QBluetoothLocalDevice::pairingFinishedシグナルに接続する。
これにより、ペアリング成功 / 失敗の通知を受け取ることができる。

 connect(&localDevice, &QBluetoothLocalDevice::pairingFinished, [](const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) {
    if (pairing == QBluetoothLocalDevice::Paired) {
       qDebug() << "ペアリング成功:" << address.toString();
    }
    else {
       qDebug() << "ペアリング失敗:" << address.toString();
    }
 });


PINコード確認のシグナル (必要な場合のみ)

PINコードが必要なデバイスは、BluetoothLocalDevice::pairingDisplayConfirmationシグナルに接続する。
これにより、PINコードの確認が必要な場合に通知を受け取ることができる。

 connect(&localDevice, &QBluetoothLocalDevice::pairingDisplayConfirmation, [](const QBluetoothAddress &address, QString pin) {
    qDebug() << "PINコード確認: " << address.toString() << pin;
 });


ペアリング要求

ペアリングを開始するため、ペアリングするデバイスのBluetoothアドレスを指定して、QBluetoothLocalDevice::requestPairingメソッドを実行する。

ペアリングの結果は、QBluetoothLocalDevice::pairingFinishedシグナルで受信して、成功 / 失敗に応じた処理を行う。

 QBluetoothAddress targetAddress("XX:XX:XX:XX:XX:XX"); // 接続先アドレス
 localDevice.requestPairing(targetAddress, QBluetoothLocalDevice::Paired);


組み合わせ
 // BLEデバイスとのペアリング操作を管理するクラス
 // ペアリングの開始と解除, ペアリング状態の監視, PINコード認証
 
 #include <QObject>
 #include <QBluetoothLocalDevice>
 #include <QBluetoothAddress>
 #include <QDebug>
 
 class BLEPairing : public QObject
 {
   Q_OBJECT
 
 private:
    QBluetoothLocalDevice localDevice;  // ローカルのBLEデバイスを管理するオブジェクト
 
    // ペアリング状態を表す文字列
    QString getPairingStatusString(QBluetoothLocalDevice::Pairing status)
    {
       switch (status) {
          case QBluetoothLocalDevice::Unpaired:         return "未ペアリング";
          case QBluetoothLocalDevice::Paired:           return "ペアリング済み";
          case QBluetoothLocalDevice::AuthorizedPaired: return "認証済みペアリング";
          default:                                      return "不明な状態";
       }
    }
 
    // BLEデバイスが使用可能な状態かどうかを確認
    bool isDeviceReady() const
    {
       if (!localDevice || !localDevice->isValid()) return false;
       return localDevice.hostMode() != QBluetoothLocalDevice::HostPoweredOff;
    }
 
    // BLEデバイスのホストモードを設定
    void setHostMode(QBluetoothLocalDevice::HostMode mode)
    {
       if (isDeviceReady()) localDevice.setHostMode(mode);
    }
 
    // ホスト側においてBLEが利用可能かどうかを確認
    bool isBluetoothAvailable() const
    {
       return QBluetoothLocalDevice::allDevices().count() > 0;
    }
 
 public:
    explicit BLEPairing(QObject *parent = nullptr) : QObject(parent)
    {
       try {
          // シグナルの接続
          connect(&localDevice, &QBluetoothLocalDevice::pairingFinished, this, &BLEPairing::onPairingFinished);
          connect(&localDevice, &QBluetoothLocalDevice::error, this, &BLEPairing::onError);
          connect(&localDevice, &QBluetoothLocalDevice::pairingDisplayConfirmation, this, &BLEPairing::onPairingDisplayConfirmation);
          connect(&localDevice, &QBluetoothLocalDevice::pairingDisplayPinCode, this, &BLEPairing::onPairingDisplayPinCode);
       }
       catch (const std::exception &e) {
          qDebug() << "初期化エラー:" << e.what();
          throw;
       }
    }
 
    ~BLEPairing()
    {
       if (localDevice) {
          disconnect(&localDevice);  // 全ての接続を解除
       }
    }
 
    // 指定したデバイスとのペアリングを開始
    // 30秒のタイムアウトを設定 (失敗時はエラーを通知)
    void requestPairing(const QBluetoothAddress &address)
    {
       try {
          if (!isDeviceReady()) {
             emit errorOccurred("ローカルBluetoothデバイスが無効です");
             return;
          }
 
          // タイムアウトの設定
          QTimer::singleShot(30000, this, [this, address]() {
             if (getPairingStatus(address) != QBluetoothLocalDevice::Paired) {
                emit errorOccurred("ペアリングがタイムアウトしました");
             }
          });
 
          qDebug() << "ペアリングを開始: " << address.toString();
          localDevice.requestPairing(address, QBluetoothLocalDevice::Paired);
       }
       catch (const std::exception &e) {
          emit errorOccurred("ペアリング開始時に予期せぬエラーが発生しました");
       }
    }
 
    // 指定したBLEデバイスとのペアリングを解除
    void removePairing(const QBluetoothAddress& address)
    {
       try {
          if (!localDevice.isValid()) {
             emit errorOccurred("ローカルBluetoothデバイスが無効");
             return;
          }
 
          qDebug() << "ペアリングを解除: " << address.toString();
          localDevice.requestPairing(address, QBluetoothLocalDevice::Unpaired);
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("ペアリング解除エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 指定したBLEデバイスとのペアリング状態を確認
    QBluetoothLocalDevice::Pairing getPairingStatus(const QBluetoothAddress &address)
    {
       try {
          if (!localDevice.isValid()) {
             emit errorOccurred("ローカルBluetoothデバイスが無効");
             return QBluetoothLocalDevice::Unpaired;
          }
 
          return localDevice.pairingStatus(address);
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("ペアリング状態確認エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
          return QBluetoothLocalDevice::Unpaired;
       }
    }
 
 signals:
    void pairingComplete(const QBluetoothAddress& address, bool success);             // ペアリング完了通知
    void pairingConfirmationRequired(const QBluetoothAddress& address, QString pin);  // PIN確認要求通知
    void errorOccurred(const QString& error);  // エラー通知
 
 private slots:
    // ペアリング完了時
    void onPairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing)
    {
       QString status = getPairingStatusString(pairing);
       qDebug() << "ペアリング完了: " << address.toString() << "-" << status;
       emit pairingComplete(address, pairing == QBluetoothLocalDevice::Paired);
    }
 
    // BLEエラー発生時
    void onError()
    {
       QBluetoothLocalDevice::Error error = localDevice->error();
       QString errorMsg;
 
       switch (error) {
          case QBluetoothLocalDevice::NoError:
             errorMsg = "エラーなし";
             break;
          case QBluetoothLocalDevice::PairingError:
             errorMsg = "ペアリングエラー";
             break;
          case QBluetoothLocalDevice::UnknownError:
          default:
             errorMsg = "不明なエラー";
             break;
       }
 
       qDebug() << "Bluetoothエラー: " << errorMsg;
       emit errorOccurred(errorMsg);
    }
 
    // ペアリング時のPIN確認要求処理
    void onPairingDisplayConfirmation(const QBluetoothAddress &address, QString pin)
    {
       qDebug() << "ペアリング確認が必要:";
       qDebug() << "  デバイス: " << address.toString();
       qDebug() << "  PIN: " << pin;
       emit pairingConfirmationRequired(address, pin);
    }
 
    // PINコード表示要求時
    void onPairingDisplayPinCode(const QBluetoothAddress &address, QString pin)
    {
       qDebug() << "PINコードの表示:";
       qDebug() << "  デバイス: " << address.toString();
       qDebug() << "  PIN: " << pin;
    }
 };



接続の確立と維持

BLEでは、Central (セントラル)およびPeripheral (ペリフェラル)という役割がある。

  • Central
    通常は、スマートフォンやPCのことを指す。(接続を開始する側)
  • Peripheral
    通常は、センサやウェアラブルデバイスのことを指す。(接続を待つ側)


データ通信の特徴を以下に示す。

  • Read
    データの読み取り
  • Write
    データの書き込み
  • Notify
    データの変更時に自動通知
  • Indicate
    Notifyと同様、確認応答あり


使用例

 #include <QObject>
 #include <QLowEnergyController>
 #include <QLowEnergyService>
 #include <QLowEnergyCharacteristic>
 #include <QTimer>
 #include <QDebug>
 
 class BLEConnection : public QObject
 {
    Q_OBJECT
 
 private:
    QLowEnergyController controller;
    QTimer               reconnectTimer;
    QBluetoothDeviceInfo currentDevice;
    bool autoReconnect = false;
 
    void connectControllerSignals()
    {
       connect(&controller, &QLowEnergyController::connected, this, &BLEConnection::onConnected);
       connect(&controller, &QLowEnergyController::disconnected, this, &BLEConnection::onDisconnected);
       connect(&controller, &QLowEnergyController::serviceDiscovered, this, &BLEConnection::onServiceDiscovered);
       connect(&controller, static_cast<void(QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error), this, &BLEConnection::onError);
       connect(&controller, &QLowEnergyController::stateChanged, this, &BLEConnection::onStateChanged);
    }
 
    void connectServiceSignals(QLowEnergyService *service)
    {
        connect(service, &QLowEnergyService::stateChanged, this, [this, service](QLowEnergyService::ServiceState newState) {
           qDebug() << "サービス状態が変更: " << getServiceStateMessage(newState);
 
           if (newState == QLowEnergyService::ServiceDiscovered) {
              // キャラクタリスティックの処理
              const QList<QLowEnergyCharacteristic> chars = service->characteristics();
              for (const QLowEnergyCharacteristic& ch : chars) {
                 qDebug() << "キャラクタリスティック発見:";
                 qDebug() << "  UUID: " << ch.
 
                 qDebug() << "キャラクタリスティック発見:";
                 qDebug() << "  UUID: " << ch.uuid().toString();
                 qDebug() << "  プロパティ: " << getCharacteristicPropertiesString(ch.properties());
 
                 // Notify対応のキャラクタリスティックの場合
                 if (ch.properties() & QLowEnergyCharacteristic::Notify) {
                    QLowEnergyDescriptor notifyDesc = ch.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
                    if (notifyDesc.isValid()) {
                       service->writeDescriptor(notifyDesc, QByteArray::fromHex("0100"));
                    }
                 }
              }
           }
        });
 
        connect(service, static_cast<void(QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this,
                [this](QLowEnergyService::ServiceError error) {
                   qDebug() << "サービスエラー:" << getServiceErrorMessage(error);
        });
 
        connect(service, &QLowEnergyService::characteristicChanged, this,
                [](const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue) {
                   qDebug() << "キャラクタリスティック値が変更:";
                   qDebug() << "  UUID:" << characteristic.uuid().toString();
                   qDebug() << "  新しい値:" << newValue.toHex();
        });
    }
 
    QString getErrorMessage(QLowEnergyController::Error error)
    {
       switch (error) {
          case QLowEnergyController::NoError:                      return "エラーなし";
          case QLowEnergyController::UnknownError:                 return "不明なエラー";
          case QLowEnergyController::UnknownRemoteDeviceError:     return "リモートデバイスが見つかりません";
          case QLowEnergyController::NetworkError:                 return "ネットワークエラー";
          case QLowEnergyController::InvalidBluetoothAdapterError: return "無効なBluetoothアダプタ";
          case QLowEnergyController::ConnectionError:              return "接続エラー";
          default:                                                 return "予期せぬエラー";
       }
    }
 
    QString getStateMessage(QLowEnergyController::ControllerState state)
    {
       switch (state) {
          case QLowEnergyController::UnconnectedState: return "未接続";
          case QLowEnergyController::ConnectingState:  return "接続中";
          case QLowEnergyController::ConnectedState:   return "接続済み";
          case QLowEnergyController::DiscoveringState: return "探索中";
          case QLowEnergyController::DiscoveredState:  return "探索完了";
          case QLowEnergyController::ClosingState:     return "切断中";
          case QLowEnergyController::AdvertisingState: return "アドバタイジング中";
          default:                                     return "不明な状態";
       }
    }
 
    QString getServiceStateMessage(QLowEnergyService::ServiceState state)
    {
       switch (state) {
          case QLowEnergyService::InvalidService:      return "無効なサービス";
          case QLowEnergyService::DiscoveryRequired:   return "探索が必要";
          case QLowEnergyService::DiscoveringServices: return "サービス探索中";
          case QLowEnergyService::ServiceDiscovered:   return "サービス探索完了";
          default:                                     return "不明なサービス状態";
       }
    }
 
    QString getServiceErrorMessage(QLowEnergyService::ServiceError error)
    {
       switch (error) {
          case QLowEnergyService::NoError:                  return "エラーなし";
          case QLowEnergyService::OperationError:           return "操作エラー";
          case QLowEnergyService::CharacteristicReadError:  return "キャラクタリスティック読み取りエラー";
          case QLowEnergyService::CharacteristicWriteError: return "キャラクタリスティック書き込みエラー";
          case QLowEnergyService::DescriptorReadError:      return "ディスクリプタ読み取りエラー";
          case QLowEnergyService::DescriptorWriteError:     return "ディスクリプタ書き込みエラー";
          case QLowEnergyService::UnknownError:             return "不明なエラー";
          default:                                          return "予期せぬサービスエラー";
       }
    }
 
    QString getCharacteristicPropertiesString(QLowEnergyCharacteristic::PropertyTypes properties)
    {
       QStringList props;
       if (properties & QLowEnergyCharacteristic::Read)            props << "Read";
       if (properties & QLowEnergyCharacteristic::Write)           props << "Write";
       if (properties & QLowEnergyCharacteristic::Notify)          props << "Notify";
       if (properties & QLowEnergyCharacteristic::Indicate)        props << "Indicate";
       if (properties & QLowEnergyCharacteristic::WriteSigned)     props << "WriteSigned";
       if (properties & QLowEnergyCharacteristic::WriteNoResponse) props << "WriteNoResponse";
       return props.join(", ");
    }
 
 public:
    explicit BLEConnection(QObject* parent = nullptr) : QObject(parent)
    {
       // 再接続タイマの初期化
       reconnectTimer.setInterval(5000);  // 5秒間隔で再接続
       reconnectTimer.setSingleShot(true);

       connect(&reconnectTimer &QTimer::timeout, this, &BLEConnection::onReconnectTimeout);
    }
 
    // デバイスへの接続
    void connectToDevice(const QBluetoothDeviceInfo &device)
    {
       try {
          qDebug() << "デバイスへの接続を開始:" << device.name();

          // コントローラの初期化
          connectControllerSignals();

          // 接続パラメータの設定
          controller.setRemoteAddressType(QLowEnergyController::PublicAddress);

          // 接続開始
          controller.connectToDevice();
          currentDevice = device;
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("接続エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 切断
    void disconnect()
    {
       try {
          if (controller) {
             controller.disconnectFromDevice();
             reconnectTimer.stop();
          }
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("切断エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 自動再接続の設定
    void setAutoReconnect(bool enable)
    {
       autoReconnect = enable;
       if (!enable) {
          reconnectTimer.stop();
       }
    }
 
 signals:
    void connected();
    void disconnected();
    void errorOccurred(const QString& error);
    void connectionStateChanged(QLowEnergyController::ControllerState state);
    void serviceDiscovered(QLowEnergyService* service);
 
 private slots:
    void onConnected()
    {
       qDebug() << "デバイスに接続";
       reconnectTimer.stop();
       emit connected();
 
       // サービスの探索を開始
       controller.discoverServices();
    }
 
    void onDisconnected()
    {
       qDebug() << "デバイスから切断";
       emit disconnected();
 
       // 自動再接続が有効な場合
       if (autoReconnect) {
          qDebug() << "再接続を試行...";
          reconnectTimer.start();
       }
    }
 
    void onServiceDiscovered(const QBluetoothUuid& uuid)
    {
       qDebug() << "サービスを発見: " << uuid.toString();
 
       QLowEnergyService* service = controller.createServiceObject(uuid, this);
       if (service) {
          connectServiceSignals(service);
          emit serviceDiscovered(service);
       }
    }
 
    void onError(QLowEnergyController::Error error)
    {
       QString errorMessage = getErrorMessage(error);
       qDebug() << "エラーが発生: " << errorMessage;
       emit errorOccurred(errorMessage);
 
       if (autoReconnect && error != QLowEnergyController::InvalidBluetoothAdapterError) reconnectTimer.start();
    }

    void onStateChanged(QLowEnergyController::ControllerState state)
    {
       qDebug() << "接続状態が変更: " << getStateMessage(state);
       emit connectionStateChanged(state);
    }
 
    void onReconnectTimeout()
    {
       if (autoReconnect && currentDevice.isValid()) {
          qDebug() << "再接続を試行...";
          controller.connectToDevice();
       }
    }
 };



データの送受信

Classic Bluetoothとの違い

  • キャラクタリスティックの操作が基本
  • 読み取り
    readCharacteristicメソッド
  • 書き込み
    writeCharacteristicメソッド
  • 通知
    characteristicChangedシグナルで受信


使用例

以下の例では、BLEを使用してデータの送受信を行っている。

BLEDataTransferクラスの機能を以下に示す。

  • データの受信
    特定のキャラクタリスティックから値を受信
    非同期の受信完了通知
  • データの送信
    特定のキャラクタリスティックに値を送信
    キューを使用した順序付き送信
    非同期の送信完了通知
  • Notify / Indicate (通知) の管理
    特定のキャラクタリスティックの通知を有効化 / 無効化
    値変更時の自動通知


 #include <QObject>
 #include <QLowEnergyService>
 #include <QLowEnergyCharacteristic>
 #include <QLowEnergyDescriptor>
 #include <QQueue>
 #include <QTimer>
 #include <memory>
 #include <QDebug>
 
 class BLEDataTransfer : public QObject
 {
    Q_OBJECT
 
 private:
    struct WriteRequest {
       QBluetoothUuid uuid;
       QByteArray value;
    };
 
    QLowEnergyService       *service = nullptr;
    std::unique_ptr<QTimer> writeTimer;
    QQueue<WriteRequest>    writeQueue;
  
    void connectServiceSignals()
    {
       // キャラクタリスティック受信完了時
       connect(service, &QLowEnergyService::characteristicRead, this, [this](const QLowEnergyCharacteristic &c, const QByteArray &value) {
          qDebug() << "キャラクタリスティック受信完了:";
          qDebug() << "  UUID:" << c.uuid().toString();
          qDebug() << "  値:" << value.toHex();
          emit characteristicRead(c.uuid(), value);
       });
 
       // キャラクタリスティック送信完了時
       connect(service, &QLowEnergyService::characteristicWritten, this, [this](const QLowEnergyCharacteristic &c, const QByteArray &value) {
          qDebug() << "キャラクタリスティック送信完了:";
          qDebug() << "  UUID:" << c.uuid().toString();
          qDebug() << "  値:" << value.toHex();
          emit characteristicWritten(c.uuid(), value);
       });
 
       // キャラクタリスティック値変更時 (Notify / Indicate)
       connect(service, &QLowEnergyService::characteristicChanged, this, [this](const QLowEnergyCharacteristic &c, const QByteArray &value) {
          qDebug() << "キャラクタリスティック値変更:";
          qDebug() << "  UUID:" << c.uuid().toString();
          qDebug() << "  新しい値:" << value.toHex();
          emit characteristicChanged(c.uuid(), value);
       });
 
       // サービスエラー発生時
       connect(service, static_cast<void(QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, [this](QLowEnergyService::ServiceError error) {
          QString errorMsg = getServiceErrorMessage(error);
          qDebug() << "サービスエラー: " << errorMsg;
          emit errorOccurred(errorMsg);
       });
    }
 
    QString getServiceErrorMessage(QLowEnergyService::ServiceError error)
    {
       switch (error) {
          case QLowEnergyService::NoError:
             return "エラーなし";
          case QLowEnergyService::OperationError:
             return "操作エラー";
          case QLowEnergyService::CharacteristicReadError:
             return "キャラクタリスティック読み取りエラー";
          case QLowEnergyService::CharacteristicWriteError:
             return "キャラクタリスティック書き込みエラー";
          case QLowEnergyService::DescriptorReadError:
             return "ディスクリプタ読み取りエラー";
          case QLowEnergyService::DescriptorWriteError:
             return "ディスクリプタ書き込みエラー";
          case QLowEnergyService::UnknownError:
             return "不明なエラー";
          default:
             return "予期せぬサービスエラー";
       }
    }
 
 public:
    explicit BLEDataTransfer(QObject* parent = nullptr) : QObject(parent)
    {
       // 送信キュー処理用タイマ
       writeTimer = std::make_unique<QTimer>(this);
       writeTimer->setInterval(100);  // 100[ms]間隔
       connect(writeTimer.get(), &QTimer::timeout, this, &BLEDataTransfer::processWriteQueue);
    }
 
    // サービスの設定
    void setService(QLowEnergyService* service)
    {
       try {
          if (!service) {
             throw std::runtime_error("無効なサービス");
          }
 
          this->service = service;
          connectServiceSignals();
          qDebug() << "サービスの設定完了: " << service->serviceUuid().toString();
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("サービス設定エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 特定のキャラクタリスティックの値を受信
    void readCharacteristic(const QBluetoothUuid& uuid)
    {
       try {
          if (!service) {
             throw std::runtime_error("サービスが未設定");
          }
 
          QLowEnergyCharacteristic characteristic = service->characteristic(uuid);
          if (!characteristic.isValid()) throw std::runtime_error("無効なキャラクタリスティック");
 
          qDebug() << "キャラクタリスティックの読み取りを開始:" << uuid.toString();
          service->readCharacteristic(characteristic);
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("読み取りエラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // 特定のキャラクタリスティックに値を送信
    void writeCharacteristic(const QBluetoothUuid& uuid, const QByteArray& value, bool useQueue = true)
    {
       try {
          if (!service) throw std::runtime_error("サービスが未設定");
 
          QLowEnergyCharacteristic characteristic = service->characteristic(uuid);
          if (!characteristic.isValid()) throw std::runtime_error("無効なキャラクタリスティックです");
 
          // 書き込みプロパティの確認
          if (!(characteristic.properties() & (QLowEnergyCharacteristic::Write | QLowEnergyCharacteristic::WriteNoResponse))) {
             throw std::runtime_error("書き込みが許可されていない");
          }
 
          if (useQueue) {
             // キューに追加
             WriteRequest request;
             request.uuid = uuid;
             request.value = value;
             writeQueue.enqueue(request);
 
             if (!writeTimer->isActive()) writeTimer->start();
          }
          else {
             // 即時書き込み
             qDebug() << "キャラクタリスティックに送信: " << uuid.toString() << "値: " << value.toHex();
             service->writeCharacteristic(characteristic, value);
          }
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("書き込みエラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
    // Notify / Indicateの有効化
    void enableNotifications(const QBluetoothUuid& uuid, bool enable = true)
    {
       try {
          if (!service) throw std::runtime_error("サービスが未設定");
 
          QLowEnergyCharacteristic characteristic = service->characteristic(uuid);
          if (!characteristic.isValid()) throw std::runtime_error("無効なキャラクタリスティック");
 
          // Notify/Indicateプロパティのチェック
          if (!(characteristic.properties() & (QLowEnergyCharacteristic::Notify | QLowEnergyCharacteristic::Indicate))) {
             throw std::runtime_error("通知が許可されていない");
          }
 
          // Client Characteristic Configuration Descriptorを取得
          QLowEnergyDescriptor cccd = characteristic.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
 
          if (!cccd.isValid()) throw std::runtime_error("CCCDが見つからない");
 
          // 通知の有効化 / 無効化
          QByteArray value = enable ? QByteArray::fromHex("0100") : QByteArray::fromHex("0000");
          qDebug() << "通知を" << (enable ? "有効" : "無効") << "にします:" << uuid.toString();
          service->writeDescriptor(cccd, value);
       }
       catch (const std::exception &e) {
          QString errorMsg = QString("通知設定エラー: %1").arg(e.what());
          qDebug() << errorMsg;
          emit errorOccurred(errorMsg);
       }
    }
 
 signals:
    void characteristicRead(const QBluetoothUuid& uuid, const QByteArray& value);
    void characteristicWritten(const QBluetoothUuid& uuid, const QByteArray& value);
    void characteristicChanged(const QBluetoothUuid& uuid, const QByteArray& value);
    void errorOccurred(const QString& error);
 
 private slots:
    void processWriteQueue()
    {
       if (writeQueue.isEmpty()) {
          writeTimer->stop();
          return;
       }
 
       if (!service) {
          writeQueue.clear();
          writeTimer->stop();
          return;
       }
 
       WriteRequest request = writeQueue.dequeue();
       writeCharacteristic(request.uuid, request.value, false);
    }
 };



BLE通信の使用例

 // BLEManager.hファイル
 
 #include <QCoreApplication>
 #include <QTimer>
 #include <memory>
 #include "bledevicescanner.h"
 #include "bleservicediscovery.h"
 #include "blepairing.h"
 #include "bleconnection.h"
 #include "bledatatransfer.h"
 
 class BLEManager : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<BLEDeviceScanner>    scanner;
    std::unique_ptr<BLEServiceDiscovery> serviceDiscovery;
    std::unique_ptr<BLEPairing>          pairing;
    std::unique_ptr<BLEConnection>       connection;
    std::unique_ptr<BLEDataTransfer>     dataTransfer;
    QBluetoothDeviceInfo                 targetDevice;
    std::unique_ptr<QTimer>              transferTimer;
 
    void connectSignals()
    {
       // スキャナのシグナル接続
        connect(scanner.get(), &BLEDeviceScanner::deviceDiscovered, this, &BLEManager::onDeviceDiscovered);
        connect(scanner.get(), &BLEDeviceScanner::errorOccurred, this, &BLEManager::onErrorOccurred);
 
        // ペアリングのシグナル接続
        connect(pairing.get(), &BLEPairing::pairingComplete, this, &BLEManager::onPairingComplete);
        connect(pairing.get(), &BLEPairing::errorOccurred, this, &BLEManager::onErrorOccurred);
 
        // 接続管理のシグナル接続
        connect(connection.get(), &BLEConnection::connected, this, &BLEManager::onConnected);
        connect(connection.get(), &BLEConnection::serviceDiscovered, this, &BLEManager::onServiceDiscovered);
        connect(connection.get(), &BLEConnection::errorOccurred, this, &BLEManager::onErrorOccurred);
 
        // データ転送のシグナル接続
        connect(dataTransfer.get(), &BLEDataTransfer::characteristicChanged, this, &BLEManager::onCharacteristicChanged);
        connect(dataTransfer.get(), &BLEDataTransfer::errorOccurred, this, &BLEManager::onErrorOccurred);
    }
 
    void startPeriodicDataTransfer()
    {
       transferTimer = std::make_unique<QTimer>(this);
       transferTimer->setInterval(1000);  // 1秒間隔
 
       connect(transferTimer.get(), &QTimer::timeout, this, [this]() {
          // サンプルデータの送信
          QByteArray data = generateSampleData();
          dataTransfer->writeCharacteristic(QBluetoothUuid(QString("YOUR_CHARACTERISTIC_UUID")), data );
       });
 
       transferTimer->start();
    }
 
    QByteArray generateSampleData()
    {
       // サンプルデータの生成 (実務では、適切なデータを生成)
       QByteArray data;
       data.append(0x01);  // コマンド
       data.append(0x02);  // データ長
       data.append(0x03);  // データ1
       data.append(0x04);  // データ2
       return data;
    }
 
    void processReceivedData(const QByteArray &data)
    {
       // 受信データの処理 (実務では、適切な処理を実装)
       if (data.isEmpty()) return;
 
       // データの解析例
       quint8 command = data.at(0);
       switch (command) {
          case 0x01:
             qDebug() << "コマンド1を受信";
             break;
          case 0x02:
             qDebug() << "コマンド2を受信";
             break;
          default:
             qDebug() << "不明なコマンド: " << command;
             break;
       }
    }
 
 public:
    explicit BLEManager(QObject* parent = nullptr) : QObject(parent)
    {
       // 各コンポーネントの初期化
       scanner = std::make_unique<BLEDeviceScanner>(this);
       serviceDiscovery = std::make_unique<BLEServiceDiscovery>(this);
       pairing = std::make_unique<BLEPairing>(this);
       connection = std::make_unique<BLEConnection>(this);
       dataTransfer = std::make_unique<BLEDataTransfer>(this);
 
       connectSignals();
    }
 
    // デバイススキャンを開始
    void startDeviceScan()
    {
       qDebug() << "BLEデバイスのスキャンを開始...";
       scanner->startScan();
    }
 
 private slots:
    void onDeviceDiscovered(const QBluetoothDeviceInfo& device)
    {
       // ターゲットデバイスかどうかを確認 (実務では、適切なフィルタリングが必要)
       if (device.name().contains("YourDeviceName", Qt::CaseInsensitive))
       {
          scanner->stopScan();
          targetDevice = device;
          qDebug() << "ターゲットデバイスを発見: " << device.name();
 
          // ペアリングを開始
          pairing->requestPairing(device.address());
       }
    }
 
    void onPairingComplete(const QBluetoothAddress &address, bool success)
    {
       if (success) {
          qDebug() << "ペアリング完了: 接続を開始...";
          connection->connectToDevice(targetDevice);
       }
       else {
          qDebug() << "ペアリングに失敗";
       }
    }
 
    void onConnected()
    {
       qDebug() << "デバイスに接続完了";
       connection->setAutoReconnect(true);  // 自動再接続を有効化
    }
 
    void onServiceDiscovered(QLowEnergyService *service)
    {
       qDebug() << "サービスを発見: " << service->serviceUuid().toString();
 
       // 対象のサービスかどうかを確認
       if (service->serviceUuid() == QBluetoothUuid(QString("YOUR_SERVICE_UUID"))) {
          dataTransfer->setService(service);
 
          // Notifyを有効化
          dataTransfer->enableNotifications(QBluetoothUuid(QString("YOUR_CHARACTERISTIC_UUID")));
 
          // 定期的なデータ送信を開始
          startPeriodicDataTransfer();
       }
    }
 
    void onCharacteristicChanged(const QBluetoothUuid &uuid, const QByteArray &value)
    {
       qDebug() << "データを受信:";
       qDebug() << "  UUID: " << uuid.toString();
       qDebug() << "  値: " << value.toHex();
 
       // 受信データの処理
       processReceivedData(value);
    }
 
    void onErrorOccurred(const QString &error)
    {
       qDebug() << "エラーが発生: " << error;
 
       // エラーに応じた適切な処理を実装
       // ...略
    }
 };


 // main.cppファイル
 
 #include <QCoreApplication>
 #include "BLEManager.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication app(argc, argv);
 
    try {
       // BLEマネージャーオブジェクトの生成と開始
       auto bleManager = std::make_unique<BLEManager>();
       bleManager->startDeviceScan();
 
       return app.exec();
    }
    catch (const std::exception &e) {
       qDebug() << "致命的なエラーが発生: " << e.what();
       return -1;
    }
 }