fbpixel
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

Afficher un Stream Vidéo Motion sur React Native

Afficher un Stream Vidéo Motion sur React Native

Nous allons voir dans ce tutoriel comment récupérer un flux vidéo Motion sur une application React Native.

Configuration du projet

Nous avons mis en place un stream vidéo avec Motion sur une machine Linux dont l’adresse est 192.168.1.92:8554 sur le réseau Wifi.

Le programme Motion sert les streams vidéo sous forme de page HTML avec le protocol HTTP. Pour récupérer cette page nous utilisons la librairie react-native-webview qui permet d’afficher des pages web complètes dans une application.

Pour installer la librairie, entrez la commande suivante

npm install --save react-native-webview

Utilisation de WebView

Pour utiliser le composant WebView, il nous suffit d’importer le composant

import {WebView} from 'react-native-webview'

et d’appeler le composant dans le rendu du composant fonctionnel App et de préciser la source de la vidéo

      <View style={{flex: 1 }}>

            <WebView
              source={{uri: "http://192.168.1.92:8554/"}}
              style={styles.webVideo} />

      </View>

Avec le style sheet suivant

var styles = StyleSheet.create({
  webVideo: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
});

Pour aller plus loin, nous avons créé une encart textInput qui nous permet de mettre à jour l’url du stream (ou de la page web) avec les états ipAddress et ipServer. Vous pouvez retrouver les détails dans le code complet.

La vidéo s’affiche avec un délai inférieur à 3secondes et une qualité qui n’est pas très fluide.

Code complet de l’application React Native pour lire un flux vidéo Motion

Dans le code suivant nous définissons textInput qui va nous permettre de mettre à jour la valeur de ipAddress (valeur d’ip stockée) et lorsqu’on appuit sur le bouton, mettre à jour l’url lu par le composant WebView (ipServer)

Nous avons aussi rajouter l’affichage de la date et de l’heure (currentDate) en temps réel pour évaluer le délai de réception du flux vidéo. Nous avons commenté cette fonction car elle rafraichit l’application toute les secondes.

/**
 * https://github.com/react-native-webview/react-native-webview
 */
import React, { useEffect, useState } from 'react'
import { Text, View, StyleSheet, TextInput, TouchableOpacity } from 'react-native'
import {WebView} from 'react-native-webview'

const App = () =>  {
	const videoRef = React.createRef();
  const [ipAddress, setIpAddress] = useState("http://192.168.1.92:8554/");
  const [ipServer, setIpServer] = useState("http://192.168.1.92:8554/");
  //const [currentDate, setCurrentDate] = useState('');

  useEffect(() => {

    /*setInterval(() => {
      var date = new Date().getDate(); //Current Date
      var month = new Date().getMonth() + 1; //Current Month
      var year = new Date().getFullYear(); //Current Year
      var hours = new Date().getHours(); //Current Hours
      var min = new Date().getMinutes(); //Current Minutes
      var sec = new Date().getSeconds(); //Current Seconds
      setCurrentDate(
        date + '/' + month + '/' + year + ' ' + hours + ':' + min + ':' + sec
      );
    }, 1000)*/


	}, []);

  console.log("rendering...");

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

      <View
              style={styles.inputBar}>        
              <TextInput
                style={styles.textInput}
                placeholder="Enter a message"
                value={ipAddress}
                onChangeText={(text) =>    setIpAddress(text)
                }
              />
              <TouchableOpacity
                        onPress={() =>  {
                          setIpServer(""); //force state change thus rendering
                          setIpServer(ipAddress);}
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={styles.buttonText}>
                          SEND
                        </Text>
                      </TouchableOpacity>
        </View>
      
      <View style={{flex: 1 }}>

            <WebView
              source={{uri: ipServer}}
              style={styles.webVideo} />

      </View>
      {/*<Text style={{textAlign: 'right'}}>{currentDate}</Text>*/}
    </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,
  },

  webVideo: {
    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,
  },
});

Sources

Vidéo stream avec Motion sur Raspberry Pi

Vidéo stream avec Motion sur Raspberry Pi

Nous allons voir dans ce tutoriel comment mettre en place un stream vidéo avec Motion à partir d’un Raspberry Pi. Ce tutoriel reste compatible avec tout système Linux.

Matériel

  • Raspberry Pi (ou autre machine linux)
  • Caméra USB ou Caméra CSI

Description de Motion

Le programme Motion a été développé comme système de surveillance. Il permet d’observer plusieurs caméras, détecter les mouvements sur chaque caméra et enregistrer des images ou des vidéos à ce moment là.

Cet outil permet donc

  • streamer les vidéos pour être affichées à distance
  • détecter des mouvements sur chacune des caméra
  • enregistrer des images ou des vidéos à partir des caméras

Installation de motion

Si ce n’est pas déjà le cas sur votre version d’OS, vous pouvez installer Motion à l’aide de la commande suivante.

sudo apt-get install motion

Vérification de l’installation

motion --version

Si une erreur apparait à propose de la permission d’accès au fichier /var/log/motion/motion.log

Vous pouvez supprimer le fichier puis redémarrer

rm -r /var/log/motion

ou accorder les droits d’accès

sudo chmod -R 777 /var/log/motion
sudo chmod -R 777 /var/lib/motion

ou créer un autre fichier de log et ajouter son chemin d’accès dan le fichier motion.conf

sudo nano ~/.motion/motion.log

Configuration de motion

Toute la configuration de motion se fait dans le fichier /etc/motion/motion.conf. Vous pouvez directement modifier le fichier de configuration après avoir fait une copie en sauvegarde

sudo cp /etc/motion/motion.conf /etc/motion/motion.conf.bckp
sudo nano /etc/motion/motion.conf

Dans les principaux paramètres, on retrouve

  • video_device pour définir la caméra à utiliser
  • paramètres de la capture (height, width, framerate)
  • texte à afficher sur l’image de la caméra (text_left, text_right, etc.)
  • paramètres de la détection de mouvement (threshold, event_gap, etc.)
  • le fichier log_file pour écrire les log (défaut: /var/log/motion/motion.log)
  • le répertoire target_dir pour enregistrer les images et vidéos (défaut; /var/lib/motion)
  • picture_output pour capturer des images à la détection
  • video_output pour enregistrer des vidéos

