Bluetooth

Introduction

La mise en œuvre du Bluetooth sous Jetpack Compose est assez ardue. La Documentation officielle est loin d'être suffisante

Je vais exposer l'exemple d'une application basique qui communique avec le module Bluetooth HC05 connecté à un Arduino. L'application va nous permettre de contrôler un petit système constitué d'une LED, un Buzzer et un capteur analogique


L'UI de l'application est constituée par :

  • Une zone de texte qui nous permet de suivre le status de l'application
  • Un bouton qui permet d'effacer la zone de status
  • Un bouton pour connecter le HC-05
  • Un bouton pour déconnecter le HC-05
  • Un Switch qui permet d'allumer/éteindre la LED. Quand on le place sur position 'ON', il envoie le caractère 'A' vers le Arduino. Quand on le place sur position 'OFF', il envoie le caractère 'B' vers le Arduino
  • Un Switch qui permet d'allumer/éteindre le buzzer. Comme le switch de la LED, celui-ci envoie soit le caractère 'C', soit le caractère 'D'
  • Le bouton READ envoie le caractère 'E' vers l'Arduino pour lui de mander la valeur du capteur analogique. La réponse retournée est affichée dans le champ de texte voisin

Association des équipement Bluetooth

Avant de commencer à travailler sur l'application, il faut associer le module HC-05 avec le téléphone. Précisons un point important:

  • Pour un équipement Bluetooth, Il y a une différence entre être associé et être connecté,
  • Être associés signifie que deux appareils sont conscients de l'existence l'un de l'autre, ont une clé de liaison partagée qui peut être utilisée pour l'authentification et sont capables d'établir une connexion chiffrée l'un avec l'autre,
  • Être connecté signifie que les appareils partagent actuellement un canal RFCOMM et sont capables de transmettre des données entre eux. Les API Bluetooth actuelles nécessitent que les appareils soient associés avant qu'une connexion RFCOMM puisse être établie,
  • Pour notre application, l'association du module bluetooth HC-05 sera faite avec le service Bluetooth du téléphone avant de lancer l'application. La première fois, il demande un code pin, normalement c'est 1234. Cette opération est à faire une seule fois.


Coté Arduino

  • Sur Arduino + HC-05, La communication Bluetooth est ramenée à une simple communication série
  • Coté Arduino, on utilise un port série software (7,8) pour connecter le HC-05. Ainsi le port série Hardware (0,1) sert (éventuellement) à afficher des messages de debugging sur le moniteur série
  • Le port série du HC-05 utilise la logique 3.3V. il faut prévoir un diviseur de tension pour abaisser un peu le niveau 5V de la broche Tx de l'Arduino
  • Pour la LED, on utilise la LED intégrée sur la carte
  • Le buzzer est branché sur la broche 11
  • Pour simplifier, on n'utilise pas un vrai capteur analogique. Quand l'application demande une mesure, on lui envoie une valeur fictive juste pour tester la connexion

Voici le code Arduino

Code pou HV-05
#include <SoftwareSerial.h>
// Rx(0), Tx(1) = Port COMM physique <--> moniteur Série
// Rx(7), Tx(8) = Port COMM Software <--> module HC-05
/*   
     _________________                ______________
     |   Arduino     |                |  Moniteur   |
     |         Rx(0) |<---------------|  Série      |
     |         Tx(1) |--------------->|             |
     |               |                ---------------
     |               |                _______________
     |         Rx(7) |<---------------|Tx  Module   |
     |         Tx(8) |----/\/\/------>|Rx   HC-05   |
     |               |                |             |
     |           5V  |----------------|Vcc          |
     |           GND |----------------|GND          |
     |               |                ---------------
     -----------------            
*/

