1. Réalisation du montage

Avant de rentrer dans la programmation du PICO, nous allons réaliser un petit cahier des charges pour le montage suivant. Il s'agit d'un chapelle médiéval en LEGO qui complètera mon village médiéval. Le scénario est le suivant : une bougie s'illumine en permanence dans le petit cimetière, tous les x secondes, la cloche s'anime et sonne automatiquement.

1.1. le cahier des charges

  • une led fonctionne en permance avec une simulation de bougie (scintillement),
  • tous les x secondes, un scénario déclence l'animation de la cloche avec le son,
  • la lecture du son de la cloche ne doit pas arrêter le scintillement de la led,
  • on doit synchroniser le déplacement de la cloche avec le son,
  • possibilité de désactiver ou non l'animation,
  • possibilité d'augmenter ou réduire l'animation de la cloche,
  • possibilité d'augmenter ou réduire le déclenchement du scénario.

Nous utiliserons le mode asynchrone pour la gestion des processus. La librairie PWM sera utilisée pour la LED. Nous utiliserons une librairier pour le servomoteur qui actionnera la cloche et la librairie vs1053 pour le module son. Nous utiliserons aussi la connexion Wifi et le mode Web pour la configuration du scénario.

2. Le mode asynchrone du PICO

La bibliothèque asyncio permet de gérer des tâches asynchrones. Ces tâches sont autonomes et semblent s'exécuter en même temps en fonction de la disponibilité du PICO. Il faut que ces tâches s'exécutent le plus rapidement possible pour avoir l'impression de la simultanéité. Pour plus d'information, je vous conseille la lecture de cette documentation https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md

Dans notre exemple nous utiliserons plusieurs tâches réparties dans ces fonctions :

  • lumiere_tombe() qui sera utilisé pour simuler la bougie,
  • Eglise_sonne() qui sera utilisé pour le tintement de la cloche de la chapelle,
  • handle_client() qui sera utilisé pour paramétrer le programme,
  • main() le programme principal qui permettra de gérer la temporisation et l'animation.

Ces fonctions s'appellent des coroutines, elles seront chargées en mémoire et s'exécuteront indépendemment.

3. Le montage

Pour ce montage, je n'utiliserai pas le module de carte SD. Pour alimenter le montage, nous utiliserons un module d'alimentation pour baisser la tension de 9v vers le 5 V. Pour information, j'utilise le 9v pour l'alimentation des moteurs LEGO et distribué dans tous les modules de mon village médiéval. Le PICO est monté sur une carte de prototypage et pour le câblage, j'utilise des connecteurs à vis miniatures. Je trouve plus pratique d'utiliser des connecteurs que de souder directement les fils sur les circuits imprimés. L'imprimante 3D est mise à contribution pour fixer les différents modules sur la plaque LEGO.

3.1. schéma de câblage

Voici le schéma de câblage.

VS 1053PicoCouleur connexion
5 V 5 V général Rouge
GND GND général Noir
XRST GP13 (17) Vert
MISO GP4 (6) Jaune
MOSI GP7 (10) Jaune
SCLK GP6 (9) Bleu
DREQ GP10 (14) Brun
XCS GP12 (16) Violet
XDCS GP 11 (15) Gris
LED GP 15 (20)  
Cmd Servomoteur GP 22 (29) Orange

 

 

4. Le programme pas à pas

4.1. Le corps du programme

Le premier montage électronique concerne le corps du programme avec la création des coroutines et la gestion de la bougie (LED) dans le cimetière. Nous allons créer la coroutine main() qui va gérer l'animation ainsi que sa temporisation. 

Commençons par la déclaration des bibliothèques

from machine import Pin, PWM

Ensuite nous initialisons la LED en mode PWM

#initialisation de la led du cimetière en PWM
led_tombe = PWM(Pin(15))
led_tombe.freq(1_000)
led_tombe.duty_u16(0)

Créons la coroutine lumiere_tombe

async def lumiere_tombe():
    while True:
        duty = random.randint(0,80)
        led_tombe.duty_u16(int((duty/100)*30_840))
        await asyncio.sleep(0.05)  # Blink interval

Puis la coroutine main

async def main():
    asyncio.create_task(lumiere_tombe())
    print('Debut programe')
    while True:
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)
        print("Ce message s'affiche toutes les 5 secondes")

et terminons par le démarrage du programme

# Création d'une boucle d'événements
loop = asyncio.get_event_loop()
# Création de la coroutin main
loop.create_task(main())

try:
    loop.run_forever()