N.B.: vous pouvez retrouver tous les paramètres de configuration sur le site officiel

Allez plus loin dans la configuration

Il existe un grand nombre de paramètre dans le fichier motion.conf qui vont vous permettre d’adapter l’acquisition d’image à votre besoin

Si plusieurs caméra sont utilisées, vous pouvez définir un jeu de paramètre pour chaque caméra (/etc/motion/camera1.conf)

Démarrer Motion depuis le terminal

sudo motion
sudo motion -c ~/.motion/motion.conf #to use another configuration file
sudo killall motion #to end the process

(utilise ~/.motion/motion.conf par défaut, s’il existe, sinon /etc/motion/motion.conf)

Démarrer Motion comme service

sudo service motion start
sudo service motion status 
sudo service motion restart
sudo service motion stop

N.B.: Dans le fichier /etc/motion/motion.conf, laisser l’option daemon à OFF

Créer un stream vidéo avec Motion

Dans le fichier /etc/motion/motion.conf, modifier les paramètres par défaut

webcontrol_port
 8080 # define http port for camera web control http://<ip_server>:<webcontrol_port>
webcontrol_localhost on # define if accessible only from the same machine
stream_port 8081 # define http port for camera display http://<ip_server>:<stream_port>
stream_localhost off
stream_auth_method
 0
stream_authentication <username>:<password>

N.B.: si vous activer le webcontrol depuis l’extérieur de la machine, il est fortement conseillé d’activer l’authentification sur le stream

N’oubliez pas de redémarrer le service motion après toute modification du fichier motion.conf

sudo service motion restart

Si vous entrez l’adresse IP de votre machine Linux (ici, 192.168.1.92), suivie du numéro de port défini dans stream_port, vous pouvez observer le stream vidéo de motion.

Si vous n’utilisez pas motion en continu, il est possible d’enregistrer une vidéo sans attendre une détection de mouvement (emulate_motion on, event_gap -1)

Tips: vous pouvez définir une image de mauvaise qualité pour améliorer la bande passante du stream (stream_quality 45) mais conserver une image de bonne qualité pour l’enregistrement de la vidéo (video_quality 100).

Fichier motion.conf pour le streaming

/etc/motion/motion.conf (avec webcontrol local et ports 8553 et 8554)

# Rename this distribution example file to motion.conf
#
# This config file was generated by motion 4.3.2
# Documentation:  /usr/share/doc/motion/motion_guide.html
#
# This file contains only the basic configuration options to get a
# system working.  There are many more options available.  Please
# consult the documentation for the complete list of all options.
#

############################################################
# System control configuration parameters
############################################################

# Start in daemon (background) mode and release terminal.
daemon off

# Start in Setup-Mode, daemon disabled.
setup_mode off

# File to store the process ID.
; pid_file value

# File to write logs messages into.  If not defined stderr and syslog is used.
log_file /var/log/motion/motion.log

# Level of log messages [1..9] (EMG, ALR, CRT, ERR, WRN, NTC, INF, DBG, ALL).
log_level 6

# Target directory for pictures, snapshots and movies
target_dir /var/lib/motion

# Video device (e.g. /dev/video0) to be used for capturing.
videodevice /dev/video0

# Parameters to control video device.  See motion_guide.html
; vid_control_params value

# The full URL of the network camera stream.
; netcam_url value

# Name of mmal camera (e.g. vc.ril.camera for pi camera).
; mmalcam_name value

# Camera control parameters (see raspivid/raspistill tool documentation)
; mmalcam_control_params value

############################################################
# Image Processing configuration parameters
############################################################

# Image width in pixels.
width 640

# Image height in pixels.
height 480

# Maximum number of frames to be captured per second.
framerate 10

# Text to be overlayed in the lower left corner of images
text_left CAMERA1

# Text to be overlayed in the lower right corner of images.
text_right %Y-%m-%d\n%T-%q - %{fps}

text_scale 3
############################################################
# Motion detection configuration parameters
############################################################

# Always save pictures and movies even if there was no motion.
emulate_motion off

# Threshold for number of changed pixels that triggers motion.
threshold 1500

# Noise threshold for the motion detection.
; noise_level 32

# Despeckle the image using (E/e)rode or (D/d)ilate or (l)abel.
despeckle_filter EedDl

# Number of images that must contain motion to trigger an event.
minimum_motion_frames 1

# Gap in seconds of no motion detected that triggers the end of an event.
event_gap 60

# The number of pre-captured (buffered) pictures from before motion.
pre_capture 3

# Number of frames to capture after motion is no longer detected.
post_capture 0

############################################################
# Script execution configuration parameters
############################################################

# Command to be executed when an event starts.
; on_event_start value

# Command to be executed when an event ends.
; on_event_end value

# Command to be executed when a movie file is closed.
; on_movie_end value

############################################################
# Picture output configuration parameters
############################################################

# Output pictures when motion is detected
picture_output off

# File name(without extension) for pictures relative to target directory
picture_filename %Y%m%d%H%M%S-%q

############################################################
# Movie output configuration parameters
############################################################

# Create movies of motion events.
movie_output on

# Maximum length of movie in seconds.
movie_max_time 60

# The encoding quality of the movie. (0=use bitrate. 1=worst quality, 100=best)
movie_quality 100

# Container/Codec to used for the movie. See motion_guide.html
movie_codec mkv

# File name(without extension) for movies relative to target directory
movie_filename %t-%v-%Y%m%d%H%M%S

############################################################
# Webcontrol configuration parameters
############################################################

# Port number used for the webcontrol.
webcontrol_port 8553

# Restrict webcontrol connections to the localhost.
webcontrol_localhost on

# Type of configuration options to allow via the webcontrol.
webcontrol_parms 0

############################################################
# Live stream configuration parameters
############################################################

# The port number for the live stream.
stream_port 8554

# Restrict stream connections to the localhost.
stream_localhost off

stream_quality 100

