fbpixel
Programmer en Python avec VSCode

Programmer en Python avec VSCode

L’éditeur de code VSCode permet de créer et développer des projets dans différents langages de programmation comme Python ou C++ grâce à de nombreuses extensions.

Installation de VSCode

Si ce n’est pas déjà fait, téléchargez et installez VSCode

L’utilisation d’un éditeur de code comme VSCode fait partie des bonnes pratiques pour gagner en productivité sur vos projets Python.

Vous pouvez installer des extensions pour vous aider dans la programmation comme Python ou Python Debugger

Créer et configurer un projet Python

Pour créer un projet, vous pouvez soit naviguer vers un dossier contenant vos projet et lancer VSCode à partir de cet emplacement dans un terminal

mkdir tuto
cd tuto
code .

Ou ouvrir un dossier à partir de VSCode.

A l’aide Command Palette, créer un environnement virtuel qui vous permettra de conserver une installation propre de Python et ses paquets pour votre projet. Python : Create Environment … , sélectionnez Venv puis choisissez la version de Python désirée

Une fois l’environnement créé, vous pouvez vérifier la configuration de l’interpréteur de l’environnement. Dans le terminal de VSCode, entrez les commandes suivantes

python --version
python -m pip freeze

La commande pip freeze devrait renvoyer une valeur vide puisqu’il n’y a pas de paquets installés sur un environnement virtuel neuf.

Créer et exécuter un script Python avec VSCode

Créer ensuite un fichier script.py, avec le code print(« hello world ») que vous pouvez exécuter à l’aide d’un clique-droit sur la page d’édition Run Python > Run Python File in Terminal

#! /user/bin/env python
# -*- coding: utf-8 -*-
"""
File: script.py
Author: Xukyo
email: email@email.com
Date:

Description: A simple Python script to print "Hello, world!"

Usage:
	python script.py 

Dependencies: None
Sources: www.aranacorp.com
"""

print("Hello world!")

Vous pouvez également utiliser le menu, Run > Start debugging (F5).

Un terminal s’ouvrira en bas de la fenêtre avec l’inscription Hello world!

Ajouter une librairie Python dans VSCode

Pour installer une librairie Python, il vous suffit de lancer la commande suivante dans le terminal de VSCode

python -m pip install <python_package>
#or
pip install <python_package>

Vous pouvez alors vérifier votre installation à l’aide de la commande pip freeze

Sources

Programmer un ESP32 avec VSCode et ESP-IDF

Programmer un ESP32 avec VSCode et ESP-IDF

Dans ce tutoriel, nous allons voir comment programmer un ESP32 à l’aide de l’environnement ESP-IDF avec VSCode. ESP-IDF ou Espressif IoT Development Framework, est l’environnement officiel de programmation des ESP32 et permet la programmation d’application de qualité en C et C++.

ESP-IDF vs ESP Arduino Core

Nous avons vu dans un précédent article comment programmer un NodeMCU ESP32 avec Arduino IDE. L’environnement ESP Arduino a été développé pour simplifier la programmation des ESP32. Elle permet aux débutants et amateurs, déjà utilisateurs des Arduino, d’utiliser les mêmes outils pour le développement de projets basés sur des ESP32. Toutefois, il faut rappeler que l’Arduino IDE utilise une version simplifiée et incomplète du langage C/C++ ce qui peut limiter les fonctionnalités et performances du code développé à l’aide de cet outil.

L’environnement ESP-IDF, quant à lui, est totalement compatible avec les standards C/C++ et propose un ensemble de librairies et de fonctionnalités spécifiques pour le développement d’objets connectés avec ESP32.

  • Gestion Wifi et Bluetooth
  • Programmation OTA
  • Fonction temps-réel avec FreeRTOS
  • Gestion des capteurs et protocoles de communication les plus courants

ESP-IDF propose également une compatibilité avec toutes les versions de microcontrôleur ESP32.

Installation et configuration de VSCode

Téléchargez et installez VSCode

Rechercher et installer l’extension ESP-IDF pour VSCode qui contient des librairies et configuration de base pour la programmation d’ESP32

Une fois l’extension installée, vous pouvez la configurer avec la palette de commande. Dans le menu, View>Command Palette…, cherchez « ESP-IDF Configure ». Sélectionner la version d’ESP-IDF à installer. La configuration installera les modules suivants:

  • ESP-IDF
  • ESP-IDF tools
  • Python venv pour ESP-IDF

Configurer un projet pour ESP32

Pour charger votre premier code, vous pouvez utiliser l’exemple Blink

Sélectionnez View -> Command Palette… et entrez ESP-IDF: Show Examples Projects. Sur la page qui s’ouvre, trouvez et appuyez sur le bouton Show Examples. Il vous sera alors demandé de sélectionner le framework installé précédemment.

Dans la liste des exemples, vous pouvez ensuite sélectionner Blink et appuyer sur le bouton « Create project using example Blink »

Vous pouvez également créer un nouveau projet. ESP-IDF: New Project . Sélectionner ensuite Extensions>template-app pour un projet vierge. Le fichier main se trouve sous myproject/main/main.c

N.B.: avec le templae arduino-as-a-component, vous pouvez écrire avec le langage Arduino dans le framework ESP-IDF

Voici un exemple Hello world, pour tester la configuration du projet.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"

static const char *TAG = "myproject example";

void app_main(void)
{

    while (1) {
        ESP_LOGI(TAG, "Hello World!");
 
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }

}

Lorsque les fichiers du projet sont édités, vous pouvez suivre les étapes suivantes pour configurer le projet

  • sélectionner le type de carte ESP-IDF: Set Espressif device target
  • sélectionner le port série ESP-IDF: Select port to use (COM, tty, usbserial)

Pour sélectionner le type et la carte utilisée. Dans Command Palette, entrez Set Espressif device target. Dans notre cas, ESP32-WROOM-32, esp32 puis ESP32 chip (via ESP USB bridge).

N.B.: une fois la carte sélectionnée, vous pourrez utiliser les librairies de base

Sélectionner le port série à l’aide de la commande ESP-IDF: Select port to use (COM, tty, usbserial)

Compiler, téléverser le code sur un ESP32

Une fois le projet configuré et le code créé, nous allons suivre un certain nombres d’étapes pour pouvoir téléverser le code sur la carte ESP32:

  • compiler le projet ESP-IDF: Build your project
  • téléverser le code ESP-IDF: Flash your project
  • déverminer le code ESP-IDF: Monitor your device

Il est possible de combiner les 3 dernières commandes avec ESP-IDF: Build, Flash and start a Monitor on your device

Maintenant que l’environnement de programmation est prêt, vous pouvez compiler le programme. Dans Command Palette, rechercher ESP-IDF: Build your project.

Pour flasher le programme sur la carte, vérifier le port série sélectionné puis flasher le code ESP-IDF: Flash your project. Dans notre cas, la programmation se fait par le port USB, nous utilisons donc le mode UART.

Une fois le code téléverser, vous pouvez observer les sorties sur le port série à l’aide du moniteur ESP-IDF: Monitor your device

Ajouter des librairies à votre code

Certaines librairies existent dans le framework ESP-IDF, pour trouver les composants disponibles vous pouvez entrer ESP-IDF: Show Component Registry dans Command Palette

Une fois votre composant trouvé, vous pouvez l’ajouter à votre projet en utilisant le bouton « install » ou la ligne de commande

idf.py add-dependency "espressif/button^3.2.0"

N.B.: une fois le composant installé, vous pouvez accéder à un exemple dans managed_components>espressif_button> examples

Vous pouvez alors développer votre propre code à partir de la librairie

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "iot_button.h"

#define BOOT_BUTTON_NUM         0 // boot button attached to GPIO0
#define BUTTON_ACTIVE_LEVEL     0

static const char *TAG = "myproject example";
int8_t btn_status=0;

const char *button_event_table[] = {
    "BUTTON_PRESS_DOWN",
    "BUTTON_PRESS_UP",
    "BUTTON_PRESS_REPEAT",
    "BUTTON_PRESS_REPEAT_DONE",
    "BUTTON_SINGLE_CLICK",
    "BUTTON_DOUBLE_CLICK",
    "BUTTON_MULTIPLE_CLICK",
    "BUTTON_LONG_PRESS_START",
    "BUTTON_LONG_PRESS_HOLD",
    "BUTTON_LONG_PRESS_UP",
};

static void button_event_cb(void *arg, void *data)
{
    ESP_LOGI(TAG, "Button event %s (%d)", button_event_table[(button_event_t)data],(button_event_t)data);
    switch ((button_event_t)data){
        case 4:
            ESP_LOGI(TAG, "Execute code if single click");
            break;
        case 5:
            ESP_LOGI(TAG, "Execute code if double click");
            break;
        default:
            //nothing
    }
}

void button_init(uint32_t button_num)
{
    button_config_t btn_cfg = {
        .type = BUTTON_TYPE_GPIO,
        //.long_press_time = CONFIG_BUTTON_LONG_PRESS_TIME_MS,
        //.short_press_time = CONFIG_BUTTON_SHORT_PRESS_TIME_MS,
        .gpio_button_config = {
            .gpio_num = button_num,
            .active_level = BUTTON_ACTIVE_LEVEL,
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
            .enable_power_save = true,
#endif
        },
    };
    button_handle_t btn = iot_button_create(&btn_cfg);
    assert(btn);
    esp_err_t err = iot_button_register_cb(btn, BUTTON_PRESS_DOWN, button_event_cb, (void *)BUTTON_PRESS_DOWN);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_UP, button_event_cb, (void *)BUTTON_PRESS_UP);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_REPEAT, button_event_cb, (void *)BUTTON_PRESS_REPEAT);
    err |= iot_button_register_cb(btn, BUTTON_PRESS_REPEAT_DONE, button_event_cb, (void *)BUTTON_PRESS_REPEAT_DONE);
    err |= iot_button_register_cb(btn, BUTTON_SINGLE_CLICK, button_event_cb, (void *)BUTTON_SINGLE_CLICK);
    err |= iot_button_register_cb(btn, BUTTON_DOUBLE_CLICK, button_event_cb, (void *)BUTTON_DOUBLE_CLICK);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_START, button_event_cb, (void *)BUTTON_LONG_PRESS_START);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_HOLD, button_event_cb, (void *)BUTTON_LONG_PRESS_HOLD);
    err |= iot_button_register_cb(btn, BUTTON_LONG_PRESS_UP, button_event_cb, (void *)BUTTON_LONG_PRESS_UP);
    ESP_ERROR_CHECK(err);
}

