Android Bluetooth の信頼性を高める:一貫した接続のための実証済みのヒント
ワイヤレスイヤホンがある日は完璧に接続したのに、次の日にはスマートフォンに一度も触れなかったかのように動作するというようなことを以前に経験したことがあるかもしれません。あるいは、スマートウォッチがランニングの途中で落ちてしまいます。 Bluetooth は機能するときは素晴らしいですが、機能しないときはイライラします。
私はスマートグラスなどのウェアラブル デバイスの Bluetooth ソフトウェア エンジニアとして働いており、これらのデバイスが壊れる原因を追求するのに、認めたくないほど多くの時間を費やしてきました。
この記事では、Android の Bluetooth スタックが実際にどのように動作するのか、時々予測不能に感じられる理由、アプリやシステムの信頼性を高めるために開発者として何ができるのかなど、カーテンの裏側を覗いてみましょう。
わかりやすい英語での Bluetooth
Bluetooth の核心は、2 つのデバイス間の単なる会話です。しかし、それは 1 つの単純なコミュニケーション ラインではなく、複数のレイヤーが積み重ねられています。
-
無線 (コントローラー): 空気媒体を介して実際の信号を送受信します。
-
ソフトウェア脳 (ホスト スタック): 誰とどのように話すか、また望むかどうかを決定します。
-
プロフィール: 会話の目的を定義します – 音楽のストリーミングや健康データの同期など。
-
プロトコル: 他のデバイスと通信する方法を定義します。
Bluetooth には 2 つの大きな「特徴」があります。
-
クラシック (BR/EDR): ヘッドフォンやカーキットなどに使用されます。より多くの重量を持ち上げることができます。
-
低エネルギー (LE): フィットネス バンド、ビーコン、およびほとんどのウェアラブルに使用されます。より長く持続できます。
最新のガジェットのほとんどは両方を同時に使用します。これは強力ですが、さらに問題が発生する扉も開きます。
Android に独自の癖が追加される理由

