Qtの基礎 - Bluetooth Low Energy

提供:MochiuWiki : SUSE, EC, PCB
2024年11月19日 (火) 02:56時点における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モジュールは、これらの状況を適切に検出して、シグナル / スロットメカニズムを通じて通知する仕組みを提供している。

※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 (可変長)


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

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


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


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


使用例

 #include <QObject>
 #include <QBluetoothDeviceDiscoveryAgent>
 #include <QBluetoothDeviceInfo>
 #include <memory>
 #include <QTimer>
 #include <QDebug>
 
 class BLEDeviceScanner : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QBluetoothDeviceDiscoveryAgent> discoveryAgent;
    std::unique_ptr<QTimer> rescanTimer;
    bool isContinuousScan = false;
 
    void connectSignals()
    {
       // デバイス探索エージェントのシグナル接続
       connect(discoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BLEDeviceScanner::onDeviceDiscovered);
       connect(discoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::finished, this, &BLEDeviceScanner::onScanFinished);
       connect(discoveryAgent.get(), static_cast<void(QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(&QBluetoothDeviceDiscoveryAgent::error),
               this, &BLEDeviceScanner::onError);
 
       // 再スキャンタイマのシグナル接続
       connect(rescanTimer.get(), &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 "不明なエラー";
       }
    }
 
 public:
    explicit BLEDeviceScanner(QObject* parent = nullptr) : QObject(parent)
    {
       // デバイス探索エージェントの初期化
       discoveryAgent = std::make_unique<QBluetoothDeviceDiscoveryAgent>(this);
 
       // BLEデバイスのみをスキャンするように設定
       discoveryAgent->setLowEnergyDiscoveryTimeout(10000);  // 10秒のタイムアウト
 
       // 自動再スキャンタイマの設定
       rescanTimer = std::make_unique<QTimer>(this);
       rescanTimer->setInterval(30000);  // 30秒間隔で再スキャン
 
       connectSignals();
    }
 
    // スキャンを開始
    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:
    void onDeviceDiscovered(const QBluetoothDeviceInfo& device)
    {
       // BLEデバイスのみを処理
       if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {
          qDebug() << "BLEデバイスを発見:";
          qDebug() << "  名前:" << device.name();
          qDebug() << "  アドレス:" << device.address().toString();
          qDebug() << "  RSSI:" << device.rssi();
 
          // アドバタイズメントデータの解析
          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;
             }
          }
 
          emit deviceDiscovered(device);
       }
    }
 
    void onScanFinished()
    {
       qDebug() << "スキャンが完了";
       emit scanFinished();
 
       // 継続的スキャンの場合は再開
       if (isContinuousScan) {
          QTimer::singleShot(1000, this, [this]() {
             discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
          });
       }
    }
 
    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デバイスが提供するサービスを探索するプロセスである。

BLEでは、GATT (Generic Attribute Profile) という階層構造を持つ。

  • サービス
    デバイスの機能をグループ化 (例: 心拍計測、温度センサ等)
    キャラクタリスティックは各サービスに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. 必要に応じてディスクリプタ探索


BLEでは、QLowEnergyServiceクラスでより複雑なサービス階層を管理する。
キャラクタリスティックとディスクリプタの概念が追加されている。

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

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


使用例

 #include <QObject>
 #include <QLowEnergyController>
 #include <QLowEnergyService>
 #include <QTimer>
 #include <memory>
 #include <QDebug>
 
 class BLEServiceDiscovery : public QObject
 {
    Q_OBJECT
 
 private:
    std::unique_ptr<QLowEnergyController> controller;
    std::unique_ptr<QTimer>               discoveryTimeout;
 
    void connectControllerSignals()
    {
       connect(controller.get(), &QLowEnergyController::connected, this, &BLEServiceDiscovery::onConnected);
       connect(controller.get(), &QLowEnergyController::disconnected, this, &BLEServiceDiscovery::onDisconnected);
       connect(controller.get(), &QLowEnergyController::serviceDiscovered, this, &BLEServiceDiscovery::onServiceDiscovered);
       connect(controller.get(), &QLowEnergyController::discoveryFinished, this, &BLEServiceDiscovery::onDiscoveryFinished);
       connect(controller.get(), static_cast<void(QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error), this, &BLEServiceDiscovery::onError);
       connect(controller.get(), &QLowEnergyController::stateChanged, this, &BLEServiceDiscovery::onStateChanged);
    }
 
    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 = std::make_unique<QTimer>(this);
       discoveryTimeout->setInterval(10000);  // 10秒のタイムアウト
       discoveryTimeout->setSingleShot(true);
 
       connect(discoveryTimeout.get(), &QTimer::timeout, this, &BLEServiceDiscovery::onDiscoveryTimeout);
    }
 
    // デバイスへの接続とサービス探索の開始
    void startDiscovery(const QBluetoothDeviceInfo& device)
    {
       try {
          qDebug() << "サービス探索を開始: " << device.name();
 
          // コントローラの初期化
          controller = std::make_unique<QLowEnergyController>(device, this);
          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
    最も簡単であるが、セキュリティは低い。
  • Passkey Entry
    PINコードを使用する。
  • Out of Band
    NFC等の別の通信手段を使用する。
  • Numeric Comparison
    両デバイスで同じ数字を確認する。


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

  • 接続パラメータの設定が可能
    接続間隔、レイテンシ、タイムアウト等
  • 省電力モードの制御
  • 接続状態の監視



接続の確立と維持

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

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


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

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


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