Add Adafuit Macropad serial connection article

This commit is contained in:
Yorick Barbanneau 2023-02-26 19:09:52 +01:00
parent 4345e877fa
commit eff6e3b6e5
7 changed files with 627 additions and 0 deletions

View file

@ -0,0 +1,452 @@
Title: Jouer avec le Macropad Adafruit! mais en série
Category: linux
Tags: Adafruit, CircuitPython, Python
Date: 2023-02-26 18:10
status: hidden
Le Macropad Adafruit est un petit clavier de 12 touches rétroéclairées avec un
écran OLED et un sélecteur. Il est motorisé par un Raspberry Pi RP2040.
Dans cet article, nous allons voir comment utiliser ce clavier et communiquer
avec via l'utilisation du port série.
## Circuitpython
Comme beaucoup de carte électronique à base de micro controlleurs, ce clavier
est compatible avec l'écosystème Arduino, il est donc possible de le programmer
en C. Il existe aussi un firmware embarqant [CircuitPython][l_circuitp], couplé
aux diférentes bibliothèques [fournies par Adafuit][l_circuitp_ada], il est
possible d'utiliser *Python* pour le programmer. C'est **ce que nous allons
utiliser ici**.
La documentation sur l'installation du firmware *CyrcuitPython* pour le Macropad
est disponible sur le site [d'Adafuit][l_ada_macropad]
[l_circuitp]:https://circuitpython.org/
[l_circuitp_ada]:https://github.com/adafruit/Adafruit_CircuitPython_MacroPad
[l_ada_macropad]:https://learn.adafruit.com/adafruit-macropad-rp2040/circuitpython
## Préparer l'environnement
Sur notre ordinateur nous allons préparer un *environnement virtuel Python* pour
y installer `circup`, module Python permettant d'installer le nécessaire **sur
le Macropad**:
```
python -m venv ~/venv_circup
source ~/venv_circup/bin/activate
```
Puis après avoir branché le Macropad et monté l'espace de stockage :
```
circup install adafruit_macropad
```
## Installer minicom
Nous allons maintenant prépare notre système pour tester la connexion série .
Tout d'abord installons un logiciel pour communiquer via le port série pour
Interagir avec le Macropad. Personnellement j'ai choisi [*minicom*][l_minicom]
disponible dans les dépôts Debian, Archlinux et sûrement d'autres distributions:
```
pacman -S minicom
```
Il est aussi nécessaire d'ajouter votre utilisateur dans le groupe `uucp` en
utilisant la commande suivante (avec `root` ou `sudo`):
```
gpasswd -a ephase uucp
```
[l_minicom]:https://salsa.debian.org/minicom-team/minicom
## Initialiser un périphérique série sur la Macropad
Afin d'initialiser un périphérique série sur le Macropad, il est nécessaire de
créer un fichier `boot.py` à la racine de votre lecteur CIRCUITPY et d'y ajouter
le code suivant:
```python
import usb_cdc
try:
"""
Initialisation du port série pour la console REPL mais
aussi pour un périphérique série, Sous Linux il sera
disponible dans /dev/ttyACM
"""
usb_cdc.enable(console=True, data=True)
except Exception as e:
print(e)
```
Lors de l'enregistrement du fichier, le Macropad devrait se relancer et prendre
en compte les modifications. Plus besoin de toucher à ce fichier, on le laissera
tranquille tout au long de cet article. Nous modifierons maintenant le fichier
`code.py` toujours à la racine de notre lecteur `CIRCUITPY`.
## Premier script, faire clignoter une DEL
Nous allons maintenant tester notre premier code. Celui-ci va simplement faire
clignoter une DEL sous une touche de notre clavier. Nous **pourrons changer la
fréquence en envoyant la commande `time` via une connexion série**. Voici le
code en question:
```python
from adafruit_macropad import MacroPad
import usb_cdc
import time
macropad = MacroPad()
serial = usb_cdc.data
def exec_command (data):
global timer
command,option = data.split()
print("cmd: {} | opt: {}".format(command, option))
if command == 'time':
timer = float(option)
print("new timer: {}".format(timer))
def blink(led, light):
print('light: {}'.format(light))
if light:
macropad.pixels[led] = (33, 45, 230)
else:
macropad.pixels[led] = (0, 0, 0)
print('c: {}'.format(macropad.pixels[led]))
return False if light else True
timer=2
light=False
in_data=bytearray()
while True:
light = blink(1, light)
time.sleep(timer)
if serial.in_waiting > 0:
while(serial.in_waiting>0):
byte = serial.read(1)
if byte == b'\r':
exec_command(in_data.decode("utf-8"))
in_data = bytearray()
else:
in_data += byte
```
Le code est plutôt simple, nous initialisons notre Macropad et la connexion
série avec le début de notre fichier:
```python
from adafruit_macropad import MacroPad
import usb_cdc
import time
macropad = MacroPad()
serial = usb_cdc.data
```
Ensuite nous définissons notre fonction `exec_command` chargée d'interpréter les
commandes envoyées depuis la connexion série puis notre fonction `blink` changer
de faire clignoter notre DEL.
Après avoir initialiser les variables `timer` `light` et `in_data` -- cette
dernière recevra les données de la connexion série le programme commence. La
partie la plus intéressante démarre avec `if serial.is_wainting`.
À ce moment, si des données sont en attente sur le port série, alors nous les
ajoutons à notre variable `in_data`. Lors de la réception d'un retour chariot
`\r`, alors nous passons les données reçues à notre fonction `exec_command`.
### Test de notre code
Le Test démarre une fois le fichier `code.py` écrit sur notre Macropad.
Vous devriez voir la DEL sous la touche 2 clignoter toute les deux secondes.
L'écran du Macropad affiche aussi les informations données par les instructions
`print` de notre code.
À partir de de moment nous allons pouvoir utiliser `minicon` pour se connecter à
la partie REPL du Macropad et ainsi visualiser aussi les `print`. Ici la console
REPL est accessible sous le périphérique `/dev/ttyACM0` :
```shell
$ minicom -D /dev/ttyACM0
Welcome to minicom 2.8
OPTIONS: I18n
Compiled on Jan 9 2021, 12:42:45.
Port /dev/ttyACM0, 18:28:08
Press CTRL-A Z for help on special keys
light: False
c: (0, 0, 0)
light: True
c: (33, 45, 230)
```
La connexion série initiée par notre code est disponible sur le périphérique
`/dev/ttyACM1`. Pour modifier le timer, alors nous pouvons entrer la commande
suivante depuis un autre terminal:
```shell
printf "time 10\r" > /dev/ttyACM1
```
Sur notre terminal de contrôle avec `minicom`, vous voyons apparaitre alors:
```
light: True
c: (33, 45, 230)
cmd: time | opt: 10
new timer: 10.0
light: False
c: (0, 0, 0)
```
Notre timer est bien pris en compte et le clignotement se fait maintenant toutes
les dix secondes.
Ce code est bien rudimentaire : il n'y a aucun contrôle et un rien -- comme
juste envoyer `time toto` avec notre `printf` -- le fait planter. Pas de panique
: le Macropad redémarrera alors tout seul.
Autre problème, il faut attendre que notre instruction `time.sleep(timer)` soit
finie avant que la modification de notre `timer` soit prise en compte. Il est
possible d'utiliser la librairie `asyncio` pour contourner ce problème, mais
nous ne traiterons pas de ce cas dans cet article.
## Envoyer des données depuis le Macroad
Maintenant que nous avons réussi à envoyer des données depuis notre ordinateur
vers le Macropad, nous allons en envoyer en sens inverse. Nous allons modifier
le fichier `code.py`, plus précisément la fonction `exec_command` comme
ci-dessous:
```python
# [...]
def exec_command (data):
global timer
try:
command,option = data.split()
except:
command = ""
option = ""
print("cmd: {} | opt: {}".format(command, option))
if command == 'time':
timer = float(option)
print("new timer: {}".format(timer))
response = "nouveau timer : {}\r\n".format(option)
buffer = bytearray(response)
serial.write(buffer)
# [...]
```
La modification est relativement simple et tient en 3 instructions. Pour tester
son fonctionnement, nous allons ouvrir deux terminaux avec `minicom` :
1. sur notre périphérique `/dev/ttyACM0` afin d'avoir un accès à la console du
Macropad;
2. sur `/dev/ttyACM1` afin d'interagir avec notre programme dans le Macropad;
Sur cette dernière fenêtre de terminal, nous allons lancer *minicom* avec la
commande `minicom -D /dev/ttyACM1 -c on` puis une fois lancé nous allons
activer l'affichage des commandes que l'on saisit avec `Ctrl + X` puis `E`. Cette
étape est facultative mais *il est plus agréable de voir ce que nous saisissons*.
À partir de là nous pouvons changer la fréquence de clignotement avec la
commande `time` saisie directement dans *minicom*, notre Macropad nous répond
alors directement en envoyant la confirmation via la connexion série:
```
Welcome to minicom 2.8
OPTIONS: I18n
Compiled on Jan 9 2021, 12:42:45
Port /dev/ttyACM1, 17:16:49
Press CTRL-A Z for help on special keys
time 1
nouveau timer : 1
time 2
nouveau timer : 2
time 4
nouveau timer : 4
time 1
nouveau timer : 1
```
![Capture d'écran montrant les deux terminaux lancés pour le test de
communication série]({attach}./images/macropad_bidirectionnal.png)
Vous devriez obtenir un fonctionnement similaire à ce que montre la capture
d'écran ci-dessus.
Nous avons maintenant obtenu une **communication série bidirectionnelle** entre
notre Macropad et notre ordinateur. Mais cet exemple est plutôt succinct, que
pouvons nous en faire **concrètement**?
## Un (début) d'exemple "réel"
Nous allons utiliser ce que nous avons vu jusqu'ici pour mettre en place un
bouton du clavier pour mettre en sourdine le microphone de notre ordinateur.
Nous allons programmer le bouton N°1 du Macropad pur envoyer une instruction
*mute*. Sa DEL sera rouge si le microphone est coupé et verte s'il est actif. Un
script *Python* sur notre ordinateur se chargera de recevoir les ordres les
exécutera et enverra l'état du microphone en retour.
### Sur le Macripad
Voici le code à mettre dans `code.py`:
```python
from adafruit_macropad import MacroPad
import usb_cdc
import time
macropad = MacroPad()
serial = usb_cdc.data
def exec_command (data):
global muted
try:
command,option = data.split()
except:
command = ""
option = ""
print("cmd: {} | opt: {}".format(command, option))
if command == 'mute':
muted = True if option == 'yes' else False
timer=2
in_data=bytearray()
muted=False
while True:
# Define muted button color
if muted:
macropad.pixels[1] = (255,0,0)
else:
macropad.pixels[1] = (0,255,0)
# Get key event
key_event = macropad.keys.events.get()
if key_event:
if key_event.pressed:
if key_event.key_number == 1:
serial.write(bytearray('mute\r\n'))
# Receive serial data
if serial.in_waiting > 0:
while(serial.in_waiting > 0):
byte = serial.read(1)
if byte == b'\r':
exec_command(in_data.decode("utf-8"))
in_data = bytearray()
else:
in_data += byte
```
Comme vous pouvez le constater nous utilisons largement le code présent dans les
deux premières parties.
### Le script Python sur notre ordinateur
Il utilise `pactl` pour piloter le micro et obtenir son état. Voici le code à
mettre dans le fichier `serial_daemon.py`:
```python
#!/usr/bin/env python
import serial
import subprocess
ser = serial.Serial(port='/dev/ttyACM1')
ser.flushInput()
print('Begin loop')
while True:
line = ser.readline()
try:
command = (line.decode()).split()
print('command received: {}'.format(command[0]))
except:
print('no valid command received')
command = ""
try:
if command[0] == "mute":
# First subprocess for toggle mote the microphone
subprocess.run(
['pactl', 'set-source-mute', '@DEFAULT_SOURCE@', 'toggle'],
)
# Second one for check the states of microphone
result = subprocess.run(
['pactl', 'get-source-mute', '@DEFAULT_SOURCE@'],
capture_output=True
)
message = "mute {}\r".format(result.stdout.split()[1].decode())
ser.write(message.encode())
print('command sent: {}'.format(message))
except Error as e:
print('Error in command: {}'.format(e))
```
Le script commence par initialiser le périphérique série, puis vide l'ensemble
des données présente dans le buffer du port série avec `ser.flushInput()` afin
de repartie de zéro. Commence ensuite une bouble infinie (notre script reste
résident en mémoire).
`line = ser.readline()` permet de mettre notre processus en attente passive tant
qu'une fin de ligne de type `\n\r` n'a pas été reçue. A partir de là le script
traite la linge reçue.
Le script lance la commande pour changer l'état du microphone puis la commande
pour en vérifier l'état. L'instruction `ser.write()` transmet l'état de ce
dernier au Macropad qui **agira en conséquence**.
Ici encore le script reste minimal, il suffit maintenant de le lancer et
d'observer les différents messages ainsi que l'état di microphone :
```shell
chmod +x serial_daemon.py
./serial_daemon.py
```
En appuyant sur le bouton adéquat du clavier, notre script `serial_daemon`
devrait réagir comme ci-dessous dans la console:
```
Begin loop
command received: mute
command sent: mute yes
command received: mute
command sent: mute no
command received: mute
command sent: mute yes
```
Et la lumière sous la touche devrait changer de couleur en fonction de
l'activation de la sourdine.
## En conclusion
Nous avons donc vu comment utiliser les ports série -- que se soir pour la
connexion REPL ou pour les données -- sur notre *Macropad Adafruit*.
Personnellement j'adore ce petit périphérique qui permet une infinité de
possibilité et se programme relativement facilement grâce à *CircuitPython*.
Bien sût cet article n'a pas pour vocation d'aller en profondeur, que se soit
dans les paramétrages des connexions séries que dans les possibilités offertes.
Mais su le sujet vous intéresse une petite recherche sur les Internets vous
donnera de quoi faire. Je ne peux cependant que vous conseiller l'excellent article
de [Carlos Olmos] qui a utilisé le Macropad pour se créer un jukebox. Je m'en
suis inspiré pour l'écriture de cet article.
[Carlos Olmos]:https://carlosolmos.dev/posts/the-macropad-jukebox/