Android では、Bluetooth は単なる 1 つのきちんとしたパッケージではありません。それは可動部分の連鎖です:
-
アプリは
BluetoothAdapterを呼び出します . -
これらはシステム サービスに含まれます。
AdapterServiceのように . -
その後、JNI を通じてネイティブ コードに変換します。 (Java ネイティブ インターフェイス)。
-
次にチップ ベンダーの Bluetooth スタックに接続します。 .
-
ついにラジオハードウェアに登場します。 .
各携帯電話メーカーは、わずかに異なる Bluetooth チップとファームウェアを出荷しています。つまり、まったく同じ Bluetooth アプリでも、Samsung、Pixel、または Android を搭載したその他の低価格スマートフォンでは動作が異なる可能性があります。
「接続が切れただけ」の背後にある本当の問題
ここでは、私が目にする一般的な頭痛のいくつかを簡単に説明します。
ボンディングの問題 (「鍵の紛失」問題)
2 つの Bluetooth デバイスがペアリングされると、暗号化キー (クラシックの場合はリンク キー、LE の場合は長期キー) を交換し、それらを不揮発性メモリに保存します。これらのキーにより、後でデバイスがお互いを認識し、ユーザーに再度尋ねることなく安全に再接続できるようになります。
「メモリの不一致」問題は、一方のデバイスに保存されているキーがもう一方のデバイスと一致しなくなったときに発生します。これは次のことが原因で発生する可能性があります。
-
キーを消去または再生成するファームウェア アップデートまたは OS アップグレード。
-
一方では工場出荷時設定へのリセットまたは「デバイスの削除」が行われますが、もう一方は行われません。
-
キーが破損しているか、ストレージを解放するためにシステムによって削除されています。
ユーザーの観点から見ると、 デバイスはまだ見えます。 ペアリングされました (Bluetooth メニューに表示されます) が、「認証に失敗しました」または「暗号化が不十分です」などのエラーで接続が不思議なことに失敗します。唯一の治療法は通常、両端のデバイスを削除して再ペアリングすることですが、技術者以外のユーザーにとってこれはばかばかしいことだと感じます。
タイミングの不一致
Bluetooth デバイスは、いつでも好きなときにチャットするだけではなく、接続間隔、つまり双方がいつ「起動」してパケットを交換するかについてのスケジュールに同意します。これは、2 人の人がカフェで 30 分ごとに会うことに同意したと考えてください。
不一致は次の場合に発生します。
-
双方は異なる間隔について交渉しますが、完全には合意しません(たとえば、一方は 30 ミリ秒だと考え、もう一方は 50 ミリ秒だと考えます)。
-
一方のファームウェアのアップデートまたは構成の変更により、タイミング ポリシーが変更されます。
-
無線状況により、一方が予定されていた複数のチェックインを逃し、時計がずれてしまいます。
-
省電力ロジック (電話が Doze モードになるなど) は、静かに間隔を延ばします。
これは、接続が最初は正常に動作しても、後で失敗し始める理由を説明しています。デバイスは、最初は一定の間隔で同期していましたが、その後、一方のポリシーまたは動作が変化しました。ユーザーの観点からは、音声の途切れ、(ゲーム コントローラーでの) 入力の遅延、または「以前は正常に動作していた」後のランダムな切断のように見えます。
予期しない切断
Bluetooth リンクが終了すると、無線層 (コントローラー) と上位レベルの OS スタック (ホスト) はクリアな信号を交換することになります。コントローラーは、HCI 切断完了イベント (基本的には、「さようなら、完了しました」) を送信します。 )。そして、ホストは内部状態を更新し、GATT/ACL セッションをクリーンアップして、再接続の準備を整える必要があります。
しかし実際には、これは常に一致するとは限りません。
-
場合によっては、コントローラーがきれいに別れを告げても、ホスト スタックがその状態を適切に更新しないことがあります。アプリはまだ接続がアクティブであると「考えている」ため、再接続の試行は通知なく失敗します。
-
一部のプラットフォーム (特に iOS) は、接続状態を積極的にキャッシュします。 OS が接続がまだ有効であると判断した場合、Bluetooth を切り替えるか再起動するまで、新しい接続の試行はトリガーされません。
-
別の操作 (サービス検出、ボンディング、暗号化セットアップなど) の実行中に切断イベントが発生すると、競合状態が発生する可能性があります。 OS は、 デバイスが実際にどのような状態であるかについて混乱する可能性があります。
-
一部のデバイスでは、完全な切断後の高速再接続の試行が内部のクールダウン タイマーと衝突します。コントローラーはそれを無視し、アプリは待機したままになります。
ユーザーの観点からは、デバイスは「動かなくなった」ように見えます。技術的には何も「失敗」していなくても、回復する唯一の方法は、Bluetooth を切り替えるか、アプリを再起動するか、アクセサリの電源を入れ直すことです。
開発者がより良くできる方法
Bluetooth アプリを構築している場合、多くの手間を省くための習慣をいくつか紹介します。
最初に結合されたデバイスを確認します
接続失敗の最も一般的な原因の 1 つは、ボンディング情報の不一致です。電話機とアクセサリが同じ暗号化キーを共有しなくなっています。デバイスが UI に表示されていても、OS がキーを失っている可能性があります。
接続を試行する前に、必ず BluetoothAdapter.getBondedDevices() を使用してシステムのボンディングされたデバイス リストをクエリします。 。例:
if (adapter.getBondedDevices().contains(targetDevice)) {
targetDevice.connectGatt(context, false, gattCallback);
} else {
showToast("Please re-pair this device to restore the connection.");
}
これにより、OS がまだ信頼しているデバイスに対してのみ安全な接続を試行することが保証されます。ターゲット デバイスが結合リストにない場合は、ユーザーにわかりにくい接続エラーを残さずに、明確な指示 (「このデバイスを再ペアリングしてください」) を与えることができます。
コールバックは慎重に処理してください
もう 1 つの微妙な落とし穴は、STATE_CONNECTED を想定していることです。 イベントは接続が成功したことを意味します。実際には、onConnectionStateChange() 基礎となる操作が失敗した場合でも接続状態を報告できます。実際の結果は status にあります。 議論。ファントム接続の追跡を避けるために、常に両方の status をチェックしてください。 と newState :
if (status == BluetoothGatt.GATT_SUCCESS &&
newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
} else {
gatt.close();
}
このパターンにより、切断された接続でサービス検出を試行することができなくなり、古いセッションが即座に閉じられ、スタックがクリーンな再試行の準備が整った状態になります。
失敗を想定する
現実世界では、Bluetooth 接続は常に失敗します。デバイスが通信範囲外になったり、2.4 GHz 帯域で干渉が急増したり、単に無線がビジー状態になったりします。アプリが行う可能性のある最悪のことは、タイトなループで即座に再試行することです。これにより、バッテリーが消耗し、スタックが不安定になります。
より良いアプローチは、次のように指数バックオフを実装することです。
long delay = (long) Math.min(250 * Math.pow(2, attempt), 30000);
new Handler(Looper.getMainLooper()).postDelayed(connectAction, delay);
これは、最初の再試行は迅速に (約 250 ミリ秒) 行われますが、その後の再試行は遅くなり (500 ミリ秒、1 秒、2 秒…)、適切な最大値に制限されていることを意味します。バックオフにより、無線や OS に負担をかけずにアプリの回復力が高まります。
適切なツールを使用する
内部で何が起こっているかを可視化できなければ、接続の問題はランダムに発生するように見えます。 nRF Connect などのツール Android の Bluetooth HCI スヌープ ログにより、実際に交換されているパケットが明らかになり、デバイスに対して対話的にスキャン、接続、GATT 操作を実行できます。例:
Settings.Secure.putInt(context.getContentResolver(), "bluetooth_hci_log", 1);
有効にすると、logcat トレースをキャプチャし、失敗の原因がキー (Insufficient Authentication) の欠落によるものかどうかを確認できます。 )、タイミングの不一致、または干渉。これらのツールを使用すると、アプリのデバッグに役立つだけでなく、問題がコード、OS、アクセサリ ファームウェアにあるのかどうかを証明することもできます。

