Bridge Serial (repl) / Http

L'idée

Une fonctionnalité très intéressante des cartes Micropython disposant du wifi, telle que ESP 8266 ou ESP 32, c'est la possibilité de pouvoir facilement créer des petits micro-serveurs embarqués capables de fournir une interface de contrôle de la carte, des périphériques, etc. via une interface web fournie par la carte elle-même.

Cette fonctionnalité n'existe pas sur les cartes sans wifi. Pourtant, on aurait envie qu'une interface web ayant été développée pour un ESP soit utilisable peu ou prou de la même façon avec une carte Pi Pico par exemple. Dans un contexte didactique, cela prend sens de pouvoir facilement obtenir une interface dans un navigateur qui permette d'interagir avec la carte.

D'où l'idée que, sur un poste local, celui sur lequel on travaille pour programmer la carte, on puisse obtenir une interface dans le navigateur qui serait fournie par la carte Pi Pico (ou autre). Cela permettrait des visualisations directement dans le navigateur à partir d'un code micropython sur la carte.

La clé d'une telle possibilité serait donc la réalisation d'un "bridge" serial / http : la requête http transmise par le navigateur serait transmise par série à la carte et en retour, la réponse de la carte serait envoyée au navigateur.

Note

D'un point de vue "conceptuel", c'est un peu la même chose que ce que l'on peut arriver à faire avec un node JS qui reçoit sur le port série des data et fourni une interface web graphique.

Quelques différences cependant :

  • ici, la carte série "sert" vraiment la page web via le port série, et pas seulement les données, sauf via les requêtes ajax.
  • on peut utiliser ce que l'on veut en terme de librairie, etc. et pas seulement ce qui est fourni/disponible par le framework dans le cas de nodejs
  • en terme de rapidité, la réactivité est excellente avec le bridge, avec des réponses Ajax de l'ordre de 10ms max pour des valeurs simples, ce qui correspond au temps de réponse carte pi Pico + bridge

Les possibilités

Dans tous les cas, ce qui semble le plus simple et facile, c'est d'implémenter un petit serveur minimal en Python, avec la librairie Bottle.py par exemple, couplé à l'utilisation de la librairie PySerial, serveur qui jouerait le rôle du bridge.

On a plusieurs possibilités envisageables pour la visualisation :

Interface PyQt5 avec "browser" intégré

Une première option est de faire une interface serveur + visualisation web... mais on perd la "spontanéité" lié à l'utilisation de n'importe quel navigateur.

Serveur Python simple + visualisation dans le navigateur

C'est la bonne option à priori car on se connecte sur le réseau local avec le navigateur et on obtient la réponse de la carte.

En sujets corrélatifs, on a :

  • gestion des librairies js : elles peuvent être fournies par le serveur "bridge serial-http" lui-même, et donc être sur le poste principal, limitant du coup la "bande passante" de la communication série.
  • la page principale : elle peut être fournie par la carte micro-python, ou bien être fournie par le serveur local, le pont ayant dans ce cas essentiellement un rôle de gestion des requêtes ajax, via une connexion en mode repl de la carte.
  • A noter que le code micropython peut être flashé par le serveur lui-même, à partir du moment où on est en mode repl. . . Et on a finalement 2 possibilités pour la gestion des requêtes : soit au niveau du serveur, on affecte des routes à du code qui est envoyé à la carte. Ou bien, on implémente dans un code python des fonctions qui seront appelées selon les différentes requêtes. C'est cette solution qui réparti au mieux les rôles serveur / code micropython. Et permet également une transposabilité facilitée vers différentes cartes.

On peut aussi envisager que ce bridge dispose d'une interface PyQt5 qui permet l'édition du code que l'on envoie à la carte. Mais c'est une étape supplémentaire, au-delà de la "proof of concept" qui peut rester très simple.

Au final, le petit serveur bridge http-serial va "filtrer" les requêtes et réagir différemment selon les cas :

  • si c'est une requête de fichier, il renvoit le fichier statique correspondant
  • si c'est une requête "index.html" au choix il renvoie le fichier depuis le pc ou via le port série, fournit par la carte micropython,
  • si c'est une requête Ajax, il transmet et attend la réponse

Autour du sujet

Exploration dans l'interpréteur :

Simple test d'un serveur bottlepy

from bottle import route, run

server_ip='0.0.0.0'
server_port=8080


@route('/') # définit une route = adresse après ip donc ip/
def ok(): # code a executer lors de l'appel ce cette url
        return "Serveur Bottle + CherryPy OK !"


run(host=server_ip, port=server_port, server='cherrypy')

