1
votes

Utiliser asyncio avec une méthode de rappel non asynchrone à partir d'une bibliothèque externe

J'utilise la bibliothèque python gpiozero pour gérer des périphériques GPIO simples sur un Raspberry Pi (j'utilise ici un MotionSensor pour l'exemple):

# will not work because MotionSensor() is not using asyncio
motionSensor.when_motion = await self.whenMotion

Mon problème ici est que j'ai essayé de donner une fonction async a un rappel vers motionSensor.when_motion .

Donc j'obtiens l'erreur que la fonction whenMotion est async mais jamais attend mais je ne peux pas vraiment l'attendre:

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        motionSensor.when_motion = self.whenMotion

    async def whenMotion(self):
        await self.__whenMotionCallback()

Avez-vous une idée de la façon dont je peux attribuer ma fonction async à personne? p>


2 commentaires

avez-vous déjà une boucle asyncio en cours d'exécution? le when_motion a-t-il besoin d'une valeur de retour, ou est-ce que ça va s'il tourne juste une tâche asynchrone?


Ce code complet s'exécute dans une autre boucle en utilisant run_until_complete et aucun when_motion n'a pas besoin de renvoyer de valeur.


4 Réponses :


2
votes

Si vous faites cela avec des coroutines, vous devrez récupérer et exécuter la boucle d'événements. Je vais supposer que vous utilisez python 3.7, auquel cas vous pouvez faire quelque chose comme:

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        loop = asyncio.get_event_loop()
        motionSensor.when_motion = loop.run_until_complete(self.whenMotion())
        loop.close()

    async def whenMotion(self):
        await self.__whenMotionCallback()

Si vous êtes sur python 3.8, vous pouvez simplement utiliser asyncio.run plutôt que toutes les étapes d'obtention et d'exécution explicites de la boucle d'événements.


1 commentaires

Merci pour votre réponse mais dans mon cas, cette loop.run_until_complete () est bloquante et donc mon script ne peut pas continuer à fonctionner de manière asynchrone. Avez-vous une idée de ce qu'est le problème?



3
votes

Étant donné que cela s'exécute dans une boucle et que when_motion n'a pas besoin d'une valeur de retour, vous pouvez faire:

        ...
        motionSensor.when_motion = self.whenMotion

    def whenMotion(self):
        asyncio.ensure_future(self.__whenMotionCallback())

Cela planifiera le rappel asynchrone dans la boucle d'événement et gardez le code d'appel synchrone pour la bibliothèque.


1 commentaires

Merci pour votre réponse mais je reçois étonnamment cette erreur RuntimeError: Il n'y a pas de boucle d'événement en cours dans le thread 'Thread-1' . Est-ce que self .__ whenMotionCallback () est plus qu'une simple fonction async / wait ?



1
votes

Donc, après des recherches, j'ai trouvé que je devais créer une nouvelle boucle asyncio pour exécuter un script asynchrone dans une méthode non asynchrone. Alors maintenant, ma méthode whenMotion () n'est plus async mais exécutez-en une en utilisant ensure_future () .

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        motionSensor.when_motion = self.whenMotion

    def whenMotion(self):
        # Create new asyncio loop
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        future = asyncio.ensure_future(self.__executeWhenMotionCallback()) # Execute async method
        loop.run_until_complete(future)
        loop.close()

    async def __executeWhenMotionCallback(self):
        await self.__whenMotionCallback()

p>


1 commentaires

Cela fonctionne un peu mais vous créez 2 boucles asyncio et vous aurez maintenant des problèmes de multi-threading si vous utilisez des objets du thread principal. J'ai ajouté une réponse qui vous permet de rappeler la boucle asyncio principale de manière thread-safe.



1
votes

Lorsque la propriété when_motion est définie, gpiozero crée un nouveau thread qui exécute le rappel (ce n'est pas très bien documenté). Si le rappel doit être exécuté dans la boucle asyncio principale, vous devez renvoyer le contrôle au thread principal.

Le call_soon_threadsafe fait cela pour vous. Essentiellement, il ajoute le rappel à la liste des tâches que la boucle asyncio principale appelle lorsqu'une attente se produit.

Cependant, les boucles asyncio sont locales à chaque thread: voir get_running_loop

Ainsi, lorsque l'objet gpiozero est créé dans le thread asyncio principal, vous devez créer cette boucle objet disponible pour l'objet lorsque le rappel est appelé.

Voici comment procéder pour un PIR qui appelle une méthode asyncio MQTT:

class PIR:
    def __init__(self, mqtt, pin):
        self.pir = MotionSensor(pin=pin)
        self.pir.when_motion = self.motion
        # store the mqtt client we'll need to call
        self.mqtt = mqtt
        # This PIR object is created in the main thread
        # so store that loop object
        self.loop = asyncio.get_running_loop()

    def motion(self):
        # motion is called in the gpiozero monitoring thread
        # it has to use our stored copy of the loop and then
        # tell that loop to call the callback:
        self.loop.call_soon_threadsafe(self.mqtt.publish,
                                       f'sensor/gpiod/pir/kitchen', True)


0 commentaires