##############################################################
# Camera config files - One for each camera.
##############################################################
; camera /usr/etc/motion/camera1.conf
; camera /usr/etc/motion/camera2.conf
; camera /usr/etc/motion/camera3.conf
; camera /usr/etc/motion/camera4.conf

##############################################################
# Directory to read '.conf' files for cameras.
##############################################################
; camera_dir /usr/etc/motion/conf.d

Applications

  • Créer un système de vidéo surveillance
  • Utilisation de MotionEye OS

Sources

Vidéo stream avec Gstreamer sur Raspberry Pi

Vidéo stream avec Gstreamer sur Raspberry Pi

Nous allons voir dans ce tutoriel comment streamer un flux vidéo à partir d’un Raspberry Pi avec Gstreamer. Un des outils de streaming les plus utilisé est FFMPEG. Nous testons ici, gstreamer car il y a moins de délai de transmission.

Précédent tutoriel de streaming : Flux vidéo entre deux machine avec FFMPEG

Matériel

  • Raspberry Pi avec Raspbian
  • USB Cam ou RPiCam

Installation de Gstreamer sur Raspberry Pi

Une partie de Gstreamer est installée par défaut sous Raspbian. Nous allons simplement compléter l’installation avec quelques librairies supplémentaires.

# install a missing dependency
$ sudo apt-get install libx264-dev libjpeg-dev
# install the remaining plugins
$ sudo apt-get install libgstreamer1.0-dev \
     libgstreamer-plugins-base1.0-dev \
     libgstreamer-plugins-bad1.0-dev \
     gstreamer1.0-plugins-ugly \
     gstreamer1.0-tools \
     gstreamer1.0-gl \
     gstreamer1.0-gtk3
# if you have Qt5 install this plugin
$ sudo apt-get install gstreamer1.0-qt5
# install if you want to work with audio
$ sudo apt-get install gstreamer1.0-pulseaudio

Pour tester l’installation, entrer la commande suivante

gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink

Trouver les appareils vidéo et audio disponibles

gst-device-monitor-1.0

Stream vidéo avec Gstreamer

Une fois le nom de l’appareil obtenu, vous pouvez commencer le stream avec la commande

gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw, width=640, height=480, framerate=30/1 ! videoconvert ! videoscale ! clockoverlay time-format="%D %H:%M:%S" ! autovideosink

Nous envoyons le flux vidéo avec le protocole UDP depuis un Raspberry Pi

gst-launch-1.0 v4l2src device=/dev/video0 num-buffers=-1 ! video/x-raw, width=640, height=480, framerate=30/1 ! clockoverlay time-format="%D %H:%M:%S" ! videoconvert ! jpegenc ! udpsink host=192.168.1.70 port=5200

Nous recevons le stream UDP depuis le client (192.168.1.70) à l’aide de la commande

gst-launch-1.0 udpsrc port=5200 ! jpegdec ! videoconvert ! autovideosink

Pour enregistrer le flux vidéo dans un fichier

gst-launch-1.0 -e v4l2src device=/dev/video0 num-buffers=-1 ! 'image/jpeg,framerate=30/1,width=640,height=480' ! queue ! avimux ! filesink location=video.avi

Installation de Gstreamer sur Windows

Pour recevoir le flux vidéo depuis le Raspberry Pi, nous utilisons un ordinateur Windows. Vous pouvez utiliser un autre appareil (RPi, Android, iOS, macOS).

Pour information, l’installation de Gstreamer sous Windows se fait à l’aide d’un installateur msi disponible sur le site.

Après avoir téléchargé et installé Gstreamer, vous devez rajouter le dossier C:\gstreamer\1.0\msvc_x86_64\bin à la variable d’environnement Path.

Pour tester l’installation, entrer la commande suivante

gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink

Pour streamer, une vidéo à partir de la caméra

gst-launch-1.0 mfvideosrc ! video/x-raw, width=1280, height=720, framerate=30/1 ! videoconvert ! videoscale ! clockoverlay time-format="%D %H:%M:%S" ! video/x-raw, width=640, height=360 ! autovideosink

Il est possible de lancer un stream avec le protocole UDP

gst-launch-1.0 mfvideosrc ! video/x-raw, width=1280, height=720, framerate=30/1 ! videoconvert ! videoscale ! clockoverlay time-format="%D %H:%M:%S" ! video/x-raw, width=640, height=360 ! jpegenc ! udpsink host=192.168.1.92 port=5200

Nous recevons le stream UDP depuis le client(192.168.1.92) à l’aide de la commande

gst-launch-1.0 udpsrc port=5200 ! jpegdec ! videoconvert ! autovideosink

Installation de Gstreamer sur Ubuntu ou Debian

Afin d’utiliser Gstreamer dans les meilleurs conditions nous auron besoin de la librairie v4l2

sudo apt install v4l-utils

Dans un terminal, entrée les commandes suivantes pour installer gstreamer.

sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

Pour tester l’installation, entrer la commande suivante

gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink

Afin de lancer le stream video depuis Ubuntu, vous pouvez lancer la commande suivante

gst-launch-1.0 v4l2src name="/dev_video0" ! decodebin ! videoconvert ! videoscale ! clockoverlay time-format="%D %H:%M:%S" ! video/x-raw,format=RGB ! queue ! videoconvert ! 

N.B.: n’oubliez pas d’ajouter decodebin

Il est possible de lancer un stream avec le protocole UDP

gst-launch-1.0 v4l2src name="/dev_video0" num-buffers=-1 ! decodebin ! videoconvert ! videoscale ! clockoverlay time-format="%D %H:%M:%S" ! video/x-raw,format=RGB ! queue ! videoconvert ! jpegenc ! udpsink host=192.168.1.70 port=5200

Nous recevons le stream UDP depuis le client (192.168.1.70) à l’aide de la commande

gst-launch-1.0 udpsrc port=5200 ! jpegdec ! videoconvert ! autovideosink

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 commandes 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

Afficher une Vidéo sur une App React Native

Afficher une Vidéo sur une App React Native