SoftwareSerial SerialGSM(7,8); // (RX, TX)
#define LEDPIN 13
#define BUZPIN 11
void setup() {
  Serial.begin(9600);
  SerialGSM.begin(9600);
  Serial.println("HC-05");
  pinMode(LEDPIN, OUTPUT);
  digitalWrite(LEDPIN, LOW);
  pinMode(BUZPIN, OUTPUT);
  digitalWrite(BUZPIN,HIGH);//buzzer active LOW
}
float R = 35.00;  // valeur fictive du capteur analogique
void loop() {
   if (SerialGSM.available() > 0){
    char c = SerialGSM.read();
    Serial.println(c);
    switch (c) {
    case 'A':
      digitalWrite(LEDPIN, HIGH);
      break;
    case 'B':
      digitalWrite(LEDPIN,LOW);
      break;
     case 'C':
      digitalWrite(BUZPIN, LOW); // active LOW
      break;
    case 'D':
      digitalWrite(BUZPIN,HIGH);
      break;
    case 'E':
      SerialGSM.print(R);
      R += 0.5;
     break;
    default:
      break;
    }
  }
}

Coté téléphone

Pour créer l'application qui sera installée sur le smartphone, les choses sont un peu plus compliquées. D'abord, il faut télécharger et installer Android Studio sur votre PC. Une fois installé, il faut commencer par faire quelques petits programmes simples pour se faire la main. Pour mettre en œuvre le Bluetooth dans une application Android il faut être familier avec la programmation sous Android: Activités, Boutons, TextView, ListView, Adaptateur de ListView, les Listners, les threads et les Handler de thread ... Voir cette rubrique

L'application

Lors de la création du projet, choisir l'Appi 31 ou ultérieure comme minimum SDK

Autorisation Dans le manifest

Ajouter la permission d'utiliser le Bluetooth au fichier AndroidManifest.xml


...
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <application
    ...

Structure de l'application

Pour faciliter l'analyse, toute l'application sera placée dans un seule fichier: MainActivity.kt

L'application sera constituée des blocs suivants:

  • Declaration de quelques constantes globales et classes Bluetooth
  • La classe MainActivity
  • Fonctions utilisateur
  • Fonction composable UI qui dessine l'interface utilisateur

Constantes globales

private val MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
const val CONNECTION_FAILED: Int = 0
const val CONNECTION_SUCCESS: Int = 1
const val WRITE_ERROR: Int = 2
const val DISCONNECTION_SUCCESS: Int = 3
const val DISCONNECTION_FAILED: Int = 4

Classe pour connecter un device Bluetooth


//######## Classe pour connecter un device Bluetooth ###############################################
@SuppressLint("MissingPermission")
class ConnectThread(private val monDevice: BluetoothDevice, private val handler: Handler) :
    Thread() {
    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        monDevice.createRfcommSocketToServiceRecord(MY_UUID)
    }

    override fun run() {
        mmSocket?.let { socket ->
            try {
                socket.connect()
                handler.obtainMessage(CONNECTION_SUCCESS).sendToTarget()
            } catch (e: Exception) {
                handler.obtainMessage(CONNECTION_FAILED).sendToTarget()
            }
            dataExchangeInstance = DataExchange(socket, handler)
        }
    }
}

Classe d'échange à travers le bluetooth

Cette classe contient les méthodes permettant de communiquer avec le HC-05

//######## Classe d'échange à travers le bluetooth ##########################################
class DataExchange(private val mmSocket: BluetoothSocket, private val handler: Handler) : Thread() {
    private val mmInStream: InputStream = mmSocket.inputStream
    private val mmOutStream: OutputStream = mmSocket.outputStream

    // Envoyer une donnée à travers le bluetooth
    fun write(str: String) {
        try {
            mmOutStream.write(str.toByteArray())
        } catch (_: IOException) {
            handler.obtainMessage(WRITE_ERROR).sendToTarget()
        }
    }

    // Envoyer une requête (string) et lire la réponse (String)
    fun request(outstr: String, length: Int): String {
        val mmBuffer = ByteArray(length)
        // vider le buffer d'entrée
        while (mmInStream.available() > 0) mmInStream.read()  // vider lebuffer
        // transmettre le string de sortie
        try {
            mmOutStream.write(outstr.toByteArray())
        } catch (_: IOException) {
        }
        // lire le string reçu
        var numBytesReaded = 0
        try {
            while (numBytesReaded < length) {
                val num = mmInStream.read(mmBuffer, numBytesReaded, length - numBytesReaded)
                if (num == -1) {
                    break
                }
                numBytesReaded += num
            }
            return String(mmBuffer, 0, numBytesReaded)
        } catch (e: IOException) {
            return "erreur"
        }
    }