void app_main(void)
{
    button_init(BOOT_BUTTON_NUM);

}

Lors de l’exécution du code, si vous appuyez une ou deux fois sur le bouton boot pour voir le code spécifique s’afficher

I (341) main_task: Returned from app_main()
I (3611) myproject example: Button event BUTTON_PRESS_DOWN (0)
I (3821) myproject example: Button event BUTTON_PRESS_UP (1)
I (4011) myproject example: Button event BUTTON_SINGLE_CLICK (4)
I (4011) myproject example: Execute code if single click
I (4011) myproject example: Button event BUTTON_PRESS_REPEAT_DONE (3)
I (12231) myproject example: Button event BUTTON_PRESS_DOWN (0)
I (12371) myproject example: Button event BUTTON_PRESS_UP (1)
I (12461) myproject example: Button event BUTTON_PRESS_DOWN (0)
I (12461) myproject example: Button event BUTTON_PRESS_REPEAT (2)
I (12601) myproject example: Button event BUTTON_PRESS_UP (1)
I (12781) myproject example: Button event BUTTON_DOUBLE_CLICK (5)
I (12781) myproject example: Execute code if double click
I (12781) myproject example: Button event BUTTON_PRESS_REPEAT_DONE (3)
I (14051) myproject example: Button event BUTTON_PRESS_DOWN (0)
I (14171) myproject example: Button event BUTTON_PRESS_UP (1)
I (14261) myproject example: Button event BUTTON_PRESS_DOWN (0)

Vous pouvez créer vos propres librairies en utilisant la commande ESP-IDF: Create New ESP-IDF component. Ceci va créer un dossier components/mycomponent dans lequel vous pourrez éditer les fichiers .h et .c.

Sources

Utilisation d’un capteur Lidar avec Python

Utilisation d’un capteur Lidar avec Python

Nous allons voir dans ce tutoriel comment mettre en place une cartographie avec un capteur Lidar sous Python. Le capteur lidar permet de se repérer dans l’espace et de cartographier.

Description du capteur Lidar

Le capteur lidar est un capteur laser de distance couplé à un moteur qui le fait tourner. Il se comporte comme un radar permettant la détection d’obstacles à 360 degrés et de se fait une cartographie de l’espace. Il s’utilise souvent en robotique pour cartographier, se repérer dans un environnement et permettre un déplacement autonome.

Le capteur Lidar utilise une carte d’interface série permettant de le connecter à un ordinateur via un port USB.

Pour savoir sur quel port se connecte l’appareil, vous pouvez aller dans le gestionnaire de périphérique dans la section Ports (COM et LPT) (ici com3)

Test du Lidar avec l’outil officiel Tool

Vous pouvez télécharger l’interface Tool sur le site officiel. Cet outil permet de visualiser la cartographie obtenu à l’aide du Lidar.

Une fois le logiciel lancé, il suffit de sélectionner le port du périphérique et le modèle de Lidar. Une fois le Lidar connecté, vous pouvez lancer l’acquisition.

Installation du paquet Python

Pour utiliser le capteur Lidar avec Python, nous utilisons la librairie PyLidar3

python -m pip install Pylidar3

Script de test du Lidar

Pour vérifier l’installation de Pylidar, vous pouvez utiliser le script suivant qui affiche simple les informations de l’appareil.

import PyLidar3
port = "COM3" #input("Enter port name which lidar is connected:") #windows
Obj = PyLidar3.YdLidarX4(port)
print("connecting to {}".format(port))

ret = Obj.Connect()
if(1): 
	print(ret)
	print("device info: ",Obj.GetDeviceInfo())
	Obj.Disconnect()
else:
	print("Error connecting to device")

Affichage du résultat du scan

Pour notre première cartographie, nous allons initialiser l’objet PyLidar3 et nous allons nous connecter à l’appareil

Obj = PyLidar3.YdLidarX4(port)
ret = Obj.Connect()

Nous allons ensuite définir quelques paramètres du scan:

  • durée du scan scanDuration
  • seuil de donnée à enregistrer dataThr
  • limite de la fenêtre winLim

Enfin nous allons lancer la mesure lidar et afficher l’acquisition avec matplotlib

import PyLidar3
import matplotlib.pyplot as plt
import math
import time

#init lidar
port =  "COM3" #input("Enter port name which lidar is connected:") #windows
Obj = PyLidar3.YdLidarX4(port) #PyLidar3.your_version_of_lidar(port,chunk_size)

ret = Obj.Connect()
print(ret)
#ret = Obj.Connect()
#print(ret)

#parameters
scanDuration = 30 #scan for 30 seconds
dataThr = 1000 # disgard data below this value
winLim = 5000 # window limit in X and Y

#init data list on 360deg
x=[]
y=[]
for _ in range(360):
	x.append(0)
	y.append(0)

if(1):
	deviceInfo = Obj.GetDeviceInfo()
	print("device info : ",deviceInfo)
	
	gen = Obj.StartScanning()
	t = time.time() # start time
	while (time.time() - t) < scanDuration:
		data = next(gen)
		for angle in range(0,360):
			if(data[angle]>dataThr):
				#x[angle] = data[angle] * math.cos(math.radians(angle))
				#y[angle] = data[angle] * math.sin(math.radians(angle))
				x[angle] = (x[angle] + data[angle] * math.cos(math.radians(angle))) / 2
				y[angle] = (y[angle] + data[angle] * math.sin(math.radians(angle))) / 2
				
		plt.clf()
		plt.axis([-winLim,winLim,-winLim,winLim])
		plt.title("Model: X4 Firmware: {} Hardware: {} SN: {}".format(deviceInfo['firmware_version'], deviceInfo['hardware_version'],deviceInfo['serial_number']))
		plt.scatter(y, x, c='r', s=8)
		plt.draw()
		plt.pause(0.1)

	Obj.StopScanning()
	Obj.Disconnect()
else:
	print("Error connecting to device")

plt.show()

Résultat

Une fois le script lancer, nous pouvons voir le process de cartographie en action et la carte se mettre à jour en temps réel.

Gestion du lidar avec threading

Il est possible de gérer la mesure dans un process différent de l’affichage pour une meilleure fluidité. A titre d’exemple, nous utilisons la librairie threading. Nous allons créer une fonction d’acquisition des données scan() que nous plaçons ensuite dans un thread.

threading.Thread(target=scan).start()

Nous pouvons ainsi traiter les données de cartographie sans perturber l’acquisition de la mesure.

import PyLidar3 
import matplotlib.pyplot as plt
import math
import time
import numpy as np
import threading


#init lidar
port =  "COM3" #input("Enter port name which lidar is connected:") #windows
Obj = PyLidar3.YdLidarX4(port) #PyLidar3.your_version_of_lidar(port,chunk_size)

ret = Obj.Connect()
print(ret)
#ret = Obj.Connect()
#print(ret)

#parameters
scanDuration = 30 #scan for 30 seconds
dataThr = 1000 # disgard data below this value
winLim = 5000 # window limit in X and Y
buffnum = 1 # matrix size to average position

#init data list on 360deg
x = np.zeros((buffnum, 360))
y = np.zeros((buffnum, 360))

def scan():
	nb=0
	while is_scanning:
		data = next(gen)
		if nb>=buffnum-1:
			nb=0
		else:
			nb+=1

		for angle in range(0,360):
			if(data[angle]>dataThr):
				x[nb][angle] = data[angle] * math.cos(math.radians(angle))
				y[nb][angle] = data[angle] * math.sin(math.radians(angle))
		

if(1):
	deviceInfo = Obj.GetDeviceInfo()
	print("device info : ",deviceInfo)
	
	gen = Obj.StartScanning()
	t = time.time() # start time
	is_scanning = True
	threading.Thread(target=scan).start()
	while (time.time() - t) < scanDuration:
		xmean = np.mean(x, axis=0)
		ymean = np.mean(y, axis=0)
		plt.clf()
		plt.axis([-winLim,winLim,-winLim,winLim])
		plt.title("Model: X4 Firmware: {} Hardware: {} SN: {}".format(deviceInfo['firmware_version'], deviceInfo['hardware_version'],deviceInfo['serial_number']))
		#plt.scatter(y,x,c='r',s=8)
		plt.scatter(ymean,xmean,c='r',s=8)
		plt.draw()
		plt.pause(0.05)
		
	is_scanning = False
	Obj.StopScanning()
	Obj.Disconnect()
else:
	print("Error connecting to device")

plt.show() #keep figure open at the end

Application

Sources

Configuration de RetroPie sur Raspberry Pi

Configuration de RetroPie sur Raspberry Pi

Le microordinateur Raspberry Pi a différentes utilisations courantes dont le retro-gaming avec RetroPie. Nous allons voir dans ce tutoriels, comment installer, configurer et utiliser RetroPie sur un Raspberry Pi

Matériel

  • Raspberry Pi 4 ou autre carte compatibles
  • écran+clavier+souris
  • manette de jeux vidéo ou autre capteur

Quelques mots sur RetroPie

RetroPie est un logiciel qui s’intègre à un OS et permet de transformer sa machine en émulateur de console de jeux vidéo. Il permet donc de faire du retro-gaming et de rassembler sur une même machine d’anciens jeux vidéo qui étaient disponibles sur diverses consoles.

RetroPie est compatible avec les cartes suivantes

  • Raspberry Pi Zero/Zero2W/1/2/3/4/400 (Raspbian)
  • PC linux (Debian/Ubuntu)
  • Odroid-C1/C2 (Ubuntu)
  • ODroid-XU3/XU4 (Ubuntu)

L’installation de RetroPie sur un Raspberry Pi permet d’en faire une console de jeux vidéo pour toute la famille à moindre coût. Cela permet aussi de réaliser une borne d’arcade.

