← Actualités

15 avril 2026

Gestion du Bluetooth Low Energy (BLE) dans nos applications mobiles

Comment garantir une connexion stable entre un smartphone et un objet connecté ?

Gestion du Bluetooth Low Energy (BLE) dans nos applications mobiles
Gestion du Bluetooth Low Energy (BLE) : comment garantir une connexion stable entre un smartphone et un objet complexe ?

Si vous avez déjà travaillé avec du Bluetooth Low Energy sur mobile, vous savez probablement déjà comment ça se passe : sur iOS, ça marche ; sur Android, ça dépend.

Chez Poolker, nous développons des applications mobiles connectées à des équipements de piscine, et nous utilisons le BLE pour échanger en temps réel des données critiques comme la température, le pH, l’ORP, l’état des équipements ou encore certains paramètres de configuration. Sur le papier, le Bluetooth Low Energy est une technologie standardisée, documentée et largement adoptée. En pratique, son implémentation varie fortement selon les systèmes d’exploitation, et plus particulièrement côté Android.

Après plusieurs années à développer et maintenir des applications BLE sur Android, nous avons été confrontés à un certain nombre de limitations, de bugs non documentés et de comportements inattendus. Certains sont connus, d’autres beaucoup moins, et nécessitent parfois de plonger directement dans le code source de l’OS pour comprendre ce qu’il se passe réellement.

Cet article ne prétend pas couvrir toute la spécification Bluetooth. L’objectif est plutôt de partager un retour d’expérience concret sur un point de friction majeur en IoT : comment garantir une connexion BLE stable entre un smartphone Android et un objet connecté complexe, contenant beaucoup de données, beaucoup de caractéristiques, et des besoins de mise à jour en temps réel.

Rappel rapide : BLE et GATT

Le Bluetooth Low Energy (BLE) est une variante du Bluetooth classique, conçue pour des communications à faible consommation d’énergie, particulièrement adaptée aux objets connectés. Contrairement au Bluetooth “classique”, le BLE fonctionne sur un modèle d’échange de données structuré appelé GATT, pour Generic Attribute Profile.

Pour une vue d’ensemble, on peut consulter la documentation Android officielle.

Le GATT décrit comment échanger des données entre un client et un serveur. Dans une application mobile, le téléphone joue généralement le rôle de client GATT, et l’équipement connecté agit comme serveur.

Les données sont organisées de manière hiérarchique :

  • des services, qui regroupent des fonctionnalités
  • des caractéristiques, qui contiennent les données elles-mêmes

Chaque caractéristique possède ensuite des propriétés qui indiquent comment elle peut être utilisée :

  • read : le client peut lire la valeur
  • write : le client peut écrire une valeur
  • notify : le serveur pousse automatiquement les nouvelles valeurs au client
  • indicate : similaire à notify, mais avec accusé de réception côté client

Sur le papier, ce modèle est simple et robuste. En pratique, son implémentation, notamment sous Android, introduit des contraintes qui compliquent fortement son utilisation dans des applications BLE réelles.

Pourquoi le BLE est plus complexe sur Android

Si le modèle GATT est standardisé, son implémentation dépend du système d’exploitation, et c’est là que les choses se compliquent.

Côté iOS, Apple contrôle à la fois le matériel et le logiciel, ce qui permet d’avoir une implémentation relativement homogène et prévisible du BLE. Les échanges semblent parfois plus lents ou plus “bridés”, mais globalement stables et fiables.

Côté Android, la situation est très différente. L’OS est utilisé sur une grande variété d’appareils, avec des constructeurs qui peuvent modifier la stack Bluetooth, changer certains comportements internes ou simplement introduire des bugs. Résultat : un même code peut fonctionner parfaitement sur un téléphone, et poser des problèmes très sérieux sur un autre.

À cela s’ajoute le fait que certaines limitations ou comportements de la stack BLE Android ne sont pas documentés officiellement. Dans plusieurs cas, il faut aller consulter directement le code source de l’AOSP pour comprendre l’origine réelle d’un problème.

Les notifications BLE et leur limite côté Android