except Exception as e:
    print('Error occurred: ', e)
except KeyboardInterrupt:
    print('Program Interrupted by the user')

Lançon le programme, la LED simule une bougie en permanence tandis que la coroutine main() affiche un message toutes les cinq secondes comme le montre la fenêtre shell de Thony.

 prg1

4.2.  La gestion du son

Maintenant, nous allons rajouter la gestion du son. Nous lancerons un son de cloche toutes les cinq secondes. On commence par la déclaration de la bibliothèque vs1053 que vous trouverez sur le github de Peter Hinch https://github.com/peterhinch/micropython-vs1053 et ne pas oublier la bibliothèque SPI.

from vs1053 import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin, PWM

Il faut initialiser le module VS1053

# initialisation du protocole SPI pour
# le module son VS1053
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4))

# broches du module VS1053
reset = Pin(13, Pin.OUT, value=1) 
xcs = Pin(12, Pin.OUT, value=1) 
xdcs = Pin(11, Pin.OUT, value=1)
dreq = Pin(10, Pin.IN)

# sdcs et mp ne sont pas initialisés car pas de module SDRam
player = VS1053(spi, reset, dreq, xdcs, xcs) 

 

Il faut insérer la commande du son dans la boucle de la coroutine main(). On utilisera un fichier mp3 que l'on chargera dans la mémoire du PICO. Vous pouvez télécharger le fichier dong1.mp3.

async def main():
    asyncio.create_task(lumiere_tombe())
    print('Debut programe')
    while True:
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)
        print("la cloche tinte")
        fichier = "dong1.mp3"
        song = open(fichier, "rb")
        player.volume(0, 0) # réglage du volume sonore
        await player.play(song)

 

Info
Les coroutines lancées avec le programme précédent sont toujours chagées en mémoire. Si vous lancez ce programme sans avoir réinitialiser le PICO vous risquez d'avoir des effets indésirables. Je vous conseille de lancer les commandes suivantes dans le shell de Thonny
reinit
Pour les essais suivants, la commande machine.reset() suffit.

 Lançons le programme, la LED s'allume en permanence et la cloche tinte toutes les cinq secondes.

prg2

4.3.  Configuration du déclenchement de l'animation.

Pour déclencher l'animation à intervalle régulier, nous utiliserons une variable compteur qui s'incrémentera toutes les 5 secondes environ. Donc si on compte jusqu'à 10 boucles, on devrait arriver environ à une animation toutes les minutes. Nous allons utiliser une coroutine Eglise_sonne(), comme celle-ci boucle en permanence, nous utiliserons une variable globale start_animation pour déclencher le son. Tout d'abord, initialisons les variables.

# Initialize variables
compteur = 0
start_animation = False

nous allons créer une nouvelle coroutine Eglise_sonne()

async def Eglise_sonne():
    global start_animation
    
    player.volume(0, 0) # réglage du volume sonore
    while True:
        if start_animation :
            fichier = "dong1.mp3"
            song = open(fichier, "rb")
            print("la cloche tinte")
            #print("En train de jouer le fichier mp3 ") # affichage du titre
            await player.play(song)
            start_animation = False
        await asyncio.sleep(0)

Modifions la coroutine main(). Nous rajoutons les variables globales start_animation et compteur (nous aurions pu éviter de créer une variable globale mais nous en aurons besoin plus tard) et le chargement de la coroutine Eglise_sonne()

async def main():
    global compteur, start_animation

    asyncio.create_task(lumiere_tombe())
    asyncio.create_task(Eglise_sonne())
    print('Debut programe')
    while True:
        compteur +=1
        print ("compteur :", compteur)
        if compteur == 10:
            start_animation = True
            compteur = 0
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)

Lançons le programme. La cloche tintera lorsque la variable compteur sera égale à 10.

prg3 

4.4. Animation de la cloche

Continuons par le déplacement de la cloche en synchonisant le son. Nous utiliserons un servomoteur pour le déplacement de la cloche. A chaque déplacement de la cloche correspondra un son de cloche. Pour éviter une uniformité du son, nous utiliserons plusieurs sons aléatoirement. Pour le servomoteur, nous utiliserons une librairie servo.py que vous trouverez dans ce tutoriel Le servo-moteur GeekServo avec un Raspberry Pi Pico.

Commençons par la déclaration de la librairie servo et random

from vs1053 import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin, PWM
from servo import Servo
import random

Puis l'initialisation du servomoteur avec le position du servo en position médiane.