Téléchargement et installation d’un OS avec RetroPie (conseillé)

Pour installer un OS contenant RetroPie, vous pouvez télécharger une image correspondant à votre machine sur le site officiel et l’écrire sur une carte SD via Raspberry Pi Imager ou Balena Etcher.

Vous pouvez également utiliser le logiciel Raspberry Pi Imager et sélectionner l’OS souhaité pour le retro-gaming

Une fois l’image écrite sur la carte SD, vous pouvez l’insérer dans le Raspberry Pi puis allumer le Raspberry Pi. L’installation de RetroPie devrait se lancer automatiquement.

Configuration de la manette de jeux

Au lancement de EmulationStation, la première chose demandé est la configuration de la manette de commande de jeux qui vous servira pour naviguer à travers les menus. Branchez une manette de jeux et suivez les instructions. Vous pourrez changer ces paramètres plus tard.

Configurer votre station d’émulation

Une fois les commandes de votre manette définies pour pouvoir naviguer dans le menu de RetroPie, vous pouvez configurer votre système en fonction de votre besoin;

  • audio
  • localisation
  • configuration WiFi
  • activation SSH
  • mise à jour
  • Installation RetroPie packages

Une fois votre système configuré, il est temps d’installer des jeux sur votre émulateur.

Où trouver et comment Transférer des ROMs pour RetroPie

Les ROMS (Read-only Memory) sont des fichiers contenant les images de jeux vidéo. Il est possible de trouver des roms sur la toile. Voici quelques sites dont le premier est le plus sûr

N.B.: attention au licence d’utilisation. Il peut être illégal de télécharger certains jeux sous licence.

Une fois les fichiers téléchargés, créez une arborescence de dossier avec un dossier par console ~/retropie/roms/$CONSOLE (ie: atari ou gb). Vous pouvez copier les fichiers décompressés dans le dossier de la console en question.

N.B.: les fichiers reconnus par RetroPie ont généralement les extensions correspondant au console (.gb, .gba, etc.) ou des images de CD ISO (.img)

Pour transférer les jeux sur RetroPie, vous pouvez utiliser le dossier Samba partagé prévu sur RetroPie (\\RETROPIE\roms)

Ou utiliser une clé USB avec un dossier par console contenant les images des jeux vidéo . Lorsque vous insérez la clé USB dans le Raspberry Pi, le transfert devrait se faire automatiquement.

Pour que les jeux soient disponibles dans RetroPie, le logiciel EmulationStation doit être redémarré. (Menu > Quit > Restart EmulationStation )

Jouer et gérer les données de jeux

Une fois EmulationStation redémarré, les consoles et jeux disponibles apparaissent dans le menu principal.

Une fois le jeux sélectionné, il est possible de sortir du jeu, sauvegarder et charger avec les commandes suivantes (Hotkey default: bouton Select)

Hotkey CombinationAction
Hotkey+StartExit
Hotkey+Right ShoulderSave
Hotkey+Left ShoulderLoad
Hotkey+RightInput State Slot Increase
Hotkey+LeftInput State Slot Decrease
Hotkey+XRGUI Menu
Hotkey+BReset

Terminal RetroPie

En cas de problème ou pour certaines opérations, vous aurez besoin de naviguer dans l’OS du RPi. Pour cela, il vous suffit de fermer EmulationStation (Menu>Quit>Exit EmulationStation)

Vous pourrez retrouver:

  • les fichiers Raspbian comme le fichier config.txt pour la configuration manuel du boot
sudo nano /boot/config.txt
  • les émulateurs installés
ls /opt/retropie/emulators
  • la configuration des joysticks
/opt/retropie/configs/all/retroarch/autoconfig

Problèmes courants

RetroPie ne boot pas

Si RetroPie ne boot pas du tout:

  • carte SD corrompue
  • mauvaise version de l’OS

Lors du premier démarrage, RetroPie reste bloqué sur le terminal de démarrage et EmulationStation ne se lance pas.

lancer les deux scripts suivants

sudo ./RetroPie-Setup/retropie_packages.sh
sudo ./RetroPie-Setup/retropie_setup.sh

puis

emulationstation

Téléchargement et installation de RetroPie sur un OS existant

Il est aussi possible d’installer RetroPie sur un OS existant afin de conserver d’autres fonction à votre Raspberry Pi.

Sources

Gestion du BLE sur un ESP32 avec MicroPython

Gestion du BLE sur un ESP32 avec MicroPython

Dans ce tutoriel, nous allons apprendre comment gérer et tester la communication BLE (Bluetooth Low Energy) sur un ESP32 avec MicroPython.

Matériels

  • Un module ESP32
  • Un ordinateur avec Python installé
  • Câble USB pour la connexion ESP32-ordinateur
  • Un appareil Android

Environnement et Configuration de l’IDE

Pour communiquer et programmer en Python votre ESP32, vous pouvez suivre ce tutoriel précédent pour utiliser MicroPython.

Vous pouvez également installer l’application BLE Terminal sur votre téléphone Android pour tester la communication BLE

Activation du Bluetooth LE

Pour activer le BLE de votre EPS32, copier le code micropython suivant dans l’éditeur Thonny IDE et exportez le sur votre module. Dans cet exemple, nous utilisons la librairie bluetooth (autre option ubluetooth)

import bluetooth #https://docs.micropython.org/en/latest/library/bluetooth.html
import ubinascii

def main():
    BLE = bluetooth.BLE()
    BLE.active(True)

    #Advertise
    name = bytes("ESP32BLEmPy", 'UTF-8')
    adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name
    BLE.gap_advertise(100, adv_data)

    #get MAC address
    mac = BLE.config('mac')[1]
    print("device MAC address is: "+ubinascii.hexlify(mac).decode())
    #print("device MAC address is: "+mac.hex())



if __name__ == "__main__":
    main()

N.B.: il est possible de récupérer l’adresse MAC au format hexadécimal en utilisant la fonction hex() ou la librairie ubinascii

Une fois le code chargé et l’annonce activé, vous pouvez retrouver l’appareil dans l’application BLE Terminal.

IMPORTANT: l’annonciation doit être activée après chaque déconnexion pour pouvoir reconnecter l’ESP32

Enregistrer des services

La communication BLE passe par le concept de services et caractéristiques avec certain droit d’accès (read, write, notify). Il existe des services par défaut (Nordic UART service (NUS), Heart rate (HR) ou vous pouvez créer vos propres services avec des identifiants uniques UUID.

Un service a un UUID unique et peut contenir différentes caractéristiques. Chaque caractéristique est définie par un UUID unique et différents droits d’accès.

  • La fonction bluetooth.UUID permet de définir les adresses des services et caractéristiques
  • bluetooth.FLAG_READ donne un accès en lecture au client
  • bluetooth.FLAG_NOTIFY permet de notifier le client sans action de sa part
  • bluetooth.FLAG_WRITE donne un accès en écriture au client

    #register
    HR_UUID = bluetooth.UUID(0x180D)
    HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
    HR_SERVICE = (HR_UUID, (HR_CHAR,),)
    UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E')
    UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
    UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,)
    UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),)
    
    MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b")
    SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
    CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,)
    MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),)
    
    SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE)
    ( (hr,), (tx, rx,), (sen,cmd,), ) = BLE.gatts_register_services(SERVICES)
    
    BLE.gatts_write(sen, str(43.256), False) #init val to be read

N.B: l’annonciation (advertising) doit être stopper avant d’enregistrer des services

Définir les fonctions évènement

Pour gérer correctement le module BLE, nous allons créer des fonctions callback pour détecter et agir en fonction des différents évènements

Pour cela, nous avons besoin de connaitre les différents identifiant des évènements

from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)
_IRQ_GATTS_INDICATE_DONE = const(20)
_IRQ_MTU_EXCHANGED = const(21)
_IRQ_L2CAP_ACCEPT = const(22)
_IRQ_L2CAP_CONNECT = const(23)
_IRQ_L2CAP_DISCONNECT = const(24)
_IRQ_L2CAP_RECV = const(25)
_IRQ_L2CAP_SEND_READY = const(26)
_IRQ_CONNECTION_UPDATE = const(27)
_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_GET_SECRET = const(29)
_IRQ_SET_SECRET = const(30)

Nous pouvons ensuite utiliser définir les actions pour chaque évènement

#event callback function
def ble_irq(event,data):
    if event == _IRQ_CENTRAL_CONNECT:
        # A central has connected to this peripheral.
        conn_handle, addr_type, addr = data
        print("BLE device connected successfully")
    elif event == _IRQ_CENTRAL_DISCONNECT:
        # A central has disconnected from this peripheral.
        conn_handle, addr_type, addr = data
        print("BLE device disconnected")
        adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name
        BLE.gap_advertise(100, adv_data)
        
    elif event == _IRQ_GATTS_WRITE:
        # A client has written to this characteristic or descriptor.
        conn_handle, attr_handle = data
        print("write event: ",BLE.gatts_read(data[1]).decode('UTF-8').strip())
    elif event == _IRQ_GATTS_READ_REQUEST:
        # A client has issued a read. Note: this is only supported on STM32.
        # Return a non-zero integer to deny the read (see below), or zero (or None)
        # to accept the read.
        conn_handle, attr_handle = data
        print("read event: ",data)

Résultat

Avec ce code simple, vous pouvez vous connecter à l’appareil puis lire et écrire sur le service choisi dans l’application

MicroPython v1.22.1 on 2024-01-05; Generic ESP32 module with ESP32

Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
device MAC address is: 3c6105315f12
>>> BLE device connected successfully
BLE device disconnected
BLE device connected successfully
write event:  hello
read event:  (0, 31)

Création d’une classe MicroPython pour gérer la communication BLE ESP32

Il est intéressant de créer une classe pour gérer la communication BLE que vous pourrez réutiliser dans différents projets. Dans le code de la classe, vous retrouvez tous les éléments décrit plus haut

  • initialisation et activation du BLE
  • définition des fonctions callback self.ble_irq
  • enregistrement des services self.register
  • annonciation self.advertise
  • Nous avons également rajouté une fonction de notification pour mettre à jour la valeur d’un capteur self.set_sensor