    // déconnecter l'équipement connecté
    fun cancel() {
        try {
            mmSocket.close()
            handler.obtainMessage(DISCONNECTION_SUCCESS).sendToTarget()
        } catch (e: IOException) {
            handler.obtainMessage(DISCONNECTION_FAILED).sendToTarget()
        }
    }
}

En plus de la classe, il faut déclarer une instance de la classe qui sera initialisée par la classe qui connecte le HC-05

//######## Instances de la classe d'échange ########################################################
var dataExchangeInstance: DataExchange? = null

La classe MainActivity

Déclarations des variables Bluetooth et autres objets

A placer avant la fonction onCreate()

    private lateinit var bluetoothAdapter: BluetoothAdapter
    private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
    private lateinit var bluetoothActivationLauncher: ActivityResultLauncher<Intent>
    private lateinit var handler: Handler
    private val status = mutableStateOf("")

Initialiser le bluetoothAdapter

A placer dans la fonction onCreate en dessous de super.onCreate()

bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter

Enregistrer le luncher d'activation du Bluetooth

Ce luncher nous servira plus tard pour activer le Bluetooth et connecter le HC-05. Un luncher démarre une tâche asynchrone (comme une coroutine ou une tâche en arrière-plan) qui se déroule sans bloquer le flux principal du code

// Enregistrement du luncher d'activation du Bluetooth
        bluetoothActivationLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) { response ->
            if (response.resultCode == RESULT_OK) {
                status.value += "Bluetooth activé\nTentative de connexion au HC-05\n"
                status.value += connectHC05(bluetoothAdapter, handler)
            } else {
                Toast.makeText(this, "L'appli ne peut pas fonctionner sans Bluetooth", Toast.LENGTH_LONG).show()
                this.finish()
            }
        }

Enregistrer le luncher de demande d'autorisation

Ce luncher nous servira plus tard pour demander l'autorisation d'utiliser le bluetooth

// Enregistrement du luncher de demande d'autorisation
        requestPermissionLauncher = registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { isGranted: Boolean ->
            if (isGranted) {
                status.value += "Autorisation acceptée\n"
                activerBluetooth()
            } else {
                Toast.makeText(this, "L'app ne peut pas fonctionner sans cette autorisation", Toast.LENGTH_LONG).show()
                this.finish()
            }
        }

Configurer le Handler de messages

Les échanges via Bluetooth sont gérés par des threads, qui sont des processus d'arrière-plan s'exécutant en parallèle du reste du code. Le Handler permet la communication entre ces threads et l'interface utilisateur. Les threads envoient des messages sous forme de codes ou d'identifiants spécifiques, que le Handler intercepte pour mettre à jour la variable status. Cette variable est ensuite utilisée dans l'interface utilisateur pour afficher les informations relatives à la connexion ou aux actions effectuées.

// Configuration du handler de messages
        handler = Handler(Looper.getMainLooper()) { msg ->
            when (msg.what) {
                CONNECTION_FAILED -> {
                    status.value += "Connexion à HC-05 échoué\n"
                    true
                }

                CONNECTION_SUCCESS -> {
                    status.value += "Connexion à HC-05 réussie\n"
                    true
                }

                WRITE_ERROR -> {
                    status.value += "Erreur de transmission\n"
                    true
                }

                DISCONNECTION_SUCCESS -> {
                    status.value += "Déconnexion réussie\n"
                    true
                }

                DISCONNECTION_FAILED -> {
                    status.value += "La déconnexion a échoué\n"
                    true
                }

                else -> false
            }
        }

Activer le Bluetooth et connecter le HC-05

C'est ici que commence réellement le travail

    • On vérifie si l'appli dispose déjà de l'autorisation d'utiliser le Bluetooth
    • Si oui, on appelle la fonction activerBluetooth(). Cette fonction est placée juste après onCreate(), nous allons en parler dans un instant.
    • Si non, on démarre le luncher qui demande l'autorisation

