fbpixel
Envoyer de longues chaînes de caractères via BLE

Envoyer de longues chaînes de caractères via BLE

Le Bluetooth Low Energy (BLE) a une limitation connue d’une 20Bytes pour la longueur des chaînes envoyées. Ils existent des méthodes pour outrepasser cette limite.

Matériel

Dans ce tutoriel, nous envoyons des données à partir d’une application Android, développée sous React Native, vers une ESP32. Les méthodes décrites peuvent être valables avec d’autres appareils.

  • Appareil Android
  • ESP32 BLE

Limitation de 20 bytes du BLE

L’application développée sous React Native est tout à fait capable d’envoyer de longues chaînes de caractères sous forme de paquet de 20bytes par défaut. Sans modification du tutoriel précédent voici la valeur reçue côté ESP32. Seul le dernier paquet est gardé en mémoire.

01:48:39.734 -> ESP32BLE server ready
01:48:40.493 -> Server address:3c:61:05:31:5f:12
01:48:41.672 -> Client connected
01:48:48.169 -> Charac value: Helloworldthisisalon
01:48:48.253 -> Charac value: gstringletseewhatcom
01:48:48.374 -> Charac value: esout;
01:48:48.374 -> this is the last packet

Résumé des méthodes pour envoyer de longues chaînes via BLE

  • Récupérer plusieurs paquets (côté récepteur)
  • Envoyer la longue chaîne sous forme de petits paquets (côté émetteur)
  • Augmenter la limite MTU de 23 bytes à 512 bytes

Récupérer plusieurs paquets

Si l’appareil qui envoie les messages est capable de gérer l’envoie de chaine de caractères qui dépassent la limite MTU, comme c’est le cas dans notre exemple, il ne reste plus qu’à configurer le module récepteur pour concaténer les différents paquets reçus. Pour cela, nous utilisons un caractères de fin de chaîne, par exemple “;”.

Dans la fonction onWrite de la caractéristique du module ESP32BLE, nous ajoutons un test de fin de chaîne et une variable longMsg qui nous servira de tampon.

std::string longMsg;
class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string value = pCharacteristic->getValue();

      if (value.length() > 0) {
        Serial.print("Charac value: ");
        for (int i = 0; i < value.length(); i++)
          Serial.print(value[i]);

        Serial.println();
        if(value.find(";") != -1){
          //Serial.println("this is the last packet");
          longMsg = longMsg + value;
          Serial.print("longMsg: ");
          for (int i = 0; i < longMsg.length(); i++)
            Serial.print(longMsg[i]);
          Serial.println();
          //do whatever you want with longMsg then erase
          longMsg = "";
        }else{
          longMsg = longMsg + value;
        }
      }
    }
};

Envoyer des paquets de taille prédéfinie

Si l’appareil qui envoie les données ne gère pas les messages de taille supérieure au MTU, alors il est possible de séparer la chaîne de caractères en paquets de taille prédéfinie (chunksize). Cette méthode permet de maitriser la taille des paquets envoyés.

Dans le projet React Native, nous créons une fonction qui va transformer la longue chaîne de caractère en Array de bytes que nous allons envoyez par paquets.

    const sendLongMessage = async (data: string, chunksize: number) => {
      console.log("data : ",data)
      var bytes = convertStringToByteArray(data)
      var times = Math.ceil(bytes.length/chunksize);
        var packets = [];
  
        console.log("split data ",times," times");
        //console.log("bytes: ",bytes)
        var i=0
        for(var time=0;time<times;time++){
          var packet = bytes.slice(time*chunksize, (time+1)*chunksize)
          packets.push(packet)
          console.log(time," : ",byteArrayToString(packet))
          sendMessage(byteArrayToString(packet));
          await delay(100);
        }
    }

N.B.: une fonction de retard peut être introduite pour laisser le temps au message d’être envoyé. delay(100)

const delay = (ms: number) => new Promise(
  resolve => setTimeout(resolve, ms)
);

Augmenter la limite MTU de l’application

Par défaut, la librairie react-native-ble-manager envoie des paquets de 20bytes, Il existe une méthode permettant de spécifier au module BLE de modifier la taille du Maximum Transmission Unit (MTU)

Dans le code de l’application, pendant la connexion, avant de récupérer les services, ajouter une requête MTU

BleManager.requestMTU(device.id, 512).then(
        (mtu) => {
          setMTU(mtu);
        }
      ) //request the MTU size in bytes

Lors de l’envoie du message, vous pouvez spécifier la taille maximum du message en bytes, “mtu”, dans la fonction BLEManager.write()

     BleManager.write(
       selectedDevice.id,
       serviceid,
       caracid,
       buffer.toJSON().data,
       mtu // or 512
     ).then(() => {
       // Success code
       console.log("Write: " + buffer.toJSON().data);
     })

Côté ESP32, il existe une méthode de la librairie BLEDevice permettant de définir la taille du MTU.

BLEDevice::setMTU(512); //between 23 and 517 (bluetooth 4.2)

N.B.: Dans cet exemple (Android+ESP32), seule la définition du nombre maximum de bytes dans la fonction BLEManager.write() permet d’augmenter la limite. Nous laissons les autres infos qui pourraient être utiles pour une autre configuration (iOS, HM10, etc.)

Une fois le code modifié, lorsqu’on renvoie la même chaîne de caractère, on reçoit la totalité de sa valeur dans la caractéristiques BLE

Applications

  • Augmenter la taille des messages envoyés au module BLE
  • Imiter le fonctionnement du Bluetooth classic de manière ponctuelle

Sources

Donner la parole à votre appareil Android avec React Native TTS

Donner la parole à votre appareil Android avec React Native TTS

Nous allons voir dans ce tutoriel comment donner la parole à votre appareil Android avec une librairie Text to Speech (TTS). Que ce soit pour développer des applications pour malvoyant ou pour donner plus de vie à votre système Android, donner la parole à votre projet peut être une fonctionnalité intéressante. Pour cela nous allons intégrer la librairie react-native-tts dans un application Android développée avec React Native.

Matériel

  • Appareil Android
  • Ordinateur pour la programmation
  • Un câble USB pour connecter l’appareil Android au PC

Configuration du projet React Native

La librairie de conversion de texte en parole, react-native-tts permet d’utiliser les voix, ou modèles de synthèse vocale, disponibles sur votre appareil Android afin de lire du texte

npm install react-native-tts --save

Afin de pouvoir sélectionner la voix désirée, nous utilisons une liste déroulante

npm install react-native-dropdown-select-list --save

Utilisation de la librairie react-native-tts

Tout d’abord, nous importons dans le code la librairie TextToSpeech ainsi que la liste déroulante

import Tts from 'react-native-tts'
import { SelectList } from 'react-native-dropdown-select-list'

Dans un composant fonctionnel, nous rajoutons les états qui nous intéresse:

  • voix sélectionnée
  • engin sélectionné
  • liste des voix
  • liste des engins

Il y a d’autres paramètres qui pourrait être intéressant à tester

  • defaultLanguage
  • defaultPitch défini la hauteur de la voix (aiguë ou grave)
  • defaultRate défini la vitesse de lecture de la voix

Dans un crochet useEffect, nous initialisons les listes de voix et des engins puis nous affectons la voix et l’engin sélectionné par défaut.