より大きな教訓
Bluetooth を使用することで、エンジニアリング全般に当てはまる教訓を得ることができました。
-
ワイヤレスは決して完璧ではないため、常に回復を念頭に置いて構築してください。
-
ログとメトリクスはオプションではありません。それらは混乱の中でのあなたの地図です。
-
乱雑な現実世界では、通常、最も単純な解決策が最もよく生き残ります。
結論
Bluetooth は、ハードウェア、ファームウェア、ソフトウェアのチェーンがすべて連携しようとするため、厄介です。 Android では、チップやベンダーが多様であるため、さらに複雑になります。
しかし、それはあなたが無力だという意味ではありません。レイヤがどのように機能するかを理解し、再試行、チェック、適切なロギングを備えたアプリを設計することで、ユーザーにとって Bluetooth の「奇妙さ」を大幅に軽減できます。
次回、イヤホンが誤動作したときは、それが自分ではないことがわかるでしょう。 Bluetooth が Bluetooth であるだけです。
⚡これは、Bluetooth 開発についてこれから書く予定のいくつかの記事のうちの最初の記事です。次回は、Android 上で安全な Bluetooth Low Energy (BLE) GATT クライアントとサーバーを構築する方法を詳しく説明します。乞うご期待!
無料でコーディングを学びましょう。 freeCodeCamp のオープンソース カリキュラムは、40,000 人以上の人々が開発者としての職に就くのに役立ちました。始めましょう
-
Jetpack Compose で UI イベントを処理する方法
この短くて実用的な記事では、Jetpack Compose で UI イベントを処理する方法について説明します。 古いシステムでは、OnClickListeners などのインターフェイスを使用していました。 Compose では、Kotlin の Sealed Classes を最大限に活用できます 、関数の種類 とラムダ式 . コンポーザブルとは何かがわからない場合は、基礎を説明しているこの記事を読むことを検討してください。 この記事は、このビデオで 3 分未満に要約されています。 Sealed クラスで UI イベントをモデル化する方法 まず、UI イベントの意味と、Seale
-
Androidの優先モードを設定する方法(およびサイレントモードに戻る方法)
Android Lollipop(5.0および5.1を含む)には、中断に対処するための新しい方法があります。これは優先モードと呼ばれ、通知のゲートキーパーのようなものです。何を通過させ、何を禁止するかを指示します。これを使用して、気を散らすものを制限し、Androidの最後のバージョンで使用していたサイレントモードに戻す方法は次のとおりです。 Android Lollipopの優れた新機能のいくつかについて説明しましたが、優先モードについては、小さなことなので言及しませんでした。さらに、Googleも従来の信頼性の高いサイレントモードを廃止したため、私たちはそれについて不安を感じていました。