class ESP32BLE():
    def __init__(self, name):
        # Create BLE device management
        self.name = name
        self.ble = bluetooth.BLE()
        self.ble.active(True)
        #get MAC address
        mac = self.ble.config('mac')[1]
        print("device MAC address is: "+mac.hex())
        self.ble.irq(self.ble_irq)
        self.connections = set()
        self.register()
        self.advertise()

    def ble_irq(self, event, data):
        #define event callback functions
        if event == _IRQ_CENTRAL_CONNECT:
            # A central has connected to this peripheral.
            conn_handle, addr_type, addr = data
            self.connections.add(conn_handle)
            print("BLE device connected successfully")
        elif event == _IRQ_CENTRAL_DISCONNECT:
            # A central has disconnected from this peripheral.
            conn_handle, addr_type, addr = data
            self.connections.remove(conn_handle)
            print("BLE device disconnected")
            self.advertise()
        elif event == _IRQ_GATTS_WRITE:
            # A client has written to this characteristic or descriptor.
            conn_handle, attr_handle = data
            print("write event: ",self.ble.gatts_read(data[1]).decode('UTF-8').strip())
        elif event == _IRQ_GATTS_READ_REQUEST:
            # A client has issued a read. Note: this is only supported on STM32.
            # Return a non-zero integer to deny the read (see below), or zero (or None)
            # to accept the read.
            conn_handle, attr_handle = data
            print("read event: ",data)
            
    def register(self):
        #define services and characteristics
        HR_UUID = bluetooth.UUID(0x180D)
        HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        HR_SERVICE = (HR_UUID, (HR_CHAR,),)
        UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E')
        UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,)
        UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),)
        
        MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b")
        SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,)
        MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),)
        
        SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE)
        ( (self.hr,), (self.tx, self.rx,), (self.sen,self.cmd,), ) = self.ble.gatts_register_services(SERVICES)
        
        self.ble.gatts_write(self.sen, str(43.256), False)

    
    def advertise(self):
        #advertise BLE module with name
        name = bytes(self.name, 'UTF-8')
        adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name
        self.ble.gap_advertise(100, adv_data)
        
    def set_sensor(self, data, notify=False):
        # Data is sint16 with a resolution of 0.01.
        self.ble.gatts_write(self.sen, str(data))
        if notify:
            for conn_handle in self.connections:
                # Notify connected centrals to issue a read.
                self.ble.gatts_notify(conn_handle, self.sen)

BLE device disconnected
BLE device connected successfully
write event:  hello
read event:  (65535, 31)
read event:  (0, 31)
read event:  (65535, 31)
read event:  (0, 31)
read event:  (65535, 31)
read event:  (0, 31)
write event:  hello World
read event:  (65535, 31)
read event:  (0, 31)
read event:  (65535, 31)
read event:  (0, 31)

Code complet de gestion du BLE ESP32 avec micropython

import bluetooth #https://docs.micropython.org/en/latest/library/bluetooth.html
import random
import time
from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)
_IRQ_GATTC_INDICATE = const(19)
_IRQ_GATTS_INDICATE_DONE = const(20)
_IRQ_MTU_EXCHANGED = const(21)
_IRQ_L2CAP_ACCEPT = const(22)
_IRQ_L2CAP_CONNECT = const(23)
_IRQ_L2CAP_DISCONNECT = const(24)
_IRQ_L2CAP_RECV = const(25)
_IRQ_L2CAP_SEND_READY = const(26)
_IRQ_CONNECTION_UPDATE = const(27)
_IRQ_ENCRYPTION_UPDATE = const(28)
_IRQ_GET_SECRET = const(29)
_IRQ_SET_SECRET = const(30)


