Title: Jouer avec le Macropad Adafruit! mais en série
Category: linux
Tags: Adafruit, CircuitPython, Python
Date: 2023-02-27 15:10
cover: assets/backgrounds/Adafruit_Macropad.jpg

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.

![Photo du Macropad, crédits Kattni Rembor / Adafruit ]({attach}./images/adafruit_products_MacroPad_top_angle.jpg)

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
pip install circup
```

Puis après avoir branché le Macropad et monté l'espace de stockage :

```
circup install adafruit_macropad 
```

## Installer minicom

Nous allons maintenant préparer 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`.

Vous pouvez télécharger le fichier `boot.py` [ici]({attach}./files/blink/boot.py).

## 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
```

Vous pouvez télécharger le fichier `code.py` [ici]({attach}./files/blink/code.py).

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 via 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)
 # [...]
```

Vous pouvez télécharger ce fichier `code.py` [ici]({attach}./files/daemon/code.py).

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 + A` 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 Macropad

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.

Ce fichier `code.py` est disponible [ici]({attach}./files/daemon/code.py)

### 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))       
```

Ce fichier `serial_daemon.py` est dispoible [ici]({attach}./files/daemon/serial_daemon.py)

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 boucle 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 du 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 soit 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 si 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/

## Credits

Les photos proviennent du site Adafuit, prisent par [Kattni Rembor] et sous licence
Creative Common By-Sa.

[Kattni Rembor]:https://learn.adafruit.com/u/kattni