// Vérifier si l'appli dispose de l'autorisation Bluetooth
        // activer le bluetooth et connecter le HC-05
        val bluetoothPermission = android.Manifest.permission.BLUETOOTH_CONNECT
        if (ContextCompat.checkSelfPermission(
                this,
                bluetoothPermission
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            status.value += "Autorisation déjà accordée\n"
            activerBluetooth()  // activer Bluetooth et connecter le HC-05
        } else {
            status.value += "On va demander l'autorisation\n"
            requestPermissionLauncher.launch(bluetoothPermission) // demander l'autorisation
        }

Appeler la fonction composable MyUI()

La fonction MyUI() compose l'Interface utilisateur. On l'appelle dans le bloc setContent { }. On a terminé la fonction onCreate()

// ================= UI Jetpack Compose ====================================================
        setContent {
            MyUI(bluetoothAdapter, handler, status)
        }

La fonction activerBluetooth()

Cette fonction est placée dans la classe MainActivity après onCreate

  • On vérifie si le Bluetooth est déjà activé
  • Si oui, on appelle la fonction connectHC05()
  • Sinon, on lance le luncher qui demande l'activation du Bluetooth
// Fonction pour Activer le Bluetooth et se connecter au HC-05
    private fun activerBluetooth() {
        if (bluetoothAdapter.isEnabled) {
            status.value += "Bluetooth déjà actif\nTentative de connexion\n"
            status.value += connectHC05(bluetoothAdapter, handler)
        } else {
            status.value += "Bluetooth non actif\nOn demande l'activation\n"
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            bluetoothActivationLauncher.launch(enableBtIntent)
        }
    }

La fonction connectHC05

Cette fonction découvre la listes des équipements Bluetooth associés. Si le HC-05 en fait partie elle le connecte.

Elle est placée en dehors de la classe MainActivity car on l'appelle aussi à partir l'interface Utilisateur MyUI()

//#### Fonction pour connecter le HC-05#############################################################
@SuppressLint("MissingPermission")
private fun connectHC05(bluetoothAdapter: BluetoothAdapter?, handler: Handler): String {
    // récupérer la liste des équipements associés
    val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
    // localiser le HC-05 dans la liste
    val hc05Device = pairedDevices?.find { it.name == "HC-05" }
    // Si le HC-05 est associé, essayer de le connecter
    if (hc05Device != null) {
        ConnectThread(hc05Device, handler).start()
        // les messages d'état sont remonté de ConnectThread() vers le handler
        return ""
    } else {
        return "HC-05 Non Associé\n"
    }
}


La fonction MyUI() qui compose l'interface utilisateur

L'interface utilisateur (UI) est assez basique. Elle est composée par la fonction composable MyUI(). Elle contient:

  • Un champ de texte qui affiche la trace des étapes importante du déroulement de l'application
  • Un bouton Clear Status qui permet d'effacer cette zone de texte
  • Un bouton Connecter qui permet de connecter le HC-05 s'il ne l'est pas déjà
  • Un bouton déconnecter qui permet de déconnecter le HC-05
  • Un switch LED qui permet d'envoyer l'ordre d'Allumer/Eteindre la LED vers le Arduino
  • Un switch BUZZER qui permet d'envoyer l'ordre d'Allumer/Eteindre le buzzer vers le Arduino
  • Un bouton READ qui permet d'envoyer la requête de lecture du capteur analogique vers le Arduino. La réponse de ce dernier est affichée dans le champ de texte à droite du bouton
//The UI ###########################################################################################
@Composable
fun MyUI(
    bluetoothAdapter: BluetoothAdapter?,
    handler: Handler,
    connectStatus: MutableState<String>
) {
    val capteur1 = remember { mutableStateOf("Rien") }
    var isLedChecked by remember { mutableStateOf(false) }
    var isBuzzerChecked by remember { mutableStateOf(false) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = connectStatus.value,
            modifier = Modifier
                .padding(8.dp)
                .fillMaxWidth()
                .background(Color(0x80E2EBEA))
                .padding(start = 16.dp)  // marge intérieure
        )

        // Bouton pour effacer le statut
        Button(onClick = { connectStatus.value = "" }) {
            Text(text = "Clear Status")
        }

        Row(
            horizontalArrangement = Arrangement.SpaceEvenly,
            modifier = Modifier.fillMaxWidth()
        ) {
            Button(
                onClick = {
                    dataExchangeInstance?.cancel()
                    connectStatus.value += "Tentative de Connexion\n"
                    connectStatus.value += connectHC05(
                        bluetoothAdapter,
                        handler
                    )
                }
            ) {
                Text("Connecter")
            }

            Button(
                onClick = {
                    dataExchangeInstance?.cancel()
                    connectStatus.value += "Tentative de Déconnexion\n"
                }
            ) {
                Text("Déconnecter")
            }
        }

        // LED et Buzzer avec Switch
        Row(
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "LED ")
            Switch(
                checked = isLedChecked,
                onCheckedChange = {
                    isLedChecked = it
                    if (isLedChecked) dataExchangeInstance?.write("A")
                    else dataExchangeInstance?.write("B")
                },
                thumbContent = if (isLedChecked) {
                    {
                        Icon(
                            imageVector = Icons.Filled.Check,
                            contentDescription = null,
                            modifier = Modifier.size(SwitchDefaults.IconSize),
                        )
                    }
                } else null
            )
            Spacer(modifier = Modifier.width(48.dp))
            Text(text = "BUZZER ")
            Switch(
                checked = isBuzzerChecked,
                onCheckedChange = {
                    isBuzzerChecked = it
                    if (isBuzzerChecked) dataExchangeInstance?.write("C")
                    else dataExchangeInstance?.write("D")
                },
                thumbContent = if (isBuzzerChecked) {
                    {
                        Icon(
                            imageVector = Icons.Filled.Check,
                            contentDescription = null,
                            modifier = Modifier.size(SwitchDefaults.IconSize),
                        )
                    }
                } else null
            )
        }

        // Lire le capteur
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
        ) {
            Button(
                onClick = {
                    val str = dataExchangeInstance?.request("E", 5)
                    if (str != null) {
                        capteur1.value = str
                    } else connectStatus.value = "rien"
                },
                modifier = Modifier.padding(start = 48.dp)
            ) {
                Text("  READ  ")
            }

            Text(
                text = capteur1.value,
                modifier = Modifier
                    .padding(start = 96.dp)
                    .background(Color(0x80E2EBEA))
                    .padding(horizontal = 16.dp)  // marge intérieure
            )
        }
    }
}