class ESP32BLE():
    def __init__(self, name):
        # Create BLE device management
        self.name = name
        self.ble = bluetooth.BLE()
        self.ble.active(True)
        #get MAC address
        mac = self.ble.config('mac')[1]
        print("device MAC address is: "+mac.hex())
        self.ble.irq(self.ble_irq)
        self.connections = set()
        self.register()
        self.advertise()

    def ble_irq(self, event, data):
        #define event callback functions
        if event == _IRQ_CENTRAL_CONNECT:
            # A central has connected to this peripheral.
            conn_handle, addr_type, addr = data
            self.connections.add(conn_handle)
            print("BLE device connected successfully")
        elif event == _IRQ_CENTRAL_DISCONNECT:
            # A central has disconnected from this peripheral.
            conn_handle, addr_type, addr = data
            self.connections.remove(conn_handle)
            print("BLE device disconnected")
            self.advertise()
        elif event == _IRQ_GATTS_WRITE:
            # A client has written to this characteristic or descriptor.
            conn_handle, attr_handle = data
            print("write event: ",self.ble.gatts_read(data[1]).decode('UTF-8').strip())
        elif event == _IRQ_GATTS_READ_REQUEST:
            # A client has issued a read. Note: this is only supported on STM32.
            # Return a non-zero integer to deny the read (see below), or zero (or None)
            # to accept the read.
            conn_handle, attr_handle = data
            print("read event: ",data)
            
    def register(self):
        #define services and characteristics
        HR_UUID = bluetooth.UUID(0x180D)
        HR_CHAR = (bluetooth.UUID(0x2A37), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        HR_SERVICE = (HR_UUID, (HR_CHAR,),)
        UART_UUID = bluetooth.UUID('6E400001-B5A3-F393-E0A9-E50E24DCCA9E')
        UART_TX = (bluetooth.UUID('6E400003-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        UART_RX = (bluetooth.UUID('6E400002-B5A3-F393-E0A9-E50E24DCCA9E'), bluetooth.FLAG_WRITE,)
        UART_SERVICE = (UART_UUID, (UART_TX, UART_RX,),)
        
        MY_UUID = bluetooth.UUID("0bd62591-0b10-431a-982e-bd136821f35b")
        SEN_CHAR = (bluetooth.UUID("0bd62592-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,)
        CMD_CHAR = (bluetooth.UUID("0bd62593-0b10-431a-982e-bd136821f35b"), bluetooth.FLAG_WRITE,)
        MY_SERVICE = (MY_UUID, (SEN_CHAR, CMD_CHAR,),)
        
        SERVICES = (HR_SERVICE, UART_SERVICE, MY_SERVICE)
        ( (self.hr,), (self.tx, self.rx,), (self.sen,self.cmd,), ) = self.ble.gatts_register_services(SERVICES)
        
        self.ble.gatts_write(self.sen, str(43.256), False)

    
    def advertise(self):
        #advertise BLE module with name
        name = bytes(self.name, 'UTF-8')
        adv_data = bytearray(b'\x02\x01\x02') + bytearray((len(name) + 1, 0x09)) + name
        self.ble.gap_advertise(100, adv_data)
        
    def set_sensor(self, data, notify=False):
        # Data is sint16 with a resolution of 0.01.
        self.ble.gatts_write(self.sen, str(data))
        if notify:
            for conn_handle in self.connections:
                # Notify connected centrals to issue a read.
                self.ble.gatts_notify(conn_handle, self.sen)
                
def main():
    BLE = ESP32BLE("ESP32BLEmPy")

    #simulate sensor
    sensorVal=43.256
    i=0
    while True:
        # Write every second, notify every 10 seconds.
        i = (i + 1) % 10
        BLE.set_sensor(sensorVal, notify=i == 0)
        # Random walk the temperature.
        sensorVal += random.uniform(-1.5, 1.5)
        time.sleep_ms(1000)


if __name__ == "__main__":
    main()

Sources

Installer Arduino IDE sur Raspberry Pi

Installer Arduino IDE sur Raspberry Pi

Nous allons voir dans ce tutoriel comment installer le logiciel Arduino IDE et CLI sur Raspberry Pi et ainsi combiner les forces des deux systèmes.

Vérifier la configuration de votre système

Pour savoir quelle version de l’IDE télécharger et installer, vous devez connaitre les spécificités de votre système. Notamment l’architecture du noyau (Linux kernel). Il existe plusieurs commandes possibles.

uname -a #display os and kernel info
cat /proc/version 
architecturekernel
i386/i486/i586/i686/armv7l32 bit
x86_64(Intel)/aarch64(Arm)/armv864 bit
cat /proc/cpuinfo #cpu architecture

La commande la plus directe est de deamnder sur combien de bit est encodé une variable LONG

getconf LONG_BIT # result is 32 or 64

Installation d’Arduino IDE

Sur la page de téléchargement d’Arduino, sélectionnez la version correspondante à votre système et téléchargez-la.

Vous pouvez effectuer cette procédure en ligne de commande. en connaissant le nom du fichier arduino-1.8.19-linux<architecture>.tar.xz

pour mon système aarch64 :arduino-1.8.19-linuxaarch64.tar.xz

wget https://downloads.arduino.cc/arduino-1.8.19-linuxaarch64.tar.xz

Naviguez jusqu’à l’archive, puis décompressez le dossier.

tar -xvf arduino-1.8.19-linuxaarch64.tar.xz
rm arduino-1.8.19-linuxaarch64.tar.xz

Puis installer le logiciel

cd arduino-1.8.19
sudo ./install.sh

Problème de connexion au port série

Si vous avez un problème de connexion avec le port série de l’Arduino, il se peut que vous ayez une autorisation limitée.

Vérifier les droits d’accès au port série

ls -l /dev/ttyACM*

Output

crw-rw---- 1 root dialout 166, 0 févr. 15 12:47 /dev/ttyACM0

Voud devez alors accordez les droits en lecture et écriture

sudo chmod a+rw /dev/ttyACM0

Bonus: Installation de Arduino-CLI sur Raspberry Pi

Si votre système n’a pas accès à une interface graphique (headless, accès SSH), vous pouvez utiliser les lignes de commandes pour créer, compiler et téléverser vos scripts Arduino.

Un autre outil plus récent et plus puissant est Arduino-CLI que vous pouvez installer avec la commande

curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
mv bin/* Arduino/
cd Arduino
chmod a+x arduino-cli
alias arduino-cli='sudo ./arduino-cli'

Mettre à jour la liste des cartes supportées

arduino-cli core update-index

Installer ensuite le gestionnaire de cartes

arduino-cli core install arduino:avr

Vérifiez les cartes connectées au Raspberry Pi et noter les noms FQBN

arduino-cli board list

Créer un nouveau sketch

arduino-cli sketch new mysketch

Modifier le sketch avec le code désiré

nano mysketch/mysketch.ino
void setup() {
        Serial.begin(9600);
        Serial.println("System ready");
        delay(1000);
}

void loop() {
        Serial.println("System running");
        delay(500);
}

Compiler le code en spécifiant le nom de la carte (retrouvez le nom sous la colonne FQBN avec

arduino-cli compile --fqbn arduino:avr:mega mysketch/

Téléverser le code dans une carte

arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega mysketch/

Installer de nouvelles librairies

arduino-cli lib install library_name

Pour ouvrir un moniteur série, vous pouvez utiliser Putty

sudo apt-get install putty putty-tools
sudo putty /dev/ttyACM0 -serial -sercfg 9600,8,n,1,N &

Rajouter des URLs de gestion de cartes ESP32 et ESP8266

Créez un fichier de configuration

arduino-cli config init

Modifiez le fichier pour rajouter les url des cartes

sudo nano /root/.arduino15/arduino-cli.yaml
board_manager:
    additional_urls: [
    "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json",
    "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
]

Ensuite mettez à jour le gestionnaire

arduino-cli core update-index
acros2@acros2-desktop:~/Arduino$ arduino-cli core update-index
Downloading index: package_index.tar.bz2 downloaded                                                                                           
Downloading index: package_esp32_index.json downloaded                                                                                        
Downloading index: package_esp8266com_index.json downloaded                                                                                   
Downloading index: package_index.tar.bz2 downloaded                                                                                           
Downloading index: package_esp32_index.json downloaded                                                                                        
Downloading index: package_esp8266com_index.json downloaded 

Installer le gestionnaire pour ESP32 et ESP8266

arduino-cli core install esp32:esp32
arduino-cli core install esp8266:esp8266

Pour connaitre le nom fqbn à utliser en fonction de la carte arduino-cli board listall esp32

arduino-cli compile --fqbn esp32:esp32:esp32 mysketch
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:esp32 mysketch

Sources

Créer un Web Crawler avec Python

Créer un Web Crawler avec Python

Pour récolter des données sur internet, il est possible de créer un Web crawler ou Web scraping avec Python. Un robot d’exploration du Web est un outil qui permet d’extraire des données d’une ou plusieurs pages Web.

Configuration de l’environnement Python

Nous partons du principe que Python3 et pip sont installés sur votre machine. Vous pouvez également utiliser un environnement virtuel pour conserver un projet propre et maitriser les versions de librairies de votre web crawler Python.

Nous allons tout d’abord installer la librairie requests qui permet de faire des requêtes HTTP au serveur pour récupérer les données.

python -m pip install requests

Pour analyser et naviguer dans les données du Web, nous utilisons la librairie Beautiful Soup qui permet de travailler avec des scripts à balises comme le HTML ou le XML

python -m pip install beautifulsoup4

Enfin, nous installons la librairie Selenium qui permet d’automatiser les tâches d’un navigateur Web. Elle permet d’afficher des pages web dynamiques et de réaliser des actions sur l’interface. Cette librairie permet à elle seule de faire du Web scraping sur internet car elle peut travailler avec un site web dynamique qui fonctionne avec JavaScript.

python -m pip install selenium

Pour faire fonctionner Selenium avec Mozilla, vous aurez besoin de télécharger Geckodriver

Récupérer une page Web avec resquest

Imaginons que nous souhaitions récupérer les données techniques d’une carte Arduino, nous pouvons charger la page désirée avec requests et bs4

page = requests.get("https://docs.arduino.cc/hardware/uno-rev3/")
content = BeautifulSoup(page.text, 'html.parser')

En observant la structure de la page, vous pouvez repérer les balises, classes, identifiants ou textes qui vous intéressent. Dans cet exemple, nous récupérons

  • le nom de la carte
  • la description de la carte

N.B.: Vous pouvez retrouver la structure de la page web sur votre navigateur avec clique-droit sur la page puis « Inspecter »

import requests
from bs4 import BeautifulSoup

print("Starting Web Crawling ...")

#website to crawl
website="https://docs.arduino.cc/hardware/uno-rev3/"

#google search
#keywords = ["arduino","datasheet"]
#googlesearch = "https://www.google.com/search?client=firefox-b-d&q="
#search = "+".join(keywords)
#website = googlesearch+search

# get page
page = requests.get(website)

#extract html data
content = BeautifulSoup(page.text, 'html.parser')

# extract tags
h1_elms = content.find_all('h1')
print("Board : ",h1_elms)

#get element by class
description = content.find(class_="product-features__description").text
print("Description : ",description)
Starting Web Crawling ...
Board :  [<h1>UNO R3</h1>]
Description :  Arduino UNO is a microcontroller board based on the ATmega328P. It has 14 digital input/output pins (of which 6 can be used as PWM outputs), 6 analog inputs, a 16 MHz ceramic resonator, a USB connection, a power jack, an ICSP header and a reset button. It contains everything needed to support the microcontroller; simply connect it to a computer with a USB cable or power it with a AC-to-DC adapter or battery to get started. You can tinker with your UNO without worrying too much about doing something wrong, worst case scenario you can replace the chip for a few dollars and start over again.

On pourrait imaginer boucler cet opération sur une liste d’URL pour plusieurs cartes.

websites = [
    "https://docs.arduino.cc/hardware/uno-rev3/",
    "https://docs.arduino.cc/hardware/nano/",
    "https://docs.arduino.cc/hardware/mega-2560/",
    "https://docs.arduino.cc/hardware/leonardo/",
]

Avec cette méthode, on ne peut malheureusement pas charger la liste détaillé des spécification « Tech Specs » pour cela nous devons nous servir du navigateur.

Mettre en place un Web Crawler avec Selenium

Pour charger une page rien de plus facile

from selenium import webdriver

GECKOPATH = "PATH_TO_GECKO"
sys.path.append(GECKOPATH)

print("Starting Web Crawling ...")

#website to crawl
website="https://docs.arduino.cc/hardware/uno-rev3/"

#create browser handler
browser = webdriver.Firefox()
browser.get(website)

#browser.quit()

Validation des cookies

En affichant la page, vous aller certainement tomber sur la bannière de cookie qu’il faudra valider ou non pour continuer la navigation. Pour cela, il faut retrouver et cliquer sur le bouton « accepter »

def acceptcookies():
	"""class="iubenda-cs-accept-btn iubenda-cs-btn-primary"""
	browser.find_elements(By.CLASS_NAME,"iubenda-cs-accept-btn")[0].click()
acceptcookies()

Attente de chargement

Comme la page est affiché dans le navigateur, il faut un certain temps pour qu’elle charge les données et que toutes les balises soient affichées. Pour attendre le chargement, vous pouvez attendre un temps arbitraire

browser.implicitly_wait(10)

Ou attendre qu’une balise particulière soit présente comme le bouton d’acceptation des cookies

from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

def waitForElement(locator, timeout ):
    elm = WebDriverWait(browser, timeout).until(expected_conditions.presence_of_element_located(locator))
    return elm

myElem =waitForElement((By.CLASS_NAME , 'iubenda-cs-accept-btn'),30)

N.B: Si vous rencontrez d’autre problème (élément inconnu , bouton non cliquable, etc.) dans le script alors qu’il n’y a pas de soucis sur la page Web, n’hésitez pas à utiliser la fonction time.sleep()

Chercher et appuyer sur un élément DOM

Pour afficher les spécifications techniques, le script doit cliquer sur l’onglet ‘Tech Specs’. Il faut donc trouver l’élément à partir du texte. Pour cela, il y a deux méthodes: tester le texte de l’élément ou utiliser Xpath

#get element by text 
btn_text = 'Tech Specs'
btn_elms = browser.find_elements(By.CLASS_NAME,'tabs')[0].find_elements(By.TAG_NAME,'button')
for btn in btn_elms:
     if btn.text == btn_text:
          btn.click()

spec_btn = browser.find_element(By.XPATH, "//*[contains(text(),'Tech Specs')]")
spec_btn.click()

Récupérer les données désirées

Une fois la page souhaitée chargée, vous pouvez récupérer les données

Soit toutes les données qui sont affichées sous forme de tableau

#get all rows and children
print("Tech specs")
print("-------------------------------------")

tr_elms = browser.find_elements(By.TAG_NAME,'tr')

for tr in tr_elms:
     th_elms = tr.find_elements(By.XPATH, '*')
     if len(th_elms)>1:
        print(th_elms[0].text, " : ", th_elms[1].text)

Soit une donnée spécifique

#get parent and siblings
print("Specific data")
print("-------------------------------------")
data_row = browser.find_element(By.XPATH, "//*[contains(text(),'Main Processor')]")
data = data_row.find_element(By.XPATH, "following-sibling::*[1]").text
print(data_row.text, " : ", data)

Résultat du crawling des spécifications

Starting Web Crawling ...
Page is ready!
Tech specs
-------------------------------------
Name  :  Arduino UNO R3
SKU  :  A000066
Built-in LED Pin  :  13
Digital I/O Pins  :  14
Analog input pins  :  6
PWM pins  :  6
UART  :  Yes
I2C  :  Yes
SPI  :  Yes
I/O Voltage  :  5V
Input voltage (nominal)  :  7-12V
DC Current per I/O Pin  :  20 mA
Power Supply Connector  :  Barrel Plug
Main Processor  :  ATmega328P 16 MHz
USB-Serial Processor  :  ATmega16U2 16 MHz
ATmega328P  :  2KB SRAM, 32KB FLASH, 1KB EEPROM
Weight  :  25 g
Width  :  53.4 mm
Length  :  68.6 mm
Specific data
-------------------------------------
Main Processor  :  ATmega328P 16 MHz
PS D:\Formation\Python\WebCrawler> 

Récupérer des données sur différentes pages

Une fois que vous maitrisez ces outils et avez une bonne idée des données à récupérer et de la structure des pages Web, vous pouvez scraper des données sur plusieurs pages. Dans ce dernier exemple, nous récupérons les données techniques de différentes cartes Arduino. Pour cela, nous créons une boucle qui va exécuter le code précédent sur une liste de site

websites = [
    "https://docs.arduino.cc/hardware/uno-rev3/",
    "https://docs.arduino.cc/hardware/nano/",
    "https://docs.arduino.cc/hardware/mega-2560/",
    "https://docs.arduino.cc/hardware/leonardo/",
]

import sys
import time
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By

from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

GECKOPATH = "D:\\AranaCorp\\Marketing\\Prospects"
sys.path.append(GECKOPATH)

print("Starting Web Crawling ...")

websites = [
    "https://docs.arduino.cc/hardware/uno-rev3/",
    "https://docs.arduino.cc/hardware/nano/",
    "https://docs.arduino.cc/hardware/mega-2560/",
    "https://docs.arduino.cc/hardware/leonardo/",
]

#create browser handler
browser = webdriver.Firefox()#Firefox(firefox_binary=binary)
def acceptcookies():
    #class="iubenda-cs-accept-btn iubenda-cs-btn-primary
    browser.find_elements(By.CLASS_NAME,"iubenda-cs-accept-btn")[0].click()
     
def waitForElement(locator, timeout ):
    elm = WebDriverWait(browser, timeout).until(expected_conditions.presence_of_element_located(locator))
    return elm

cookie_accepted=False
for website in websites:
    browser.get(website)
    time.sleep(2)

    if not cookie_accepted: #accept cookie once
        myElem =waitForElement((By.CLASS_NAME , 'iubenda-cs-accept-btn'),30)
        print("Page is ready!")
        acceptcookies()
        cookie_accepted = True
    else:
         myElem =waitForElement((By.CLASS_NAME , 'tabs__item'),30)
         
    #get board name
    name = browser.find_element(By.TAG_NAME,'h1').text

    #get tab Tech Specs
    btn_text = 'Tech Specs'
    spec_btn = WebDriverWait(browser, 20).until(expected_conditions.element_to_be_clickable((By.XPATH, "//*[contains(text(),'{}')]".format(btn_text))))
    spec_btn.click()
    #browser.execute_script("arguments[0].click();", spec_btn) #use script to click

    #get all rows and children
    print(name+" "+btn_text)
    print("-------------------------------------")

    tr_elms = browser.find_elements(By.TAG_NAME,'tr')

    for tr in tr_elms:
        th_elms = tr.find_elements(By.XPATH, '*')
        if len(th_elms)>1:
            print(th_elms[0].text, " : ", th_elms[1].text)


    #get parent and siblings
    print("Specific data")
    print("-------------------------------------")
    try:
        data_row = browser.find_element(By.XPATH, "//*[contains(text(),'Main Processor')]")
    except:
        data_row = browser.find_element(By.XPATH, "//*[contains(text(),'Processor')]")  
    data = data_row.find_element(By.XPATH, "following-sibling::*[1]").text
    print(data_row.text, " : ", data)

browser.quit()
Starting Web Crawling ...
Page is ready!
UNO R3 Tech Specs
-------------------------------------
Main Processor  :  ATmega328P 16 MHz
Nano Tech Specs
-------------------------------------
Processor  :  ATmega328 16 MHz
Mega 2560 Rev3 Tech Specs
-------------------------------------
Main Processor  :  ATmega2560 16 MHz
Leonardo Tech Specs
-------------------------------------
Processor  :  ATmega32U4 16 MHz

Combiner Selenium et BeautifulSoup

Il est possible de combiner les deux librairies afin de vous apporter toutes leurs fonctionnalités

from bs4 import BeautifulSoup
from selenium import webdriver

browser = webdriver.Firefox()
browser.get(website)

html = browser.page_source
content = BeautifulSoup(html, 'lxml')

browser.quit()

Applications

  • Automatiser des tâches de relevée de données sur le Web
  • Créer votre banque d’image pour l’entrainement de réseau de neurone
  • Trouver des prospect
  • Faire un étude de marché

Sources

Créer une application de bureau avec Electron

Créer une application de bureau avec Electron

Nous allons voir dans ce tutoriel comment créer une application de bureau avec le framework Electron. Cet environnement de programmation permet de développer des IHM à l’aide des langages Web JavaScript, HTML et CSS compatible sur plusieurs OS.

Mise en place de l’environnement de programmation

Pour ce tutoriel, nous utilisons l’éditeur de code VS Code.

Le framework Electron fonctionne avec le logiciel Node.js. Téléchargez et installez la dernière version

ou entrez les commandes suivantes sur Linux

sudo apt-get install nodejs npm

Vérifier les versions installées. Cela vous servira pour la compatibilité des librairies

node -v
npm -v

Dans le terminal de VSCode, créer votre répertoire de travail

mkdir ElectronApp && cd ElectronApp

Dans le répertoire de l’application, entrer la commande d’initialisation pour créer les fichiers par défaut

npm init

Entrez les informations demandées. Notez que Electron prend le fichier main.js en point d’entrée (entry point).

Press ^C at any time to quit.
package name: (electronapp)
version: (1.0.0)
description: custom electron app for dev and tutorials
entry point: (index.js) main.js
test command:
git repository:
keywords:
author: aranacorp
license: (ISC) MIT
About to write to D:\Formation\Javascript\Electron\ElectronApp\package.json:

{
  "name": "electronapp",
  "version": "1.0.0",
  "description": "custom electron app for dev and tutorials",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "electron ."
  },
  "author": "aranacorp",
  "license": "MIT"
}

Vous pouvez ensuite installer le paquet electron

npm install --save-dev electron

Une fois le fichier package.json créé, ajouter la ligne suivante dans scripts

    "start": "electron ."

Ce scirpt permet de lancer l’application à l’aide la commande

npm start

Création de la première application Electron

Dans cet exemple, nous allons récupérer les versions de node et electron comme dans le tutoriel de base. Nous allons également ajouter un champ de saisie pour définir une éventuelle URL.

Pour créer l’application, nous allons créer 5 fichiers

  • main.js gère le cycle de vie de l’application
  • index.html contient la page HTML
  • ./src/ac_style.css contient le style de la page HTML
  • preload.js contient le script de préchargement, notamment les fonctions de communication inter-processus
  • renderer.js contient les fonctions qui gère la page HTML

  • main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      //icon:'./src/logo_araignee.png',
      webPreferences: {
        preload: path.join(__dirname, 'preload.js')
      }
    })

    ipcMain.on('set-url', (event, url) => {
        console.log("url set to: ",url)
      })

    //win.webContents.openDevTools() //Open devtools on launch
    win.setTitle(app.getName()+"@"+app.getVersion());
    win.loadFile('index.html')
  }

  app.whenReady().then(() => {
    createWindow()

    app.on('activate', () => { //for macOS
        if (BrowserWindow.getAllWindows().length === 0) createWindow()
      })
  })

  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') app.quit()
  })

  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
	<link rel="stylesheet" href="./src/ac_style.css">
  </head>
  <body>
    <h1>Hello World!</h1>
    <p>Nous utilisons les versions: </p>
    <ul>
      <li>Node.js : <span id="node-version"></span></li>
      <li>Chromium : <span id="chrome-version"></span></li>
      <li>Electron : <span id="electron-version"></span></li>
  </ul>
  <p>CurrentURL: <span id="current-url">None</span></p>
  <p>URL: <input id="url"/> <button id="btn" type="button">Set</button></p>
  <script src="./renderer.js"></script>
  </body>
</html>
  • ./src/ac_style.css
:root {
  --bg-color: #161616;
  --btn-color: #346751;
  --title-color: #3aaa35; /*#c84b31; #ff9900*/
  --text-color: #ecdbba;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

h1 {
  color: var(--title-color);
}

p {
  font-size: 18px;
  margin-bottom: 20px;
}
  • preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  setURL: (url) => ipcRenderer.send('set-url', url)
})

window.addEventListener('DOMContentLoaded', () => {
    const replaceText = (selector, text) => {
      const element = document.getElementById(selector)
      if (element) element.innerText = text
    }
    for (const dependency of ['chrome', 'node', 'electron']) {
      replaceText(`${dependency}-version`, process.versions[dependency])
    }
  })
  • renderer.js
const urlInput = document.getElementById('url')
const currURL = document.getElementById('current-url')

const setButton = document.getElementById('btn')
setButton.addEventListener('click', () => {
  const url = urlInput.value
  window.electronAPI.setURL(url)
  currURL.innerHTML = url
})

Résultat

Une fois le script lancé à l’aide de « npm start », une fenêtre s’ouvre. Vous pouvez mettre à jour l’URL à l’aide du champ de saisie et du bouton « Set ».

Créer un exécutable pour l’application

Pour créer l’exécutable nous utilisons le paquet electron-packager

npm install --save-dev electron-packager

Vous pouvez ensuite compiler votre projet à l’aide de la commande

npx electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]

Exemple pour Windows

npx electron-packager . myElectronApp --overwrite --asar --platform=win32 --arch=x64 --icon=./src/logo_araignee.ico --prune=true --out=release-builds

Exemple pour Linux

npx electron-packager . myElectronApp --overwrite --asar --platform=linux --arch=x64 --icon=./src/logo_araignee.ico --prune=true --out=release-builds
OSplatform architecture
Windowswin32x86, x86_64, and arm64
Linuxlinuxx86, x86_64, armv7l, arm64, and mips64el
macOSdarwinx86_64, arm64, and universal

Dans le fichier package.json, ajoutez ces deux scripts comme raccourcis;

    "build": "electron-packager . myElectronApp --overwrite",
    "clean": "rm -rf release-builds"

Vous pouvez créer un exécutable avec la commande

npm run build

ou effacer les dossiers générés avec

npm run clean

Vous pouvez maintenant lancer l’application à partir du fichier exécutable myElectronApp.exe

Bonus: Créer une application avec Electron et React

Nous allons créer la même application avec Electron et React. Pour cela, nous créons un nouveau projet

npm init electron-app@latest react-electron-app -- --template=webpack
cd react-electron-app
npm install --save-dev @babel/core @babel/preset-react babel-loader

Modifiez ensuite le fichier webpack.rules.js

  {
    test: /\.jsx?$/,
    use: {
      loader: 'babel-loader',
      options: {
        exclude: /node_modules/,
        presets: ['@babel/preset-react']
      }
    }
  }

npm install –save react react-dom

  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>

  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  • renderer.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./app.jsx";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  • app.jsx
import React from "react";

class App extends React.Component {
  constructor(props) {    
        super(props);    
        this.state = {      
            url: "None",    
        };  
    }

    setUrl = () => {
        //() => {this.setState({url: document.getElementById("url").value})}
        this.setState({url: document.getElementById("url").value})
    }

    render() {
    return (
        <div>
        
        <h1>Hello World!</h1>
        <p>Welcome to your Electron application.</p>
        <p>CurrentURL: <span id="current-url">{this.state.url}</span></p>
        <p>URL: <input id="url" /> <button id="btn" type="button" onClick={this.setUrl}>Set</button></p>
    
        </div>
    );
    }
}

export default App;

Vous pouvez ensuite lancer l’application avec la commande

npm start

Pour créer l’exécutable

npm run make

Sources

Utilisation des WebSockets avec React Native

Utilisation des WebSockets avec React Native

Nous allons créer une application React Native qui servira de client Websockets et pourra communiquer avec un serveur distant. WebSockets est un protocole populaire de communication web simple et robuste permettant la communication en temps réel entre client et serveur.

Matériel

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

Configuration du projet React Native

Pour communiquer via Websocket nous allons utiliser la librairie Websocket, react-use-websocket

npm install --save react-use-websocket

Utilisation de la librairie

A partir de la librairie, nous importons les objets et fonctions qui nous intéresse

import useWebSocket, { ReadyState } from 'react-use-websocket';
  • ReadyState état de la connexion avec le serveur
  • useWebScoket permet d’initialiser une connexion websockets

Nous créons ensuite un composant fonctionnel avec les états désirés

  const [ipAddress, setIpAddress] = useState('');
  const [ipServer, setIpServer] = useState('ws://192.168.1.52:8765');
  const [messageText, setMessageText] = useState("");
  const [messageHistory, setMessageHistory] = useState([]);
  const { sendMessage, sendJsonMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(ipServer);

  const connectionStatus = {
    [ReadyState.CONNECTING]: 'Connecting',
    [ReadyState.OPEN]: 'Open',
    [ReadyState.CLOSING]: 'Closing',
    [ReadyState.CLOSED]: 'Closed',
    [ReadyState.UNINSTANTIATED]: 'Uninstantiated',
  }[readyState];

  const paramCmd = {
    type: 'setParam',
    param1: 30,
    param2: 2.3,
  }

Dans les états nous récupérons les fonctions et états de useWebSokcet

  • sendMessage la fonction pour envoyer des messages au format String
  • sendJsonMessage pour pour envoyer des messages au format JSON
  • lastMessage contient la réponse du serveur au format String
  • lastJsonMessage contient le dernier message du serveur au format JSON
  • readyState contient l’état de la connexion

Nous définissons aussi une constante au format Json paramCmd.

Nous utilisons un hook useEffect dans lequel nous récupérons l’adresse IP de l’appareil et nous gérons les messages reçus du serveur.

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

    fetchIpAddress();
	
    if (lastMessage !== null) {
      setMessageHistory((prev) => prev.concat(lastMessage));
    }
    if (lastJsonMessage !== null) {
      console.log(JSON.stringify(lastJsonMessage));
    }
    return () => {
    };
  }, [lastMessage, setMessageHistory,lastJsonMessage]);

Enfin, nous créons le rendu de l’application avec les éléments suivant

  • un texte pour afficher l’adresse IP de l’appareil
  • un bouton pour envoyer un message JSON
  • zone de texte pour le texte à écrire pour écrire le message au format texte
  • bouton Send pour envoyer le message
  • zone de texte pour entrer l’adresse du serveur
  • zone de texte pour afficher les réponses du serveur
  return (
    <View style={styles.mainBody}>
      <Text
        style={styles.mainTitle}>
        AC Websocket Terminal
      </Text>
      <ScrollView>

      <View style={styles.deviceItem}>
                      <View style={{flex:2}}>
                        <Text style={styles.deviceName}>{ipAddress}</Text>
                        <Text style={styles.deviceInfo}>{connectionStatus}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() => sendJSON()}
                        disabled={readyState !== ReadyState.OPEN}
                        style={styles.deviceButton}>
                        <Text
                          style={styles.buttonText}>
                          Send JSON
                        </Text>
                      </TouchableOpacity>
                    </View>


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

      <TextInput
        placeholder="Server IP"
        onChangeText={setIpServer}
        value={ipServer}
      />
      <View style={{flex:1,minHeight:200}}>
      <Text>Received Message:</Text>
              <ScrollView style={styles.textOutput}>
                {lastMessage ? <Text>last message : {lastMessage.data}</Text> : null}
                
                {messageHistory.map((message, idx) => (
                  <Text key={idx}>{message ? message.data : null}</Text>
                ))}
              </ScrollView>
              </View>
      </ScrollView>
    </View>
  );

