fbpixel
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

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

Communication entre serveur et client WebSockets avec Python

Communication entre serveur et client WebSockets avec Python

Nous allons voir comment mettre en place une communication entre un serveur et un client en utilisant le protocole Websockets sous Python. WebSockets est un protocole de communication web simple et robuste permettant la communication en temps réel.

Installation de la librairie Websockets

Pour utiliser les WebSockets avec Python, nous installons le paquet nécessaire..

python3 -m pip install websockets

Nous utilisons aussi la librairie asyncio qui permet de faire de la programmation asynchrone pour le développement de serveur performant

Récupérer l’adresse IP du serveur

Comme dans toute communication internet, pour établir une connexion entre le client et le serveur, il faut connaître l’addresse IP du serveur.

Pour récupérer l’adresse IP de la machine serveur vous pouvez utiliser les commandes ipconfig (Windows) ou ifconfig/ ip addr (Linux) (ici: 192.168.1.59)

Il est aussi possible d’utiliser le paquet socket dans le script Python

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()

Notez l’adresse, vous l’utiliserez dans le code client.

Code Python pour le serveur WebSocket

Pour lancer le serveur, nous définissons une fonction main qui va ouvrir le serveur sur le port 8765 et le faire tourner en boucle. Nous appelons aussi la fonction callback echo() qui va gérer la réception de message. Dans cet exemple, nous renvoyons le message tel quel au client.

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

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)
		await websocket.send(message)

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())

N.B.: pour que le serveur soit visible sur le réseau extérieur à l’ordinateur vous devez spécifier l’adresse “0.0.0.0” à la place de “localhost”

Code Python pour le client WebSocket

Dans le code client, nous nous connectons au serveur en spécifiant l’adresse IP et le port. Puis, nous envoyons un message. Enfin on attend le message de réponse pour l’afficher.

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

import asyncio
from websockets.sync.client import connect

def hello():
    #with connect("ws://localhost:8765") as websocket:
    with connect("ws://192.168.1.52:8765") as websocket:
    
        websocket.send("Hello world!")
        message = websocket.recv()
        print(f"Received from server : {message}")

hello()

Résultat

Dans un terminal, lancez d’abord le script serveur: python websocket_server.py

Dans un second terminal, lancez ensuite le script client ; python websocket_client.py

Échange de message JSON via WebSocket

Le format JSON(JavaScript Object Notation) est un format d’échange de donnée très populaire dans la communication web. NousPour échanger des données JSON entre serveur et client nous utilisons la libraire pré-installée json

Dans le script client, nous allons envoyer une requête au format JSON. Le serveur va recevoir cette requête vérifier les valeurs et envoyer une réponse avec le résultat de la validation.

Les fonctions à connaitre pour gérer le format JSON sont:

  • json.loads() pour passer d’un String à un dict
  • json.dumps() pour passer d’un dict à un String

D’autre format Python peuvent être converti en leur équivalent JSON

PythonJSON
dictobject
list, tuplearray
strstring
int, float, intnumber
Truetrue
Falsefalse
Nonenull

script client

#!/usr/bin/env python
# python3 -m pip install websockets
import json
import asyncio
from websockets.sync.client import connect

jsondata = {"type": "setParam", "param1": 30, "param2": 2.3}
jsonwrong = {"type": "setParam", "param1": 30, "param2": -2.3}
jsonunkn = {"type": "setData", "param1": 30, "param2": 2.3}

def hello():
	with connect("ws://192.168.1.52:8765") as websocket:
		websocket.send("Hello world!")
		message = websocket.recv()
		print(f"Received from server : {message}")

		websocket.send(json.dumps(jsondata))
		message = websocket.recv()
		print(f"Received from server : {message}")

		websocket.send(json.dumps(jsonwrong))
		message = websocket.recv()
		print(f"Received from server : {message}")
				
		websocket.send(json.dumps(jsonunkn))
		message = websocket.recv()
		print(f"Received from server : {message}")

hello()

script serveur

#!/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

Sources

Créer un script Python sous ROS2

Créer un script Python sous ROS2

Dans ce tutoriel, nous allons voir comment créer et lancer des script Python sous ROS2. Vous pourrez ainsi créer vos propres noeuds et commencer à développer sous ROS.

Créer un espace de travail

Une bonne pratique pour développer sous ROS2 est de créer des workspaces dans lesquels on va installer les paquets désirés, séparés de l’installation principale.

Installation de colcon

sudo apt install python3-colcon-common-extensions

Créer le dossier

mkdir -p ~/tuto_ws/src
cd ~/tuto_ws

Copier ensuite un projet type, comme tutoriel contenant turtlesim

git clone https://github.com/ros/ros_tutorials.git -b iron

Puis construisez le projet à l’aide de la commande

colcon build

