Some corrections, thanks my wife and heuzef

This commit is contained in:
Yorick Barbanneau 2023-07-17 18:57:34 +02:00
parent 5c089947e7
commit febf52d368

View file

@ -1,7 +1,7 @@
Title: Adafruit Macropad : multitâche coopératif avec asyncio Title: Adafruit Macropad : multitâche coopératif avec asyncio
Category: linux Category: linux
Tags: Adafruit, CircuitPython, Python Tags: Adafruit, CircuitPython, Python
Date: 2023-07-11 15:10 Date: 2023-07-17 18:46
cover: assets/backgrounds/Adafruit_Macropad.jpg cover: assets/backgrounds/Adafruit_Macropad.jpg
status: draft status: draft
@ -10,7 +10,7 @@ Nous avons découvert dans mon
le Macropad et comment utiliser la connexion série pour envoyer et recevoir des le Macropad et comment utiliser la connexion série pour envoyer et recevoir des
données depuis un ordinateur. données depuis un ordinateur.
Dans la première partie de cet article, nous avions été confronté à un problème Dans la première partie de cet article, nous avions été confrontés à un problème
alors que nous voulions changer la fréquence du clignotement de la DEL : il alors que nous voulions changer la fréquence du clignotement de la DEL : il
fallait attendre que l'instruction `time.sleep` soit terminée. fallait attendre que l'instruction `time.sleep` soit terminée.
@ -18,7 +18,7 @@ Nous allons vois ici comment utiliser `asyncio` et l'implémentation de `async`
`await` pour corriger ce problème. Nous essaierons ensuite d'aller plus loin `await` pour corriger ce problème. Nous essaierons ensuite d'aller plus loin
afin d'appréhender cette bibliothèque. afin d'appréhender cette bibliothèque.
## Qu'est-ce que `asyncio` ## Qu'est-ce qu'`asyncio`?
C'est une bibliothèque de *Python* qui permet de réaliser de la **programmation C'est une bibliothèque de *Python* qui permet de réaliser de la **programmation
asynchrone**. Le principe ici est d'éviter que le programme attende sans rien asynchrone**. Le principe ici est d'éviter que le programme attende sans rien
@ -32,7 +32,7 @@ s'en charge. Mais d'abord *CircuitPython* ne [les supporte pas](l_threads_cp) et
ils sont plus difficiles à gérer (concurrences, sections critiques, etc.). ils sont plus difficiles à gérer (concurrences, sections critiques, etc.).
Dans le cas d'`asyncio`, nous parlerons [**de coroutines**][l_coroutines] pour Dans le cas d'`asyncio`, nous parlerons [**de coroutines**][l_coroutines] pour
des fonctions déclarée avec le mot clef `async`. des fonctions déclarées avec le mot clef `async`.
[l_threads_cp]:https://github.com/adafruit/circuitpython/issues/1124 [l_threads_cp]:https://github.com/adafruit/circuitpython/issues/1124
[l_coroutines]:https://fr.wikipedia.org/wiki/Coroutine [l_coroutines]:https://fr.wikipedia.org/wiki/Coroutine
@ -40,7 +40,7 @@ des fonctions déclarée avec le mot clef `async`.
## Installer `asyncio` sur le Macropad ## Installer `asyncio` sur le Macropad
Il est possible que la bibliothèque `asyncio` et ses dépendances ne soient pas Il est possible que la bibliothèque `asyncio` et ses dépendances ne soient pas
installées, il est alors possible de les installer avec `circup`. Commençons pas installées, il est alors possible de les installer avec `circup`. Commençons par
créer un *environnement virtuel Python*: créer un *environnement virtuel Python*:
``` ```
@ -121,17 +121,20 @@ ce module qui se charge maintenant de l'attente, ainsi il pourra gérer
La partie du code utilisé pour la gestion des entrées par le port série est elle La partie du code utilisé pour la gestion des entrées par le port série est elle
aussi placée dans la fonction asynchrone `check_serial()`. Cette fois aussi placée dans la fonction asynchrone `check_serial()`. Cette fois
`asyncio.sleep` est définis à 0 afin de laisser l'ordonnanceur du module `asyncio` `asyncio.sleep` est définie à 0 afin de laisser l'ordonnanceur du module `asyncio`
interrompre notre fonction et ainsi **empêcher une coroutine à l'exécution interrompre notre fonction et ainsi **empêcher une coroutine de bloquer les
longue bloquer les autres**. autres en ne rendant jamais la main**[^coroutine].
[^coroutine]: Cette technique permet aussi de rendre une fonction compatible
asynchrone
Une nouvelle fonction asynchrone fait son apparition : `main()`. C'est ici que Une nouvelle fonction asynchrone fait son apparition : `main()`. C'est ici que
nous définissions nos tâches (contenants nos *coroutines*) `t_led` et`t_serial`. nous définissons nos tâches (contenants nos *coroutines*) `t_led` et`t_serial`.
Ensuite `asyncio.gather()` permet de lancer l'exécution concurrente de nos deux Ensuite `asyncio.gather()` permet de lancer l'exécution concurrente de nos deux
tâches. tâches.
Enfin, notre fonction `main()` est lancée via `asyncio.run` permettant alors de Enfin, notre fonction `main()` est lancée via `asyncio.run` permettant alors de
de l'exécuter jusqu'à sa fin. l'exécuter jusqu'à sa fin.
Une fois le fichier sauvegardé, la DEL n°0 devrait se mettre à clignoter avec Une fois le fichier sauvegardé, la DEL n°0 devrait se mettre à clignoter avec
une fréquence d'une seconde. une fréquence d'une seconde.
@ -139,15 +142,15 @@ une fréquence d'une seconde.
### Oui mais! ### Oui mais!
Lançons *minicom* sur le port série affichant la console . Nous observons Lançons *minicom* sur le port série affichant la console . Nous observons
ainsi les actions effectuées par notre programme grâce au différents `print` ainsi les actions effectuées par notre programme grâce aux différents `print`
disséminées dans le code. disséminés dans le code.
```shell ```shell
minicom -D /dev/ttyACM0 -c on minicom -D /dev/ttyACM0 -c on
``` ```
Maintenant, depuis un autre terminal, changeons la fréquence de clignotement de Maintenant, depuis un autre terminal, changeons la fréquence de clignotement de
la DEL pour la placer à 20 secondes puis le remettre à 1 seconde: la DEL à 20 secondes pour la remettre aussitôt à 1 seconde:
```shell ```shell
printf "time 20\n" > /dev/ttyACM1 printf "time 20\n" > /dev/ttyACM1
@ -176,20 +179,20 @@ l:0 c:0xad20b4 t:1.0
Les lignes `serial data received` et `cmd:time opt:<>` sont apparues Les lignes `serial data received` et `cmd:time opt:<>` sont apparues
immédiatement, mais lors du changement de fréquence de 20 à 1, il a fallu immédiatement, mais lors du changement de fréquence de 20 à 1, il a fallu
**attendre que les 20 secondes définies précédement soient ecoulées** pour que **attendre que les 20 secondes définies précédemment soient écoulées** pour que
la modification soit prise en compte par la coroutine `blink()`. la modification soit prise en compte par la coroutine `blink()`.
## Prendre en compte immédiatement la modification de fréquence ## Prendre en compte immédiatement la modification de fréquence
Comment pouvons nous faire en sorte que la modification de fréquence soit Comment pouvons nous faire en sorte que la modification de fréquence soit
immédiatement répercutée? Tout simplement en **utilisant la méthode `cancel()`** immédiatement répercutée ? Tout simplement en **utilisant la méthode `cancel()`**
sur notre tache `t_led`. Celle-ci lève l'exception `CancelError` dans la sur notre tâche `t_led`. Celle-ci lève l'exception `CancelError` dans la
coroutine `blink()`. C'est bien entendu à nous d'implémenter la gestion de cette coroutine `blink()`. C'est bien entendu à nous d'implémenter la gestion de cette
exception. exception.
Vous pouvez télécharger le fichier `code.py` Vous pouvez télécharger le fichier `code.py`
[ici]({attach}./files/async_blink/code.py), voici le code: [ici]({attach}./files/async_blink/code.py), voici le code :
```python ```python
from adafruit_macropad import MacroPad from adafruit_macropad import MacroPad
@ -242,19 +245,18 @@ async def main():
asyncio.run(main()) asyncio.run(main())
``` ```
C'est notre fonction `check_serial()` qui lance le `cancel()` à partir de la C'est notre fonction `check_serial()` qui lance le `cancel()` à partir de la
tache `t_led` que nous lui passons en paramètre. L'effet est alors immédiat : tâche `t_led` que nous lui passons en paramètre. L'effet est alors immédiat :
l'exception `CancelError` est lancée dans `blink()` forçant cette dernière à l'exception `CancelError` est lancée dans `blink()` forçant cette dernière à
recommencer depuis le début du `while`. Notre modification **est donc appliquée recommencer depuis le début du `while`. Notre modification **est donc appliquée
immédiatement**. immédiatement**.
## Gestions dynamique des tâches ## Gestion dynamique des tâches
Maintenant que nous avons vu le fonctionnement de base d'`asyncio`, prenons un Maintenant que nous avons vu le fonctionnement de base d'`asyncio`, prenons un
exemple un peu plus complet. exemple un peu plus complet.
Vous pouvez télécharger le fichier `code.py` Vous pouvez télécharger le fichier `code.py`
[ici]({attach}./files/async_array/code.py), voici le code: [ici]({attach}./files/async_array/code.py), voici le code :
```python ```python
from adafruit_macropad import MacroPad from adafruit_macropad import MacroPad
@ -302,27 +304,27 @@ asyncio.run(main())
``` ```
Lorsque nous appuyons sur une des douze touches du clavier de notre *MacroPad*, Lorsque nous appuyons sur une des douze touches du clavier de notre *MacroPad*,
la DEL en dessous clignote cinq fois. Vous l'aurez compris chaque clignotement la DEL en dessous clignote cinq fois. Vous l'aurez compris, chaque clignotement
est en fait une coroutine gérée par `asyncio`. est en fait une coroutine gérée par `asyncio`.
Les différentes tâche sont répertoriée dans un tableau qui est passé à la Les différentes tâches sont répertoriées dans un tableau qui est passé à la
fonction `asyncio.gather()`. Plus intéressant, afin de supprimer de notre fonction `asyncio.gather()`. Plus intéressant, afin de supprimer de notre
tableau les coroutines terminée, nous passons par la fonction `manage_tasks()` tableau les coroutines terminées, nous passons par la fonction `manage_tasks()`
en utilisant `asyncio.done()` afin de vérifier que la tâche testée soit bien en utilisant `asyncio.done()` afin de vérifier que la tâche testée soit bien
terminée. terminée.
Nous avons donc deux tâches lancées dès le départ: Nous avons donc deux tâches lancées dès le départ :
* `get_key()` changée d'écouter les évènements clavier et de lancer les * `get_key()` chargée d'écouter les évènements clavier et de lancer les
différents clignotements en fonction de la touche appuyée; différents clignotements en fonction de la touche appuyée ;
* `manage_tasks` chargée de nettoyer le tableau des tâches; * `manage_tasks` chargée de nettoyer le tableau des tâches ;
Notre fonction `manage_tasks()` est simple, nous parcourons l'ensemble du Notre fonction `manage_tasks()` est simple, nous parcourons l'ensemble du
tableau et lorsque nous trouvons une coroutine terminée nous supprimons tableau et lorsque nous trouvons une coroutine terminée nous supprimons
l'élément du tableau. Remarquez le `break` lorsque notre fonction supprime un l'élément du tableau. Remarquez le `break` lorsque notre fonction supprime un
élément de notre `taskslist`, c'est nécessaire afin d'éviter une erreur d'index élément de notre `taskslist`, c'est nécessaire afin d'éviter une erreur d'index
dut au fait que **notre tableau contiendra un élément de moins**; dû au fait que **notre tableau contiendra un élément de moins**.
Voici l'affichage des informations sur l'écran du *Macropad* (ou sur la console Voici l'affichage des informations sur l'écran du *Macropad* (ou sur la console
@ -345,9 +347,9 @@ leur destruction dans `manage_tasks()` -- merci les `print()`.
Contrairement à sa grande [sœur *CPython*][l_cpython], notre implémentation de Contrairement à sa grande [sœur *CPython*][l_cpython], notre implémentation de
Python ici présente ne contient pas de primitive de synchronisation comme les Python ici présente ne contient pas de primitive de synchronisation comme les
sémaphores, variables conditions, ... : seul le `Lock` sont présent. sémaphores, variables conditions, ... : seul le `Lock` est présent.
C'est l'implémentation Python des [Mutex][l_mutex]: l'accès à une C'est l'implémentation Python des [Mutex][l_mutex] : l'accès à une
partie du code protégée par un *Mutex* sera sérialisé. Une seule coroutine y partie du code protégé par un *Mutex* sera sérialisé. Une seule coroutine y
aura accès à la fois, les autres attendent leur tour une à une. aura accès à la fois, les autres attendent leur tour une à une.
Voici un exemple de code pour notre Macropad utilisant `Lock`, vous pouvez Voici un exemple de code pour notre Macropad utilisant `Lock`, vous pouvez
@ -413,7 +415,7 @@ Lors de l'appui sur une touche, la fonction `blink()` DEL passe au vert, si le
en attente sera matérialisée. en attente sera matérialisée.
Nous pouvons observer sur l'écran du *Macropad* différentes informations Nous pouvons observer sur l'écran du *Macropad* différentes informations
relatives à l'attente / acquisitions de notre `blink_mutex`. Ces informations relatives à l'attente / acquisition de notre `blink_mutex`. Ces informations
permettent de comprendre ce qui se passe lors de l'exécution de nos coroutines : permettent de comprendre ce qui se passe lors de l'exécution de nos coroutines :
```shell ```shell
@ -437,16 +439,24 @@ Aquire mutex l:6
Nous avons vu dans cet article, au fils des différents exemples, comment Nous avons vu dans cet article, au fils des différents exemples, comment
utiliser `asyncio` pour gérer des coroutines, permettant d'implémenter dans nos utiliser `asyncio` pour gérer des coroutines, permettant d'implémenter dans nos
programme pour notre Macropad du multitĉhe coopératif. programmes pour notre Macropad du multitâche coopératif.
Un petit tous sur [la documentation][L-circuip_async] spécifique à `asyncio` Un petit tour sur [la documentation][L-circuip_async] spécifique à `asyncio`
dans CircuitPython vous permettra d'aller plus loin. Elle est cependant très dans CircuitPython vous permettra d'aller plus loin. Elle est cependant très
succincte et manque cruellement d'exemples. Si vous voulez en apprendre plus succincte et manque cruellement d'exemples[^doc]. Si vous voulez en apprendre plus
sur la programmation asynchrone en Python, vous avez aussi l'excellent tutoriel sur la programmation asynchrone en Python, vous avez aussi l'excellent tutoriel
de nohar sur [Zeste de savoir][l_zeste], il m'a été d'une grande aide pour la de *nohar* sur [Zeste de savoir][l_zeste], il m'a été d'une grande aide pour la
rédaction de cet article. rédaction de cet article.
[^doc] La dernière mise à jour date de juin 2022 au moment de la rédaction de
cet article
[L-circuip_async]:https://docs.circuitpython.org/projects/asyncio/en/latest/index.html [L-circuip_async]:https://docs.circuitpython.org/projects/asyncio/en/latest/index.html
[l_zeste]:https://zestedesavoir.com/articles/1568/decouvrons-la-programmation-asynchrone-en-python/ [l_zeste]:https://zestedesavoir.com/articles/1568/decouvrons-la-programmation-asynchrone-en-python/
## Credits
Les photos proviennent du site Adafuit, prisent par [Kattni Rembor] et sous licence
Creative Common By-Sa.
*[DEL]: Diode ÉlectroLuminescente *[DEL]: Diode ÉlectroLuminescente