Création d’un serveur WebSockets avec Python

Pour tester notre application, nous créons un serveur websockets sur le PC

#!/usr/bin/env python
# python3 -m pip install websockets

import json
import asyncio
from websockets.server import serve

def getIpAddress():
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    ipaddress = s.getsockname()[0]
    s.close()
    return ipaddress

ipaddress = getIpAddress()
port = 8765

async def echo(websocket):
    async for message in websocket:
        print("received from {}:{} : ".format(websocket.remote_address[0],websocket.remote_address[1]) + message)
        
        if('{' not in message):
            await websocket.send(message)
        else:
            request = json.loads(message)
            answer = {}
            if(request['type'] == 'setParam'):
                answer['type'] = request['type']
                if(request['param1']<100 and request['param2']>1.2):
                    answer['valid'] = True
                    for key, val in request.items():
                        print("\t"+key+": ", val)
                else:
                    answer['valid'] = False
            else:
                answer['type'] = 'unknown'
                answer['valid'] = False
                
            await websocket.send(json.dumps(answer))

async def main():
    print("Server is activated on ws://{}:{}".format(ipaddress,port))
    #async with serve(echo, "localhost", 8765):
    async with serve(echo, "0.0.0.0", port):
        await asyncio.Future()  # run forever

asyncio.run(main())