Le code entier

Voici le code entier de l'application


Bluetooth - HC05
@file:Suppress("LiftReturnOrAssignment")

package com.example.bluetoothhc05a

import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID

//######## constantes  partagés ####################################################################
private val MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
const val CONNECTION_FAILED: Int = 0
const val CONNECTION_SUCCESS: Int = 1
const val WRITE_ERROR: Int = 2
const val DISCONNECTION_SUCCESS: Int = 3
const val DISCONNECTION_FAILED: Int = 4

//######## Classe pour connecter un device Bluetooth ###############################################
@SuppressLint("MissingPermission")
class ConnectThread(private val monDevice: BluetoothDevice, private val handler: Handler) :
    Thread() {
    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        monDevice.createRfcommSocketToServiceRecord(MY_UUID)
    }

    override fun run() {
        mmSocket?.let { socket ->
            try {
                socket.connect()
                handler.obtainMessage(CONNECTION_SUCCESS).sendToTarget()
            } catch (e: Exception) {
                handler.obtainMessage(CONNECTION_FAILED).sendToTarget()
            }
            dataExchangeInstance = DataExchange(socket, handler)
        }
    }
}

//######## Classe d'échange à travers le bluetooth ##########################################
class DataExchange(private val mmSocket: BluetoothSocket, private val handler: Handler) : Thread() {
    private val mmInStream: InputStream = mmSocket.inputStream
    private val mmOutStream: OutputStream = mmSocket.outputStream

    // Envoyer une donnée à travers le bluetooth
    fun write(str: String) {
        try {
            mmOutStream.write(str.toByteArray())
        } catch (_: IOException) {
            handler.obtainMessage(WRITE_ERROR).sendToTarget()
        }
    }

