ljcq 09
ljcq 09

Reputation: 135

PyQt5 and Asyncio

Is it possible to keep a UDP server running as an asynchronous function receiving data and then passing it to an (PyQt5) widget which is also running as an asynchronous function??

The idea is that when the data coming into the server is updated, it also updates the widget.

I have got a simple UDP server and a (PyQt5) widget already which are working fine independently but I am struggling trying to combine them and keep them both running asynchronously and exchanging data(Server transmitting data to widget)

[UPDATE]

Below is a widget that I am trying out

import sys
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
import asyncio


class Speedometer(QMainWindow):

    angleChanged = QtCore.pyqtSignal(float)

    def __init__(self, parent = None):

        QtWidgets.QWidget.__init__(self, parent)

        self._angle = 0.0

        self._margins = 20

        self._pointText = {0: "40", 30: "50", 60: "60", 90: "70", 120: "80",
                           150:"" , 180: "", 210: "",
                          240: "0", 270: "10", 300: "20", 330: "30", 360: ""}
    def paintEvent(self, event):

        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        painter.fillRect(event.rect(), self.palette().brush(QtGui.QPalette.Window))
        self.drawMarkings(painter)
        self.drawNeedle(painter)

        painter.end()

    def drawMarkings(self, painter):

        painter.save()
        painter.translate(self.width()/2, self.height()/2)
        scale = min((self.width() - self._margins)/120.0,
                    (self.height() - self._margins)/60.0)
        painter.scale(scale, scale)

        font = QtGui.QFont(self.font())
        font.setPixelSize(10)
        metrics = QtGui.QFontMetricsF(font)

        painter.setFont(font)
        painter.setPen(self.palette().color(QtGui.QPalette.Shadow))

        i = 0

        while i < 360:

                if i % 30 == 0 and (i <150 or i > 210):
                    painter.drawLine(0, -40, 0, -50)
                    painter.drawText(-metrics.width(self._pointText[i])/2.0, -52,
                                     self._pointText[i])
                elif i <135 or i > 225:
                    painter.drawLine(0, -45, 0, -50)

                painter.rotate(15)
                i += 15

        painter.restore()

    def drawNeedle(self, painter):

        painter.save()
        painter.translate(self.width()/2, self.height()/1.5)
        painter.rotate(self._angle)
        scale = min((self.width() - self._margins)/120.0,
                    (self.height() - self._margins)/120.0)
        painter.scale(scale, scale) 

        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(self.palette().brush(QtGui.QPalette.Shadow))

        painter.drawPolygon(
            QtGui.QPolygon([QtCore.QPoint(-10, 0), QtCore.QPoint(0, -45), QtCore.QPoint(10, 0),
                      QtCore.QPoint(0, 5), QtCore.QPoint(-10, 0)])
            )

        painter.setBrush(self.palette().brush(QtGui.QPalette.Highlight))

        painter.drawPolygon(
            QtGui.QPolygon([QtCore.QPoint(-5, -25), QtCore.QPoint(0, -45), QtCore.QPoint(5, -25),
                      QtCore.QPoint(0, -30), QtCore.QPoint(-5, -25)])
            )

        painter.restore()

    def sizeHint(self):

        return QtCore.QSize(150, 150)

    def angle(self):
        return self._angle

#    @pyqtSlot(float)
    def setAngle(self, angle):

        if angle != self._angle:
            self._angle = angle
            self.angleChanged.emit(angle)
            self.update()

    angle = QtCore.pyqtProperty(float, angle, setAngle)

    @staticmethod
    def mainLoopSpd():
      while True:
            app = QApplication(sys.argv)

            window = QtWidgets.QWidget()
            spd = Speedometer()
            spinBox = QtWidgets.QSpinBox()
            #spd.setAngle(100)
            spinBox.setRange(0, 359)
            spinBox.valueChanged.connect(spd.setAngle)

            layout = QtWidgets.QVBoxLayout()
            layout.addWidget(spd)
            layout.addWidget(spinBox)
            window.setLayout(layout)

            window.show()
            app.exec_()
            #await asyncio.sleep(1)

            sys.exit(app.exec_())

Below is an implementation of a UDP socket end which also is printing the values in the console

import socket