Nous allons voir dans ce tutoriel comment intégrer une vidéo dans une application React Native. Pour cela nous allons créer un stream vidéo à partir d’un ordinateur et récupérer le signal vidéo sur l’appareil Android.

Configuration du projet React Native

Pour intégrer une vidéo à l’application, nous utilisons la librairie react-native-video qui permet de lire des vidéo locales et distantes.

Pour ajouter la librairie à votre projet React Native, entrez la commande suivante

npm install --save react-native-video

N.B.: il est aussi possible d’utiliser la librairie react-native-live-stream

Code principal du composant Video

Pour utiliser la librairie, nous commençons par l’importer dans le code principal avec les autres paquets nécessaires

import React from 'react'
import { View, StyleSheet } from 'react-native'
import Video from 'react-native-video';

Lecture d’une vidéo locale

source={require(<PATH_TO_VIDEO>)}

Pour lire une vidéo distante

source={{uri: '<VIDEO_REMOTE_URL>'}}
/**
 * https://www.npmjs.com/package/react-native-live-stream
 * https://github.com/react-native-video/react-native-video
 * https://www.npmjs.com/package/react-native-video
 */
import React, { useEffect } from 'react'
import { Text, View, StyleSheet } from 'react-native'
import Video from 'react-native-video';

const App = () =>  {
	const videoRef = React.createRef();

  useEffect(() => {
		// videoRef.current.

		//console.log(videoRef.current);
	}, []);

  return (
    <View style={{ flexGrow: 1, flex: 1 }}>
      <Text style={styles.mainTitle}>AC Video Player</Text>
      <View style={{flex: 1 }}>
        <Video 
            //source={require("PATH_TO_FILE")} //local file
            source={{uri: 'https://www.aranacorp.com/wp-content/uploads/rovy-avoiding-obstacles.mp4'}} //distant file
            //source={{uri: "http://192.168.1.70:8554"}}
            ref={videoRef}                                      // Store reference
            repeat={true}
            //resizeMode={"cover"}
            onLoadStart={(data) => {
              console.log(data);
            }}
            onError={(err) => {
              console.error(err);
            }}
            onSeek={(data) => {
              console.log(`seeked data `, data);
            }}
            onBuffer={(data) => {
              console.log('buffer data is ', data);
            }}               // Callback when video cannot be loaded
            style={styles.backgroundVideo} />
      </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: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
});

Mise en place du stream video avec ffmpeg

Pour tester notre application, nous utilisons l’outil ffmpeg pour créer un stream vidéo à partir d’un ordinateur connecté au même réseau wifi que l’appareil. L’ordinateur joue le role d’émetteur (le serveur) et l’application joue le rôle du récepteur (client).

Nous allons récupérer l’adresse IP du serveur (ipconfig ou ifconfig) ici 192.168.1.70 et nous allons émettre le fllux vidéo sur le port 8554

  • côté serveur linux
ffmpeg -re -f v4l2 -i /dev/video0 -r 10 -f mpegts http://192.168.1.70:8554
  • côté serveur windows
ffmpeg -f dshow -rtbufsize 700M -i video="USB2.0 HD UVC WebCam" -listen 1 -f mpegts -codec:v mpeg1video -s 640x480 -b:v 1000k -bf 0 http://192.168.1.70:8554

Pour lire le flux vidéo côté client, nous changeons l’adresse de la source vidéo pour spécifier

  • le protocole HTTP
  • l’adresse IP du serveur 192.168.1.70
  • le port utilisé 8554
source={{uri: "http://192.168.1.70:8554"}}

N.B.: N’oubliez pas de passer l’option repeat à false (repeat={false})

Sources

Créer un menu de navigation avec React Native

Créer un menu de navigation avec React Native

Nous allons voir dans ce tutoriel comment mettre en place un menu de navigation avec différents écrans dans React Native. Pour cela, nous allons utiliser la librairie React Navigation

N.B.: Une autre alternative react-native-navigation existe mais ne fonctionne pas avec mon environnement

Configuration du projet React Native

npm install @react-navigation/native
npm install @react-navigation/native-stack

Puis installez les paquets suivants

npm install --save react-native-screens react-native-safe-area-context

Pour utiliser la navigation, vous devez mettre à jour le fichier MainActivity.java

android/app/src/main/java/<your package name>/MainActivity.java

Ajouter import android.os.Bundle; au début du fichier

et la fonction onCreate dans la classe MainActivity

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(null);
  }

MainActivity.java

package com.menuapp;

import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;

import android.os.Bundle;

public class MainActivity extends ReactActivity {

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  @Override
  protected String getMainComponentName() {
    return "MenuApp";
  }

  /**
   * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
   * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
   * (aka React 18) with two boolean flags.
   */
  @Override
  protected ReactActivityDelegate createReactActivityDelegate() {
    return new DefaultReactActivityDelegate(
        this,
        getMainComponentName(),
        // If you opted-in for the New Architecture, we enable the Fabric Renderer.
        DefaultNewArchitectureEntryPoint.getFabricEnabled());
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(null);
  }
}

Code principal de l’application

Dans le code principal de l’application, index.js ou App.tsx, nous importons l’objet NavigationContainer. Pour l’utiliser, ils suffit d’entourer le code de rendu avec le tag correspondant. Dans cet exemple, nous définissons deux pages avec chacune un bouton permettant de se rendre à l’autre page.

import * as React from 'react';
import { View, Button, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

function DetailsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button
        title="Go to Details... again"
        onPress={() => navigation.push('Details')}
      />
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
      <Button title="Go back" onPress={() => navigation.goBack()} />
    </View>
  );
}
const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }}/>
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

N.B.: vous pouvez afficher ou non la barre de navigation avec l’option headerShown: false

Exemple: Création d’un menu

Dans cet exemple, nous allons créer une application simple pour afficher différents écrans. Ces écrans parte d’une même composant qui est adapté en fonction des informations contenus dans une liste.

Tout d’abord nous créons un array contenant toutes les informations dont nous avons besoin