N.B.: Nous utilisons la promesse qui résulte de l’initialisation de l’engin TextToSpeech afin de nous assurer qu’il sera disponible à l’initialisation du composant (Tts.getInitStatus().then(() => {)

  useEffect(() => {    

   //console.log("init availableVoice",availableVoice)
   if(availableVoice.length === 0){
    console.log("No data found")
    Tts.getInitStatus().then(() => {
    
      Tts.voices().then(voices =>  {
        voicesList = voices.map((voice,index) => {
          //console.log(voice);
          return {key: index, value: voice.name+"("+voice.language+")", disabled: voice.notInstalled==true ? true:false}
        }) ;
        langList =  voices.map((voice,index) => {
          return {key: index, value: voice.language, disabled: voice.notInstalled==true ? true:false}
        }) ;
        //console.log("voicesList", voicesList)
        setAvailableVoice(voicesList);
      }, (err) => {console.log("no voice loaded")}); //see all supported voices 
    
    
      Tts.engines().then(engines =>  {
        engList = engines.map((engine,index) => {
          return {key: index, value: engine.name}
        });
    
        //console.log("engList", engList)
        setAvailableEng(engList);
      }); //see all supported voices 
      
    
    
      }, (err) => {
        console.log("TTS not loaded");
        if (err.code === 'no_engine') {
          Tts.requestInstallEngine();
        }
      });

   }
    

    if(voice !== ""){
      Tts.setDefaultVoice(voice);
    }

    if(lang !== ""){
      Tts.setDefaultLanguage(lang);
    }
    
	}, [availableVoice,availableEng, voice,lang, eng]);

Enfin nous retournons le rendu qui inclus dans l’ordre:

  • la liste déroulante contenant les voix disponibles
  • la liste déroulante contenant les engins disponibles
  • une zone de texte pour écrire le texte à lire
  • un bouton pour lire le texte
  return (
    <View style={{ flexGrow: 1, flex: 1 }}>
      <Text style={styles.mainTitle}>AC Text to Speech</Text>

      <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setVoice(val)} 
              data={availableVoice} 
              save="value"
          />
      </View>

     {/*} <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setLang(val)} 
              data={availableLang} 
              save="value"
          />
  </View>*/}

      <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setEng(val)} 
              data={availableEng} 
              save="value"
          />
      </View>

      <View
              style={styles.inputBar}>        
              <TextInput
                style={styles.textInput}
                placeholder="Enter a message"
                value={msgText}
                onChangeText={(text) =>    setMsgText(text)
                }
              />
              <TouchableOpacity
                        onPress={() => Tts.speak(msgText, {
  androidParams: {
    KEY_PARAM_PAN: -1,
    KEY_PARAM_VOLUME: 0.5,
    KEY_PARAM_STREAM: 'STREAM_MUSIC',
  },
})
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={styles.buttonText}>
                          Speak
                        </Text>
                      </TouchableOpacity>
        </View>
      {/*<Text>{currentDate}</Text>*/}
      <View style={{flex: 1 }}>

      </View>
    </View>

  )

Résultat

Une fois votre code compilé et installé sur votre appareil, vous pouvez sélectionner et tester les voix disponibles (les voix non-installées sont grisées). Entrez un texte et appuyer sur le bouton “Speak” pour faire parler votre application.

Code source complet : Text To Speech

/**
 * npm install react-native-tts --save
 * https://www.netguru.com/blog/react-native-text-to-speech
 * https://www.npmjs.com/package/react-native-tts
 * 
 * npm install react-native-dropdown-select-list --save
 */
 
import React, { useEffect, useState } from 'react'
import { Text, View, StyleSheet, TextInput, TouchableOpacity } from 'react-native'
import Tts from 'react-native-tts'
import { SelectList } from 'react-native-dropdown-select-list'

//Tts.setDefaultLanguage('en-GB')
Tts.setDefaultLanguage('fr-FR')
//Tts.setDefaultVoice('com.apple.ttsbundle.Daniel-compact')
//Tts.setDefaultRate(0.6);
//Tts.setDefaultPitch(1.5);
//Tts.setDefaultEngine('engineName');

//request install. app need reinstall after
let voicesList: { key: number; value: string; disabled: boolean }[] | ((prevState: never[]) => never[])= []
let langList = []
let engList: { key: number; value: string }[] | ((prevState: never[]) => never[]) = []


const App = () =>  {
  const [msgText, setMsgText] = useState("");
  const [voice, setVoice] = useState("")
  const [lang, setLang] = useState("")
  const [eng, setEng] = useState("")
  
  const [availableVoice, setAvailableVoice] = useState([])
  const [availableLang, setAvailableLang] = useState([])
  const [availableEng, setAvailableEng] = useState([])

  
  useEffect(() => {    

   //console.log("init availableVoice",availableVoice)
   if(availableVoice.length === 0){
    console.log("No data found")
    Tts.getInitStatus().then(() => {
    
      Tts.voices().then(voices =>  {
        voicesList = voices.map((voice,index) => {
          //console.log(voice);
          return {key: index, value: voice.name+"("+voice.language+")", disabled: voice.notInstalled==true ? true:false}
        }) ;
        langList =  voices.map((voice,index) => {
          return {key: index, value: voice.language, disabled: voice.notInstalled==true ? true:false}
        }) ;
        //console.log("voicesList", voicesList)
        setAvailableVoice(voicesList);
      }, (err) => {console.log("no voice loaded")}); //see all supported voices 
    
    
      Tts.engines().then(engines =>  {
        engList = engines.map((engine,index) => {
          return {key: index, value: engine.name}
        });
    
        //console.log("engList", engList)
        setAvailableEng(engList);
      }); //see all supported voices 
      
    
    
      }, (err) => {
        console.log("TTS not loaded");
        if (err.code === 'no_engine') {
          Tts.requestInstallEngine();
        }
      });

   }
    

    if(voice !== ""){
      Tts.setDefaultVoice(voice);
    }

    if(lang !== ""){
      Tts.setDefaultLanguage(lang);
    }
    
	}, [availableVoice,availableEng, voice,lang, eng]);

  return (
    <View style={{ flexGrow: 1, flex: 1 }}>
      <Text style={styles.mainTitle}>AC Text to Speech</Text>

      <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setVoice(val)} 
              data={availableVoice} 
              save="value"
          />
      </View>

     {/*} <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setLang(val)} 
              data={availableLang} 
              save="value"
          />
  </View>*/}

      <View style={styles.inputBar}>
            <SelectList 
              setSelected={(val: React.SetStateAction<string>) => setEng(val)} 
              data={availableEng} 
              save="value"
          />
      </View>

      <View
              style={styles.inputBar}>        
              <TextInput
                style={styles.textInput}
                placeholder="Enter a message"
                value={msgText}
                onChangeText={(text) =>    setMsgText(text)
                }
              />
              <TouchableOpacity
                        onPress={() => Tts.speak(msgText, {
  androidParams: {
    KEY_PARAM_PAN: -1,
    KEY_PARAM_VOLUME: 0.5,
    KEY_PARAM_STREAM: 'STREAM_MUSIC',
  },
})
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={styles.buttonText}>
                          Speak
                        </Text>
                      </TouchableOpacity>
        </View>
      {/*<Text>{currentDate}</Text>*/}
      <View style={{flex: 1 }}>

      </View>
    </View>

  )

}

export default App;

let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8
var styles = StyleSheet.create({
  mainTitle:{
    color: TEXT_COLOR,
    fontSize: 30,
    textAlign: 'center',
    borderBottomWidth: 2,
    borderBottomColor: ERROR_COLOR,
  },

  backgroundVideo: {
    borderWidth: 2,
    borderColor: 'red',
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },

  webVideo: {
    borderWidth: 2,
    borderColor: 'green',
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },

  buttonText: {
    color: TEXT_COLOR,
    fontWeight: 'bold',
    fontSize: 12,
	  textAlign: 'center',
    textAlignVertical: 'center',
  },

  sendButton: {
    backgroundColor: BUTTON_COLOR,
    padding: 15,
    borderRadius: 15,
    margin: 2,
    paddingHorizontal: 20,
    },

  inputBar:{
    flexDirection: 'row',
    justifyContent: 'space-between',
    margin: 5,
  },  

  textInput:{
    backgroundColor: '#888888',
    margin: 2,
    borderRadius: 15,
    flex:3,
  },
});

Applications

  • Faites parler votre système Android que ce soit un téléphone une voiture ou un robot!

Sources

Créer un composant fonctionnel avec React Native

Créer un composant fonctionnel avec React Native

Une fois votre première application React Native créée et opérationnelle, vous voudrez peut-être réutiliser certains éléments comme composant fonctionnel. Ces composants peuvent alors être configurés pour d’autres applications et être partagés sous forme de librairie.

Description

Dans ce tutoriel, nous allons partir d’une application simple contenant quatre boutons. Nous allons créer un composant fonctionnel React Native configurable dans un fichier source que nous allons réutiliser dans le fichier principal l’application.

Nous allons voir comment:

  • passer des propriétés à un composant
  • récupérer les données d’un composant à partir d’évènement
  • utiliser un composant défini dans un fichier source dans le fichier principal

Application de base : 4 boutons

Dans ce code, nous allons avoir une vue principale (View) qui recouvre l’écran, le titre de l’application, et la vue qui va contenir les 4 boutons. Les fonctions exécutés lors des appuis et la définition des styles.

/**
 * https://github.com/jim-at-jibba/react-native-game-pad
 * https://github.com/ionkorol/react-native-joystick
 * 
 */
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";


const GamePad = () =>  {

  let btnAColor = "red",
      btnBColor = "orange",
      btnXColor = "royalblue",
      btnYColor = "limegreen"

  const onButtonAPress = () => {
    console.log('You clicked button A');
  };
  const onButtonBPress = () => {
    console.log('You clicked button B');
  };

  const onButtonXPress = () => {
    console.log('You clicked button X');
  };
  const onButtonYPress = () => {
    console.log('You clicked button Y');
  };

 
    return (
      <View style={styles.mainBody}>
          <Text
            style={styles.mainTitle}>
            AC Controller
          </Text>
          <View style={{ flex: 1,justifyContent: "center", alignItems: "center",}}>
              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"green",
                  zIndex: 100,
                }}
              >
                <TouchableOpacity
                  style={[styles.button, {  top:"30%",backgroundColor: `${btnXColor}` }]} //top:"30%", right:"1%", 
                  onPress={() => onButtonXPress()}
                >
                  <Text style={styles.buttonText}>X</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"gold",
                }}
              >
                <TouchableOpacity
                  style={[styles.button, { top: "50%", right:"20%", backgroundColor: `${btnYColor}` }]} //top:"2%", left:"10%",
                  onPress={() => onButtonYPress()}
                >
                  <Text style={styles.buttonText}>Y</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"blue",
                }}
              >
                <TouchableOpacity
                  style={[styles.button, { bottom:"50%", left:"20%", backgroundColor: `${btnAColor}` }]} //bottom:"2%", 
                  onPress={() => onButtonAPress()}
                >
                  <Text style={styles.buttonText}>A</Text>
                </TouchableOpacity>
              </View>


            <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"red",
                }}
              >
                <TouchableOpacity
                  style={[styles.button, { bottom:"30%", backgroundColor: `${btnBColor}` }]} //bottom:"30%", left:"1%",
                  onPress={() => onButtonBPress()}
                >
                  <Text style={styles.buttonText}>B</Text>
                </TouchableOpacity>
              </View>

          </View>
      </View>            

    );
}
export default GamePad;

