Serveur web Python intégré à une boucle infinie

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Dans le cadre d’un projet d’objet connecté réalisé à ce stade avec une Raspberry, j’aimerais pouvoir faire quelque chose de la sorte en Python :

import requests

VALVE_PIN = 0

config = None
server = ...

def write_valve(state):
    rpi.write_output(VALVE_PIN, state)

@server.post("/valve")
def route_valve(request):
    write_valve(request.POST.get("state"))
    
    
@server.post("/config")
def route_config(request):
    global config
    config = json.loads(request.body)
    
while True:
    # Récupère les requêtes HTTP entrantes et appelle la route associée à l'url
    handle_http_requests()
    measures = read_sensors()
    # ... autres opérations faites à partir des mesures obtenues

Seulement je ne trouve pas. J’ai l’impression que handle_http_requests() est géré par le WSGI mais qu’on ne peut pas ajouter des instructions à la boucle.

Merci !

+0 -0

Salut,

Est-ce que tu peux donner plus de contexte sur ce que sont server et handle_http_requests ?

Et qu’est-ce que tu cherches concrètement à faire ? Que le serveur se relance après chaque exception ? Qu’il se coupe après chaque requête traitée ?

Salut,

Est-ce que tu peux donner plus de contexte sur ce que sont server et handle_http_requests ?

Et qu’est-ce que tu cherches concrètement à faire ? Que le serveur se relance après chaque exception ? Qu’il se coupe après chaque requête traitée ?

entwanne

server est un objet de type application Flask/FastAPI qui me permet de définir des routes HTTP facilement.

handle_http_requests() est une fonction qui "lit" les requêtes HTTP depuis le dernier appel et appelle les callbacks correspondant aux urls.

Concrètement, je souhaite avoir un algo qui tourne en boucle (qui lit des capteurs, commande des composants électroniques en fonction des valeurs des capteurs et transmet les valeurs lues à un autre service numérique, par exemple en appelant une API web) que je puisse interrompre/avec qui je peux communiquer en envoyant des messages (ici des requêtes HTTP mais ça pourrait être un autre protocole).

Ce que fait le serveur web (se maintenir ou se couper) m’importe peu.

+0 -0

De ce que je comprends, tu as une application WSGI qui attend des appels et fait des trucs.

De ce que je comprends, handle_http_requests c’est grosso modo juste server.run(...). Qui est, une fonction uniquement dédiée aux développements locaux dans le cas où on n’a pas accès à un serveur WSGI.

Elle lance un serveur, c’est donc une fonction bloquante. Voir la documentation: https://flask.palletsprojects.com/en/3.0.x/api/

Et ensuite, tu as une partie qui doit lire un capteur et faire des requêtes.
Qui fonctionne en mode push donc.

Pour ça, tu dois avoir une deuxième application. L’application WSGI et une autre qui lit les capteurs et pousse les données ailleurs.

+0 -0

Concrètement, je souhaite avoir un algo qui tourne en boucle (qui lit des capteurs, commande des composants électroniques en fonction des valeurs des capteurs et transmet les valeurs lues à un autre service numérique, par exemple en appelant une API web) que je puisse interrompre/avec qui je peux communiquer en envoyant des messages (ici des requêtes HTTP mais ça pourrait être un autre protocole).

Vayel

Ça me semble être un bon cas d’usage pour des threads :

  • L’un où ton serveur web tournerait en boucle infinie en attendant des requêtes d’interruption ou autre
  • L’autre qui lit les capteurs et commande les composants en boucle.

Ou à des coroutines si les outils que tu utilises se prêtent à l’asynchrone.

@ache Je me demande justement s’il me faut deux applications ou s’il y a un moyen d’en n’avoir qu’une. Dans mon exemple, handle_http_requests() n’est pas bloquant.

Si j’ai deux applications, ça veut dire qu’il me faut un moyen de communication entre les deux, par exemple des fichiers. En effet, je veux pouvoir configurer à partir d’une requête HTTP l’application qui lit les capteurs.

@entwanne je me suis posé la question des coroutines mais n’ai pas trouvé l’équivalent de mon handle_http_requests(). Peut-être en effet que je vais partir sur les threads.

+0 -0