La gestion des notifications est un élément central du BLE. Elle permet de recevoir des mises à jour en temps réel sans avoir à interroger en permanence l’équipement.

En théorie, il devrait être possible de s’abonner à toutes les caractéristiques qui supportent la propriété notify. En pratique, ce n’est pas le cas sur Android.

En investiguant des problèmes de notifications manquantes, nous avons découvert l’existence d’une limitation interne à la stack Bluetooth Android, définie directement dans le code source de l’OS :

Dans les anciennes versions, on trouve notamment :

``
#ifndef BTA_GATTC_NOTIF_REG_MAX
#define BTA_GATTC_NOTIF_REG_MAX 15
#endif
``

Cette constante correspond au nombre maximum d’enregistrements de notifications côté client GATT. Selon les versions d’Android, cette limite a évolué :

  • Android 4.3 (SDK 18) : 4
  • Android 4.4 (SDK 19) : 7
  • Android 5.0 (SDK 21) : 15
  • Android 12 (SDK 31) : 64

Le problème ne vient pas seulement de la limite elle-même, mais aussi de la manière dont elle est exposée. Lorsqu’elle est atteinte, une erreur est bien détectée dans la couche native de l’OS, mais elle ne remonte pas proprement jusqu’à la couche Java. Du point de vue de l’application, tout semble fonctionner, alors que certaines notifications ne sont tout simplement jamais reçues.

Dans notre cas, un seul appareil nécessitait déjà plus de 15 caractéristiques en notify. Nous étions donc obligés de trouver une autre solution pour continuer à recevoir les données en temps réel dans l’application Android.

Les notifications dynamiques : une fausse bonne idée

Une première idée a été de gérer dynamiquement les notifications selon l’écran affiché :

  • abonnement aux notifications à l’ouverture d’un écran
  • désabonnement à la fermeture

En théorie, cette approche permet de rester sous la limite imposée par Android, en ne gardant actives que les notifications réellement utiles à un instant donné.

En pratique, elle ne fonctionne pas sur le long terme.

Dans le code source d’Android on peut voir qu’il y a bien une erreur qui est tracée lorsque l’on atteint la limite des notifications supportée, mais cette erreur détectée dans la couche C de l’OS (bta_gattc_api.c) ne remonte pas à la couche Java (GattService.java), et nous n’avons donc aucun moyen de savoir lorsque l’on dépasse cette limite imposée par Android.

Nous avons constaté que, sur certaines versions d’Android, le compteur interne des notifications ne semblait pas être correctement décrémenté lors d’un désabonnement. Le désabonnement est bien effectif dans le sens où l’on ne reçoit plus les nouvelles valeurs, mais Android continue malgré tout à considérer que la notification est encore active.

Le résultat est simple : une fois qu’on s’est abonné à un certain nombre de caractéristiques depuis l’ouverture de l’application, il devient impossible d’enregistrer de nouvelles notifications, même si les précédentes ont été désactivées.

Autrement dit, les notifications dynamiques semblaient élégantes sur le papier, mais se sont révélées inutilisables dans une application réelle sur la durée.

Le polling : une solution de secours

Face aux limitations et aux bugs des notifications BLE sur Android, une autre solution consiste à abandonner le modèle push et à passer sur du polling.

Le principe est simple : au lieu d’attendre que l’équipement envoie les données, l’application vient les lire à intervalles réguliers.

Nous avons donc défini des fréquences de lecture adaptées selon le type de données, en donnant la priorité aux valeurs critiques pour l’application, comme le pH, l’ORP ou la température.

Le polling permet de contourner une partie des limitations liées aux notifications, mais il introduit lui-même plusieurs contraintes :

  • consommation batterie accrue
  • charge BLE plus importante
  • multiplication des opérations de lecture
  • surcharge UI si les valeurs remontent en boucle
  • nécessité de gérer des états “dirty” pour éviter d’écraser les modifications utilisateur

Le polling est donc une solution viable, mais clairement pas idéale. Il doit être utilisé avec parcimonie et encadré par une logique plus globale.

Sérialisation des opérations BLE : la queue

Un autre point critique du BLE sous Android concerne la gestion des opérations GATT.