    // Envoyer une requête (string) et lire la réponse (String)
    fun request(outstr: String, length: Int): String {
        val mmBuffer = ByteArray(length)
        // vider le buffer d'entrée
        while (mmInStream.available() > 0) mmInStream.read()  // vider lebuffer
        // transmettre le string de sortie
        try {
            mmOutStream.write(outstr.toByteArray())
        } catch (_: IOException) {
        }
        // lire le string reçu
        var numBytesReaded = 0
        try {
            while (numBytesReaded < length) {
                val num = mmInStream.read(mmBuffer, numBytesReaded, length - numBytesReaded)
                if (num == -1) {
                    break
                }
                numBytesReaded += num
            }
            return String(mmBuffer, 0, numBytesReaded)
        } catch (e: IOException) {
            return "erreur"
        }
    }

    // déconnecter l'équipement connecté
    fun cancel() {
        try {
            mmSocket.close()
            handler.obtainMessage(DISCONNECTION_SUCCESS).sendToTarget()
        } catch (e: IOException) {
            handler.obtainMessage(DISCONNECTION_FAILED).sendToTarget()
        }
    }
}

//######## Instances de la classe d'échange ########################################################
var dataExchangeInstance: DataExchange? = null


//###### Classe main activity Mainactivity #########################################################
class MainActivity : ComponentActivity() {
    // Déclarations des variables Bluetooth et autres objets
    private lateinit var bluetoothAdapter: BluetoothAdapter
    private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
    private lateinit var bluetoothActivationLauncher: ActivityResultLauncher<Intent>
    private lateinit var handler: Handler
    private val status = mutableStateOf("")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialisation du Bluetooth Adapter
        bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter

        // Enregistrement du luncher d'activation du Bluetooth
        bluetoothActivationLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) { response ->
            if (response.resultCode == RESULT_OK) {
                status.value += "Bluetooth activé\nTentative de connexion au HC-05\n"
                status.value += connectHC05(bluetoothAdapter, handler)
            } else {
                Toast.makeText(
                    this,
                    "L'appli ne peut pas fonctionner sans Bluetooth",
                    Toast.LENGTH_LONG
                ).show()
                this.finish()
            }
        }

        // Enregistrement du luncher de demande d'autorisation
        requestPermissionLauncher = registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { isGranted: Boolean ->
            if (isGranted) {
                status.value += "Autorisation acceptée\n"
                activerBluetooth()
            } else {
                Toast.makeText(
                    this,
                    "L'app ne peut pas fonctionner sans cette autorisation",
                    Toast.LENGTH_LONG
                ).show()
                this.finish()
            }
        }

        // Configuration du handler de messages
        handler = Handler(Looper.getMainLooper()) { msg ->
            when (msg.what) {
                CONNECTION_FAILED -> {
                    status.value += "Connexion à HC-05 échoué\n"
                    true
                }

                CONNECTION_SUCCESS -> {
                    status.value += "Connexion à HC-05 réussie\n"
                    true
                }

                WRITE_ERROR -> {
                    status.value += "Erreur de transmission\n"
                    true
                }

                DISCONNECTION_SUCCESS -> {
                    status.value += "Déconnexion réussie\n"
                    true
                }

                DISCONNECTION_FAILED -> {
                    status.value += "La déconnexion a échoué\n"
                    true
                }

                else -> false
            }
        }

        // ##################### C'set ici que le travail commence #################################
        // Vérifier si l'appli dispose de l'autorisation Bluetooth
        // activer le bluetooth et connecter le HC-05
        val bluetoothPermission = android.Manifest.permission.BLUETOOTH_CONNECT
        if (ContextCompat.checkSelfPermission(
                this,
                bluetoothPermission
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            status.value += "Autorisation déjà accordée\n"
            activerBluetooth()  // activer Bluetoouth et connecter le HC-05
        } else {
            status.value += "On va demander l'autorisation\n"
            requestPermissionLauncher.launch(bluetoothPermission) // demander l'autorisation
        }

        // ================= UI Jetpack Compose ====================================================
        setContent {
            MyUI(bluetoothAdapter, handler, status)
        }
    }


    // ==============================================================================================
    // Fonction pour Activer le Bluetooth et se connecter au HC-05
    private fun activerBluetooth() {
        if (bluetoothAdapter.isEnabled) {
            status.value += "Bluetooth déjà actif\nTentative de connexion\n"
            status.value += connectHC05(bluetoothAdapter, handler)
        } else {
            status.value += "Bluetooth non actif\nOn demande l'activation\n"
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            bluetoothActivationLauncher.launch(enableBtIntent)
        }
    }
}