Bottle v0.12.15 server starting up (using CherryPyServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.

Test d'une route qui envoie une commande par repl :

import pyboard
pyb=pyboard.Pyboard('/dev/ttyACM3', 115200)


pyb.enter_raw_repl()

# code initial 
ret=pyb.exec("""from machine import Pin

led=Pin(25,Pin.OUT)

""")


ret=pyb.exec("led.on()")

ret=pyb.exec("led.off()")

@route('/') # définit une route = adresse après ip donc ip/
def ok(): # code a executer lors de l'appel ce cette url

        ret=pyb.exec("led.toggle()") # action sur la route

        return "Serveur Bottle + CherryPy OK !"


run(host=server_ip, port=server_port, server='cherrypy')
Bottle v0.12.15 server starting up (using CherryPyServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.

A chaque mise à jour du navigateur, on a bien la LED onboard qui change d'état ! Donc les bases du principe sont posées.

Code de serveur bridge serial repl - http

L'idée ici est plutôt que le code Micro-python reçoivent directement la route reçue et qu'elle soit traitée par le code micropython.

On peut faire par exemple :

#!/usr/bin/env python3

# import
from bottle import route, run

server_ip='0.0.0.0'
server_port=8080


import pyboard
pyb=pyboard.Pyboard('/dev/ttyACM3', 115200)


pyb.enter_raw_repl()

# code initial Micropython envoyé à la carte
ret=pyb.exec("""from machine import Pin
import random

led=Pin(25,Pin.OUT)

# fonction de gestion des routes - appelee par bridge
def get(route):

    if route=='/':

        print("Server Micropython serial to http ok !")

    elif route=='/ledon/':
        led.on()
        print('led ON')

    elif route=='/ledoff/':
        led.off()
        print('led OFF')

    elif route=='/ajax/':

        value=random.random()

        print(str(value))

""")

@route('/') # définit une route = adresse après ip donc ip/
def home(): # code a executer lors de l'appel ce cette url

        print('/')

        ret=pyb.exec("get('/')") # envoie la route au code micropython

        return ret # reponse pour le navigateur

@route('/ledon/') # définit une route = adresse après ip donc ip/
def ledon(): # code a executer lors de l'appel ce cette url

        print('/ledon/')

        ret=pyb.exec("get('/ledon/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur


@route('/ledoff/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ledoff/')

        ret=pyb.exec("get('/ledoff/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

@route('/ajax/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ajax/')

        ret=pyb.exec("get('/ajax/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

run(host=server_ip, port=server_port, server='cherrypy')

Une fois ce code lancé, ouvrir un navigateur sur 127.0.0.1:8080 :

  • la route / renvoie le message d'accueil
  • la route ledon allume la LED onboard
  • la route ledoff eteint la LED onboard
  • la route ajax renvoie une valeur randomisée

Les bases sont clairement posées ici et noter au passage que le code Python lui-même permet d'envoyer le code initial utile à la carte Micropython . Ce principe est très général !

Le code Micropython va logiquement s'étoffer, et on peut vouloir qu'il soit chargé à partir d'un fichier ce qui se fait en faisant :

#!/usr/bin/env python3

# import
from bottle import route, run

server_ip='0.0.0.0'
server_port=8080


import pyboard
pyb=pyboard.Pyboard('/dev/ttyACM3', 115200)


pyb.enter_raw_repl()

# code initial Micropython envoyé à la carte à partir fichier
file=open("upy_test.py",'r') # ouverture fichier
code=file.read() # lecture fichier
file.close() # fermeture fichier
ret=pyb.exec(code) # exécution du fichier

@route('/') # définit une route = adresse après ip donc ip/
def home(): # code a executer lors de l'appel ce cette url

        print('/')

        ret=pyb.exec("get('/')") # envoie la route au code micropython

        return ret # reponse pour le navigateur

@route('/ledon/') # définit une route = adresse après ip donc ip/
def ledon(): # code a executer lors de l'appel ce cette url

        print('/ledon/')

        ret=pyb.exec("get('/ledon/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur


@route('/ledoff/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ledoff/')

        ret=pyb.exec("get('/ledoff/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

@route('/ajax/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ajax/')

        ret=pyb.exec("get('/ajax/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

run(host=server_ip, port=server_port, server='cherrypy')

Le fichier upy_test.py placé dans le même répertoire contient du coup :

# code micropython  pour bridge serial repl - http
# X.H. - www.micropython.fr 

from machine import Pin
import random

led=Pin(25,Pin.OUT)

# fonction de gestion des routes - appelee par bridge
def get(route):

    if route=='/':

        print("Server Micropython serial to http ok !")

    elif route=='/ledon/':
        led.on()
        print('led ON')

    elif route=='/ledoff/':
        led.off()
        print('led OFF')

    elif route=='/ajax/':

        value=random.random()

        print(str(value))

Gestion des fichiers statiques

On peut à présent ajouter une route de gestion des fichiers statiques, non pas à partir de la carte micropython mais local. Pour cela, on va utiliser la route générique de bottlepy permettant de renvoyer les fichiers statiques en faisant :

#!/usr/bin/env python3

# import
from bottle import route, run, static_file

server_ip='0.0.0.0'
server_port=8080

import os
serverDir=os.getcwd()# le répertoire à servir - par défaut le répertoire courant

import pyboard
pyb=pyboard.Pyboard('/dev/ttyACM3', 115200)


pyb.enter_raw_repl()

# code initial Micropython envoyé à la carte à partir fichier
file=open("upy_test.py",'r') # ouverture fichier
code=file.read() # lecture fichier
file.close() # fermeture fichier
ret=pyb.exec(code) # exécution du fichier

## route home
@route('/') # définit une route = adresse après ip donc ip/
def home(): # code a executer lors de l'appel ce cette url

        print('/')

        ret=pyb.exec("get('/')") # envoie la route au code micropython

        return ret # reponse pour le navigateur

## route fichiers statiques
#-- route pour gestion fichier statique 
@route('/static/<filename:path>')
def send_static(filename):
    print (filename)

    return static_file(filename, root=serverDir) # utilisation chemin relatif

## routes personnalisées - doivent être gérées dans le code micropython

@route('/ledon/') # définit une route = adresse après ip donc ip/
def ledon(): # code a executer lors de l'appel ce cette url

        print('/ledon/')

        ret=pyb.exec("get('/ledon/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur


@route('/ledoff/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ledoff/')

        ret=pyb.exec("get('/ledoff/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

@route('/ajax/') # définit une route = adresse après ip donc ip/
def ledoff(): # code a executer lors de l'appel ce cette url

        print('/ajax/')

        ret=pyb.exec("get('/ajax/')") # envoie la route au code micropython

        print(ret)

        return ret # reponse pour le navigateur

run(host=server_ip, port=server_port, server='cherrypy')

Une fois fait, tout fichier présent dans le répertoire du serveur et appelé avec la route /static/ sera renvoyé !

Une webapp complète de test

Nous allons réaliser à présent une webapp de test qui va afficher un graphique live de valeur aléatoires renvoyées par Ajax.

Le code micropython est le suivant :

# code micropython  pour bridge serial repl - http
# X.H. - www.micropython.fr 

from machine import Pin
import random

led=Pin(25,Pin.OUT)

# fonction de gestion des routes - appelee par bridge
def get(route):

    if route=='/':

        print("Server Micropython serial to http ok !")

    elif route=='/ledon/':
        led.on()
        print('led ON')

    elif route=='/ledoff/':
        led.off()
        print('led OFF')

    elif route=='/ajax/':

        #value=random.random()
        value=random.randrange(0,4095)
        print(str(value))

    elif route=='/test/':

        page="""<!DOCTYPE HTML>

<!-- Debut de la page HTML  -->
<html>

        <head> <!-- Debut entete -->

                <meta charset="utf-8" /> <!-- Encodage de la page  -->
                <title>JavaScript: Test librairie Smoothie avec ajax et jquery </title> <!-- Titre de la page -->

                <!-- Inclusion librairies / codes Javascript externes -->

                <!-- en premier -->
                 <!--<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> -->
                <script src="/static/jquery.min.js" type="text/javascript" ></script>

                <script src="/static/smoothie.js" type="text/javascript" ></script>


        </head> <!-- Fin entete -->

        <body > <!-- Debut Corps de page HTML -->


                <!-- === placer ici le code HTML de la page === -->   
                <canvas id="smoothieCanvas" width="800" height="300"></canvas> <!-- canvas pour graphique smoothie -->   
                <br>
                <textarea id="textarea"></textarea> 

        </body> <!-- Fin de corps de page HTML  -->




                <!-- Debut du code Javascript  -->
                <script type="text/javascript">

                // mettre après le body pour meilleur chargement...

                // variables globales

                var randomSerie = new TimeSeries(); // crée un objet série de date - fournit par lib smoothie
                var smoothieGraph = new SmoothieChart({millisPerPixel:1}); // crée un objet graphique smoothie - fournit par lib smoothie

                //window.onload = function () { // au chargement de la page        
                $(document).ready(function(){
                //$(window).load(function(){

                                        // initialisation du graphique smoothie
                                        smoothieGraph.addTimeSeries(randomSerie, { strokeStyle: 'rgba(0, 255, 0, 1)', fillStyle: 'rgba(0, 255, 0, 0.2)', lineWidth: 2 });
                                        smoothieGraph.streamTo(document.getElementById("smoothieCanvas"), 1000); // définition canva et vitesse défilement

                                //setInterval(function() { refreshSmoothieGraph()}, 1000); // fixe délai actualisation
                                setTimeout(function() { refreshSmoothieGraph()}, 1000); // fixe délai actualisation

                                // le résultat final est le fruit du mixage entre vitesse de défilement et délai actualisation valeur

                        //} // fin onload

                 }); // fin ready 

                        // fonction d'actualisation de la série de valeur à intervalle régulier
                        function refreshSmoothieGraph(){
                                //randomSerie.append(new Date().getTime(), Math.random() * 4096); // ajoute donnée à la série de données - ici aléatoire


                               jQuery.get("/ajax/", manageReponseAjaxServeur);
                               // pas slim sinon $=erreur !

                        } // fin refresh smootthie

                        function manageReponseAjaxServeur(response) {
                            //compt=compt+1;
                            //console.log(response)

                            // bien avoir le bon nom du text area
                            //$("#textarea").append(String(response)+"\\n"); 
                            //$("#textarea").get(0).setSelectionRange($("#textarea").get(0).selectionEnd-1,$("#textarea").get(0).selectionEnd-1); // se place en derniere ligne -1 pour avant saut de ligne 

                            // met a jour le graphique
                            randomSerie.append(new Date().getTime(), parseInt(response));

                            //setTimeout(function() { refreshSmoothieGraph()}, 100); // fixe délai actualisation // seulement si reponse
                            refreshSmoothieGraph(); // rappel immediat = vitesse max

                        } // fin manage Response Ajax Serveur

                </script> <!-- Fin du code Javascript -->



</html> <!-- Fin de la page HTML  -->

"""
        print(page)

        # pas besoin de gerer les entetes, etc qui sont gerees par le bridge 

amélioration

  • on voit que les routes spécifiques implémentées dans le bridge sont assez semblables... et on devrait pouvoir les "généraliser", le bridge pouvant ainsi être commun à toutes les apps, à la façon du pyboard.py
  • on pourrait aussi envisager une copie sur la carte de fichiers utiles au code, notamment mettre dans des fichiers les pages webs plutôt que dans le code micropython

  • une interface gérant dans des éditeurs différents le code js, le code html, le code micropython serait intéressante pour la partie développement.

  • concernant la sécurisation / sécurité : le serveur peut être configuré pour ne servir que sur 127.0.0.1, ce qui limite accès au poste local. On peut également connaître l'IP de la requête et donc, on peut limiter les acceptations au réseau local, voir à des ip précises.

quelques exemples / idées d'applications simples ou utiles :

Outils dév'

  • un "serveur" des fichiers de la carte dans le navigateur
  • un interpréteur dans le navigateur (basé plutôt sur une page fournie par le bridge lui-même et qui transmet à la carte)

Interfaces didactiques

  • interface didactique PWM : slider de réglage + la largeur d'impulsion appliquée s'affiche (dessinée) dans l'interface
  • interface didactique fréquence : slider de réglage + fréquence d'impulsion s'affiche (dessinée) dans l'interface

  • contrôle d'une LED rgb par slider

  • un miroir "segment display" d'une valeur affichée localement sur tm1617

Interfaces utilitaires

  • Etalonneur de capteur : oscillo live et courbe de points valider

Interfaces opérationnelles

  • interfaces de n'importe quoi : contrôle de moteur pas à pas, etc.

  • contrôle d'un robot roulant 2 roues,

Autres

  • visu 3D à partir accéléromètre

  • un configurateur de la carte "graphique" : broche en entrée, en sortie

Conclusion

Une solution plutôt sympa pour disposer d'interfaces graphiques faciles avec le Pi Pico ou tout autre carte Micropython de disposant pas du wifi, et même avec ces cartes en développement local.

Ce qui est intéressant, c'est que le serveur est actif sur le réseau local, et par conséquent, tout autre poste du réseau local se connectant sur la bonne ip peut visualiser la sortie du Pi Pico ! Et par conséquent, un smartphone... ce qui peut être particulièrement intéressant pour du développement didactique !

D'autre part, on peut apprendre les principes de la création d'une webapp, sans avoir à gérer le wifi.