fbpixel
Test et optimisation d’un code C/C++ avec GNU

Test et optimisation d’un code C/C++ avec GNU

Dans le cas de développement de code C/C++ sur systèmes embarqués, notamment, l’optimisation et le test du code sont des points essentiels afin de limiter l’empreinte mémoire et le temps d’exécution.

Optimisation du code C pour les systèmes embarquées

L’optimisation du code C est nécessaire pour les systèmes embarqués car, afin de gagner sur le coût du hardware, on prévoit le strict nécessaire en terme d’interface, de mémoire et de capacité de calcul. Pour un ordinateur fixe, on va tout de même préférer des temps d’exécution faibles afin de ne pas encombrer la charge CPU. L’optimisation va avoir deux objectifs concurrentiels

  • réduire la quantité de mémoire utilisée par un programme
  • augmenter la vitesse d’exécution des fonctions du programme

Une programme peut être le plus rapide possible ou le plus petit possible mais pas les deux. L’optimisation d’un des critères peut fortement impacter l’autre.

Le compilateur se charge souvent d’optimiser le code mais il est primordiale de l’optimiser manuellement. On va généralement se concentrer sur l’optimisation des sections critiques du code et laisser le compilateur se charger du reste.

Même s’il y a de bonnes pratiques à garder à l’esprit lorsqu’on développe un code, un code ne devrait être otpimisé que lorque c’est strictement nécessaire (limite mémoire, lenteur d’exécution). Un code doit avant tout être lisible, maintenable et fonctionnel.

Améliorer l’efficacité du code

Les fonctions inline

Le mot clé inline permet de spécifier au compilateur de remplacer un appel à la fonction par le code de la fonction. Lorsqu’une fonction est présente dans quelques sections de code mais appelée un grand nombre de fois, transformer la fonction en fonction inline peut améliore les performances d’exécution du code

Les tables lookup

Les lookup permettent de remplacer des fonctions demandant un calcul compliqué par une simple association de variables. On peut ainsi remplacer une fonction sin() ou des sections swtich par des tables à choix multiples.

Code assembleur manuel

Un compilateur transforme le code C en code assembleur optimisé. Un développeur confirmé peut, lorsque la fonction est critique, créer son propre code assembleur.

Réduire la taille du code (ROM)

Taille et type de variable

Choisir correctement la structure, le type et la taille de variable nécessaire pour stocker les données améliore considérablement les performances du code.

Goto statement

La fonction goto permet d’éviter des algorithmes d’arborescences compliqués. Cela rend le code plus difficile à lire et peut être la source de plus d’erreur.

Éviter les librairies standards

Les librairies standards sont généralement lourdes en taille et gourmandes en calculs car elles tentent de couvrir tous les cas. Développer ses propres fonctions correspondant à son strict besoin est un bon moyen d’optimiser son code C/C++

Réduire l’usage de la mémoire (RAM)

Mots-clés const et static

Une variables static correspond à une variable uniquement accessible dans le contexte d’une fonction mais dont l’état est maintenu entre les appels de la fonction. Cela permet de limiter l’utlisation de variable globale

Tester les performances du code C/C++

Mesurer le temps d’exécution d’une section de code à l’aide de la librairie time.h

#include <iostream>
#include <time.h>

clock_t start, end;
double cpu_time_used;
int main()
{
    std::cout << "Hello World" << std::endl;
    start = clock();
    for(;i<0xffffff;i++);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Execution time: %f seconds\n", cpu_time_used);
    return 0;
}

Utilisation de gprof

L’outil gprof fournit avec le compilateur permet d’avoir les temps d’exécution de différentes sections de code ou fonctions.

compiler avec le flag -pg pour que le code génère un fichier gmon.out

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp  #compile for profiling
./helloworld  #execute and write gnom.out
gprof -b helloworld.exe gmon.out > perfo.txt  #translate gnom.out

Exemple de résultat de fichier perfo.txt: vous obtenez un tableau contenant le temps d’exécution cumulé sur la durée total d’exécution, le nombre d’appels et le temps d’exécution moyen.

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
 37.21      4.26     4.26        2     2.13     3.95  func1()
 31.79      7.90     3.64        1     3.64     3.64  new_func1()
 30.83     11.43     3.53                             func2()
  0.17     11.45     0.02                             main
  0.00     11.45     0.00        1     0.00     0.00  count_to(int, int)

Utilisation de la mémoire

L »option de compilation –stats donne des statistiques générales sur le code

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp --stats

resultat