//parameters
let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8

const styles = StyleSheet.create({

  mainBody: {
    flex:1,
    justifyContent: 'center',
    alignItems: "center",
    color:TEXT_COLOR,
    backgroundColor: BACKGROUND_COLOR,
  },

  mainTitle:{
    color: TEXT_COLOR,
    fontSize: 30,
    textAlign: 'center',
    borderBottomWidth: 2,
    borderBottomColor: ERROR_COLOR,
    width:"100%"
  },

  button: {
    height: 90,
    width: 90,
    borderRadius: 90 / 2,
    justifyContent: "center",
    alignItems: "center"
  },
  buttonText: {
    fontSize: 22,
    color: "white",
    fontWeight: "700"
  },


});

l’appui sur les boutons exécute bien les fonctions évènements

 BUNDLE  ./index.js

 LOG  Running "CustomApp" with {"rootTag":31}
 LOG  You clicked button Y
 LOG  You clicked button X
 LOG  You clicked button A
 LOG  You clicked button B

Appeler le composant à partir d’un fichier source

Nous allons placer ce code dans un fichier ./src/ACJoysticks.tsx et nous changeons le nom du composant en ButtonPad

Pour pouvoir l’utiliser, nous utilisons le mot-clé export pour définir la fonction

export const ButtonPad = ()  =>  {
...
}

Il est ensuite possible d’utiliser le composant en l’important

import {ButtonPad} from "./src/ACJoysticks";
const GamePad = () =>  {
    return (
      <View style={styles.mainBody}>
          <Text
            style={styles.mainTitle}>
            AC Controller
          </Text>
          <ButtonPad/>
      </View>            

    );
};
export default GamePad;

Passer des propriétés à un composant fonctionnel

Pour rendre le composant configurable, nous définissons des paramètres dans le fichier principale qui seront passés dans les propriétés du composant. Les propriétés que nous souhaitons passer au composant sont:

  • le texte affiché sur les boutons
  • la couleur des boutons
  • la taille des boutons
  • la fonction onClick qui gère tous les boutons

Les paramètres du composant son placés dans un type ButtonPadProps

type ButtonPadProps = {
  radius?: number;
  names?: string[4];
  colors?: string[4];
  onClick?:(evt: ButtonPadEvent) => void;
};

Nous pouvons ensuite définir ces propriétés en entrée du composant et les appelés avec des valeurs par défaut.

export const ButtonPad = (props : ButtonPadProps)  =>  {
  const { onClick, radius = 45, names= ['X','Y','A','B'], colors = ["royalblue","limegreen","red","orange"] } = props;

Nous définissons une fonction unique d’appel des boutons

  const onButtonPress = (index:number) => {
    console.log('You clicked button ' + names[index]);
  };

Enfin, nous modifions le code pour prendre en compte les différentes propriétés.

/**
 * ButtonPad
 */

type ButtonPadProps = {
  radius?: number;
  names?: Array<string>;
  colors?: Array<string>;
  onClick?:(evt: ButtonPadEvent) => void;
};

export const ButtonPad = (props : ButtonPadProps)  =>  {
  const { onClick, radius = 45, names= ['X','Y','A','B'], colors = ["royalblue","limegreen","red","orange"] } = props;
  let textSize = radius;

  const onButtonPress = (index:number) => {
    console.log('You clicked button ' + names[index]);
  };
  

 
    return (

          <View style={{ flex: 1,justifyContent: "center", alignItems: "center",}}>
              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"green",
                  zIndex: 100,
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, {  top:"30%",backgroundColor: `${colors[0]}` }]} //top:"30%", right:"1%", 
                  onPress={() => onButtonPress(0)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[0]}</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"gold",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { top: "50%", right:"20%", backgroundColor: `${colors[1]}` }]} //top:"2%", left:"10%",
                  onPress={() => onButtonPress(1)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[1]}</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"blue",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { bottom:"50%", left:"20%", backgroundColor: `${colors[2]}` }]} //bottom:"2%", 
                  onPress={() => onButtonPress(2)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[2]}</Text>
                </TouchableOpacity>
              </View>


            <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"red",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { bottom:"30%", backgroundColor: `${colors[3]}` }]} //bottom:"30%", left:"1%",
                  onPress={() => onButtonPress(3)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[3]}</Text>
                </TouchableOpacity>
              </View>

          </View>
    );
}

