Article original : How to Make Bluetooth on Android More Reliable

Cela vous est peut-être déjà arrivé : vos écouteurs sans fil se connectent parfaitement un jour, et le lendemain, ils agissent comme s'ils n'avaient jamais rencontré votre téléphone. Ou votre montre connectée se déconnecte au milieu d'un footing. Le Bluetooth est fantastique quand il fonctionne, mais exaspérant quand ce n'est pas le cas.

Je travaille en tant qu'ingénieur logiciel Bluetooth sur des appareils connectés tels que des lunettes intelligentes, et j'ai passé plus de temps que je ne voudrais l'admettre à chercher pourquoi ces choses tombent en panne.

Dans cet article, je vais vous donner un aperçu des coulisses : comment fonctionne réellement la pile (stack) Bluetooth d'Android, pourquoi elle semble parfois imprévisible, et ce que vous pouvez faire en tant que développeur pour rendre vos applications ou votre système plus fiables.

Le Bluetooth en termes simples

À la base, le Bluetooth n'est qu'une conversation entre deux appareils. Mais il ne s'agit pas d'une simple ligne de communication unique ; ce sont plusieurs couches superposées les unes sur les autres.

  • La radio (Contrôleur) : Envoie et reçoit les signaux réels via le milieu aérien.

  • Le cerveau logiciel (Host stack) : Décide avec qui parler et comment, tout en déterminant s'il souhaite le faire.

  • Profils : Définissent l'objectif de la conversation (comme diffuser de la musique ou synchroniser des données de santé).

  • Protocoles : Définissent la manière de parler à l'autre appareil.

Il existe deux grandes « variantes » de bluetooth :

  • Classic (BR/EDR) : Utilisé pour des choses comme les casques et les kits de voiture. Peut transporter plus de données.

  • Low Energy (LE) : Utilisé pour les bracelets de fitness, les balises (beacons) et la plupart des wearables. Peut durer plus longtemps.

La plupart des gadgets modernes utilisent les deux en même temps. C'est puissant, mais cela ouvre aussi la porte à plus de problèmes potentiels.

Pourquoi Android ajoute ses propres particularités

Schéma montrant les couches de la pile Bluetooth Android.

Sur Android, le Bluetooth n'est pas un bloc monolithique. C'est une chaîne de pièces mobiles :

  • Votre application appelle BluetoothAdapter.

  • Ces appels vont vers des services système comme AdapterService.

  • Puis dans le code natif via JNI (Java Native Interface).

  • Ensuite, dans la pile Bluetooth du fournisseur de la puce.

  • Enfin, cela atteint le matériel radio.

Chaque fabricant de téléphones livre une puce Bluetooth et un firmware légèrement différents. Cela signifie que la même application Bluetooth peut se comporter différemment sur un Samsung, un Pixel ou n'importe quel autre téléphone d'entrée de gamme sous Android.

Les vrais problèmes derrière le fameux « Il s'est juste déconnecté »

Voici quelques-uns des casse-têtes courants que je rencontre, expliqués simplement :

Problèmes d'appairage (le problème des « clés perdues »)

Lorsque deux appareils Bluetooth s'appairent (bonding), ils échangent des clés de chiffrement (link keys pour le Classic, Long Term Keys pour le LE) et les stockent dans une mémoire non volatile. Ce sont ces clés qui permettent aux appareils de se reconnaître plus tard et de se reconnecter en toute sécurité sans solliciter à nouveau l'utilisateur.

Un problème de « mémoire désaccordée » survient lorsque les clés stockées d'un appareil ne correspondent plus à celles de l'autre. Cela peut être causé par :

  • Une mise à jour du firmware ou du système d'exploitation qui efface ou régénère les clés.

  • Une réinitialisation d'usine ou l'action « oublier l'appareil » d'un côté mais pas de l'autre.

  • Des clés corrompues ou expulsées par le système pour libérer de l'espace de stockage.

Du point de vue de l'utilisateur, l'appareil peut toujours sembler appairé (il apparaît dans le menu Bluetooth), mais les connexions échouent mystérieusement avec des erreurs telles que « Échec de l'authentification » ou « Chiffrement insuffisant ». Le seul remède consiste généralement à supprimer l'appareil des deux côtés et à procéder à un nouvel appairage, ce qui semble ridicule pour les utilisateurs non techniques.