Contrairement à ce que l’on pourrait croire, il n’est pas possible d’enchaîner librement plusieurs opérations read, write ou subscribe. La stack BLE Android gère mal les appels concurrents, ce qui peut entraîner des opérations ignorées, des callbacks qui ne remontent jamais, ou des comportements incohérents.

Pour stabiliser cela, nous avons mis en place une queue d’opérations BLE, de type BleCommandProcessor, afin de garantir une exécution strictement séquentielle des actions.

Le principe est le suivant :

  • chaque opération BLE est ajoutée dans une file
  • une seule opération est exécutée à la fois
  • la suivante ne démarre qu’après callback ou timeout

Nous utilisons également la bibliothèque RxAndroidBle, mais même avec une bonne bibliothèque, une logique applicative reste nécessaire pour prioriser correctement les échanges. Nous avons donc introduit des priorités dans notre queue :

  • les écritures déclenchées par l’utilisateur passent en premier
  • les lectures issues du polling passent derrière

Cela évite qu’un polling trop agressif ne monopolise la communication BLE et ne bloque des actions critiques.

Une approche hybride : la Notify Strategy

Ni les notifications seules, ni le polling seul ne permettent de construire une solution fiable sur Android.

Les notifications sont limitées et instables. Le polling fonctionne, mais il est coûteux et peu élégant. Nous avons donc adopté une approche hybride, basée sur une gestion intelligente des notifications disponibles : la Notify Strategy.

L’idée est de considérer les notifications comme une ressource limitée, un budget, et de les attribuer uniquement aux caractéristiques les plus importantes à un instant donné.

Toutes les caractéristiques ne sont pas traitées de la même manière :

  • certaines données critiques doivent remonter en temps réel
  • d’autres peuvent être rafraîchies périodiquement
  • d’autres encore peuvent être regroupées ou transportées différemment

Nous avons donc défini des niveaux de priorité, par exemple :

  • priorité 1 : états critiques de fonctionnement
  • priorité 2 : erreurs et alertes
  • priorité 3 : mesures principales
  • priorité 4 et plus : données secondaires ou moins sensibles

Les notifications disponibles sont attribuées en priorité aux caractéristiques les plus importantes, dans la limite imposée par Android. Les autres caractéristiques sont gérées via du polling.

Cette stratégie nous a permis de réduire fortement les effets de bord, tout en garantissant une mise à jour rapide des données essentielles.

Limitation du nombre de caractéristiques par service

Au-delà des notifications, nous avons rencontré d’autres limitations plus inattendues en testant l’application sur différentes combinaisons d’appareils Android.

Lors de tests sur un Sony Xperia sous Android 8 équipé d’une stack Bluetooth 4.0, nous avons constaté qu’il était impossible de se connecter correctement à certains équipements BLE pourtant parfaitement fonctionnels sur d’autres téléphones.

Côté firmware, les logs remontaient des erreurs du type data format mismatch.

Après plusieurs investigations, nous avons identifié un comportement anormal dans la gestion des caractéristiques côté Android. Sur certains appareils, il semble exister une limite logicielle sur le nombre de caractéristiques pouvant être correctement manipulées au sein d’un même service.

Concrètement :

  • la couche native semble utiliser un tableau qui déborde aux alentours de 36 caractéristiques
  • la couche Java, elle, contient bien la liste complète
  • lorsqu’on tente de lire une caractéristique au-delà de cette limite, aucune erreur claire n’est levée
  • Android peut renvoyer la valeur précédente, par exemple la 36e, au lieu de la bonne

Ce comportement peut produire des erreurs de type data format mismatch, notamment si les deux caractéristiques n’ont pas le même format de donnée.

Nous avons observé ce problème sur plusieurs appareils, notamment sur un Samsung Galaxy A11 sous Android 11, ce qui suggère qu’il ne s’agit pas d’un cas isolé.

Pour contourner cela, nous avons revu notre architecture firmware afin de limiter le nombre de caractéristiques par service, en découpant les services trop volumineux en plusieurs services distincts. Cela a impliqué des adaptations côté firmware, mais aussi côté Android et iOS.

Les “BLE Fest” et la limite des installations trop complexes