Passer des données du composant vers l’application

Pour retourner des données vers l’application principale, nous utilisons une interface contenant les états des boutons et le nom du bouton pressé

interface ButtonPadEvent {
  states : Array<boolean>;
  pressed: string;
}

Nous modifions la fonction onButtonPress pour alimenter les états et les envoyer à la fonction onClick

  const onButtonPress = (index:number) => {

    let btnStates= [false,false,false,false];
    btnStates[index] = true;

    if(typeof(onClick)==="function"){
      onClick({
             states: btnStates,
             pressed: names[index]
         });
       }
  };

N.B.: La notation if(typeof(onClick)===”function”){ onClick() } est équivalente à onClick && onClick()

Nous pouvons maintenant appelé notre composant dans l’application principale, créer une fonction d’écoute et modifier ses paramètres.

const GamePad = () =>  {
  const onPadClick = (data: any) => {
    console.log("form ButtonPad: ",data);
  }
    return (
      <View style={styles.mainBody}>
          <Text
            style={styles.mainTitle}>
            AC Controller
          </Text>
          <ButtonPad radius={60} names={['\u2573', '\u25EF', '\u25B3', "\u25A2"]} colors={['black', 'black', 'black', 'black']} onClick={onPadClick} />
      </View>            

    );
}
export default GamePad;
info Reloading app...
 BUNDLE  ./index.js

 LOG  Running "CustomApp" with {"rootTag":81}
 LOG  form ButtonPad:  {"pressed": "╳", "states": [true, false, false, false]}
 LOG  form ButtonPad:  {"pressed": "△", "states": [false, false, true, false]}
 LOG  form ButtonPad:  {"pressed": "▢", "states": [false, false, false, true]}
 LOG  form ButtonPad:  {"pressed": "◯", "states": [false, true, false, false]}

N.B.: pour faire les choses bien il faudrait créer les observateurs onPressIn et onPressOut pour repasser les états à zéro et modifier la logique pour écouter plusieurs bouton à la fois.

Code complet

./src/ACJoysticks.tsx

/**
 * ButtonPad
 */

interface ButtonPadEvent {
  states : Array<boolean>;
  pressed: string;
}

type ButtonPadProps = {
  radius?: number;
  names?: Array<string>;
  colors?: Array<string>;
  onClick?:(evt: ButtonPadEvent) => void;
};



export const ButtonPad = (props : ButtonPadProps)  =>  {
  const { onClick, radius = 45, names= ['X','Y','A','B'], colors = ["royalblue","limegreen","red","orange"] } = props;
  let textSize = radius;
  let btnStates= [false,false,false,false];

  const onButtonPress = (index:number) => {
    btnStates[index] = true;

    if(typeof(onClick)==="function"){
      onClick({
             states: btnStates,
             pressed: names[index]
         });
       }
  };
  
  const onButtonRel = (index:number) => {
    btnStates[index] = false;
    
    if(typeof(onClick)==="function"){
      onClick({
             states: btnStates,
             pressed: names[index]
         });
       }
  };
  
    return (

          <View style={{ flex: 1,justifyContent: "center", alignItems: "center",}}>
              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"green",
                  zIndex: 100,
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, {  top:"30%",backgroundColor: `${colors[0]}` }]} //top:"30%", right:"1%", 
                  //onPress={() => onButtonPress(0)}
                  onPressIn={() => onButtonPress(0)}
                  onPressOut={() => onButtonRel(0)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[0]}</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"gold",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { top: "50%", right:"20%", backgroundColor: `${colors[1]}` }]} //top:"2%", left:"10%",
                  //onPress={() => onButtonPress(1)}
                  onPressIn={() => onButtonPress(1)}
                  onPressOut={() => onButtonRel(1)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[1]}</Text>
                </TouchableOpacity>
              </View>

              <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"blue",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { bottom:"50%", left:"20%", backgroundColor: `${colors[2]}` }]} //bottom:"2%", 
                  //onPress={() => onButtonPress(2)}
                  onPressIn={() => onButtonPress(2)}
                  onPressOut={() => onButtonRel(2)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[2]}</Text>
                </TouchableOpacity>
              </View>


            <View
                style={{
                  //flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                  //backgroundColor:"red",
                }}
              >
                <TouchableOpacity
                  style={[{
                    height: 2*radius,
                    width: 2*radius,
                    borderRadius: radius,
                    justifyContent: "center",
                    alignItems: "center"
                  }, { bottom:"30%", backgroundColor: `${colors[3]}` }]} //bottom:"30%", left:"1%",
                  //onPress={() => onButtonPress(3)}
                  onPressIn={() => onButtonPress(3)}
                  onPressOut={() => onButtonRel(3)}
                >
                  <Text style={{
                            fontSize: textSize,
                            color: "white",
                            fontWeight: "700"
                          }}>{names[3]}</Text>
                </TouchableOpacity>
              </View>

          </View>
    );
}

./App.tsx

import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import {ButtonPad} from "./src/ACJoysticks";

const GamePad = () =>  {
  const onPadClick = (data: any) => {
    console.log("from ButtonPad: ",data);
    //data.states[0], data.pressed
  }

    return (
      <View style={styles.mainBody}>
          <Text
            style={styles.mainTitle}>
            AC Controller
          </Text>
          <ButtonPad radius={60} names={[ '\u25B3', '\u25A2', '\u25EF',  '\u2573',]} colors={['black', 'black', 'black', 'black']} onClick={onPadClick} />
      </View>            

    );
}
export default GamePad;

//parameters
let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8

const styles = StyleSheet.create({

  mainBody: {
    flex:1,
    justifyContent: 'center',
    alignItems: "center",
    color:TEXT_COLOR,
    backgroundColor: BACKGROUND_COLOR,
  },

  mainTitle:{
    color: TEXT_COLOR,
    fontSize: 30,
    textAlign: 'center',
    borderBottomWidth: 2,
    borderBottomColor: ERROR_COLOR,
    width:"100%"
  },
});

Applications

  • Créer des composants réutilisables et configurables pour vos applications
  • Créer des applications pour piloter vos projets en Bluetooth, BLE ou Wifi

Sources

Premier pas avec Android sur Rock Pi 4

Premier pas avec Android sur Rock Pi 4

La carte Rock Pi 4 de chez Radxa peut tourner avec un OS Debian, Ubuntu ou Android. Nous allons voir dans ce tutoriel comment configurer et utiliser votre micro-ordinateur avec Android.

Matériel

  • Rock Pi 4 SE
  • Carte SD 32 Go
  • Un écran HDMI
  • Clavier+souris
  • Câble USB A vers USB A

Télécharger et installer l’OS

Télécharger l’OS Android puis utiliser balenaEtcher pour écrire l’image sur la carte SD.

Une fois la carte écrite, veillez à fermer toutes les fenêtres avant d’éjecter la carte SD

Insérer la carte SD dans le Rock Pi 4 puis alimenter le Rock Pi.

Vous pouvez ensuite suivre la procédure d’installation d’Android sur Rock Pi 4