Désynchronisations temporelles

Les appareils Bluetooth ne discutent pas n'importe quand, ils conviennent d'un intervalle de connexion – essentiellement un calendrier indiquant quand chaque côté va se « réveiller » et échanger des paquets. Voyez cela comme deux personnes acceptant de se retrouver toutes les 30 minutes dans un café.

Un décalage se produit lorsque :

  • Les deux parties négocient des intervalles différents sans être pleinement d'accord (par exemple, l'une pense que c'est 30 ms, l'autre 50 ms).

  • Une mise à jour du firmware ou un changement de configuration d'un côté modifie sa politique de timing.

  • Les conditions radio font qu'un côté manque plusieurs rendez-vous programmés, ce qui entraîne une dérive des horloges.

  • La logique d'économie d'énergie (comme le mode Doze d'un téléphone) étire silencieusement l'intervalle.

Ceci explique pourquoi une connexion peut bien fonctionner au début mais échouer plus tard : les appareils se sont initialement synchronisés sur un intervalle, puis la politique ou le comportement d'un côté a changé. Pour l'utilisateur, cela se traduit par des saccades audio, une latence d'entrée (sur les manettes de jeu) ou des déconnexions aléatoires alors que « ça marchait très bien avant ».

Déconnexions inattendues

Lorsqu'une liaison Bluetooth prend fin, la couche radio (le contrôleur) et la pile logicielle supérieure (l'hôte/host) sont censées échanger des signaux clairs. Le contrôleur envoie un événement HCI Disconnection Complete (en gros : « Au revoir, nous avons fini »). L'hôte doit alors mettre à jour son état interne, nettoyer la session GATT/ACL et être prêt pour une reconnexion.