Pour charger votre workspace dans un nouveau terminal

source /opt/ros/iron/setup.bash
cd ~/ros2_ws
source install/local_setup.bash

N.B.: vous pouvez mettre les commandes suivantes dans un fichier ros_profile pour le source en une seule commande “source ros_profile”

Créer un Package python

ROS2 offre un outil simple pour créer l’architecture de fichiers d’un paquets Python ou C++. Vous devez alors spécifier au moins le nom du paquet (my_paquet). Il est possible également de donner le nom du nœud.

Dans le répertoire ~/ros2_sw/src, entrez la commande suivante

ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name my_node my_package --license Apache-2.0

le script python se trouve alors dans le répertoire ~/ros2_ws/src/my_package/my_package

Une fois le paquet créé, vous pouvez ajouter autant de nœud que vous le souhaitez dans ce dossier.

Il faudra penser à mettre à jour les fichiers setup.py et package.xml avec les dépendances et points d’entrée

Installer les dépendances

Avant de compiler le projet, il est conseillé de résoudre les dépendances.

rosdep install -i --from-path src --rosdistro iron -y

Compiler le projet

Il est possible de compiler le projet entier ou de sélectionner un paquet en particulier

colcon build --packages_select my_package --symlink-install

N.B.: symlink-install permet de ne pas recompiler à chaque changement du script Python

Lancer le nœud

Une fois le paquet compilé, vous pouvez lancer le nœud à partir de la commande

ros2 run my_package my_node

Le code de base lors de la création d’un nœud

def main():
    print('Hi from my_package.')

if __name__ == '__main__':
    main()

Au lancement ce code affichera simplement le text du print

Création d’un Publisher et d’un Subscriber pour Turtlesim

Nous allons voir dans cet exemple, comment créer un paquet avec deux nœuds, l’un venant contrôler la tortue, l’autre, venant lire la position.

Création du paquet

ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name projects control_turtle --license Apache-2.0

Voici l’architecture de fichiers désiré

.
├── LICENSE
├── package.xml
├── projects
│   ├── control_turtle.py
│   └── read_turtle.py
├── setup.cfg
└── setup.py

Ajouter les scripts Python

  • Publisher control_turtle.py

On importe le type de message à publier Twist pour le contrôle de la vitesse de la tortue

On se connecte au topic en précisant le nom (‘/turtle1/cmd_vel’) et le type de donnée (Twist)

On crée un timer qui nous permettra de publier régulièrement sur le topic (create_timer)

Enfin on publie la vitesse linéaire et angulaire

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  control_turtle.py


import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist

import os

# Node should have the same name as the node file
nodename= os.path.splitext(os.path.basename(__file__))[0]

class TurtleController(Node):

	def __init__(self):
		super().__init__(nodename)
		self.get_logger().info(nodename+" started")
		self.publisher_ = self.create_publisher(
			Twist,
			'/turtle1/cmd_vel',
			10)
		timer_period = 0.5  # seconds
		self.timer = self.create_timer(timer_period, self.timer_callback)
		self.i = 0

	def timer_callback(self):
		#msg = String()
		#msg.data = 'Hello World: %d' % self.i
		msg = Twist()
		msg.linear.x = 1.0
		msg.angular.z = 0.5
		self.publisher_.publish(msg)
		self.get_logger().info('Publishing: "%s"' % msg)
		self.i += 1


def main(args=None):
	rclpy.init(args=args)

	turtle_controller = TurtleController()
	try:
		rclpy.spin(turtle_controller)
		turtle_controller.destroy_node()
		rclpy.shutdown()
	except:
		turtle_controller.get_logger().info(nodename+" stopped")


if __name__ == '__main__':
	main()

  • Subscriber read_turtle.py

On importe le type de message à publier Pose

On se connecte au topic en précisant le nom (‘/turtle1/pose’) et le type de donnée (Pose)

On écrit la fonction d’écoute qui s’exécute lorsqu’une une nouvelle donnée est disponible sur le topic

On crée un timer qui nous permettra d’afficher les valeurs à la fréquence désirée

Enfin on publie la vitesse linéaire et angulaire

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  control_turtle.py


import rclpy #rospy
from rclpy.node import Node
from turtlesim.msg import Pose


import os

# Node should have the same name as the node file
nodename= os.path.splitext(os.path.basename(__file__))[0]

class TurtleReader(Node):

	def __init__(self):
		super().__init__(nodename)
		self.get_logger().info(nodename+" started")
		self.subscription = self.create_subscription(
			Pose,
			'/turtle1/pose',
			self.listener_callback,
			10)
		self.subscription  # prevent unused variable warning
		self.timer = self.create_timer(2, self.timer_callback)
		self.msg = None
		
	def listener_callback(self, msg):
		self.msg = msg
		
	def timer_callback(self):
		self.get_logger().info("I read %s" % self.msg )
		