Pour mieux comprendre ces comportements, nous avons organisé en interne des sessions de test que nous appelions les “BLE Fest”. Le principe était simple : chacun venait avec son téléphone personnel et tentait de se connecter à différents environnements BLE.

Ces sessions nous ont permis de mettre en évidence une autre classe de problèmes. Au-delà d’un certain nombre total de caractéristiques sur une installation, certains téléphones n’étaient plus seulement incapables de se connecter correctement : ils devenaient parfois eux-mêmes instables.

Sur certains modèles Sony, Nothing Phone ou Redmi, lorsque l’installation exposait plus d’une centaine de caractéristiques, en pratique autour de 103 à 107 selon les cas, les symptômes pouvaient être très violents :

  • échec de connexion sans message clair
  • téléphone qui ne réagit plus correctement après la tentative
  • dans certains cas, appareil presque inutilisable jusqu’au redémarrage
  • sur au moins un cas observé, boot suivant avec écran de démarrage en erreur

Nous n’avons évidemment pas de visibilité complète sur ce qui se passe dans les couches basses du système, mais le comportement faisait fortement penser à un overflow sévère ou à une corruption d’état interne dans la stack Bluetooth du téléphone.

Pour nous, ce point était particulièrement critique, car nous allions travailler sur une gamme de produits BLE only. Pas d’AWS, pas d’accès à distance, pas de protocole alternatif pour la configuration initiale. Si la connexion BLE échoue au premier démarrage, le produit devient tout simplement inutilisable.

L’erreur GATT 133

Impossible de parler de BLE sur Android sans évoquer la célèbre erreur GATT 133.

Il s’agit d’une erreur générique renvoyée par la stack Bluetooth Android, généralement sans indication claire sur son origine. Dans la pratique, elle peut être liée à de nombreux facteurs :

  • appareil BLE hors de portée
  • advertising arrêté
  • timeout de connexion
  • rejet côté firmware
  • état interne incohérent côté Android

Le principal problème de cette erreur est qu’elle ne permet pas d’identifier précisément la cause du problème, ce qui rend le debugging particulièrement difficile.

Au fil des tests, nous avons identifié plusieurs cas concrets :

  • tentative de connexion alors qu’un scan BLE est encore en cours
  • reconnexion trop rapide juste après une déconnexion

Dans les deux cas, nous avons dû introduire des délais arbitraires d’environ 500 ms pour stabiliser le comportement sur une large gamme d’appareils. Ce n’est pas idéal, mais c’est représentatif du développement BLE sur Android : on finit souvent par devoir compenser des comportements non documentés au niveau de l’OS.

Conclusion

Le Bluetooth Low Energy est une technologie extrêmement puissante pour les applications mobiles et l’IoT, mais son utilisation sur Android reste complexe, parfois imprévisible, et très dépendante du matériel utilisé.

Avec le temps, nous avons constaté que les principales difficultés ne viennent pas du protocole BLE lui-même, mais de son implémentation dans la stack Android : limitations internes non documentées, comportements variables selon les constructeurs, erreurs génériques difficiles à diagnostiquer, et nécessité fréquente de mettre en place des contournements.

À l’inverse, iOS propose une implémentation plus homogène et généralement plus stable, même si les lectures et écritures semblent parfois plus contraintes en débit.

Dans notre cas, la stabilité obtenue aujourd’hui sur Android 12+ repose sur une combinaison de plusieurs briques :

  • sérialisation stricte des opérations BLE
  • priorisation des échanges
  • combinaison entre notifications et polling
  • réduction du nombre de caractéristiques exposées
  • protocole generic pour transporter une partie des données dynamiques

Ce n’est pas une solution magique, mais un ensemble de choix d’architecture issus du terrain. Et c’est probablement le vrai sujet lorsqu’on développe une application BLE pour des objets complexes : il ne suffit pas d’implémenter le protocole GATT, il faut aussi concevoir un système capable d’absorber les limites réelles des smartphones Android.

En ce sens, la question n’est pas seulement “comment faire du BLE sur mobile ?”, mais plutôt : comment construire un protocole et une architecture capables de rester fiables malgré les faiblesses de l’écosystème.