diff --git a/content/articles/2023/adafruit_macropad_conn_serie/files/bidirectionnal/code.py b/content/articles/2023/adafruit_macropad_conn_serie/files/bidirectionnal/code.py new file mode 100644 index 0000000..bd3e219 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/files/bidirectionnal/code.py @@ -0,0 +1,47 @@ +from adafruit_macropad import MacroPad +import usb_cdc +import time + +macropad = MacroPad() +serial = usb_cdc.data + +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) + +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 diff --git a/content/articles/2023/adafruit_macropad_conn_serie/files/blink/boot.py b/content/articles/2023/adafruit_macropad_conn_serie/files/blink/boot.py new file mode 100644 index 0000000..c435961 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/files/blink/boot.py @@ -0,0 +1,5 @@ +import usb_cdc +try: + usb_cdc.enable(console=True, data=True) # Enable console and data# Write your code here :-) +except Exception as e: + print(e) diff --git a/content/articles/2023/adafruit_macropad_conn_serie/files/blink/code.py b/content/articles/2023/adafruit_macropad_conn_serie/files/blink/code.py new file mode 100644 index 0000000..8be0139 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/files/blink/code.py @@ -0,0 +1,44 @@ +from adafruit_macropad import MacroPad +import usb_cdc +import time + +macropad = MacroPad() +serial = usb_cdc.data + +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)) + +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 diff --git a/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/code.py b/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/code.py new file mode 100644 index 0000000..26ff449 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/code.py @@ -0,0 +1,45 @@ +from adafruit_macropad import MacroPad +import usb_cdc +import time + +macropad = MacroPad() +serial = usb_cdc.data + +def exec_command (data): + global muted + try: + print(data) + command,option = data.split() + except: + command = "" + option = "" + print("cmd: {} | opt: {}".format(command, option)) + if command == 'mute': + muted = True if option == 'yes' else False + print('muted: {}'.format(muted)) + +timer=2 +in_data=bytearray() +muted=False + +while True: + key_event = macropad.keys.events.get() + if muted: + macropad.pixels[1] = (255,0,0) + else: + macropad.pixels[1] = (0,255,0) + + if key_event: + if key_event.pressed: + if key_event.key_number == 1: + print('get key 1') + serial.write(bytearray('mute\r\n')) + + 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 diff --git a/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/serial_daemon.py b/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/serial_daemon.py new file mode 100755 index 0000000..4f77299 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/files/daemon/serial_daemon.py @@ -0,0 +1,34 @@ +#!/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)) diff --git a/content/articles/2023/adafruit_macropad_conn_serie/images/macropad_bidirectionnal.png b/content/articles/2023/adafruit_macropad_conn_serie/images/macropad_bidirectionnal.png new file mode 100644 index 0000000..da5d344 Binary files /dev/null and b/content/articles/2023/adafruit_macropad_conn_serie/images/macropad_bidirectionnal.png differ diff --git a/content/articles/2023/adafruit_macropad_conn_serie/index.md b/content/articles/2023/adafruit_macropad_conn_serie/index.md new file mode 100644 index 0000000..b7af043 --- /dev/null +++ b/content/articles/2023/adafruit_macropad_conn_serie/index.md @@ -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/