N.B.: Vous pouvez ignorer la partie sur les services Google car vous aurez du mal à faire valider cet appareil.

Une fois Android installé, vous pouvez commencer à vous amuser.

N.B.: dans les paramètres, désactiver les notifications de toutes les applications par défaut

Dans paramètres > à propos du téléphone, récupérer l’adresse IP de l’appareil

Installation et configuration d’Android Studio

L’installation d’Android Studio sur votre ordinateur vous sera très utile pour développer sous Android.

Téléchargez et installez Java JDK 11

Configurez JAVA_HOME dans les variables d’environnement (C:\Program Files\Microsoft\jdk-11.0.17.8-hotspot\)

Téléchargez et exécutez l’installateur de Android Studio

Configurez ANDROID_HOME dans les variables d’environnement (C:\Users\ADMIN\AppData\Local\Android\Sdk)

Vérifier la version de Gradle (graddle-wrapper.properties) et du plugin Gradle (build.gradle)

Utilisation d’Android Debug Bridge

L’installation d’Android Studio vous donnera accès à Android Debug Bridge (ADB). ADB est un terminal de commande qui vous permet de communiquer avec un appareil Android depuis votre ordinateur.

Brancher le port USB 3 du Rock Pi, situé au-dessus, à l’ordinateur à l’aide du câble USB A vers USB A

Puis activer l’interrupteur, situé sous les ports USB (OTG switch), côté Rock Pi pour activer le mode Device. Une fois que l’appareil est connecté et reconnu par l’ordinateur, vous pouvez utiliser les commande ADB depuis un terminal.

Pour vérifier que votre appareil est bien détecté

adb devices

Il est possible de transférer des fichiers depuis l’ordinateur au Rock pi à l’aide de la commande

adb push <local path> <remote path>

Pour transférer des fichiers depuis Rock Pi vers l’ordinateur

adb pull <remote path> <local path>

Accéder au fichier log Android

adb logcat

Pour vous connecter au Terminal de l’appareil

adb shell
or
 
adb -s <serial number> shell

N.B.: vous pouvez exécuter directement une commande sur l’appareil avec la commande adb shell <cmd>

Une fois dans le shell, vous pouvez naviguer dans le système de l’appareil. Voici quelques commandes utiles

getprop # display all properties
getprop ro.build.version.release # get android version
getprop ro.build.version.sdk # get API level
getprop vendor.serialno # get serial number
getprop ro.vendor.product.cpu.abilist # get CPU info
getprop ro.product.vendor.name #get device name

ifconfig #get network informations

Pour terminer la connexion du shell entrez la commande

exit

La plupart des accès aux fichiers système sont bloqués. Pour permettre l’accès aux dossiers, utiliser la commande su

su # enter super user mode
<enter command>
exit # exit super user mode

Pour éteindre l’appareil, vous pouvez utiliser la commande reboot

reboot -p 

Télécharger un fichier APK

Il est possible de télécharger une application sur Google Play store ou sur internet directement.

Vous pouvez aussi récupérer un fichier APK que vous avez créé:

  • avec un clé USB
  • avec le drive ou la boite mail de Gmail
  • transférer le fichier avec SSH ou ADB

L’installation de l’apk peut se faire via l’interface graphique ou avec la ligne de commande

adb install -r <apk_file.apk>

Télécharger un terminal Linux sur votre RockPi

Pour pouvoir développer sous Android sur Rock Pi, il peut être intéressant d’utiliser un terminal. Voici deux applications que vous pouvez utiliser afin de naviguer sur votre appareil Android:

Éteindre l’appareil

Pour éteindre l’appareil, au lieu de couper l’alimentation, vous pouvez activer le menu d’accessibilité dans Paramètres > Accessibilité > Menu d’accessibilité

Un bouton apparaitra en bas de l’écran et vous donnera accès à un menu d’icône contenant le bouton “Eteindre”

Sources

Envoyer de longues chaînes de caractères via BLE

Créer une application BLE pour ESP32 avec React Native

Nous allons voir comment créer une application React Native pour Adnroid permettant la communication BLE (Bluetooth Low Energy) avec un ESP32. Nous utilisons React Native pour développer un terminal BLE sur Android permettant la communication avec un NodeMCU ESP32 ou tout autres appareils compatibles.

Matériel

  • Un ordinateur avec installation de React Native et Node.js
  • Un appareil Android avec BLE
  • Un câble USB pour relier l’ordinateur à l’appareil
  • Un appareil BLE (ESP32)

Code de gestion du BLE pour ESP32

Pour tester l’application React Native, nous allons utiliser le code de gestion du BLE pour ESP32.

/*
    Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleWrite.cpp
    Ported to Arduino ESP32 by Evandro Copercini
*/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLECharacteristic *pCharacteristic = NULL;

std::string msg;

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string value = pCharacteristic->getValue();

      if (value.length() > 0) {
        Serial.println("*********");
        Serial.print("New value: ");
        for (int i = 0; i < value.length(); i++)
          Serial.print(value[i]);

        Serial.println();
        Serial.println("*********");
      }
    }
};

class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    Serial.println("Client connected");
  }
  void onDisconnect(BLEServer* pServer) {
    Serial.println("Client disconnected");
    BLEDevice::startAdvertising(); // needed for reconnection
  }
};

void setup() {
  Serial.begin(115200);

  Serial.println("1- Download and install an BLE Terminal Free");
  Serial.println("2- Scan for BLE devices in the app");
  Serial.println("3- Connect to ESP32BLE");
  Serial.println("4- Go to CUSTOM CHARACTERISTIC in CUSTOM SERVICE and write something");

  BLEDevice::init("ESP32BLE");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  
  BLEService *pService = pServer->createService(SERVICE_UUID);

  pCharacteristic = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );

  pCharacteristic->setCallbacks(new MyCallbacks());

  pCharacteristic->setValue("Hello World");
  pService->start();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMinPreferred(0x12);
  pAdvertising->start();

  Serial.print("Server address:");
  Serial.println(BLEDevice::getAddress().toString().c_str());
}

void loop() {
  readSerialPort();

  //Send data to slave
  if(msg!=""){
    pCharacteristic->setValue(msg);
    msg="";
  }
  
  delay(2000);
}

void readSerialPort(){
 while (Serial.available()) {
   delay(10);  
   if (Serial.available() >0) {
     char c = Serial.read();  //gets one byte from serial buffer
     msg += c; //add to String
   }
 }
 Serial.flush(); //clean buffer
}

Nous rajoutons la fonction BLEServerCallbacks à la gestion du Serveur BLE pour détecter la déconnexion et démarrer l’advertising pour pouvoir reconnecter l’ESP32

  pServer->setCallbacks(new MyServerCallbacks());

Application React Native pour la gestion du BLE

Pour gérer la communciation BLE (Bluetooth Low Energy) sur l’appareil Android, nous utilisons la librairie react-native-ble-manager

npm install react-native-ble-manager --save

Pour mettre en place le projet de l’application, suivez le tutoriel précédent.

Dans le fichier App.tsx, pour utiliser la bibliothèque nous l’importons à l’aide de la commande

import BleManager from 'react-native-ble-manager';

Nous créons un composant fonctionnel qui contiendra les éléments nous permettant de gérer la communication BLE

let serviceid="4fafc201-1fb5-459e-8fcc-c5c9c331914b";
let caracid="beb5483e-36e1-4688-b7f5-ea07361b26a8";