class UDPserver:
    def __init__(self, parent= None):

        self.localIP     = "127.0.0.1"
        self.localPort   = 20002
        self.bufferSize  = 1024

        self.UDPServerSocket = socket.socket(family= socket.AF_INET, type=socket.SOCK_DGRAM)  # Create a socket object

        self.UDPServerSocket.bind((self.localIP, self.localPort))                                      

        print("UDP server up and listening")

        self.counter= 1

    @staticmethod
    def  mainLoopUDPserver():

            serv= UDPserver()
        #while(True):

            bytesAddressPair = serv.UDPServerSocket.recvfrom(serv.bufferSize)                     # Receive data from the socket
            message = bytesAddressPair[0]                                               # The output of the recvfrom() function is a 2-element array
                                                                                      # First element is the message
            address = bytesAddressPair[1]                                               # Second element is the address of the sender

            newMsg= "{}".format(message)

            serv.counter=serv.counter+1
            NumMssgReceived = "#Num of Mssg Received:{}".format(serv.counter)


            newMsg= newMsg.replace("'","")
            newMsg= newMsg.replace("b","")
            newMsg= newMsg.split("/")


            eastCoord= float(newMsg[0])
            northCoord= float(newMsg[1])
            vehSpeed= float(newMsg[2])

            agYaw= float(newMsg[3])


            eastCoordStr="East Coordinate:{}".format(newMsg[0])
            northCoordStr="North Coordinate:{}".format(newMsg[1])
            vehSpeedStr= "Vehicle Speed:{}".format(newMsg[2])
            agYawStr="Yaw Angle:{}".format(newMsg[3])


            print(NumMssgReceived)
            print(vehSpeedStr)

and below is the main function which is calling them both

from speedometer import Speedometer
import asyncio
from pyServer import UDPserver

class mainApp:
    #vel = 0
    def __init__(self):
        self.velo = 0
        self.queue= asyncio.Queue(0)

    async def server(self):

        while True:

            self.velo= UDPserver.mainLoopUDPserver()
            print("THIS IS VELO{}",self.velo)
            #await self.queue.put(self.velo)

            #vel= await self.queue.get()
            #return vel
            #print("ASSDASDSADSD{}",vel)

            await asyncio.sleep(0)
            #print("HI, vel Received={}",self.veloc)
        #return velo

    async def widget(self):
        while True:
            #vel =  await self.queue.get()
            #print("Hola xDDDDDDD", vel)
            print(">>>>>>>>>>>>>>>NextIteration>>>>>>>>>>>>>>")
            await Speedometer.mainLoopSpd()
            await asyncio.sleep(0)


loop= asyncio.get_event_loop()
mApp= mainApp()

loop.create_task(mApp.server())
loop.create_task(mApp.widget())
loop.run_forever()

So, when I run it, it listens to the server and once I start sending data over UDP, it receives the first piece of data and open the widget which is runs just fine but it makes the server to stop, it does not receive any data anymore.

As you can see in the comments, I have also been playing around with Asyncio queues, but I haven't got anything really.

My ideal scenario would be the server receiving the data and passing it to the widget so it gets updated with the incoming data but for now I just want them both working independently.

Thanks

Upvotes: 9

Views: 17639

Answers (1)

eyllanesc
eyllanesc

Reputation: 244132

It should be clear that your UDP server does not run asynchronously.

The logic of asyncio is that everything uses an eventloop as a base, and by default Qt does not support it, so you must use libraries such as qasync(python -m pip install qasync) and asyncqt(python -m pip install asyncqt)

Considering the above, the solution is:

speedometer.py

from PyQt5 import QtCore, QtGui, QtWidgets