#initialisation du servomoteur
my_servo = Servo(pin=22)
#position médiane du servo
my_servo.move(90)

Ensuite il faudra modifier la coroutine Eglise_sonne(). Deux variables locales sont créés : pos indique les deux positions de la cloche (à modifier en fonction du modèle de servomoteur, nb_ring indique le nombre de déplacement de la cloche. Pour éviter la répétion du même son, on utilisera une fonction aléatoire et quatre fichiers de son respectivement dong11.mp3, dong12.mp3, dong13.mp3 et dong14.mp3.

async def Eglise_sonne():
    global start_animation
    
    nb_ring = 1
    pos = 78
    player.volume(0, 0) # réglage du volume sonore
    while True:
        if start_animation and nb_ring < 10:
            my_servo.move(pos)
            duty = random.randint(1, 4)
            fichier = "dong1"+str(duty)+".mp3"
            song = open(fichier, "rb")
            print("la cloche tinte")
            #print("En train de jouer le fichier mp3 ") # affichage du titre
            await player.play(song)
            if pos == 78:
                pos = 100
            else:
                pos = 78
            nb_ring +=1
        else:
            nb_ring = 1
            start_animation = False
            #position médiane du servo
            my_servo.move(90)
        await asyncio.sleep(0) 

Comme il y a plusieurs déplacement de cloche, il faudra arrêter le comptage tant que l'animation est en cours de fonctionnement. Pour cela il faut modifier la coroutine main() avec un test de l'animation en cours.

        if not start_animation:
            compteur +=1

Voici le résultat sur la console Shell

 prog4

4.5.  Configuration de l'animation via un serveur Web

Comme nous n'aurons pas d'entrée physique pour modifier les paramètres de l'animation, nous utiliserons une connexion Wifi avec un serveur Web installé sur notre PICO. J'ai choisi la méthode Point d'accès pour éviter le blocage en absence d'un accès Wifi. Ensuite il faudra initialiser un serveur Web qui délivera une page de configuration. Commençons par modifier notre programme.

Il faudra rajouter les bibliothèques network, socket et rp2.

from vs1053 import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin, PWM
from servo import Servo
import random
import os
import network
import asyncio
import socket
import rp2

Ensuite configurons les paramètres Wifi.

# Wi-Fi credentials
ssid = 'PicoCastle'
password = '12345678'

# Set country to avoid possible errors
rp2.country('FR')

Nous rajouterons quelques variables supplémentaires pour la configuration.

# Initialize variables
state = "ON"
duree_value = 10
compteur = 0
start_animation = False
total_ring = 20

Ensuite nous rajoutons une fonction pour afficher le contenu de la page Web.

# HTML template for the webpage
def webpage(duree_value, state, total_ring):
    html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Pico Web Server - Animation Church</title>
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>
        <body>
            <h1>Raspberry Pi Pico Web Server</h1>
            <h2>Module Church V2</h2>
            <h2>Animation Control</h2>
            <form action="./lighton">
                <input type="submit" value="Animation on" />
            </form>
            <br>
            <form action="./lightoff">
                <input type="submit" value="Animation off" />
            </form>
            <p>Animation state: {state}</p>
            <h2>Animation duration</h2>
            <form action="./value10">
                <input type="submit" value="10" />
            </form>
            <form action="./value20">
                <input type="submit" value="20" />
            </form>
            <form action="./value30">
                <input type="submit" value="30" />
            </form>
            <p>Animation duration value: {duree_value}</p>
            <h2>Number of bell rings</h2>
            <form action="./ring10">
                <input type="submit" value="10" />
            </form>
            <form action="./ring20">
                <input type="submit" value="20" />
            </form>
            <form action="./ring30">
                <input type="submit" value="30" />
            </form>
            <p>value number of bell strokes: {total_ring}</p>
        </body>
        </html>
        """
    return str(html)

Une nouvelle fonction sera ajoutée pour initialiser le point d'accès

# Init Wi-Fi Interface
def init_wifi(ssid, password):
    ap = network.WLAN(network.AP_IF)
    ap.config(essid=ssid, password=password)
    ap.active(True)
    while ap.active() == False:
        pass
    print('AP Mode Is Active, You can Now Connect')
    print('IP Address To Connect to:: ' + ap.ifconfig()[0])
    return True

Et une nouvelle coroutine pour gérer les réponses du serveur web.

# Asynchronous functio to handle client's requests
async def handle_client(reader, writer):
    global state, duree_value, compteur, total_ring
    
    print("Client connected")
    request_line = await reader.readline()
    print('Request:', request_line)
    
    # Skip HTTP request headers
    while await reader.readline() != b"\r\n":
        pass
    
    request = str(request_line, 'utf-8').split()[1]
    print('Request:', request)
    
    # Process the request and update variables
    if not start_animation:
        if request == '/lighton?':
            print('Animation on')
            #led_control.value(1)
            state = 'ON'
        elif request == '/lightoff?':
            print('Animation off')
            #led_control.value(0)
            state = 'OFF'
        elif request == '/value10?':
            compteur = 0
            duree_value = 10
        elif request == '/value20?':
            compteur = 0
            duree_value = 20
        elif request == '/value30?':
            compteur = 0
            duree_value = 30
        elif request == '/ring10?':
            total_ring = 10
        elif request == '/ring20?':
            total_ring = 20
        elif request == '/ring30?':
            total_ring = 30

    # Generate HTML response
    response = webpage(duree_value, state, total_ring)  

    # Send the HTTP response and close the connection
    writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    writer.write(response)
    await writer.drain()
    await writer.wait_closed()
    print('Client Disconnected')

Et finalement on modifiera la coroutine main()

 

async def main():
    global compteur, state, duree_value, start_animation
    
    if not init_wifi(ssid, password):
        print('Exiting program.')
        return
    
    # Start the server and run the event loop
    print('Setting up server')
    server = asyncio.start_server(handle_client, "0.0.0.0", 80)
    asyncio.create_task(server)
    asyncio.create_task(lumiere_tombe())
    asyncio.create_task(Eglise_sonne())
    
    while True:
        if not start_animation:
            compteur +=1
        print ("compteur :", compteur)
        if compteur == duree_value:
            if state == "ON":
                start_animation = True
            compteur = 0
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)
        print('This message will be printed every 5 seconds')

Au démarrage du programme, la console nous envoie les informations de réseau.

 prog5

 Maintenant, une fois connecté au point d'accès avec l'adresse http://192.168.4.1, vous pouvez modifiez la durée de l'intervalle de l'animation, le nombre de tintement de la sonnerie et l'activation ou la désactivation de l'animation.

conf page web

4.6. Le code complet

j'ai rajouté une coroutine (blink_led) pour afficher la led interne.

'''
Programme Eglise village médiéval
date 25/07/2025
version 2
'''

from vs1053 import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin, PWM
from servo import Servo
import random
import os
import network
import asyncio
import socket
import rp2

# Wi-Fi credentials
ssid = 'PicoCastle'
password = '12345678'

# Set country to avoid possible errors
rp2.country('FR')

my_servo = Servo(pin=22)

# initialisation du protocole 
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4))

#initialisation de la led du pico
# Create several LEDs
led_blink = Pin("LED", Pin.OUT)
#led_onboard = machine.Pin("LED", machine.Pin.OUT)

#initialisation de la led de la teombe
led_tombe = PWM(Pin(15))
led_tombe.freq(1_000)
led_tombe.duty_u16(0)

# broches du module VS1053
reset = Pin(13, Pin.OUT, value=1) 
xcs = Pin(12, Pin.OUT, value=1) 
xdcs = Pin(11, Pin.OUT, value=1)
dreq = Pin(10, Pin.IN)

# sdcs et mp ne sont pas initialisés car pas de module SDRam
player = VS1053(spi, reset, dreq, xdcs, xcs) 

my_servo.move(90)
# Initialize variables
state = "ON"
duree_value = 10
compteur = 0
start_animation = False
total_ring = 20

async def lumiere_tombe():
    while True:
        duty = random.randint(0,80)
        led_tombe.duty_u16(int((duty/100)*30_840))
        #time.sleep_ms(random.randint(0, 200))
        #print("led")
        await asyncio.sleep(0.05)  # Blink interval

async def Eglise_sonne():
    global start_animation, total_ring
    
    nb_ring = 1
    pos = 78
    while True:
        if start_animation and nb_ring < total_ring:
            my_servo.move(pos)
            duty = random.randint(1, 4)
            fichier = "dong1"+str(duty)+".mp3"
            song = open(fichier, "rb")
            #print("En train de jouer le fichier mp3 ") # affichage du titre
            player.volume(10, 10) # réglage du volume sonore
            await player.play(song)
            if pos == 78:
                pos = 100
            else:
                pos = 78
            nb_ring +=1
        else:
            nb_ring = 1
            start_animation = False
            my_servo.move(90)
        await asyncio.sleep(0) 


# HTML template for the webpage
def webpage(duree_value, state, total_ring):
    html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Pico Web Server - Animation Church</title>
            <meta name="viewport" content="width=device-width, initial-scale=1">
        </head>
        <body>
            <h1>Raspberry Pi Pico Web Server</h1>
            <h2>Module Church V2</h2>
            <h2>Animation Control</h2>
            <form action="./lighton">
                <input type="submit" value="Animation on" />
            </form>
            <br>
            <form action="./lightoff">
                <input type="submit" value="Animation off" />
            </form>
            <p>Animation state: {state}</p>
            <h2>Animation duration</h2>
            <form action="./value10">
                <input type="submit" value="10" />
            </form>
            <form action="./value20">
                <input type="submit" value="20" />
            </form>
            <form action="./value30">
                <input type="submit" value="30" />
            </form>
            <p>Animation duration value: {duree_value}</p>
            <h2>Number of bell rings</h2>
            <form action="./ring10">
                <input type="submit" value="10" />
            </form>
            <form action="./ring20">
                <input type="submit" value="20" />
            </form>
            <form action="./ring30">
                <input type="submit" value="30" />
            </form>
            <p>value number of bell strokes: {total_ring}</p>
        </body>
        </html>
        """
    return str(html)

# Init Wi-Fi Interface
def init_wifi(ssid, password):
    ap = network.WLAN(network.AP_IF)
    ap.config(essid=ssid, password=password)
    ap.active(True)
    while ap.active() == False:
        pass
    print('AP Mode Is Active, You can Now Connect')
    print('IP Address To Connect to:: ' + ap.ifconfig()[0])
    return True

# Asynchronous functio to handle client's requests
async def handle_client(reader, writer):
    global state, duree_value, compteur, total_ring
    
    print("Client connected")
    request_line = await reader.readline()
    print('Request:', request_line)
    
    # Skip HTTP request headers
    while await reader.readline() != b"\r\n":
        pass
    
    request = str(request_line, 'utf-8').split()[1]
    print('Request:', request)
    
    # Process the request and update variables
    if not start_animation:
        if request == '/lighton?':
            print('Animation on')
            #led_control.value(1)
            state = 'ON'
        elif request == '/lightoff?':
            print('Animation off')
            #led_control.value(0)
            state = 'OFF'
        elif request == '/value10?':
            compteur = 0
            duree_value = 10
        elif request == '/value20?':
            compteur = 0
            duree_value = 20
        elif request == '/value30?':
            compteur = 0
            duree_value = 30
        elif request == '/ring10?':
            total_ring = 10
        elif request == '/ring20?':
            total_ring = 20
        elif request == '/ring30?':
            total_ring = 30

    # Generate HTML response
    response = webpage(duree_value, state, total_ring)  

    # Send the HTTP response and close the connection
    writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    writer.write(response)
    await writer.drain()
    await writer.wait_closed()
    print('Client Disconnected')
    
async def blink_led():
    while True:
        led_blink.toggle()  # Toggle LED state
        await asyncio.sleep(0.5)  # Blink interval

async def main():
    global compteur, state, duree_value, start_animation
    
    if not init_wifi(ssid, password):
        print('Exiting program.')
        return
    
    # Start the server and run the event loop
    print('Setting up server')
    server = asyncio.start_server(handle_client, "0.0.0.0", 80)
    asyncio.create_task(server)
    asyncio.create_task(blink_led())
    asyncio.create_task(lumiere_tombe())
    asyncio.create_task(Eglise_sonne())
    
    while True:
        if not start_animation:
            compteur +=1
        print ("compteur :", compteur)
        if compteur == duree_value:
            if state == "ON":
                start_animation = True
            compteur = 0
        # Add other tasks that you might need to do in the loop
        await asyncio.sleep(5)
        print('This message will be printed every 5 seconds')
        

# Create an Event Loop
loop = asyncio.get_event_loop()
# Create a task to run the main function
loop.create_task(main())

try:
    # Run the event loop indefinitely
    loop.run_forever()
except Exception as e:
    print('Error occurred: ', e)
except KeyboardInterrupt:
    print('Program Interrupted by the user')

5. Conclusion

La gestion de la programmation asynchrone est un peu prise de tête et il faut repenser la manière de programmer ses scénarios d'animation. Je suis un peu déçu de la puissance sonore du module son. Autre point, je n'ai pas réussi à modifier le réglage du volume en mode asynchrone alors qu'en mode synchrone, la fonction fonctionne très bien.