Jouer avec le Macropad Adafruit! mais en série

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

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, couplé aux diférentes bibliothèques fournies par Adafuit, 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

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

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:

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.

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:

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.

Le code est plutôt simple, nous initialisons notre Macropad et la connexion série avec le début de notre fichier:

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 :

$ 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:

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:

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

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

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:

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

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:

#!/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

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 :

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.

Credits

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