def main(args=None):
	rclpy.init(args=args)

	turtle_reader = TurtleReader()
	try:
		rclpy.spin(turtle_reader)
		turtle_reader.destroy_node()
		rclpy.shutdown()
	except:
		turtle_reader.get_logger().info(nodename+" stopped")





if __name__ == '__main__':
	main()

Modifier le fichier setup.py

Dans les points d’entrée du fichier setup.py, vous devez spécifier les noms des noeuds ainsi que la fonction à lancer (main)

entry_points={
        'console_scripts': [
                'control_turtle = projects.control_turtle:main',
                'read_turtle = projects.read_turtle:main',
        ],
},

Compiler le paquet

colcon build --packages_select projects --symlink-install

Lancer les noeuds

Pour lancer un noeud dans un terminal, vous devez sourcer le projet

source /opt/ros/iron/setup.bash
cd ~/ros2_ws
source install/local_setup.bash

Dans trois terminaux différents, lancez les commandes suivantes

ros2 run turtlesim turtlesim_node #terminal1
ros2 run projects read_turtle #terminal2
ros2 run projects control_turtle #terminal3

Résultat

Vous pouvez voir la tortue décrire la forme d’un cercle et la position évoluer dans la fenêtre du subscriber

Lancer un noeud avec des arguments

Il est possible de configurer les noeuds lors de leur exécution. Dans cet exemple, nous allons définir des paramètre d’entrée pour la vitesse linéaire et angulaire. Nous allons donc modifier le code du Publieur ci-dessus

Dans l’initialisation du noeud, nous déclarons les paramètres avec les valeurs par défaut

    self.declare_parameter('x', 1.0)
    self.declare_parameter('z', 0.5)

Nous pouvons ensuite récupérer la valeur de ces paramètres pour les stocker dans des variables que nous utilisons dans la fonction callback

    self.linx = self.get_parameter('linx').get_parameter_value().double_value
    self.angz = self.get_parameter('angz').get_parameter_value().double_value

Vous pouvez désormais lancer un noeud avec et sans paramètre

ros2 run projects circle_turtle #default param
ros2 run projects circle_turtle --ros-args -p linx:=0.5 -p angz:=1.0

Code complet

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  control_turtle.py
import rclpy #rospy
from rclpy.node import Node
from std_msgs.msg import String

from turtlesim.msg import Pose
from geometry_msgs.msg import Twist

import os

nodename= os.path.splitext(os.path.basename(__file__))[0]

class TurtleController(Node):

	def __init__(self):
		super().__init__(nodename)
		self.get_logger().info(nodename+" started")
		self.publisher_ = self.create_publisher(
			Twist,
			'/turtle1/cmd_vel',
			10)
			
		self.declare_parameter('linx', 1.0)
		self.declare_parameter('angz', 0.5)
		self.linx = self.get_parameter('linx').get_parameter_value().double_value
		self.angz = self.get_parameter('angz').get_parameter_value().double_value
			
		timer_period = 0.5  # seconds
		self.timer = self.create_timer(timer_period, self.timer_callback)
		self.i = 0

	def timer_callback(self):
		#msg = String()
		#msg.data = 'Hello World: %d' % self.i
		msg = Twist()
		msg.linear.x = self.linx
		msg.angular.z = self.angz
		self.publisher_.publish(msg)
		self.get_logger().info('Publishing: "%s"' % msg)
		self.i += 1


def main(args=None):
	#os.system('ros2 service call /reset std_srvs/srv/Empty') #reset pos
	rclpy.init(args=args)

	turtle_controller = TurtleController()
	try:
		rclpy.spin(turtle_controller)
		turtle_controller.destroy_node()
		rclpy.shutdown()
	except:
		turtle_controller.get_logger().info(nodename+" stopped")


if __name__ == '__main__':
	main()

Ouvrir les différents terminaux avec un script

Pour gagner du temps lors de votre développement, vous pouvez ouvrir les terminaux et lancer les noeuds à partir d’un seul script bash

#open terminal and run turtlesim
gnome-terminal -- $SHELL -c "source ros_profile && ros2 run turtlesim turtlesim_node;exec $SHELL"
gnome-terminal -- $SHELL -c "source ros_profile && ros2 run projects control_turtle;exec $SHELL"

Troubleshoot

  • SetupTools deprecation

Vérifier la version setuptools

python3
import setuptools
setuptools.__version__

‘59.6.0’

Installer pip

sudo apt install python3-pip

puis installer la version 58.2.0 de setuptools

python3 -m pip install setuptools==58.2.0

Sources

Installer ROS2 sur Raspberry Pi

Installer ROS2 sur Raspberry Pi