Résultat

Grâce à cette application, nous pouvons envoyer des String et JSON au serveur et afficher ses réponses

Code complet de communication WebSockets avec React Native

/**
 * https://reactnative.dev/docs/network
 * https://www.npmjs.com/package/react-use-websocket
 * https://github.com/robtaussig/react-use-websocket
 * test on python
 * https://websockets.readthedocs.io/en/stable/
 */

import React, {useState, useEffect, useCallback} from 'react';
import {   
  View, 
  ScrollView, 
  Text,
  TextInput,
  TouchableOpacity, 
  StyleSheet} from 'react-native';
import { NetworkInfo } from 'react-native-network-info'
import useWebSocket, { ReadyState } from 'react-use-websocket';



const WebsocketTerminal = () =>  {

  const [ipAddress, setIpAddress] = useState('');
  const [ipServer, setIpServer] = useState('ws://192.168.1.52:8765');
  const [messageText, setMessageText] = useState("");
  const [messageHistory, setMessageHistory] = useState([]);
  const { sendMessage, sendJsonMessage, lastMessage, lastJsonMessage, readyState } = useWebSocket(ipServer);


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

    fetchIpAddress();
	
    if (lastMessage !== null) {
      setMessageHistory((prev) => prev.concat(lastMessage));
    }
    if (lastJsonMessage !== null) {
      console.log(JSON.stringify(lastJsonMessage));
    }
    return () => {
    };
  }, [lastMessage, setMessageHistory,lastJsonMessage]);

  const connectionStatus = {
    [ReadyState.CONNECTING]: 'Connecting',
    [ReadyState.OPEN]: 'Open',
    [ReadyState.CLOSING]: 'Closing',
    [ReadyState.CLOSED]: 'Closed',
    [ReadyState.UNINSTANTIATED]: 'Uninstantiated',
  }[readyState];

  const sendJSON = () => {
    const paramCmd = {
      type: 'setParam',
      param1: 30,
      param2: 2.3,
    }

    sendJsonMessage(paramCmd);

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

      <View style={styles.deviceItem}>
                      <View style={{flex:2}}>
                        <Text style={styles.deviceName}>{ipAddress}</Text>
                        <Text style={styles.deviceInfo}>{connectionStatus}</Text>
                      </View>
                      <TouchableOpacity
                        onPress={() => sendJSON()}
                        disabled={readyState !== ReadyState.OPEN}
                        style={styles.deviceButton}>
                        <Text
                          style={styles.buttonText}>
                          Send JSON
                        </Text>
                      </TouchableOpacity>
                    </View>


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

      <TextInput
        placeholder="Server IP"
        onChangeText={setIpServer}
        value={ipServer}
      />
      <View style={{flex:1,minHeight:200}}>
      <Text>Received Message:</Text>
              <ScrollView style={styles.textOutput}>
                {lastMessage ? <Text>last message : {lastMessage.data}</Text> : null}
                
                {messageHistory.map((message, idx) => (
                  <Text key={idx}>{message ? message.data : null}</Text>
                ))}
              </ScrollView>
              </View>
      </ScrollView>
    </View>
  );
}

export default WebsocketTerminal;


let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8
var styles = StyleSheet.create({

  mainBody: { flex: 1, justifyContent: 'center',  backgroundColor: BACKGROUND_COLOR},

  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,
    },

    deviceItem: {
      flexDirection: 'row',
      flex: 3,
      marginBottom: 2,
    },
    deviceName: {
      fontSize: 14,
      fontWeight: 'bold',
    },
    deviceInfo: {
      fontSize: 8,
    },
    deviceButton: {
      backgroundColor: '#2196F3',
      padding: 10,
      borderRadius: 10,
      margin: 2,
      paddingHorizontal: 20,
    },

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

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

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

});

Sources

Lire et écrire un fichier avec React Native

Lire et écrire un fichier avec React Native