const screensArr = [
  {
    id: 1,
    color: "blue",
    text: "Screen 1",
    items: ["item1","item2"]
  },
  {
    id: 2,
    color: "red",
    text: "Screen 2",
    items: ["item2","item3"]
  },
  {
    id: 3,
    color: "green",
    text: "Screen 3",
    items: ["item1","item2","item3"]
  },
];

Composant principal

Dans le composant principal, nous créons une liste d’écran Stack.Screen à partir de l’Array screensArr

const Stack = createNativeStackNavigator();

function App() {

  const screensListArr = screensArr.map(buttonInfo => (
    <Stack.Screen
    key={buttonInfo.id} 
    name={buttonInfo.text}
    component={DetailsScreen}
    options={{
      headerStyle: {
        backgroundColor: buttonInfo.color,
      },
      headerTintColor: '#fff',
      headerTitleStyle: {
        fontWeight: 'bold',
      },
    }}
    />
  ));

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }}/>
        {/*<Stack.Screen name="Details" component={DetailsScreen} />*/}
        {screensListArr}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

Écran Home

Nous créons ensuite l’écran principal qui contient les boutons permettant de naviguer vers les autres écrans

Sur cet écran, nous ajoutons un bouton pour chaque écran avec la fonction buttonListArr que nous personnalisons avec les données contenu dans screensArr.

function HomeScreen({ navigation }) {
  const menuItemClick = (buttonInfo: { id: any; color?: string; text: any; }) => {
    console.log(buttonInfo.id,' clicked :',buttonInfo.text);
    navigation.navigate(buttonInfo.text,buttonInfo); //'Details')
  };
  
  const buttonsListArr = screensArr.map(buttonInfo => (
    <Button 
    key={buttonInfo.id} 
    text={buttonInfo.text}
    onPress={() => menuItemClick(buttonInfo)}
    btnStyle={[styles.menuBtn,{backgroundColor:buttonInfo.color}]} 
    btnTextStyle={styles.menuText}
    />
  ));


  return (
      <ScrollView>
        <View style={styles.mainBody}>
        {buttonsListArr}
        </View>
      </ScrollView>

  );
}

N.B.: notez qu’il y a une seule fonction pour gérer plusieurs boutons menuItemClick(buttonInfo)

Écran Details

Enfin, nous créons la fonction DetailsScreen qui va contenir les informations à afficher pour chaque page.

Nous passons les paramètres de la page lors du l’appel du changement de page

navigation.navigate(buttonInfo.text,buttonInfo);
const buttonInfo = route.params;

function DetailsScreen({ route, navigation }) {
  const buttonInfo = route.params;

  const itemsListArr = buttonInfo.items.map(item => (
    <View key={item} style={{marginTop:10,marginBottom:10,flexDirection:'row', width: "100%", borderBottomWidth: 1,borderBottomColor:"white",}} >
      <Text style={{marginBottom:5,fontSize: 20,}}>{item}</Text>
    </View>
  ));

  return (
    <View style={{
      flex: 1,
      //justifyContent: 'center',
      alignItems: 'center',
      //minHeight: windowHeight,
      color: "white",
      backgroundColor: "black",
    }}>
      {itemsListArr}
      {/*<Button text="Go back" btnStyle={styles.deviceButton} btnTextStyle={styles.buttonText} onPress={() => navigation.goBack()} />*/}
    </View>
  );
}

Résultat

Lorsque l’application est compilée et chargée sur l’appareil, nous pouvons naviguer d’un écran à l’autre.

Code complet de l’exemple

App.tsx

/**
 * npm install --save react-native-navigation NOK
 * npm install @react-navigation/native
 * npm install @react-navigation/native-stack
 * https://reactnavigation.org/docs/getting-started
 */

import React from 'react';
import {   
  View, 
  Text,
  ScrollView,
} from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import Button from './src/ui_comp';
import {styles} from './src/styles/styles';

const screensArr = [
  {
    id: 1,
    color: "blue",
    text: "Screen 1",
    items: ["item1","item2"]
  },
  {
    id: 2,
    color: "red",
    text: "Screen 2",
    items: ["item2","item3"]
  },
  {
    id: 3,
    color: "green",
    text: "Screen 3",
    items: ["item1","item2","item3"]
  },
];

function HomeScreen({ navigation }) {
  const menuItemClick = (buttonInfo: { id: any; color?: string; text: any; }) => {
    console.log(buttonInfo.id,' clicked :',buttonInfo.text);
    navigation.navigate(buttonInfo.text,buttonInfo); //'Details')
  };


  
  const buttonsListArr = screensArr.map(buttonInfo => (
    <Button 
    key={buttonInfo.id} 
    text={buttonInfo.text}
    onPress={() => menuItemClick(buttonInfo)}
    btnStyle={[styles.menuBtn,{backgroundColor:buttonInfo.color}]} 
    btnTextStyle={styles.menuText}
    />
  ));


  return (
      <ScrollView>
        <View style={styles.mainBody}>
        {buttonsListArr}
        </View>
      </ScrollView>

  );
}