const BluetoothBLETerminal = () =>  {
 const [devices, setDevices] = useState<any[]>([]);
 const [paired, setPaired] = useState<any[]>([]);
 const [selectedDevice, setSelectedDevice] = useState<Peripheral>();
 const [messageToSend, setMessageToSend] = useState("");
 const [receivedMessage, setReceivedMessage] = useState("");
 const [isConnected, setIsConnected] = useState(false);
 const [intervalId, setIntervalId] = useState<NodeJS.Timer>();
 const [isScanning, setIsScanning] = useState(false);

N.B.: il est possible de créer un composant qui dérive de ReactNative.Components

Gestion des permissions

Pour pouvoir découvrir et se connecter à des appareils Bluetooth, il faut 3 permissions au minimum:

  • BLUETOOTH_SCAN
  • BLUETOOTH_CONNECT
  • ACCESS_FINE_LOCATION

N.B.: ces permissions dépendent de la version et de l’OS utilisé

Voici les balises à ajouter dans le fichier AndroidManifest.xml

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Dans le fichier App.tsx, nous créons la fonction requestBluetoothPermission()

    if (Platform.OS === 'android' && Platform.Version >= 23) {
  
        PermissionsAndroid.requestMultiple([
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
        ]).then(result => {
          if (
            (result['android.permission.BLUETOOTH_SCAN'] &&
            result['android.permission.BLUETOOTH_CONNECT'] &&
            result['android.permission.ACCESS_FINE_LOCATION'] === 'granted')
            ||
            (result['android.permission.BLUETOOTH_SCAN'] &&
            result['android.permission.BLUETOOTH_CONNECT'] &&
            result['android.permission.ACCESS_FINE_LOCATION'] === 'never_ask_again')
          ) {
            console.log('User accepted');
          } else {
            console.log('User refused');        }
        });
    }

Fonction de gestion du BLE

Les fonctions permettant la gestion du Bluetooth LE sont les suivantes:

  • découvrir des appareils bluetooth startDeviceDiscovery() (j’utilise les appareils apairés)
  • se connecter à l’appareil connectToDevice()
  • se déconnecter disconnectFromDevice()
  • envoyer des messages sendMessage()
  • lire les messages provenant de la communication readData()

N.B.: Dans cet exemple, nous écrivons et lisons sur la même caractéristique. Nous lisons donc la valeur enregistré avec un appui bouton

const checkBluetoothEnabled = async () => {
   try {
         // turn on bluetooth if it is not on
   BleManager.enableBluetooth().then(() => {
     console.log('Bluetooth is turned on!');
   });
     
   } catch (error) {
     console.error('BLE is not available on this device.');
   }
 }
 
 const startScan = () => {
  if (!isScanning) {
    BleManager.scan([], 5, true)
      .then(() => {
        console.log('Scanning...');
        setIsScanning(true);
      })
      .catch(error => {
        console.error(error);
      });
  }
};

 const startDeviceDiscovery = async () => {

  BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => {
    // Each peripheral in returned array will have id and name properties
    console.log("Bonded peripherals: " + bondedPeripheralsArray.length);
    setPaired(bondedPeripheralsArray);
  });

  /*BleManager.getDiscoveredPeripherals().then((peripheralsArray) => {
    // Success code
    console.log("Discovered peripherals: " + peripheralsArray.length);
  });*/
 }

 const connectToDevice = async (device: Peripheral) => {
 BleManager.connect(device.id)
     .then(() => {
     // Success code
     console.log("Connected");
     setSelectedDevice(device);
     setIsConnected(true);
     BleManager.retrieveServices(device.id).then(
       (deviceInfo) => {
       // Success code
       console.log("Device info:", deviceInfo);
       }
     );


     })
     .catch((error) => {
     // Failure code
     console.log(error);
     });
 }


const sendMessage = async () => {
 if(selectedDevice && isConnected){
   try {

    const buffer = Buffer.from(messageToSend);
    BleManager.write(
      selectedDevice.id,
      serviceid,
      caracid,
      buffer.toJSON().data
    ).then(() => {
      // Success code
      console.log("Write: " + buffer.toJSON().data);
    })
    .catch((error) => {
      // Failure code
      console.log(error);
    });
     
   } catch (error) {
     console.error('Error sending message:', error);
   }
 }
}


const readData = async () => {  
 if (selectedDevice && isConnected) {
    BleManager.read(
      selectedDevice.id,
      serviceid,
      caracid
    )
      .then((readData) => {
        // Success code
        console.log("Read: " + readData);
        const message = Buffer.from(readData);
        //const sensorData = buffer.readUInt8(1, true);
        if(receivedMessage.length>300){
          setReceivedMessage(""); //reset received message if length higher than 300
        }
        setReceivedMessage(receivedMessage => receivedMessage + message +"\n" );
        console.log("receivedMessage length",receivedMessage.length)
      })
      .catch((error) => {
        // Failure code
        console.log("Error reading message:",error);
      });
 }
}

 // disconnect from device
 const disconnectFromDevice = (device: Peripheral) => {
   BleManager.disconnect(device.id)
   .then(() => {
        setSelectedDevice(undefined);
        setIsConnected(false);
        setReceivedMessage("");
        clearInterval(intervalId);
        console.log("Disconnected from device");
   })
   .catch((error) => {
     // Failure code
     console.log("Error disconnecting:",error);
   });
 };

La fonction de rendu de l’écran

Pour l’affichage, nous choisissons de tous mettre sur un même écran. Il y aura :

  • Un titre
  • La liste des appareils qui n’apparait que si on n’est pas connecté (!isConnected &&)
  • Un encart type terminal de communication qui n’apparait que si on est connecté (selectedDevice && isConnected &&)
    • TextInput pour écrire le message à envoyer messageToSend
    • Un bouton d’envoi
    • Un bouton de lecture
    • Une zone de texte pour afficher receivedMessage
    • Un bouton de déconnexion
    return (
      <View>
      <Text
            style={{
              fontSize: 30,
              textAlign: 'center',
              borderBottomWidth: 1,
            }}>
            AC Bluetooth Terminal
          </Text>
        <ScrollView>

          {!isConnected && (
          <>
          {/*
          <Text>Available Devices:</Text>
          {devices.map((device) => (
            <Button
              key={device.id}
              title={device.name || 'Unnamed Device'}
              onPress={() => this.connectToDevice(device)}
            />
          ))}
          */}
          <Text>Paired Devices:</Text>
          {paired.map((pair: BluetoothDevice) => (
                      <View
                      style={{
                        flexDirection: 'row',
                        justifyContent: 'space-between',
                        marginBottom: 2,
                      }}>
                      <View style={styles.deviceItem}>
                        <Text style={styles.deviceName}>{pair.name}</Text>
                        <Text style={styles.deviceInfo}>{pair.id}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() =>
                          isConnected
                            ?  disconnect()
                            :  connectToDevice(pair)
                        }
                        style={styles.deviceButton}>
                        <Text
                          style={[
                            styles.scanButtonText,
                            {fontWeight: 'bold', fontSize: 12},
                          ]}>
                          {isConnected ? 'Disconnect' : 'Connect'}
                        </Text>
                      </TouchableOpacity>
                    </View>
          ))}
          </>  
          )}
          {selectedDevice && isConnected && (
            <>
              <View
                      style={{
                        flexDirection: 'row',
                        justifyContent: 'space-between',
                        margin: 5,
                      }}>
                      <View style={styles.deviceItem}>
                        <Text style={styles.deviceName}>{selectedDevice.name}</Text>
                        <Text style={styles.deviceInfo}>{selectedDevice.id}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() =>
                          isConnected
                            ?  disconnect()
                            :  connectToDevice(selectedDevice)
                        }
                        style={styles.deviceButton}>
                        <Text
                          style={styles.scanButtonText}>
                          {isConnected ? 'Disconnect' : 'Connect'}
                        </Text>
                      </TouchableOpacity>
                    </View>

          <View
              style={{
                flexDirection: 'row',
                justifyContent: 'space-between',
                margin: 5,
              }}>        
              <TextInput
                style={{
                  backgroundColor: '#888888',
                  margin: 2,
                  borderRadius: 15,
                  flex:3,
                  }}
                placeholder="Enter a message"
                value={messageToSend}
                onChangeText={(text) => setMessageToSend(text )}
              />
              <TouchableOpacity
                        onPress={() => sendMessage()
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={[
                            styles.scanButtonText,
                          ]}>
                          SEND
                        </Text>
                      </TouchableOpacity>
        </View>
              <Text>Received Message:</Text>
              <TextInput
              editable = {false}
              multiline
              numberOfLines={20}
              maxLength={100}
                style={{
                  backgroundColor: '#333333',
                  margin: 10,
                  borderRadius: 2,
                  borderWidth: 1,
                  borderColor: '#EEEEEE',
                  textAlignVertical: 'top',
                  }} >
                    {receivedMessage}
              </TextInput>
              
            </>
          )}
        </ScrollView>
      </View>
    );