(No per-node statistics)
Type hash: size 32749, 23922 elements, 1.178881 collisions
DECL_DEBUG_EXPR  hash: size 1021, 0 elements, 0.000000 collisions
DECL_VALUE_EXPR  hash: size 1021, 18 elements, 0.031250 collisions
decl_specializations: size 8191, 6113 elements, 1.427515 collisions
type_specializations: size 8191, 3619 elements, 1.569889 collisions

******
time in header files (total): 0.862000 (38%)
time in main file (total): 1.416000 (62%)
ratio = 0.608757 : 1

******
time in ./include/utils.cpp: 0.002000 (0%)
time in <built-in>: 0.006000 (0%)
time in <command-line>: 0.000000 (0%)
time in <top level>: 0.009000 (0%)

Les options -fstack-usage et -Wstack-usage permet de vérifier l’usage de la mémoire stack à la compilation

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -fstack-usage
g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -Wstack-usage=32

La deuxième vérifie la taille de la mémoire stack attendu pour une valeur données (32).

In file included from <command-line>:
./include/utils.cpp: In function 'void count_to(int, int)':
./include/utils.cpp:6:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    6 | void count_to(int n, int delay)
      |      ^~~~~~~~
helloworld.cpp: In function 'void new_func1()':
helloworld.cpp:7:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    7 | void new_func1(void)
      |      ^~~~~~~~~
helloworld.cpp: In function 'void func1()':
helloworld.cpp:17:6: warning: stack usage is 64 bytes [-Wstack-usage=]
   17 | void func1(void)
      |      ^~~~~
helloworld.cpp: In function 'void func2()':
helloworld.cpp:28:13: warning: stack usage is 64 bytes [-Wstack-usage=]
   28 | static void func2(void)
      |             ^~~~~
helloworld.cpp: In function 'int main()':
helloworld.cpp:40:5: warning: stack usage is 96 bytes [-Wstack-usage=]
   40 | int main(void)

Simulation de l’empreinte mémoire sur un hardware spécifique

Il est possible de simuler un hardware particulier avec un link script personnalisé

myldscript.lds