Mais en pratique, cela ne s'aligne pas toujours :

  • Parfois, le contrôleur dit au revoir proprement, mais la pile hôte ne met pas à jour son état correctement. L'application « pense » toujours que la connexion est active, donc les tentatives de reconnexion échouent silencieusement.

  • Certaines plateformes mettent agressivement en cache l'état de la connexion (particulièrement iOS). Si l'OS croit que la connexion est toujours valide, il ne déclenchera pas de nouvelle tentative de connexion avant d'avoir redémarré le Bluetooth ou le téléphone.

  • Une condition de concurrence (race condition) peut survenir si l'événement de déconnexion se produit pendant qu'une autre opération (par exemple, la découverte de services, l'appairage ou la configuration du chiffrement) est en cours. L'OS peut être confus quant à l'état réel de l'appareil.

  • Sur certains appareils, une tentative de reconnexion rapide après une déconnexion propre entre en collision avec des temporisateurs de refroidissement internes. Le contrôleur l'ignore, laissant l'application en attente.

Du point de vue de l'utilisateur, l'appareil semble « bloqué ». La seule façon de récupérer est de désactiver/réactiver le Bluetooth, de redémarrer l'application ou de redémarrer l'accessoire, même si techniquement rien n'a « échoué ».

Comment les développeurs peuvent faire mieux

Si vous développez une application Bluetooth, voici quelques habitudes qui évitent bien des désagréments :

Vérifiez d'abord les appareils appairés

L'une des causes les plus courantes d'échec de connexion est une information d'appairage incohérente : le téléphone et l'accessoire ne partagent plus les mêmes clés de chiffrement. Même si l'appareil apparaît dans l'interface, l'OS peut avoir perdu ses clés.

Avant de tenter une connexion, interrogez toujours la liste des appareils appairés du système avec BluetoothAdapter.getBondedDevices(). Par exemple :

if (adapter.getBondedDevices().contains(targetDevice)) {
    targetDevice.connectGatt(context, false, gattCallback);
} else {
    showToast("Veuillez ré-appairer cet appareil pour restaurer la connexion.");
}

Cela garantit que vous ne tentez des connexions sécurisées qu'avec des appareils en lesquels l'OS a toujours confiance. Si l'appareil cible n'est pas dans la liste, vous pouvez donner à l'utilisateur une instruction claire (« Veuillez ré-appairer cet appareil ») au lieu de le laisser avec des erreurs de connexion déroutantes.

Gérez les callbacks avec soin

Un autre piège subtil consiste à supposer qu'un événement STATE_CONNECTED signifie que la connexion a réussi. En réalité, onConnectionStateChange() peut rapporter un état connecté même lorsque l'opération sous-jacente a échoué ; le résultat réel se trouve dans l'argument status. Pour éviter de poursuivre des connexions fantômes, vérifiez toujours à la fois status et newState :

if (status == BluetoothGatt.GATT_SUCCESS &&
    newState == BluetoothProfile.STATE_CONNECTED) {
    gatt.discoverServices();
} else {
    gatt.close();
}

Ce modèle vous empêche de tenter une découverte de services sur une connexion morte et garantit que les sessions obsolètes sont fermées rapidement, laissant la pile prête pour une nouvelle tentative propre.

Anticipez les échecs

Les connexions Bluetooth échouent tout le temps dans le monde réel – les appareils sortent de la portée, les interférences augmentent dans la bande 2,4 GHz, ou la radio est simplement occupée. La pire chose qu'une application puisse faire est de réessayer instantanément dans une boucle serrée, ce qui vide la batterie et rend la pile instable.

Une meilleure approche consiste à implémenter un backoff exponentiel comme ceci :

long delay = (long) Math.min(250 * Math.pow(2, attempt), 30000);
new Handler(Looper.getMainLooper()).postDelayed(connectAction, delay);

Cela signifie que votre première tentative de reconnexion se produit rapidement (~250 ms), mais que les tentatives suivantes ralentissent (500 ms, 1 s, 2 s…), avec un maximum raisonnable. Le backoff rend votre application résiliente sans submerger la radio ou l'OS.

Utilisez les bons outils

Sans visibilité sur ce qui se passe sous le capot, les problèmes de connexion semblent aléatoires. Des outils comme nRF Connect vous permettent de scanner, de vous connecter et d'exécuter des opérations GATT de manière interactive sur votre appareil, tandis que le journal de capture HCI Bluetooth d'Android révèle les paquets réellement échangés. Par exemple :

Settings.Secure.putInt(context.getContentResolver(), "bluetooth_hci_log", 1);

Une fois activé, vous pouvez capturer une trace logcat et confirmer si un échec est dû à des clés manquantes (Insufficient Authentication), à un décalage de timing ou à des interférences. L'utilisation de ces outils aide non seulement à déboguer votre application, mais prouve également si le problème réside dans votre code, l'OS ou le firmware de l'accessoire.

Le tout nouveau nRF Connect pour iOS – Blog BeaconZone

Leçons plus larges

Travailler avec le Bluetooth m'a appris des leçons qui s'appliquent à l'ingénierie en général :

  • Le sans-fil n'est jamais parfait, alors concevez toujours en pensant à la récupération.

  • Les journaux (logs) et les métriques ne sont pas optionnels. Ils sont votre carte à travers le chaos.

  • La solution la plus simple est généralement celle qui survit le mieux dans le monde réel.

Conclusion

Le Bluetooth est complexe parce qu'il s'agit d'une chaîne de matériel, de firmware et de logiciel qui essaient tous de coopérer. Sur Android, la variété des puces et des fournisseurs rend la tâche encore plus délicate.

Mais cela ne signifie pas que vous êtes impuissant. En comprenant comment les couches fonctionnent et en concevant vos applications avec des tentatives de reconnexion, des vérifications et une journalisation appropriée, vous pouvez rendre le Bluetooth beaucoup moins « bizarre » pour vos utilisateurs.

La prochaine fois que vos écouteurs feront des siennes, vous saurez : ce n'est pas vous. C'est juste le Bluetooth qui fait du Bluetooth.

Ceci est le premier d'une série d'articles que je vais écrire sur le développement Bluetooth. Dans le prochain, nous approfondirons la manière de construire un client et un serveur GATT Bluetooth Low Energy (BLE) sécurisés sur Android. Restez à l'écoute !