class Speedometer(QtWidgets.QWidget):
    angleChanged = QtCore.pyqtSignal(float)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._angle = 0.0

        self._margins = 20

        self._pointText = {
            0: "40",
            30: "50",
            60: "60",
            90: "70",
            120: "80",
            150: "",
            180: "",
            210: "",
            240: "0",
            270: "10",
            300: "20",
            330: "30",
            360: "",
        }

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        painter.fillRect(event.rect(), self.palette().brush(QtGui.QPalette.Window))
        self.drawMarkings(painter)
        self.drawNeedle(painter)

    def drawMarkings(self, painter):

        painter.save()
        painter.translate(self.width() / 2, self.height() / 2)
        scale = min(
            (self.width() - self._margins) / 120.0,
            (self.height() - self._margins) / 60.0,
        )
        painter.scale(scale, scale)

        font = QtGui.QFont(self.font())
        font.setPixelSize(10)
        metrics = QtGui.QFontMetricsF(font)

        painter.setFont(font)
        painter.setPen(self.palette().color(QtGui.QPalette.Shadow))

        i = 0

        while i < 360:

            if i % 30 == 0 and (i < 150 or i > 210):
                painter.drawLine(0, -40, 0, -50)
                painter.drawText(
                    -metrics.width(self._pointText[i]) / 2.0, -52, self._pointText[i]
                )
            elif i < 135 or i > 225:
                painter.drawLine(0, -45, 0, -50)

            painter.rotate(15)
            i += 15

        painter.restore()

    def drawNeedle(self, painter):

        painter.save()
        painter.translate(self.width() / 2, self.height() / 1.5)
        painter.rotate(self._angle)
        scale = min(
            (self.width() - self._margins) / 120.0,
            (self.height() - self._margins) / 120.0,
        )
        painter.scale(scale, scale)

        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(self.palette().brush(QtGui.QPalette.Shadow))

        painter.drawPolygon(
            QtGui.QPolygon(
                [
                    QtCore.QPoint(-10, 0),
                    QtCore.QPoint(0, -45),
                    QtCore.QPoint(10, 0),
                    QtCore.QPoint(0, 5),
                    QtCore.QPoint(-10, 0),
                ]
            )
        )

        painter.setBrush(self.palette().brush(QtGui.QPalette.Highlight))

        painter.drawPolygon(
            QtGui.QPolygon(
                [
                    QtCore.QPoint(-5, -25),
                    QtCore.QPoint(0, -45),
                    QtCore.QPoint(5, -25),
                    QtCore.QPoint(0, -30),
                    QtCore.QPoint(-5, -25),
                ]
            )
        )

        painter.restore()

    def sizeHint(self):

        return QtCore.QSize(150, 150)

    def angle(self):
        return self._angle

    @QtCore.pyqtSlot(float)
    def setAngle(self, angle):

        if angle != self._angle:
            self._angle = angle
            self.angleChanged.emit(angle)
            self.update()

    angle = QtCore.pyqtProperty(float, angle, setAngle)


if __name__ == "__main__":
    import sys
    import asyncio
    from asyncqt import QEventLoop

    app = QtWidgets.QApplication(sys.argv)
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)
    with loop:
        w = Speedometer()
        w.angle = 10
        w.show()
        loop.run_forever()

server.py

import asyncio

from PyQt5 import QtCore


class UDPserver(QtCore.QObject):
    dataChanged = QtCore.pyqtSignal(float, float, float, float)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._transport = None
        self._counter_message = 0

    @property
    def transport(self):
        return self._transport

    def connection_made(self, transport):
        self._transport = transport

    def datagram_received(self, data, addr):
        self._counter_message += 1
        print("#Num of Mssg Received: {}".format(self._counter_message))
        message = data.decode()
        east_coord_str, north_coord_str, veh_speed_str, ag_yaw_str, *_ = message.split(
            "/"
        )
        try:
            east_coord = float(east_coord_str)
            north_coord = float(north_coord_str)
            veh_speed = float(veh_speed_str)
            ag_yaw = float(ag_yaw_str)
            self.dataChanged.emit(east_coord, north_coord, veh_speed, ag_yaw)
        except ValueError as e:
            print(e)

main.py

import sys
import asyncio

from PyQt5 import QtCore, QtWidgets
from asyncqt import QEventLoop

from speedometer import Speedometer
from server import UDPserver


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.spd = Speedometer()
        self.spinBox = QtWidgets.QSpinBox()
        self.spinBox.setRange(0, 359)
        self.spinBox.valueChanged.connect(lambda value: self.spd.setAngle(value))

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.spd)
        layout.addWidget(self.spinBox)

    @QtCore.pyqtSlot(float, float, float, float)
    def set_data(self, east_coord, north_coord, veh_speed, ag_yaw):
        print(east_coord, north_coord, veh_speed, ag_yaw)
        self.spd.setAngle(veh_speed)


async def create_server(loop):
    return await loop.create_datagram_endpoint(
        lambda: UDPserver(), local_addr=("127.0.0.1", 20002)
    )


def main():
    app = QtWidgets.QApplication(sys.argv)
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)

    w = Widget()
    w.resize(640, 480)
    w.show()

    with loop:
        _, protocol = loop.run_until_complete(create_server(loop))
        protocol.dataChanged.connect(w.set_data)
        loop.run_forever()


if __name__ == "__main__":
    main()

Upvotes: 7

Related Questions