//#### Fonction pour connecter le HC-05#############################################################
@SuppressLint("MissingPermission")
private fun connectHC05(bluetoothAdapter: BluetoothAdapter?, handler: Handler): String {
    // récupérer la liste des équipements associés
    val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
    // localiser le HC-05 dans la liste
    val hc05Device = pairedDevices?.find { it.name == "HC-05" }
    // Si le HC-05 est associé, essayer de le connecter
    if (hc05Device != null) {
        ConnectThread(hc05Device, handler).start()
        // les messages d'état sont remonté de ConnectThread() vers le handler
        return ""
    } else {
        return "HC-05 Non Associé\n"
    }
}


//The UI ###########################################################################################
@Composable
fun MyUI(
    bluetoothAdapter: BluetoothAdapter?,
    handler: Handler,
    connectStatus: MutableState<String>
) {
    val capteur1 = remember { mutableStateOf("Rien") }
    var isLedChecked by remember { mutableStateOf(false) }
    var isBuzzerChecked by remember { mutableStateOf(false) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = connectStatus.value,
            modifier = Modifier
                .padding(8.dp)
                .fillMaxWidth()
                .background(Color(0x80E2EBEA))
                .padding(start = 16.dp)  // marge intérieure
        )

        // Bouton pour effacer le statut
        Button(onClick = { connectStatus.value = "" }) {
            Text(text = "Clear Status")
        }

        Row(
            horizontalArrangement = Arrangement.SpaceEvenly,
            modifier = Modifier.fillMaxWidth()
        ) {
            Button(
                onClick = {
                    dataExchangeInstance?.cancel()
                    connectStatus.value += "Tentative de Connexion\n"
                    connectStatus.value += connectHC05(
                        bluetoothAdapter,
                        handler
                    )
                }
            ) {
                Text("Connecter")
            }

            Button(
                onClick = {
                    dataExchangeInstance?.cancel()
                    connectStatus.value += "Tentative de Déconnexion\n"
                }
            ) {
                Text("Déconnecter")
            }
        }

        // LED et Buzzer avec Switch
        Row(
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "LED ")
            Switch(
                checked = isLedChecked,
                onCheckedChange = {
                    isLedChecked = it
                    if (isLedChecked) dataExchangeInstance?.write("A")
                    else dataExchangeInstance?.write("B")
                },
                thumbContent = if (isLedChecked) {
                    {
                        Icon(
                            imageVector = Icons.Filled.Check,
                            contentDescription = null,
                            modifier = Modifier.size(SwitchDefaults.IconSize),
                        )
                    }
                } else null
            )
            Spacer(modifier = Modifier.width(48.dp))
            Text(text = "BUZZER ")
            Switch(
                checked = isBuzzerChecked,
                onCheckedChange = {
                    isBuzzerChecked = it
                    if (isBuzzerChecked) dataExchangeInstance?.write("C")
                    else dataExchangeInstance?.write("D")
                },
                thumbContent = if (isBuzzerChecked) {
                    {
                        Icon(
                            imageVector = Icons.Filled.Check,
                            contentDescription = null,
                            modifier = Modifier.size(SwitchDefaults.IconSize),
                        )
                    }
                } else null
            )
        }

        // Lire le capteur
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth()
        ) {
            Button(
                onClick = {
                    val str = dataExchangeInstance?.request("E", 5)
                    if (str != null) {
                        capteur1.value = str
                    } else connectStatus.value = "rien"
                },
                modifier = Modifier.padding(start = 48.dp)
            ) {
                Text("  READ  ")
            }

            Text(
                text = capteur1.value,
                modifier = Modifier
                    .padding(start = 96.dp)
                    .background(Color(0x80E2EBEA))
                    .padding(horizontal = 16.dp)  // marge intérieure
            )
        }
    }
}