Résultat

Comme l’appairage n’est pas gérer par l’application, il faut appairer l’ESP32 avant l’utilisation de l’application. Une fois le code chargé sur l’ESP32, vous pouvez lancer l’application sur le téléphone à l’aide de la commande

npx react-native start

Code complet de l’application React Native

/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* https://github.com/innoveit/react-native-ble-manager
* https://blog.logrocket.com/using-react-native-ble-manager-mobile-app/
*/



import React, {useState, useEffect} from 'react';
import {   
  StyleSheet,
  Dimensions,
  View, 
  ScrollView, 
  Text,
  TextInput,
  PermissionsAndroid,
  TouchableOpacity,
  Platform} from 'react-native';
import BleManager,{Peripheral} from 'react-native-ble-manager';
import { Buffer } from 'buffer';

let serviceid="4fafc201-1fb5-459e-8fcc-c5c9c331914b";
let caracid="beb5483e-36e1-4688-b7f5-ea07361b26a8";

const BluetoothBLETerminal = () =>  {
 const [devices, setDevices] = useState<any[]>([]);
 const [paired, setPaired] = useState<any[]>([]);
 const [selectedDevice, setSelectedDevice] = useState<Peripheral>();
 const [messageToSend, setMessageToSend] = useState("");
 const [receivedMessage, setReceivedMessage] = useState("");
 const [isConnected, setIsConnected] = useState(false);
 const [intervalId, setIntervalId] = useState<NodeJS.Timer>();
 const [isScanning, setIsScanning] = useState(false);

 const checkBluetoothEnabled = async () => {
   try {
         // turn on bluetooth if it is not on
   BleManager.enableBluetooth().then(() => {
     console.log('Bluetooth is turned on!');
   });
     
   } catch (error) {
     console.error('BLE is not available on this device.');
   }
 }
 
 const startScan = () => {
  if (!isScanning) {
    BleManager.scan([], 5, true)
      .then(() => {
        console.log('Scanning...');
        setIsScanning(true);
      })
      .catch(error => {
        console.error(error);
      });
  }
};

 const startDeviceDiscovery = async () => {

  BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => {
    // Each peripheral in returned array will have id and name properties
    console.log("Bonded peripherals: " + bondedPeripheralsArray.length);
    setPaired(bondedPeripheralsArray);
  });

  /*BleManager.getDiscoveredPeripherals().then((peripheralsArray) => {
    // Success code
    console.log("Discovered peripherals: " + peripheralsArray.length);
  });*/
 }

 const connectToDevice = async (device: Peripheral) => {
 BleManager.connect(device.id)
     .then(() => {
     // Success code
     console.log("Connected");
     setSelectedDevice(device);
     setIsConnected(true);
     BleManager.retrieveServices(device.id).then(
       (deviceInfo) => {
       // Success code
       console.log("Device info:", deviceInfo);
       }
     );


     })
     .catch((error) => {
     // Failure code
     console.log(error);
     });
 }


const sendMessage = async () => {
 if(selectedDevice && isConnected){
   try {

    const buffer = Buffer.from(messageToSend);
    BleManager.write(
      selectedDevice.id,
      serviceid,
      caracid,
      buffer.toJSON().data
    ).then(() => {
      // Success code
      console.log("Write: " + buffer.toJSON().data);
    })
    .catch((error) => {
      // Failure code
      console.log(error);
    });
     
   } catch (error) {
     console.error('Error sending message:', error);
   }
 }
}


const readData = async () => {  
 if (selectedDevice && isConnected) {
    BleManager.read(
      selectedDevice.id,
      serviceid,
      caracid
    )
      .then((readData) => {
        // Success code
        console.log("Read: " + readData);
        const message = Buffer.from(readData);
        //const sensorData = buffer.readUInt8(1, true);
        if(receivedMessage.length>300){
          setReceivedMessage("");
        }
        setReceivedMessage(receivedMessage => receivedMessage + message +"\n" );
        console.log("receivedMessage length",receivedMessage.length)
      })
      .catch((error) => {
        // Failure code
        console.log("Error reading message:",error);
      });
 }
}