Nous allons créer une application React Native qui permet de lire ou écrire un fichier dans un espace de stockage interne ou externe. Avoir accès au fichier du système peut être intéressant pour sauvegarder les données d’une session de l’application à une autre ou pour utiliser ou modifier les données d’un fichier.

Matériel

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

Configuration du projet React Native

Pour venir lire et écrire dans un fichier nous allons utiliser la librairie File System, react-native-fs

npm install --save react-native-fs

Utilisation de librairie

A partir de la librairie, nous importons les objets et fonctions qui nous intéresse

import { DocumentDirectoryPath, writeFile, readDir, readFile, unlink } from 'react-native-fs'; //send files for testing
  • DocumentDirectoryPath permet de récupérer le chemin d’accès des fichiers de l’application
  • writeFile permet d’écrire dans un fichier
  • readDir permet de lire un répertoire
  • readFile permet de lire un fichier
  • unkink permet de supprimer un fichier

Nous créons ensuite un composant fonctionnel avec les états désirés

const FileSysComp = () =>  {
  const [dirPath, setDirPath] = useState(DocumentDirectoryPath);
  const [filename, setFilename] = useState("myfile.txt");
  const [textToWrite, setTextToWrite] = useState("");
  const [textFromFile, setTextFromFile] = useState("");
  const [fileInDir, setFileInDir] = useState([]);

Nous ajoutons ensuite les fonctions utiles pour le système de fichier lire, écrire, supprimer

	const makeFile = async (filePath : string, content : string) => {
	  try {
		  //create a file at filePath. Write the content data to it
		  await writeFile(filePath, content, "utf8");
		  console.log("written to file");
	  } catch (error) { //if the function throws an error, log it out.
		  console.log(error);
	  }
	};

	const getFile = async (filePath : string) => {
	  try{
		  const response = await readFile(filePath);
		  setTextFromFile(response)
		  console.log("File read : ",response)
	  } catch (error) {
		  console.log(error);
	  }
	};

  const deleteFile = async (filePath : string) => {
    try {
      await unlink(filePath); //delete the item present at 'path'
      console.log("deleted : ",filePath);
    } catch (error) {
      console.log(error);
    }
  };

Lors de la création du composant, nous venons lire le répertoire défini par défaut

  useEffect(() => {
    /* file system*/
    console.log("FS directory path : ",dirPath);
    readDir(dirPath).then((files) => {
      console.log("files on FS: ",files);
      setFileInDir(files);
    });
  }, [filename,dirPath]);

Enfin, nous créons le rendu de l’application avec les éléments suivant

  • barre d’entrée pour spécifier le répertoire
  • barre d’entrée pour spécifier le nom du fichier
  • zone de texte pour le texte à écrire
  • bouton Write pour écrire le texte dans le fichier
  • zone de texte pour afficher les fichiers et sous-répertoires contenu dans le répertoire
  • bouton Read pour lire le texte contenu dans le fichier
  • zone de texte pour afficher le texte lu
  • bouton Delete pour effacer le fichier spécifié
  return (
    <View style={styles.mainBody}>
      <Text
        style={styles.mainTitle}>
        AC File System
      </Text>


      <View style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Directory"
          value={dirPath}
          onChangeText={setDirPath} />
      </View>
      <View style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Filename"
          value={filename}
          onChangeText={setFilename} />
      </View>

      <View
        style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Text to write"
          value={textToWrite}
          onChangeText={setTextToWrite} />
        <TouchableOpacity
          onPress={() => makeFile(dirPath + "/" + filename, textToWrite)}
        style={[styles.sendButton]}>
        <Text
          style={styles.buttonText}>
          WRITE
        </Text>
      </TouchableOpacity>
    </View>
    
    
        <Text>Directory:</Text>

        <ScrollView style={styles.textOutput}>
          {fileInDir.length>0 ? fileInDir.map((filedir, idx) => (
            <Text key={idx}>{filedir.name}</Text>
          )) : <Text>Folder empty or unknown</Text>}
        </ScrollView>


        <View style={{ flexDirection: 'row', justifyContent:'space-between'}}>
          <Text>Text from file:</Text>
          <TouchableOpacity
            onPress={() => getFile(dirPath + "/" + filename)}
            style={[styles.sendButton]}>
            <Text
              style={styles.buttonText}>
              READ
            </Text>
          </TouchableOpacity>
        </View>
        <ScrollView style={styles.textOutput}>
          {textFromFile ? <Text>{textFromFile}</Text> : null}
        </ScrollView>


        <TouchableOpacity
            onPress={() => deleteFile(dirPath + "/" + filename)}
            style={[styles.sendButton]}>
            <Text
              style={styles.buttonText}>
              DELETE
            </Text>
          </TouchableOpacity>

      
    </View>
  );

Résultat

Grâce à cette application, nous pouvons afficher le contenu d’un répertoire, lire, écrire et supprimer un fichier

Lire et écrire un fichier dans un espace de stockage externe

Par défaut, une application n’a d’autorité que sur son propre répertoire

Il est possible de donner les droits d’accès à un espace de stockage externe comme une clé USB.

Pour se faire, vous pouvez accorder les droits depuis l’interface Android. Aller dans les paramètres > App et notifications > Gestionnaire des autorisations > Fichiers et contenus multimédia

Brancher la clé USB à votre appareil, à l’aide d’adb repérer le chemin d’accès de l’espace de stockage dans ls /storage/ (ici: /storage/70A8-C229)

Une fois l’autorisation activée, vous pourrez afficher et créer des fichiers sur la clés USB

Code complet de gestion de fichier avec React Native

/**
 * https://www.npmjs.com/package/react-native-fs?activeTab=readme
 * https://github.com/itinance/react-native-fs
 */

import React, {useState, useEffect, useCallback} from 'react';
import {   
  View, 
  ScrollView, 
  Text,
  TextInput,
  TouchableOpacity, 
  StyleSheet,
  Alert} from 'react-native';
import { DocumentDirectoryPath, writeFile, readDir, readFile, unlink } from 'react-native-fs'; //send files for testing

let storagePath = "/storage/70A8-C229"

/**
 * Maincomp
 */
const FileSysComp = () =>  {
  const [dirPath, setDirPath] = useState(storagePath) //DocumentDirectoryPath);
  const [filename, setFilename] = useState("myfile.txt");
  const [textToWrite, setTextToWrite] = useState("");
  const [textFromFile, setTextFromFile] = useState("");
  const [fileInDir, setFileInDir] = useState([]);

	const makeFile = async (filePath : string, content : string) => {
	  try {
		  //create a file at filePath. Write the content data to it
		  await writeFile(filePath, content, "utf8");
		  console.log("written to file");
	  } catch (error) { //if the function throws an error, log it out.
		  console.log(error);
      Alert.alert("Cannot write file. Permission denied")
	  }
	};

	const getFile = async (filePath : string) => {
	  try{
		  const response = await readFile(filePath);
		  setTextFromFile(response)
		  console.log("File read : ",response)
	  } catch (error) {
		  console.log(error);
	  }
	};

  const deleteFile = async (filePath : string) => {
    try {
      await unlink(filePath); //delete the item present at 'path'
      console.log("deleted : ",filePath);
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    /* file system*/
    console.log("FS directory path : ",dirPath);
    readDir(dirPath).then((files) => {
      console.log("files on FS: ",files);
      setFileInDir(files);
    }).catch((e) =>{
      console.log("foler does not exist");
      setFileInDir([])
    });
  }, [filename,dirPath]);

  return (
    <View style={styles.mainBody}>
      <Text
        style={styles.mainTitle}>
        AC File System
      </Text>

      <View style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Directory"
          value={dirPath}
          onChangeText={setDirPath} />
      </View>
      <View style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Filename"
          value={filename}
          onChangeText={setFilename} />
      </View>

      <View
        style={styles.inputBar}>
        <TextInput
          style={styles.textInput}
          placeholder="Text to write"
          value={textToWrite}
          onChangeText={setTextToWrite} />
        <TouchableOpacity
          onPress={() => makeFile(dirPath + "/" + filename, textToWrite)}
        style={[styles.sendButton]}>
        <Text
          style={styles.buttonText}>
          WRITE
        </Text>
      </TouchableOpacity>
    </View>
    
    
        <Text>Directory:</Text>

        <ScrollView style={styles.textOutput}>
          {fileInDir.length>0 ? fileInDir.map((filedir, idx) => (
            <Text key={idx}>{filedir.name}</Text>
          )) : <Text>Folder empty or unknown</Text>}
        </ScrollView>


        <View style={{ flexDirection: 'row', justifyContent:'space-between'}}>
          <Text>Text from file:</Text>
          <TouchableOpacity
            onPress={() => getFile(dirPath + "/" + filename)}
            style={[styles.sendButton]}>
            <Text
              style={styles.buttonText}>
              READ
            </Text>
          </TouchableOpacity>
        </View>
        <ScrollView style={styles.textOutput}>
          {textFromFile ? <Text>{textFromFile}</Text> : null}
        </ScrollView>


        <TouchableOpacity
            onPress={() => deleteFile(dirPath + "/" + filename)}
            style={[styles.sendButton]}>
            <Text
              style={styles.buttonText}>
              DELETE
            </Text>
          </TouchableOpacity>

    </View>
  );
}

export default FileSysComp;


let BACKGROUND_COLOR = "#161616"; //191A19
let BUTTON_COLOR = "#346751"; //1E5128
let ERROR_COLOR = "#C84B31"; //4E9F3D
let TEXT_COLOR = "#ECDBBA"; //D8E9A8
var styles = StyleSheet.create({

  mainBody: { flex: 1, justifyContent: 'center',  backgroundColor: BACKGROUND_COLOR},

  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,
    right:0,
    },

    deviceItem: {
      flexDirection: 'row',
      flex: 3,
      marginBottom: 2,
    },
    deviceName: {
      fontSize: 14,
      fontWeight: 'bold',
    },
    deviceInfo: {
      fontSize: 8,
    },
    deviceButton: {
      backgroundColor: '#2196F3',
      padding: 10,
      borderRadius: 10,
      margin: 2,
      paddingHorizontal: 20,
    },

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

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

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

});

Sources