function DetailsScreen({ route, navigation }) {
  const buttonInfo = route.params;

  const itemsListArr = buttonInfo.items.map(item => (
    <View key={item} style={{marginTop:10,marginBottom:10,flexDirection:'row', width: "100%", borderBottomWidth: 1,borderBottomColor:"white",}} >
      <Text style={{marginBottom:5,fontSize: 20,}}>{item}</Text>
    </View>
  ));

  return (
    <View style={{
      flex: 1,
      //justifyContent: 'center',
      alignItems: 'center',
      //minHeight: windowHeight,
      color: "white",
      backgroundColor: "black",
    }}>
      {itemsListArr}
      {/*<Button text="Go back" btnStyle={styles.deviceButton} btnTextStyle={styles.buttonText} onPress={() => navigation.goBack()} />*/}
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {

  const screensListArr = screensArr.map(buttonInfo => (
    <Stack.Screen
    key={buttonInfo.id} 
    name={buttonInfo.text}
    component={DetailsScreen}
    options={{
      headerStyle: {
        backgroundColor: buttonInfo.color,
      },
      headerTintColor: '#fff',
      headerTitleStyle: {
        fontWeight: 'bold',
      },
    }}
    />
  ));

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }}/>
        {/*<Stack.Screen name="Details" component={DetailsScreen} />*/}
        {screensListArr}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

styles.jsx

/* ./src/styles/styles.jsx */
import {StyleSheet, Dimensions} from 'react-native';

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

const windowHeight = Dimensions.get('window').height;
const windowWidth = Dimensions.get('window').width;

export const styles = StyleSheet.create({
  mainBody: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    minHeight: windowHeight,
    color:TEXT_COLOR,
    backgroundColor: BACKGROUND_COLOR,
  },

  menuBtn: {
    backgroundColor: BUTTON_COLOR,
    paddingBottom: 10,
    paddingTop: 10,
    borderRadius: 10,
    margin: 10,
    //height: windowHeight/3,
    width: windowHeight/3,
    justifyContent: 'center', 
  },

  menuText: {
    color: TEXT_COLOR,
    fontWeight: 'bold',
    fontSize: 30,
	  textAlign: 'center',
  },


  buttonText: {
    color: TEXT_COLOR,
    fontWeight: 'bold',
    fontSize: 12,
	  textAlign: 'center',
    textAlignVertical: 'center',
  },
  
  noDevicesText: {
    color: TEXT_COLOR,
    textAlign: 'center',
    marginTop: 10,
    fontStyle: 'italic',
  },
  deviceItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 2,
  },
  deviceName: {
    color: TEXT_COLOR,
    fontSize: 14,
    fontWeight: 'bold',
  },
  deviceInfo: {
    color: TEXT_COLOR,
    fontSize: 10,
  },
  deviceButton: {
    backgroundColor: BUTTON_COLOR,
    padding: 10,
    borderRadius: 10,
    margin: 2,
    paddingHorizontal: 20,
    fontWeight: 'bold', 
    fontSize: 12,
  },

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

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

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

  textOutput:{
    backgroundColor: '#333333',
    margin: 10,
    borderRadius: 2,
    borderWidth: 1,
    borderColor: '#EEEEEE',
    textAlignVertical: 'top',
  }

});

Bonus: Afficher une Icone sur le bouton

Il est possible d’ajouter ou de remplacer le texte par une icône.

npm install react-native-vector-icons --save

Pour cela vous devez importer chaque banque d’icone. Trouver les icônes que vous souhaitez ajouter et dans quelle banque

import Icon from 'react-native-vector-icons/FontAwesome6';
import IonIcon from 'react-native-vector-icons/Ionicons'
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'

Vous pouvez ensuite placer l’objet Icon correspondant à la banque contenant l’icone

<Button>
    <Icon name="ice-cream" size={icon_size} color={'white'} />
</Button>

Sources

Communication UDP avec React Native

Communication UDP avec React Native

Dans ce tutoriel, nous allons mettre en place une communication avec le protocole UDP sur une application React Native. L’application React Native pourra se comporter comme Serveur ou comme Client UDP. Nous utilisons un ordinateur avec un script Python comme partenaire de communication.

Configuration de React Native

Premièrement, créer un projet React Native UDPTerminalApp

Installer les librairies udp et netinfo

npm install react-native-udp
npm install react-native-network-info

Description du code de l’application

Importation

Pour utiliser les librairies, nous importons tout d’abord leur code.

   import dgram from 'react-native-udp'
   import UdpSocket from 'react-native-udp/lib/types/UdpSocket.js';
   import { NetworkInfo } from 'react-native-network-info'

Composant principal

Puis nous créons le composant fonctionnel qui nous permettra de gérer la communication Serveur/Client UDP.

