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