Dans ce tutoriel, nous allons voir comment installer ROS2 sur une machine Linux et notamment un Raspberry Pi 4 avec une distribution Ubuntu. ROS2 est un framework intéressant à abordé lorsqu’on travaille sur des systèmes embarqués comme les robots.

Matériel

  • Ordinateur avec accès internet
  • Carte SD 32Go
  • Raspberry Pi+écran HDMI+clavier/souris

Ce tutoriel peut être suivit pour d’autres machines Linux

Téléchargement Ubuntu et configuration de la carte SD

Nous allons voir comment configurer la carte SD pour installer ROS2 (Iron Irwini) sur Raspberry Pi. Cette version de ROS2 est compatible avec Ubuntu Jammy.

Pour cela télécharger l’image d’Ubuntu (Jammy jellyfish) ou sélectionnez l’OS sous Raspberry Pi Imager > General OS > Ubuntu > Ubuntu Desktop 22.04.3 LTS (64bits)

N.B.: vous pouvez retrouver la compatibilité des versions entre ROS et OS et les noms des versions Ubuntu pour vérifier que vous installez les bonnes versions. Sélectionnez la version 64bits pour un meilleur support (tier1)

Installer l’image de l’OS à l’aide de Raspberry Pi Imager sur la carte SD

Une fois l’image écrite sur la carte SD vous pouvez l’insérer dans le Raspberry Pi puis le mettre sous tension.

Vous pouvez ensuite suivre les instructions de configuration sur l’écran du Raspberry Pi.

Configurer la connexion à distance

Vous pouvez, si vous le désirez, configurer la connexion à distance sur Raspberry Pi. Cette étape est optionnelle mais peut vous simplifier la vie lorsque vous développerez sur ROS2 sous Raspberry Pi.

N.B.: sur Ubuntu, pour vous connecter à distance sans moniteur, vous devez modifier le fichier /boot/firmware/config.txt

#dtoverlay=vc4-kms-v3d
hdmi_force_hotplug=1
hdmi_group=1
hdmi_mode=16

Installation de ROS2

Pour l’installation de ROS2, vous devez tout d’abord vérifier que le téléchargement de dépôt Universe est activé. Dans Software & Updates > Ubuntu Software

Ajouter ensuite la clé GPG pour ROS2

sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg

Puis ajouter le dépôt à la liste des sources (/etc/source.list.d)

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

Pour le développement, vous pouvez installer les outils de dev

sudo apt update && sudo apt install ros-dev-tools

Vous pouvez enfin installer ROS2 en fonction de la version choisi (desktop ou base)

sudo apt install ros-iron-desktop

ou

sudo apt install ros-iron-ros-base

Pour désinstaller ROS2

Pour la désinstallation, il faut supprimer le paquet ainsi que le dépôt

sudo apt remove ~nros-iron-* && sudo apt autoremove
sudo rm /etc/apt/sources.list.d/ros2.list
sudo apt update

Premier pas avec ROS2

Une fois ROS2 installé, vous pouvez charger l’environnement avec

source /opt/ros/iron/setup.bash

Vous pouvez tester l’installation à l’aide de l’exemple talker/listener

Dans un terminal, lancer le talker

source /opt/ros/iron/setup.bash
ros2 run demo_nodes_cpp talker

Dans un second terminal lancer le listener

source /opt/ros/iron/setup.bash
ros2 run demo_nodes_py listener

Pour vérifier la liste des variables d’environnement

printenv | grep -i ROS

Pour avoir la liste des objets en exécution, vous pouvez utiliser les commandes

ros2 node list
ros2 topic list
ros2 service list
ros2 action list

Installation de TurtleSim

Un bel outil pour apprendre et bien comprendre le fonctionnement de ROS2 est TurtleSim

Vérifier son installation à l’aide de la commande

ros2 pkg executables turtlesim

Si ce n’est pas le cas, vous pouvez installer turtlesim à l’aide de la commande

sudo apt install ros-iron-turtlesim

Dans un terminal lancer turtle sim

ros2 run turtlesim turtlesim_node

Dans une autre terminal, lancer le contrôle par clavier

ros2 run turtlesim turtle_teleop_key

Pour observer ce qu’il se passe, dans un troisième terminal, espionner le topic avec la commande

ros2 topic echo /turtle1/pose

Vous pouvez retrouver la liste des topic disponible à l’aide de la commande

ros2 topic list

Application

Sources

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

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

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

Matériel

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

  • Appareil Android
  • ESP32 BLE

Limitation de 20 bytes du BLE

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

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

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

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

Récupérer plusieurs paquets

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

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

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

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

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

Envoyer des paquets de taille prédéfinie

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

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

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

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

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

Augmenter la limite MTU de l’application

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

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

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

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

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

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

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

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

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

Applications

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

Sources