const UDPTerminal = () =>  {
  const [isServer, setIsServer] = useState(false);
  const [connectionStatus, setConnectionStatus] = useState('');
  const [socket, setSocket] = useState<UdpSocket>();
  const [ipAddress, setIpAddress] = useState('');
  const [ipServer, setIpServer] = React.useState('');
  const [messageText, setMessageText] = useState("");
  const [messageToSend, setMessageToSend] = useState("Hello from server");
  const [receivedMessage, setReceivedMessage] = useState("");
  • isServer est True si l’application se comporte comme un serveur et false si elle se comporte comme un client
  • connectionStatus affiche le status de la connexion
  • socket contient la variable socket de la communication
  • ipAddress contient l’adresse IP de l’appareil Android sur le réseau
  • ipServer contient l’adresse IP entrée sur l’interface
  • messageText contient le texte tapé sur l’interface
  • messageToSend contient le message à envoyer au client ou au serveur
  • receivedMessage contient les messages reçu depuis le client ou le serveur

Gestion de la communication UDP

Le code qui permet la gestion de la communication UDP, que ce soit en mode serveur ou client, est entièrement contenu dans un hook useEffect

  useEffect(() => {
    const fetchIpAddress = async () => {
      const ip = await NetworkInfo.getIPV4Address();
      setIpAddress(ip);
      console.log("ip adresses ; ", ip)
    };

    fetchIpAddress();

    if (isServer) {
      // Configure app as server
      const server = dgram.createSocket('udp4');

      server.on('message', (data, rinfo) => {
        
        server.send(messageToSend, undefined, undefined, rinfo?.port, rinfo?.address, (error) => {
          if (error) {
            console.log('Error sending message:', error);
          } else {
            console.log('Message sent correctly');
          }
        });
        console.log('Received message:', data.toString());
        setReceivedMessage(receivedMessage => receivedMessage+data.toString()+"\n")
      });

      server.on('listening', () => {
        console.log('Server listening on port:', server.address().port);
        setConnectionStatus(`Server listening on port ${server.address().port}`);
      });
      try{
        setReceivedMessage("");
        server.bind(8888);
        setSocket(server);
      }catch(error){
        console.log("error binding server",error);
      }
      
    } else {
      setConnectionStatus(`Server disconnected`);
      // Configure app as client
      const client = dgram.createSocket('udp4');
      try{
        setReceivedMessage("");
        client.bind(8887);
        setSocket(client);
      }catch(error){
        console.log("error binding client",error);
      }
      
    }

    return () => {
      socket && socket.close();
    };
  }, [isServer, messageToSend, messageText]);

La fonction fetchIpAdress permet de récupérer l’adresse IP de l’appareil.

En mode serveur

  • nous créons le socket dgram.createSocket
  • lorsqu’un message est reçu: nous envoyons messageToSend au client
  • Puis nous affichons le message reçu
  • nous lions le serveur au port 8888

En mode client

  • nous créons le socket dgram.createSocket
  • nous lions le client au port 8887

Dans la fonction suivante

  • nous envoyons le message messageToSend au serveur
  • puis nous attendons la réponse que nous affichons dans receivedMessage

Fonction envoyer message

Nous définissons également une fonction qui permet de mettre à jour le message envoyé en mode Server et d’envoyer le message en mode Client.

  const sendMessage = () => {
    setMessageToSend(messageText);
    if (isServer) return;

    const client = socket;
    console.log("send message to "+ipServer)
    client.send(messageToSend, undefined, undefined, 8888, ipServer, (error) => {
      if (error) {
        console.log('Error sending message:', error);
      } else {
        console.log('Message sent correctly');
      }
    });
    client.on('message', async (message: { toString: () => string; }) => {
      setReceivedMessage(receivedMessage+message.toString()+"\n")
    });
  };

Définition de l’affichage

Enfin l’interface graphique de l’application est défini comme suit:

  • Le titre de l’application
  • L’adresse IP de l’appareil
  • Un bouton pour sélectionner le mode client ou serveur
  • Un encart permettant d’éditer le message à envoyer
  • Un bouton d’envoi
  • Un encart pour spécifier l’adresse IP du serveur en mode client
  • Une zone de texte pour afficher les messages reçu
return (
    <View style={styles.mainBody}>
      <Text
        style={styles.mainTitle}>
        AC UDP Terminal
      </Text>
      <ScrollView>

      <View style={styles.deviceItem}>
                      <View>
                        <Text style={styles.deviceName}>{ipAddress}</Text>
                        <Text style={styles.deviceInfo}>{connectionStatus}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() => setIsServer(!isServer)}
                        style={styles.deviceButton}>
                        <Text
                          style={styles.buttonText}>
                          {isServer ? 'Server' : 'Client'}
                        </Text>
                      </TouchableOpacity>
                    </View>


      <View
              style={styles.inputBar}>        
              <TextInput
                style={styles.textInput}
                placeholder="Enter a message"
                value={messageText}
                onChangeText={(text) =>    setMessageText(text)
                }
              />
              <TouchableOpacity
                        onPress={() => sendMessage()
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={styles.buttonText}>
                          SEND
                        </Text>
                      </TouchableOpacity>
        </View>

      <TextInput
        placeholder="Server IP"
        onChangeText={setIpServer}
        value={ipServer}
      />
      <Text>Received Message:</Text>
              <TextInput
              editable = {false}
              multiline
              numberOfLines={20}
              maxLength={300}
              style={styles.textOutput} >
                    {receivedMessage}
              </TextInput>
      </ScrollView>
    </View>
  );

N.B.: le style de l’interface est défini dans le fichier /src/styles/styles.jsx

React-Native UDP Client – Python UDP Server

Dans cet exemple, nous configurons l’application comme client et l’ordinateur comme serveur. Nous allons envoyer un message au serveur, depuis l’application, qui renverra une réponse au client.

Python UDP Server Code

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket

localIP     = "0.0.0.0" #"127.0.0.1"
localPort   = 8888
bufferSize  = 1024

msgFromServer       = "Hello UDP Client"
bytesToSend         = str.encode(msgFromServer)

# Create a datagram socket
UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
 
# Bind to address and ip
UDPServerSocket.bind((localIP, localPort))
print("UDP server up and listening on port {}".format(localPort))

# Listen for incoming datagrams
while(True):

	data,addr = UDPServerSocket.recvfrom(bufferSize)
	print("{} > {}".format(addr,data))

	# Sending a reply to client
	UDPServerSocket.sendto(bytesToSend, addr)

N.B.: Pour écouter sur toutes les adresses locales nous utilisons 0.0.0.0. Si vous souhaitez tester la communication entre client et serveur sur le même ordinateur vous pouvez utiliser l’adresse 127.0.0.1

Sur l’application, dans l’encart ServerIp, entrez l’adresse IP de l’ordinateur (ici: 192.168.1.70). Modifiez ensuite le message à envoyer puis appuyer sur le bouton « SEND ».

Python terminal

UDP server up and listening on port 8888
('192.168.1.5', 8887) > b'Hello server'
('192.168.1.5', 8887) > b'Hello server'

Sur le terminal Python, nous recevons le message « Hello server » envoyé depuis l’application. Le serveur UDP renvoie « Hello UDP Client »

React-Native UDP Server – Python UDP Client

Ici, nous configurons l’application comme serveur et l’ordinateur comme client. Dans le code Python, nous entrons l’adresse IP du serveur.

Python UDP Client

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import time
HOST = "192.168.1.5" #Server IP #"127.0.0.1" local address
PORT = 8888
bufferSize = 1024

counter=0
while True:
	# Create a UDP socket at client side
	with socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) as client:
		# Send to server using created UDP socket
		client.sendto(str.encode("client counter : "+str(counter)), (HOST,PORT))
		data,addr= client.recvfrom(bufferSize)
		msg = "{} > {}".format(addr,data)

		print(msg)
	counter+=1
	time.sleep(1)

Python terminal

('192.168.1.5', 8888) > b'Hello client '
('192.168.1.5', 8888) > b'Hello client '

Code complet de l’application

App.tsx

/**
 * npm install react-native-udp
 * npm install react-native-network-info
 * 
 */

import React, {useState, useEffect} from 'react';
import {   
  View, 
  ScrollView, 
  Text,
  TextInput,
  PermissionsAndroid,
  Platform, 
  TouchableOpacity } from 'react-native';
   import dgram from 'react-native-udp'
   import UdpSocket from 'react-native-udp/lib/types/UdpSocket.js';
   import { NetworkInfo } from 'react-native-network-info'
   import {styles} from "./src/styles/styles.jsx"


const UDPTerminal = () =>  {
  const [isServer, setIsServer] = useState(false);
  const [connectionStatus, setConnectionStatus] = useState('');
  const [socket, setSocket] = useState<UdpSocket>();
  const [ipAddress, setIpAddress] = useState('');
  const [ipServer, setIpServer] = React.useState('');
  const [messageText, setMessageText] = useState("");
  const [messageToSend, setMessageToSend] = useState("Hello from server");
  const [receivedMessage, setReceivedMessage] = useState("");

  useEffect(() => {
    const fetchIpAddress = async () => {
      const ip = await NetworkInfo.getIPV4Address();
      setIpAddress(ip);
      console.log("ip adresses ; ", ip)
    };

    fetchIpAddress();

    if (isServer) {
      // Configure app as server
      const server = dgram.createSocket('udp4');

      server.on('message', (data, rinfo) => {
        
        server.send(messageToSend, undefined, undefined, rinfo?.port, rinfo?.address, (error) => {
          if (error) {
            console.log('Error sending message:', error);
          } else {
            console.log('Message sent correctly');
          }
        });
        console.log('Received message:', data.toString());
		if(receivedMessage.length>300){
          setReceivedMessage("");
        }
        setReceivedMessage(receivedMessage => receivedMessage+data.toString()+"\n")
      });

      server.on('listening', () => {
        console.log('Server listening on port:', server.address().port);
        setConnectionStatus(`Server listening on port ${server.address().port}`);
      });
      try{
        setReceivedMessage("");
        server.bind(8888);
        setSocket(server);
      }catch(error){
        console.log("error binding server",error);
      }
      
    } else {
      setConnectionStatus(`Server disconnected`);
      // Configure app as client
      const client = dgram.createSocket('udp4');
      try{
        setReceivedMessage("");
        client.bind(8887);
        setSocket(client);
      }catch(error){
        console.log("error binding client",error);
      }
      
    }

    return () => {
      socket && socket.close();
    };
  }, [isServer, messageToSend, messageText]);

  const sendMessage = () => {
    setMessageToSend(messageText);
    if (isServer) return;

    const client = socket;
    console.log("send message to "+ipServer)
    client.send(messageToSend, undefined, undefined, 8888, ipServer, (error) => {
      if (error) {
        console.log('Error sending message:', error);
      } else {
        console.log('Message sent correctly');
      }
    });
    client.on('message', async (message: { toString: () => string; }) => {
	  if(receivedMessage.length>300){
          setReceivedMessage("");
      }
      setReceivedMessage(receivedMessage+message.toString()+"\n")
    });
  };

  return (
    <View style={styles.mainBody}>
      <Text
        style={styles.mainTitle}>
        AC UDP Terminal
      </Text>
      <ScrollView>

      <View style={styles.deviceItem}>
                      <View>
                        <Text style={styles.deviceName}>{ipAddress}</Text>
                        <Text style={styles.deviceInfo}>{connectionStatus}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() => setIsServer(!isServer)}
                        style={styles.deviceButton}>
                        <Text
                          style={styles.buttonText}>
                          {isServer ? 'Server' : 'Client'}
                        </Text>
                      </TouchableOpacity>
                    </View>


      <View
              style={styles.inputBar}>        
              <TextInput
                style={styles.textInput}
                placeholder="Enter a message"
                value={messageText}
                onChangeText={(text) =>    setMessageText(text)
                }
              />
              <TouchableOpacity
                        onPress={() => sendMessage()
                        }
                        style={[styles.sendButton]}>
                        <Text
                          style={styles.buttonText}>
                          SEND
                        </Text>
                      </TouchableOpacity>
        </View>

      <TextInput
        placeholder="Server IP"
        onChangeText={setIpServer}
        value={ipServer}
      />
      <Text>Received Message:</Text>
              <TextInput
              editable = {false}
              multiline
              numberOfLines={20}
              maxLength={300}
              style={styles.textOutput} >
                    {receivedMessage}
              </TextInput>
      </ScrollView>
    </View>
  );
}

export default UDPTerminal;

style.jsx

/* ./src/styles/styles.jsx */
import {StyleSheet, Dimensions} from 'react-native';
/**
 * links to palette
 *  https://colorhunt.co/palette/161616346751c84b31ecdbba
 * 
*/
//parameters
let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8

const windowHeight = Dimensions.get('window').height;
export const styles = StyleSheet.create({
  mainBody: {
    flex: 1,
    justifyContent: 'center',
    height: windowHeight,
    color:TEXT_COLOR,
    backgroundColor: BACKGROUND_COLOR,
  },

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

  scanButton: {
    backgroundColor: BUTTON_COLOR,
    paddingBottom: 10,
    paddingTop: 10,
    borderRadius: 10,
    margin: 2,
    marginBottom: 10,
    marginTop: 10,
    
    paddingHorizontal: 20,
    fontWeight: 'bold', 
    fontSize: 12,
  },

  buttonText: {
    color: TEXT_COLOR,
    fontWeight: 'bold',
    fontSize: 12,
	  textAlign: 'center',
  },
  
  noDevicesText: {
    color: TEXT_COLOR,
    textAlign: 'center',
    marginTop: 10,
    fontStyle: 'italic',
  },
  deviceItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 2,
  },
  deviceName: {
    color: TEXT_COLOR,
    fontSize: 14,
    fontWeight: 'bold',
  },
  deviceInfo: {
    color: TEXT_COLOR,
    fontSize: 10,
  },
  deviceButton: {
    backgroundColor: BUTTON_COLOR,
    padding: 10,
    borderRadius: 10,
    margin: 2,
    paddingHorizontal: 20,
    fontWeight: 'bold', 
    fontSize: 12,
  },

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

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

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

  textOutput:{
    backgroundColor: '#333333',
    margin: 10,
    borderRadius: 2,
    borderWidth: 1,
    borderColor: '#EEEEEE',
    textAlignVertical: 'top',
  }

});

Applications

  • Pilotez votre projet Arduino, ESP32 ou Raspberry Pi avec une application

Sources

Créer une application BLE pour ESP32 avec React Native

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