Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing asyncio and Kivy: How to start the asyncio loop and the Kivy application at the same time?

Lost in asyncio.

I'm learning Kivy and asyncio at the same time, and got stuck into solving the problem of running the Kivy and running the asyncio loop, as whatever the way I turn it, both are blocking calls and need to be executed sequentially (well, I hope I'm wrong), e.g.

loop = asyncio.get_event_loop()
loop.call_soon(MyAsyncApp().run())
loop.run_forever()

My current attempt results in the application being launched, but no coroutine is being executed, e.g. when I click the "Connect" button, I should start scheduling and executing tasks using loop.call_soon, but nothing happens.

Could someone have a look at my code and suggest a proper approach to the problem?


import asyncio
import random
import time
from kivy.app import App
from kivy.lang import Builder

ui = Builder.load_string('''
BoxLayout:
    orientation: 'vertical'
    GridLayout:
        rows: 2
        cols: 2
        Label:
            text: 'Status:'
            size_hint: 0.3, 1
        Label:
            id: status
            text: ''
        Label:
            text: 'Data:'
            size_hint: 0.7, 1
        Label:
            id: data
            text: ''
    BoxLayout:
        direction: 'horizontal'
        Button:
            text: 'Get Data'
            on_press: app.connect()
        Button:
            text: 'Stop Data'
            on_press: pass
''')

class MyAsyncApp(App):

    def __init__(self):
        super(self.__class__, self).__init__()

        self.x_connected = None
        self.x_widget_data = None
        self.x_widget_status = None
        self.x_loop = asyncio.get_event_loop()

    def build(self):
        return ui

    def connect(self):
        # Get widget
        self.x_widget_status = self.root.ids.status

        # Update status
        self.x_widget_status.text = 'Preparing to connect...'

        # Connect using asyncio
        # --> But the loop must be already running <---
        self.x_loop.call_soon(self.do_connect)

    async def do_connect(self):
        # Connect asynchronously

        # Get widget
        self.x_widget_data = self.root.ids.data

        # Update status
        self.x_connected = False
        self.x_widget_status.text = 'Connecting...'

        # Perform actual actions
        try:
            result = await self.feed_kivy()
            if result:
                self.x_widget_status.text = 'Service not available: ' + result
                return
        except Exception as e:
            self.x_widget_status.text = 'Error while connecting'
            return

        # Update status
        self.x_connected = True
        self.x_widget_status.text = 'Connected'

    async def feed_kivy(self):
        # Deliver fresh data at random interval

        # Some slow process to get data
        result = await asyncio.sleep(random.randint(1, 5), time.time())
        self.x_widget_data.text = result

        # Reschedule ourselves
        await self.x_loop.call_soon(self.feed_kivy())


def main():
    # If loop started here, app is never started
    loop = asyncio.get_event_loop()
    loop.call_soon(MyAsyncApp().run())
    loop.run_forever()
    loop.close()


if __name__ == '__main__':
    main()
like image 683
mins Avatar asked Dec 14 '17 14:12

mins


1 Answers

My current attempt results in the application being launched, but no coroutine is being executed

It happens because MyAsyncApp().run() blocks execution flow and control never returns to asyncio's event loop. This is how all event loops work.

Instead of trying cross two loops manually shorter way would be to use existing attempt:

https://github.com/kivy/kivy/pull/5241

This PR is from one of Kivy's developers and contains working implementation with explanation and usage examples.

However it's not merged into master yet: you would need to build Kivy with this PR manually.

like image 129
Mikhail Gerasimov Avatar answered Nov 14 '22 21:11

Mikhail Gerasimov