MemoryStart AddressSize
Internal Flash0x00000000256 Kbytes
Internal SRAM0x2000000032 Kbytes
MEMORY
{
  rom      (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram      (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

STACK_SIZE = 0x2000;

/* Section Definitions */
SECTIONS
{
    .text :
    {
        KEEP(*(.vectors .vectors.*))
        *(.text*)
        *(.rodata*)
    } > rom

    /* .bss section which is used for uninitialized data */
    .bss (NOLOAD) :
    {
        *(.bss*)
        *(COMMON)
    } > ram

    .data :
    {
        *(.data*);
    } > ram AT >rom

    /* stack section */
    .stack (NOLOAD):
    {
        . = ALIGN(8);
        . = . + STACK_SIZE;
        . = ALIGN(8);
    } > ram

    _end = . ;
}
gcc -g -c helloworld.cpp # compile object file
ld -o helloworld -T ldscript.lds helloworld.o --print-memory-usage #compile with specific link script
Memory region         Used Size  Region Size  %age Used
             rom:       12704 B       256 KB      4.85%
             ram:        4128 B        32 KB     12.60%

Sources

Programmer en C++ avec VS Code

Programmer en C++ avec VS Code

Installation et configuration de VS Code

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

Installation de l’extension C/C++ dans VS Code

Installation de l’extension Code Runner. cette extension est facultative mais vous permettra de lancer un code dans un terminal externe pour utiliser les entrées utilisateurs (cin). Dans les paramètres, activez l’option Code-runner: Run In Terminal.

Installation d’un compilateur

Pour compiler, exécuter et analyser votre code vous aurez besoin d’outil comme GNU.

Téléchargez et dézipper une version de GNU Compiler Collection (GCC) s’il n’est pas déjà présent sur votre ordinateur.

g++ --version

Vous pouvez également installer une version de GCC à partir de MSYS2

$ pacman -S mingw-w64-ucrt-x86_64-gcc

Créer votre projet C++

Une fois les différentes extensions installées, vous pouvez créer votre projet C++. Pour cela, créez simplement un dossier, ouvrez VS Code sur ce dossier puis créer un fichier .cpp. Vous pouvez le faire via l’interface VS Code ou en ligne de commande

mkdir myproject
cd myproject
code .

Copier le code Hello world dans le fichier source.

#include <iostream>

int main()
{
    std::cout << "Hello World" << std::endl;
    return 0;
}

Compiler et tester votre programme

Pour lancer le code vous pouvez utiliser le menu Run > Start Debugging (F5) qui lancera le programme dans la console de debug ou faire un clique-droit sur l’éditeur et lancer Run Code (Ctrl+Alt+N) qui le lancera dans le terminal (si Code runner est activé)

Vous pouvez également le faire en ligne de commande à partir du terminal

g++ helloworld.cpp # -> a.exe executabl. compile and link
./a.exe # execute file
g++ -S helloworld.cpp # -> helloworld.s assembly source file. compile
g++ -c helloworld.cpp # -> helloworld.o object file. compile and assemble

Ajouter un librairie externe C++ dans VS Code

Vous pouvez ajouter des librairies externes ou créer vos propres librairies. Pour que VS Code compile les librairies avec le programme, vous devez spécifier le chemin d’accès dans le fichier tasks.json (ex: « –include=include/utils.cpp »)

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++.exe build active file",
            "command": "C:\\MinGW\\bin\\g++.exe",
            "args": [
                "-fdiagnostics-color=always",
                "-g",
                "${file}",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe",
                "--include=include/utils.cpp",
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Task generated by Debugger."
        }
    ],
    "version": "2.0.0"
}

pour compiler le programmer, il faut compiler et lier les deux fichiers

g++ helloworld.cpp include/utils.cpp -o helloworld
# or
g++ -c helloworld.cpp include/utils.cpp # get object files
g++ -o helloworld.exe helloworld.o include/utils.o
./helloworld.exe #run program

Mesurer le temps d’exécution du programme

Une méthode assez direct pour mesurer le temps d’exécution du programme est d’utiliser la librairie time.h et mesurer le temps écouler entre le début du programme et la fin.

Pour tester le temps d’exécution nous créons une fonction qui compte de 0 à n avec un certain délai entre chaque itération.

void count_to(int n = 10, int delay = 0);
#include <iostream>
#include <windows.h>
//#include <unistd.h>

void count_to(int n, int delay)
{
    for (int i=0;i<=n;i++){
        std::cout << i << std::endl;
        Sleep(delay);
    }
}

Puis dans le code principal, nous mesurons le temps avant et après l’exécution de la fonction count_to()

#include <iostream>
#include <time.h>
#include "include/utils.h"

clock_t start, end;
double cpu_time_used;

int main()
{
    std::cout << "Hello World" << std::endl;

    start = clock();
    
    count_to(10,100);

    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Execution time: %f seconds\n", cpu_time_used);

    return 0;
}

La console donne le résultat suivant

Hello World
0
1
2
3
4
5
6
7
8
9
10
Execution time: 1.180000 seconds

Utiliser l’outil de Debug de VS Code

La fonction Run and Debug permet d’exécuter le code pas à pas en plaçant des points d’arrêts (breakpoints) à différents endroits du code (identifier à gauche du numéro de ligne par un point rouge). Il permet aussi d’observer les variables et registres au fur et à mesure de l’exécution. Vous pouvez ainsi suivre l’évolution du code pas à pas à la recherche d’erreur éventuelles.

Le code va s’exécuter normalement jusqu’au premier point d’arrêt, vous pouvez ensuite continuer la progression jusqu’au prochain point d’arrêt, ou exécuter le code pas à pas à partir de ce point à l’aide des boutons de navigations

Vous pouvez observer les valeurs des variables ou des registres à chaque étapes dans l’onglet VARIABLES. Ou observer certaines variables dans WATCH.

Vous pouvez également observer le code assembleur compilé à ouvrant la vue « disassembly » à l’aide de clique-droit sur l’éditeur puis « Open Disassembly View ».

Troubleshoot

  • « thread » n’est pas reconnu

essayer d’installer MinGW-w64 à partir de MSYS2 et d’ajouter C:\msys64\ucrt64\bin aux variables d’environnement. Vérifier que le compilateur utilisé est g++

  • Utilisation de cout lève une erreur segmentation

comme solution intermédiaire, vous pouvez remplacer le cout par des printf

vérifier que la librairie libstdc++-6.dll se trouve au même endroit que le compilateur à l’aide la console de debug de vs code

Loaded 'C:\Program Files\Tesseract-OCR\libstdc++-6.dll'. Symbols loaded.
#instead of
Loaded 'C:\msys64\ucrt64\bin\libstdc++-6.dll'. Symbols loaded.

  • Run Code donne des résultats différents de Debug Code

Assurez-vous que les commandes sont les mêmes pour les deux fonctions (pour ma part le flage « -g » posais soucis)

dans le fichier tasks.json (debug)

//
            "command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
            "args": [
                "-fdiagnostics-color=always",
                "-g",
                "${file}",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe"
            ],

dans le fichier settings (code-runner)

   "code-runner.executorMap": {

        "cpp": "cd $dir && g++ -g $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",

    },

Sources

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