/*useEffect(() => {
 let intervalId: string | number | NodeJS.Timer | undefined;
 if (selectedDevice && isConnected) {
   intervalId = setInterval(() => readData(), 100);
   setIntervalId(intervalId);
 }
 return () => {
   clearInterval(intervalId);
 };
}, [isConnected,selectedDevice]);*/

 // disconnect from device
 const disconnectFromDevice = (device: Peripheral) => {
   BleManager.disconnect(device.id)
   .then(() => {
        setSelectedDevice(undefined);
        setIsConnected(false);
        setReceivedMessage("");
        clearInterval(intervalId);
        console.log("Disconnected from device");
   })
   .catch((error) => {
     // Failure code
     console.log("Error disconnecting:",error);
   });
   
   /*BleManager.removeBond(peripheral.id)
     .then(() => {
       peripheral.connected = false;
       peripherals.set(peripheral.id, peripheral);
       setConnectedDevices(Array.from(peripherals.values()));
       setDiscoveredDevices(Array.from(peripherals.values()));
       Alert.alert(`Disconnected from ${peripheral.name}`);
     })
     .catch(() => {
       console.log('fail to remove the bond');
     });*/



 };
 
 
 useEffect(() => {

    checkBluetoothEnabled();

    if (Platform.OS === 'android' && Platform.Version >= 23) {
  
        PermissionsAndroid.requestMultiple([
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
        ]).then(result => {
          if (
            (result['android.permission.BLUETOOTH_SCAN'] &&
            result['android.permission.BLUETOOTH_CONNECT'] &&
            result['android.permission.ACCESS_FINE_LOCATION'] === 'granted')
            ||
            (result['android.permission.BLUETOOTH_SCAN'] &&
            result['android.permission.BLUETOOTH_CONNECT'] &&
            result['android.permission.ACCESS_FINE_LOCATION'] === 'never_ask_again')
          ) {
            console.log('User accepted');
          } else {
            console.log('User refused');        }
        });

    }

    BleManager.start({showAlert: false}).then(() => {
      console.log('BleManager initialized');
      startDeviceDiscovery();
    }).catch((error) => {
      // Failure code
      console.log("Error requesting permission:",error);
    });
   
BleManager.checkState().then((state) =>
   console.log(`current BLE state = '${state}'.`)
 );

 BleManager.getConnectedPeripherals([]).then((peripheralsArray) => {
   // Success code
   console.log("Connected peripherals: " + peripheralsArray.length);
 });

 BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => {
   // Each peripheral in returned array will have id and name properties
   console.log("Bonded peripherals: " + bondedPeripheralsArray.length);
   //setBoundedDevices(bondedPeripheralsArray);
 });

 BleManager.getDiscoveredPeripherals().then((peripheralsArray) => {
   // Success code
   console.log("Discovered peripherals: " + peripheralsArray.length);
 });

 /*let stopDiscoverListener = BleManagerEmitter.addListener(
   'BleManagerDiscoverPeripheral',
   peripheral => {
     peripherals.set(peripheral.id, peripheral);
   },
 );*/

 /*let stopConnectListener = BleManagerEmitter.addListener(
   'BleManagerConnectPeripheral',
   peripheral => {
     console.log('BleManagerConnectPeripheral:', peripheral);
     peripherals.set(peripheral.id, peripheral);
     setConnectedDevices(Array.from(peripherals.values()));
   },
 );*/

 /*let stopScanListener = BleManagerEmitter.addListener(
   'BleManagerStopScan',
   () => {
     setIsScanning(false);
     console.log('scan stopped');
     BleManager.getDiscoveredPeripherals().then((peripheralsArray) => {
       // Success code
       console.log("Discovered peripherals: " + peripheralsArray.length);
       for (let i = 0; i < peripheralsArray.length; i++) {
         let peripheral = peripheralsArray[i];
         console.log("item:", peripheral);
         //peripheral.connected = true;
         peripherals.set(peripheral.id, peripheral);
         setDiscoveredDevices(peripheralsArray);
       }

       
     });
   },
 );*/

 return () => {
   /*stopDiscoverListener.remove();
   stopConnectListener.remove();
   stopScanListener.remove();*/
 };
 }, [])


   return (
     <View style={[styles.mainBody]}>
     <Text
           style={{
             fontSize: 30,
             textAlign: 'center',
             borderBottomWidth: 1,
           }}>
           AC BLE Terminal
         </Text>
       <ScrollView>

         {!isConnected && (
         <>

        <TouchableOpacity
                       onPress={() => startDeviceDiscovery()
                       }
                       style={[styles.deviceButton]}>
                       <Text
                         style={[
                           styles.scanButtonText,
                         ]}>
                         SCAN
                       </Text>
              </TouchableOpacity>

         {/*
         <Text>Available Devices:</Text>
         {devices.map((device) => (
           <Button
             key={device.id}
             title={device.name || 'Unnamed Device'}
             onPress={() => this.connectToDevice(device)}
           />
         ))}
         */}
         <Text>Paired Devices:</Text>
         {paired.map((pair,i) => (
                     <View key={i}
                     style={{
                       flexDirection: 'row',
                       justifyContent: 'space-between',
                       marginBottom: 2,
                     }}>
                     <View style={styles.deviceItem}>
                       <Text style={styles.deviceName}>{pair.name}</Text>
                       <Text style={styles.deviceInfo}>{pair.id}, rssi: {pair.rssi}</Text>
                     </View>
                     <TouchableOpacity
                       onPress={() =>
                         isConnected
                           ?  disconnectFromDevice(pair)
                           :  connectToDevice(pair)
                       }
                       style={styles.deviceButton}>
                       <Text
                         style={[
                           styles.scanButtonText,
                           {fontWeight: 'bold', fontSize: 12},
                         ]}>
                         {isConnected ? 'Disconnect' : 'Connect'}
                       </Text>
                     </TouchableOpacity>
                   </View>
         ))}
         </>  
         )}
         {selectedDevice && isConnected && (
           <>
             <View
                     style={{
                       flexDirection: 'row',
                       justifyContent: 'space-between',
                       margin: 5,
                     }}>
                     <View style={styles.deviceItem}>
                       <Text style={styles.deviceName}>{selectedDevice.name}</Text>
                       <Text style={styles.deviceInfo}>{selectedDevice.id}, rssi: {selectedDevice.rssi}</Text>
                     </View>
                     <TouchableOpacity
                       onPress={() =>
                         isConnected
                           ?  disconnectFromDevice(selectedDevice)
                           :  connectToDevice(selectedDevice)
                       }
                       style={styles.deviceButton}>
                       <Text
                         style={styles.scanButtonText}>
                         {isConnected ? 'Disconnect' : 'Connect'}
                       </Text>
                     </TouchableOpacity>
                   </View>

         <View
             style={{
               flexDirection: 'row',
               justifyContent: 'space-between',
               margin: 5,
             }}>        
             <TextInput
               style={{
                 backgroundColor: '#888888',
                 margin: 2,
                 borderRadius: 15,
                 flex:3,
                 }}
               placeholder="Enter a message"
               value={messageToSend}
               onChangeText={(text) => setMessageToSend(text)}
             />
             <TouchableOpacity
                       onPress={() => sendMessage()
                       }
                       style={[styles.sendButton]}>
                       <Text
                         style={[
                           styles.scanButtonText,
                         ]}>
                         SEND
                       </Text>
                     </TouchableOpacity>
       </View>
       <View
             style={{
               flexDirection: 'row',
               justifyContent: 'space-between',
               margin: 5,
             }}>
             <Text style={{textAlignVertical: 'center'}}>Received Message:</Text>
             <TouchableOpacity
                       onPress={() => readData()
                       }
                       style={[styles.deviceButton]}>
                       <Text
                         style={[
                           styles.scanButtonText,
                         ]}>
                         READ
                       </Text>
              </TouchableOpacity>
        </View>
             <TextInput
             editable = {false}
             multiline
             numberOfLines={20}
             maxLength={300}
               style={{
                 backgroundColor: '#333333',
                 margin: 10,
                 borderRadius: 2,
                 borderWidth: 1,
                 borderColor: '#EEEEEE',
                 textAlignVertical: 'top',
                 }} >
                   {receivedMessage}
             </TextInput>
             
           </>
         )}
       </ScrollView>
     </View>
   );

};//end of component

//https://medium.com/supercharges-mobile-product-guide/reactive-styles-in-react-native-79a41fbdc404
export const theme = {
  smallPhone: 0,
  phone: 290,
  tablet: 750,
  }

const windowHeight = Dimensions.get('window').height;
const styles = StyleSheet.create({
 mainBody: {
   flex: 1,
   justifyContent: 'center',
   height: windowHeight,

   ...Platform.select ({
    ios: {
      fontFamily: "Arial",
    },
    
    android: {
      fontFamily: "Roboto",

    },
  }),
 },

 scanButtonText: {
   color: 'white',
   fontWeight: 'bold',
   fontSize: 12,
   textAlign: 'center',
 },
 noDevicesText: {
   textAlign: 'center',
   marginTop: 10,
   fontStyle: 'italic',
 },
 deviceItem: {
   marginBottom: 2,
 },
 deviceName: {
   fontSize: 14,
   fontWeight: 'bold',
 },
 deviceInfo: {
   fontSize: 8,
 },
 deviceButton: {
   backgroundColor: '#2196F3',
   padding: 10,
   borderRadius: 10,
   margin: 2,
   paddingHorizontal: 20,
 },
 sendButton: {
  backgroundColor: '#2196F3',
  padding: 15,
  borderRadius: 15,
  margin: 2,
  paddingHorizontal: 20,
 },
});

export default BluetoothBLETerminal;

Sources