@ache Je me demande justement s’il me faut deux applications ou s’il y a un moyen d’en n’avoir qu’une. Dans mon exemple, handle_http_requests() n’est pas bloquant.

Y a forcément moyen d’en avoir qu’une seule, mais est-ce vraiment la bonne chose à faire ?

Si j’ai bien compris, ton application est une application WSGI ? Je me trompe ? Tu comptes la lancer avec un serveur WSGI type gunicorn ou équivalent ?

Si oui, et c’est le plus courant, c’est pour ça que je n’ai pas proposé de solution à base de threads.

Ça posera des problèmes si tu as plusieurs processus (par exemple avec des workers). En effet du coup ton thread / coroutines sera présente dans chaque processus. C’est pour ça que je ne comprends pas la réponse d'@entwanne du coup.

Je ne comprends pas ce qu’est cette fonction handle_http_requests. Elle lance un serveur WSGI HTTP ? Ça sera un serveur de production ? Si oui alors ok, tu te fiches du passage à l’échelle et c’est juste pour avoir un serveur HTTP. Dans ce cas, alors oui, tu peux l’utiliser dans un threads.

+1 -0

Ça posera des problèmes si tu as plusieurs processus (par exemple avec des workers). En effet du coup ton thread / coroutines sera présente dans chaque processus. C’est pour ça que je ne comprends pas la réponse d'@entwanne du coup.

ache

Je pense qu’on n’a pas compris de la même manière le problème initial.

De ce que je lis de tes messages, tu as compris qu’il s’agissait de deux entités : l’une étant une API HTTP et l’autre le contrôleur d’un capteur, le contrôleur faisant des appels à l’API précédente.
Donc le contrôleur peut être totalement distinct de l’API puisqu’il lui suffit d’un client HTTP pour communiquer avec elle.

Personnellement j’ai compris que l’entité API recevait des actions externes (d’un autre acteur) de façon à modifier le comportement du contrôleur, qui lui-même peut-être amené à faire des appels HTTP mais vers une API externe.
Il y aurait donc une liaison plus forte API → contrôleur qui ne possède pas d’interface particulière et mène donc à ce que les deux entités cohabitent dans un même programme (d’où l’idée de threads pour les faire s’exécuter simultanément).

C’est effectivement cela @entwanne. En fait on peut oublier les appels API faits par le contrôleur, ce n’est pas très important. L’idée de base est vraiment celle-ci : avoir un contrôleur qui lit des capteurs et commande des actionneurs (en l’occurrence des électrovannes) qui à la fois a un comportement automatique (ex : si la température est trop basse, ouvrir la vanne pour éviter le gel de l’eau stockée) et peut être commandé à distance par une requête HTTP (ou LoRaWAN ou autre, peu importe).

+0 -0

Voici ce que j’obtiens avec asyncio et FastAPI :

import asyncio
from fastapi import FastAPI
import uvicorn

webapp = FastAPI()

config = {
    # ...
}

@webapp.post("/config")
def configure():
    global config
    # Modifie config à partir du body de la requête
    
async def loop():
    while True:
        # ...
        await asyncio.sleep(5)

async def main():
    config = uvicorn.Config(webapp, port=5000, log_level="info")
    server = uvicorn.Server(config)
    asyncio.wait([
        asyncio.create_task(server.serve()),
        asyncio.create_task(loop()),
    ])

if __name__ == "__main__":
    asyncio.run(main())

En l’occurrence cela ne fonctionne pas avec fastapi par que KeyboardInterrupt est capturée : https://github.com/tiangolo/fastapi/discussions/6210

J’ai donc utilisé https://fastapi-utils.davidmontague.xyz/user-guide/repeated-tasks/, qui se base sur les threads :

import asyncio
from fastapi import FastAPI
from fastapi_utils.tasks import repeat_every
import uvicorn

webapp = FastAPI()

config = {
    # ...
}

@webapp.post("/config")
def configure():
    global config
    # Modifie config à partir du body de la requête

@webapp.on_event("startup")
@repeat_every(seconds=5)
def loop():
    # ...

def main():
    uvicorn.run(webapp, port=5000, log_level="info")

if __name__ == "__main__":
    main()
+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte