fbpixel
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

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