BLE再接続が実運用で失敗する理由
Bluetooth Low Energyの接続は、本質的に一時的なものです。デバイスは通信範囲外へ移動し、再起動し、アドバタイズを停止し、省電力モードへ入り、電源を失い、意図的に接続を終了することがあります。モバイルOSも、アプリを一時停止・終了したり、Bluetoothを無効化したり、権限を取り消したり、バックグラウンド処理を遅延させたりします。
Bluetoothリンク層でも、有効なパケットを受信できず supervision timeout に達すれば接続は失われます。したがってアプリケーションは、切断を例外ではなく、想定される運用状態として扱う必要があります。
脆弱な実装では、次のような処理になりがちです。
接続 → 切断コールバックを受信 → 直ちに再接続 → 無期限に繰り返す
この方法には複数の問題があります。
- 複数の接続試行が重複する
- 古いGATTオブジェクトが残る
- デバイス再起動直後で、まだアドバタイズしていない
- サービスと通知購読が復元されない
- 過剰なスキャンでバッテリーを消費する
- アプリが利用可能になる前にUIが「接続済み」と表示する
- 一時的な障害が永続的な再試行ループになる
信頼性の高いBLE再接続ステートマシンは、これらの処理を明示的な状態として管理します。
BLE再接続ステートマシンとは
BLE再接続ステートマシンは、現在のBluetooth Low Energy通信フローを定義済みの状態として表現する接続管理機構です。
画面、サービス、コールバック、タイマーがそれぞれ独立してBluetooth APIを呼ぶのではなく、すべてのイベントを一つの接続コントローラーへ渡します。コントローラーだけが、許可される状態遷移と処理を決定します。
実運用向けのステートマシンには、次の状態を含められます。
IdleWaitingForBluetoothScanningConnectingDiscoveringServicesConfiguringConnectionReadyDisconnectingRetryBackoffStopped
重要なのは、ConnectedとReadyが同じ状態ではないことです。
OSが物理的なGATT接続の成立を通知した後も、アプリケーションには次の処理が残っています。
- GATTサービスの検出
- 必須サービスとCharacteristic UUIDの検証
- CharacteristicのNotificationまたはIndicationの有効化
- 必要に応じたMTUや接続設定の復元
- デバイスとの認証
- アプリケーション状態の同期
- デバイスがコマンドを受け付けられることの確認
Androidの標準的なBLEフローでも、GATT接続成功後にサービス検出を実行します。これらの初期化が完了して初めて、ステートマシンをReadyへ遷移させます。
推奨するBLE接続状態
Idle
アプリケーションが周辺機器との通信を試行していない状態です。
代表的な開始条件は次のとおりです。
- 対象デバイスが未選択
- 自動再接続が無効
- ユーザーが意図的に切断
- 短時間の通信処理が完了
許可する遷移例:
Idle → ScanningIdle → ConnectingIdle → WaitingForBluetoothIdle → Stopped
WaitingForBluetooth
対象デバイスは判明しているものの、接続の前提条件を満たしていない状態です。
- Bluetoothが無効
- 必要な権限がない
- Bluetoothアダプターを利用できない
- OSがバックグラウンド処理を制限している
Android 12以降では、通常、スキャンと通信に対応するBLUETOOTH_SCANおよびBLUETOOTH_CONNECTの実行時権限が必要です。権限状態はUIだけの問題ではなく、ステートマシンへの入力として扱います。
ユーザーが権限を許可したりBluetoothを有効化したりするまでの待機時間は、再接続失敗回数へ含めません。
Scanning
アプリケーションが対象の周辺機器を探している状態です。
スキャンでは、利用できる中で最も確実な識別条件を使用します。
- 既知のService UUID
- Manufacturer Specific Data
- 安定したアプリケーションレベルのデバイスID
- 保存済みのプラットフォーム固有ID
- 複数のアドバタイズ情報の組み合わせ
表示名だけに依存してはいけません。デバイス名は重複、変更、省略される可能性があり、アドバタイズパケットに常に含まれるとも限りません。
スキャンにはタイムアウトを設定します。Androidでも、対象を発見したらスキャンを停止し、バッテリーを消費する連続スキャンを避けることが推奨されています。
実用的な方針例:
- フォアグラウンドスキャン:10〜15秒
- 再試行までの待機:バックオフポリシーで制御
- 対象デバイスを発見したら直ちに停止
Connecting
ステートマシンがデバイスを選択し、接続試行を開始した状態です。
接続管理機構が所有する接続試行は、常に一つに限定します。各試行には次の情報を持たせます。
attemptIddeviceIdstartTimetimeoutconnectionModecancellationReason
attemptIdを使用すると、過去の試行から遅れて届いたコールバックが、新しい接続状態を変更することを防げます。
- 試行17を開始
- 試行17がタイムアウト
- 試行18を開始
- 試行17のコールバックが遅れて到着
- 現在の試行ではないため、そのコールバックを無視
Bluetoothのコールバックは非同期に届くため、この識別が重要です。
DiscoveringServices
リンク接続後にGATTサービスを検出し、期待するプロファイルを検証する状態です。
必須要素がすべて存在するか確認します。
- Primary Service UUID
- コマンド用Characteristic
- 応答用Characteristic
- NotificationまたはIndication用Characteristic
- 設定用Characteristic
- ファームウェアまたはデバイスプロトコルのバージョン情報
一部のサービスが検出できただけでReadyへ進めてはいけません。必須Characteristicがない場合、別のデバイスへ接続した、ファームウェアに互換性がない、GATTデータベースが変更された、検出結果が不完全、アプリのプロトコル定義が古い、といった可能性があります。
ConfiguringConnection
実際のアプリケーション通信に備えて、接続済みセッションを初期化する状態です。
- NotificationまたはIndicationの有効化
- Client Characteristic Configuration Descriptorへの書き込み
- デバイスプロトコルのバージョン交渉
- アプリケーション認証
- 初期デバイス状態の読み取り
- 購読の復元
- 時刻や設定の同期
- 別クライアントによる使用中状態の確認
各処理に個別のタイムアウトとエラー結果を定義します。
リンク接続済み ≠ アプリケーション利用可能
Ready
BLE接続の初期化が完了し、業務データを安全に送受信できる状態です。
Readyへ入った時点で、連続再試行回数をリセットします。
retryAttempt = 0
低レベル接続が成功した直後にリセットしてはいけません。接続後のサービス検出や通知設定で繰り返し失敗するデバイスは、復旧に成功していません。
RetryBackoff
接続試行は失敗したものの、自動再接続が許可されている状態です。
再試行まで一定時間待機し、失敗が続くほど待機時間を延ばします。これにより、バッテリー消費、無線の連続使用、GATT要求の重複、周辺機器への過剰な接続、不要なログとエラー通知を抑えます。
Stopped
自動再接続を無効化した状態です。
- ユーザーが「切断」を選択
- デバイスを削除または登録解除
- アカウントへの割り当てが解除
- 互換性のないプロトコルを検出
- セキュリティまたは認証障害により操作が必要
Stoppedは予期しないDisconnectedと分ける必要があります。分けなければ、ユーザーが切断した直後に自動再接続が始まります。
状態とイベントを分けて定義する
状態は現在の条件を、イベントは発生した事象を表します。
代表的なイベント:
StartRequestedStopRequestedBluetoothEnabledBluetoothDisabledPermissionGrantedPermissionDeniedDeviceDiscoveredScanTimedOutConnectionSucceededConnectionFailedDisconnectedServicesDiscoveredServiceDiscoveryFailedNotificationsEnabledConfigurationFailedRetryTimerExpiredAppEnteredForegroundAppEnteredBackgroundDeviceIdentityChanged
遷移表を作ると、動作を決定的にできます。
Idle+StartRequested→Scanning: フィルター付きスキャンを開始Scanning+DeviceDiscovered→Connecting: スキャンを停止して接続Scanning+ScanTimedOut→RetryBackoff: 再試行を予約Connecting+ConnectionSucceeded→DiscoveringServices: サービス検出を開始Connecting+ConnectionFailed→RetryBackoff: セッションを解放DiscoveringServices+ServicesDiscovered→ConfiguringConnection: プロファイルを検証ConfiguringConnection+NotificationsEnabled→Ready: 利用可能状態を公開Ready+Disconnected→RetryBackoff: 原因を記録して再試行- 任意のアクティブ状態 +
StopRequested→Stopped: タイマーを解除して切断 - 任意のアクティブ状態 +
BluetoothDisabled→WaitingForBluetooth: 接続を解放
ジッター付き指数バックオフを使用する
固定1秒の再試行は開発中には便利でも、デバイスが数分間利用できない状況では適切に動作しません。
指数バックオフの例:
delay = min(maxDelay, baseDelay × 2^attempt)
- 試行0:1秒
- 試行1:2秒
- 試行2:4秒
- 試行3:8秒
- 試行4:16秒
- 試行5:30秒
- 以降:30秒
複数のスマートフォンやゲートウェイが同時に再試行しないよう、少量のランダムなジッターを加えます。
actualDelay = delay × 0.8〜1.2のランダム値
短い待機が適する例:短時間の電波断、ファームウェア処理中の再起動、一時的な接続タイムアウト、ユーザーが接続画面で待機中の場合。
長い待機が適する例:デバイスが繰り返し見つからない、通信失敗が続く、アプリがバックグラウンド、周辺機器の電源が切れている可能性がある場合。
自動再試行を停止する例:ユーザーによる切断、Bluetooth権限の恒久的拒否、認証の連続失敗、互換性のないファームウェア、無効なデバイスID、アカウント権限の解除。
適切な再接続経路を選ぶ
再接続時に常にフルスキャンから始める必要はありません。
既知のプラットフォームデバイス参照があるか
→ ある:プラットフォームから直接再接続できるか
→ ない:対象がOSレベルですでに接続済みか
→ ない:フィルター付きスキャンを実行
→ 発見後に接続
iOSのCore Bluetoothでは、既知のPeripheralの取得、システムへ接続済みのPeripheralの取得、再スキャンを選択できます。毎回検出から始める前に、取得APIを利用できるか確認します。
AndroidのconnectGatt()には、即時接続を試みるautoConnect = falseと、既知の周辺機器が利用可能になったときの接続を待つautoConnect = trueがあります。製品の利用フローとバックグラウンド要件に応じて選択します。
ただし、autoConnectを使用してもステートマシンは必要です。Bluetooth無効化、権限不足、プロセス終了、GATT初期化、認証、サービス変更、ユーザー切断、再試行停止、UI状態は引き続きアプリ側で管理します。
失敗したGATTセッションを確実に解放する
接続試行が失敗またはキャンセルされた場合、新しい試行の前にリソースを整理します。
- 現在の試行をキャンセル済みにする
- 接続関連タイマーを解除する
- 古い試行のコールバックを無視する
- 実行中のスキャンを停止する
- 必要に応じて切断を要求する
- 古いGATTクライアントをcloseまたは解放する
- アクティブセッション参照を消去する
RetryBackoffまたはStoppedへ遷移する
クリーンアップは冪等にします。二回呼び出してもクラッシュしたり、新たな接続を開始したりしてはいけません。
画面のライフサイクルだけにBluetoothの解放処理を置くことも避けます。BLE接続は個々の画面より長く存続するため、アプリケーションレベルのService、Repository、Connection Manager、Device Session Controllerで管理します。
GATT初期化を直列化する
接続後、すべての初期化処理を同時に開始しないでください。
サービス検出
→ 必須UUIDの検証
→ 応答通知の有効化
→ 通知設定結果を待機
→ プロトコルバージョンの読み取り
→ 認証
→ 初期状態の読み取り
→ Ready
各処理は、依存する次の処理を開始する前に、成功、失敗、タイムアウトのいずれかへ確定させます。
これにより、初期化途中の接続を利用可能と表示する問題を防ぎ、どの段階で失敗したかを特定できます。再接続後も、通知購読、アプリケーション認証、キャッシュ済み状態、未完了コマンドが新しいGATTセッションへ引き継がれると仮定してはいけません。
処理中のコマンドを安全に扱う
コマンド送信中に接続が切れることがあります。自動再送する前に、そのコマンドが冪等かを判断します。
通常、再試行しやすい処理:
- 現在状態の読み取り
- バッテリー残量の読み取り
- 設定の読み取り
- 絶対値による目標設定
- 最新計測値の要求
再試行に注意が必要な処理:
- 商品を一つ払い出す
- 一度だけ解錠する
- モーター動作を開始する
- カウンターを増加する
- 決済関連処理を送信する
- ファームウェア更新手順を進める
非冪等コマンドには、アプリケーションレベルのトランザクションIDと応答プロトコルを使用します。
アプリがtransactionId付きコマンドを送信
→ デバイスがtransactionIdを記録または処理
→ デバイスがtransactionId付き応答を返す
→ アプリが処理済みか確認してから再試行を判断
BLE再接続は無線リンクを戻すだけでなく、製品ワークフローの正しさも維持しなければなりません。
AndroidにおけるBLE再接続
Androidアプリケーションでは、次の要素を明示的にモデル化します。
- Bluetooth実行時権限
- Bluetoothアダプター状態
- フォアグラウンド・バックグラウンド実行
- アプリケーションプロセスの寿命
- 直接接続とauto-connect
- GATT接続・サービス検出コールバック
- 長時間通知を受けるための要件
長時間のコネクテッドデバイス通信では、CompanionDeviceServiceや適切に宣言したForeground Serviceなど、現在のAndroidガイドラインに沿った方法を検討します。アプリケーションプロセスだけが所有する接続は、そのプロセスが終了すると失われます。
BLUETOOTH_CONNECT権限を確認
→ Bluetoothアダプターを確認
→ 既知デバイスを取得またはスキャン
→ connectGatt()を呼び出す
→ onConnectionStateChange()を受信
→ サービスを検出
→ Notificationを設定
→ アプリケーションプロトコルを検証
→ Ready
アクティブなBluetoothGattインスタンスは、接続管理機構だけが所有します。
iOSにおけるBLE再接続
iOSでは、デバイス検出後にCore Bluetoothが割り当てたPeripheral Identifierを保存します。
実用的な再接続順序:
- 保存済みIDから既知のPeripheralを取得
- システムへ接続済みのPeripheralを確認
- 必須Service UUIDを指定してスキャン
- 対象を発見したら接続
バックグラウンドでBluetooth通信が必要な場合は、適切なCore Bluetooth Background Modeを設定し、実機で検証します。ただし、バックグラウンド動作は無制限なアプリ実行ではなく、OS管理下の動作として設計します。
Central Managerの状態変化も扱います。
poweredOnpoweredOffresettingunauthorizedunsupported
Central Managerが利用可能な状態になってから接続要求を開始します。
ステートマシンの疑似コード例
次の言語非依存の疑似コードは、接続処理の単一所有者、明示的な状態遷移、遷移状態ごとのタイムアウト、古い非同期コールバックの排除を示します。
state = Idle
retryAttempt = 0
activeAttemptId = null
function handle(event):
if event is StopRequested:
stopEverything()
transitionTo(Stopped)
return
if event.attemptId exists
and event.attemptId != activeAttemptId:
recordIgnoredStaleEvent(event)
return
switch state:
case Idle:
if event is StartRequested:
if prerequisitesAvailable():
beginScan()
else:
transitionTo(WaitingForBluetooth)
case WaitingForBluetooth:
if event is PrerequisitesAvailable:
beginScan()
case Scanning:
if event is DeviceDiscovered:
stopScan()
activeAttemptId = newAttemptId()
transitionTo(Connecting)
startStateTimeout(Connecting)
connect(event.device, activeAttemptId)
else if event is StateTimedOut:
scheduleRetry("scan_timeout")
case Connecting:
if event is ConnectionSucceeded:
transitionTo(DiscoveringServices)
startStateTimeout(DiscoveringServices)
discoverServices(activeAttemptId)
else if event is ConnectionFailed:
scheduleRetry(event.reason)
else if event is StateTimedOut:
scheduleRetry("connection_timeout")
case DiscoveringServices:
if event is ServicesDiscovered:
if requiredProfileExists(event.services):
transitionTo(ConfiguringConnection)
startStateTimeout(ConfiguringConnection)
configureConnection(activeAttemptId)
else:
failPermanently("incompatible_gatt_profile")
else if event is ServiceDiscoveryFailed:
scheduleRetry(event.reason)
case ConfiguringConnection:
if event is ConfigurationSucceeded:
cancelStateTimeout()
retryAttempt = 0
transitionTo(Ready)
else if event is ConfigurationFailed:
scheduleRetry(event.reason)
case Ready:
if event is Disconnected:
scheduleRetry(event.reason)
case RetryBackoff:
if event is RetryTimerExpired:
retryAttempt += 1
beginScan()
本番実装では、stopEverything()がスキャン、再試行タイマー、状態タイマー、接続をすべて解除する必要があります。各コールバックには、その処理を開始した試行IDを関連付けます。過去の試行から届いたコールバックは診断情報であり、有効な状態遷移ではありません。
すべての遷移状態にタイムアウトを設定する
次の状態へ無期限に留まらないようにします。
ScanningConnectingDiscoveringServicesConfiguringConnectionDisconnecting
初期値の例:
- フォアグラウンドスキャン:10〜15秒
- 直接接続:10〜20秒
- サービス検出:10秒
- Notification設定:5〜10秒
- アプリケーション認証:10秒
- 正常切断:3〜5秒
これらはBLE共通の固定値ではありません。実機の計測結果を基に製品ごとに調整します。
再接続メトリクスを記録する
接続管理機構は、構造化した診断情報を出力する必要があります。
deviceId- プラットフォーム、OS、スマートフォン機種
- アプリ・ファームウェアのバージョン
- 遷移前後の状態とイベント
- 失敗理由と試行回数
- スキャン、接続、初期化の所要時間
Readyまでの時間- 電波強度
- バックグラウンド状態
- 権限状態
有用な指標には、初回接続成功率、Readyまでの中央値、再接続成功率、セッションごとの試行回数、サービス検出失敗率、Notification設定失敗率、時間あたりの予期しない切断、端末・ファームウェア別の失敗率があります。
測定がなければ、「BLEが不安定」という問題を診断可能な単位へ分解できません。
BLE再接続ステートマシンをテストする
自動化した状態遷移テストと、実際のBluetoothハードウェアを使用するテストの両方を実施します。
中核となる再接続シナリオ
- デバイスを圏外へ移動し、再び戻す
- 周辺機器の電源を切り、再起動する
- スマートフォンのBluetoothを無効化・再有効化する
- Bluetooth権限を拒否し、後から許可する
- アプリをバックグラウンドへ移す
- アプリを終了して再起動する
- スマートフォンを再起動する
- サービス検出中に周辺機器を再起動する
- Notification設定中に切断する
- コマンド応答待ちの途中で切断する
- ファームウェアまたはGATTプロファイルを変更する
- 周辺機器を数分間利用不可にする
- 短時間の切断を繰り返す
- UIから意図的に切断する
- 同じような名前の複数デバイスで確認する
実機カバレッジ
- 複数のAndroidメーカーとOSバージョン
- 現行および一世代前のiOS
- バッテリー残量が低い・高い状態
- 電波が弱い・強い状態
- フォアグラウンド・バックグラウンド
- 複数の周辺機器ファームウェア
開発用スマートフォン一台で動作するだけでは、本番品質のBLE再接続とはいえません。
よくあるBLE再接続の誤り
無期限に即時再試行する
バッテリーを消費し、アプリと周辺機器の双方を失敗ループへ陥らせます。
ConnectedをReadyとして扱う
物理接続が成立していても、サービス、通知、認証が未完了の場合があります。
スキャンと接続を重複して開始する
各スキャン、タイマー、コールバック、接続試行を一つのステートマシンセッションへ所属させます。
デバイス名だけで識別する
名前は一意でも安定でもありません。
古いセッション状態を再利用する
新しい接続では、必要なGATT状態とアプリケーション状態を再構築します。
すべてのコマンドを自動再送する
切断前にコマンドが完了している可能性があります。
ユーザーが切断した後も再接続する
ユーザーの意図を自動再試行より優先します。
すべての失敗をユーザーから隠す
自動復旧は有用ですが、Bluetooth無効、権限不足、非互換デバイス、手動操作が必要な状況はUIで説明します。
最終チェックリスト
リリース前に、BLE再接続ステートマシンが次を満たすか確認します。
- 状態とイベントを明示している
- BLE接続処理の所有者が一つである
ConnectedとReadyを区別している- スキャンに上限がある
- ジッター付き指数バックオフを使用する
- 古い接続試行をキャンセルできる
- Bluetoothと権限の変化を処理する
- サービスとNotificationを復元する
- 期待するGATTプロファイルを検証する
- 非冪等コマンドを保護する
- ユーザー切断後に再試行しない
- 状態遷移と失敗の診断情報を記録する
- アプリとデバイスの再起動後も復旧する
- 実際のAndroid・iOS端末で検証済みである
まとめ
信頼性の高いBLE再接続は、接続APIを繰り返し呼ぶだけでは実現できません。デバイス検出、接続試行、GATTサービス検出、Notification設定、アプリケーション認証、再試行バックオフ、コマンド復旧、ユーザーの意図を、決定的なステートマシンで統合する必要があります。
Scanning、Connecting、DiscoveringServices、ConfiguringConnection、Ready、RetryBackoffを分離することで、コネクテッドデバイスアプリケーションはデバッグ、テスト、保守、拡張が容易になります。
適切なBLE再接続ステートマシンは、無線リンクだけでなく製品セッション全体を復元し、アプリケーションを検証済みの利用可能状態へ戻します。
BLE接続アプリの新規開発や既存アプリの修復が必要ですか。YUNJIは、BLEアプリ開発、デバイスプロトコル統合、GATTトラブルシューティング、実機テスト、既存IoTプロジェクトの技術引継ぎを支援します。



