Qtの基礎 - Bluetooth Low Energy
概要
QtのBluetoothサポートは、Qt Bluetoothモジュールを通じて提供されている。
Qt Bluetoothモジュールは、クロスプラットフォームなBluetooth通信機能を実現するための包括的なAPIセットとなっている。
主要なコンポーネントとして、Classic BluetoothとBluetooth Low Energy (BLE) の両方をサポートしている。
- Classic Bluetooth
- 従来型の高帯域幅通信
- BLE
- 省電力デバイスとの通信
デバイスの検出と管理において、Qt Bluetoothモジュールは以下に示す機能を提供している。
- デバイススキャンと検出
- サービスディスカバリ
- ペアリング管理
- 接続の確立と維持
上記の機能は相互に関連しており、アプリケーションでは以下に示すような流れで使用される。
- まず、デバイススキャンを行い、周囲のデバイスを探索する。
- 目的のデバイスが存在する場合、サービスディスカバリでそのデバイスの機能を確認する。
- 次に、必要に応じてペアリングを実行する。
- 最後に、接続を確立してデータ通信を開始する。
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デバイスのスキャン中は、アドバタイズメントと呼ばれる特別なブロードキャストパケットを発信する。
このアドバタイズメントパケットには、以下に示す情報が含まれる。
- デバイス名
- メーカー固有データ
- 提供するサービスのUUID
- 電波強度 (RSSI)
- その他のカスタムデータ
また、スキャン時間や範囲を設定可能であり、バッテリー消費とスキャン精度のバランスを取ることができる。
BLEでは、同様のQBluetoothDeviceDiscoveryAgent
クラスを使用するが、LowEnergyDiscoveryTimeout
の設定が必要となる。
また、フィルタリングでBLEデバイスのみを検出する。
サービスディスカバリ
特定のBluetoothデバイスが提供するサービスを探索するプロセスである。
BLEでは、GATT (Generic Attribute Profile) という階層構造を持つ。
- サービス
- デバイスの機能をグループ化 (例: 心拍計測、温度センサ等)
- キャラクタリスティック
- 実際のデータ値や操作 (例: 心拍値、温度値、設定値等)
- ディスクリプタ
- キャラクタリスティックの追加情報 (単位や説明等)
サービスディスカバリでは、これらの階層構造を探索して、利用可能な機能を特定する。
標準的なサービスには決められたUUIDが割り当てられている。(例: 心拍サービス、電池残量サービス等)
BLEでは、QLowEnergyService
クラスでより複雑なサービス階層を管理する。
キャラクタリスティックとディスクリプタの概念が追加されている。
Classic Bluetoothとの違いを以下に示す。
- QLowEnergyControllerクラスを使用
- connectToDeviceメソッドで接続する
- discoveryFinishedシグナルで探索完了を検知する
- createServiceObjectメソッドを使用して、サービスオブジェクトを生成する
ペアリング管理 (ボンディング)
以下に示すように、セキュリティレベルが定義されている。
- 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);
}
};