Plasmoid erstellen, das Meßwerte von einem Arduino anzeigt

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

Hallo,

ich habe einen Arduino, der per UDP-Broadcast alle Sekunden Meßwerte sendet.
Mit einem kleinem Python-Programm kann ich die Daten empfangen und anzeigen. Dann habe ich ein Plasmoid erstellt. Zur Zeit aktualisiert es alle Sekunde die Anzeige.
Wenn ich in der Aktualisierung die Daten abfrage, stockt das Plasmoid.
Dann habe ich es mit einem Thread versucht. Da stürzte der Plasmoidviewer sofort ab.

Welchen Lösungsansatz könnt ihr mir empfehlen, um asynchron die Daten einzulesen.

Bernd
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Wenn eine einzelne Abfrage zu lange dauert, gehts nur mit Threads. Beachten musst Du, dass GUI-Elemente nur vom Hauptthread aus manipuliert werden dürfen. Zeig doch mal den Teil, der nicht laufen will.
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

Hallo,

ich bin schon weiter. Ich hatte einen einfachen Thread genutzt. Mit dem QThread geht es jetzt schon ohne Absturz.

Erst mal die Klasse, in der ich die Werte einlese

Code: Alles auswählen

from socket import *
import datetime

PORT=8888

class WetterLeser:
    def __init__(self):
        self.udp = socket(AF_INET, SOCK_DGRAM)
        self.udp.settimeout(3)
        self.udp.bind(("",PORT))

    def _zeit(self,zeit):
        d=zeit[0]
        z=zeit[1]
        return datetime.datetime(int(d[0:4]),int(d[4:6]),int(d[6:]),int(z[0:2]),int(z[2:4]),int(z[4:]))
        
    def _temperatur(self,d):
        r={}
        r['wert']=float(d[8])/100
        r['min']=float(d[9])/100
        r['max']=float(d[10])/100
        r['id']= '%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X%2.2X' % (d[0],d[1],d[2],d[3],d[4],d[5],d[6],d[7])
        return r
    
    def lesen(self,mit_temperatur):
        result={'temperatur':[]}
        startblock=False
        while True:
            try:
                data=self.udp.recv(256)
                if data[0]==':' and data[-1]==':':
                    k=data[1];
                    data=data[2:-1].split(',')
                    if k!='U':
                        for j in range(len(data)):
                            data[j]=int(data[j])
                    if k=='A':
                        startblock=True
                        result['adresse']='%3.3d.%3.3d.%3.3d.%3.3d' % (data[0],data[1],data[2],data[3])
                        result['temperatur_count']=data[4]
                    elif startblock:
                        if k=='E':
                            startblock=False
                            if not mit_temperatur or result['temperatur_count']==0 or len(result['temperatur'])>0:
                                break
                        elif k=='U':
                          result['zeit']=self._zeit(data)
                        elif k=='T':
                           result['temperatur'].append(self._temperatur(data))
                        else:
                            print 'unbekannter Block %s' % k
            except timeout:
                break
        return result

wichtig ist hier die Methode lesen. Dort wird gewartet, bis alle Blöcke empfangen wurden.

Und so sieht mein Plasmoid aus

Code: Alles auswählen

from PyQt4.QtCore import Qt,QRect,pyqtSignature,QThread
from PyQt4.QtGui import QImage
from PyKDE4.plasma import Plasma
from PyKDE4 import plasmascript
#from PyKDE4.kdecore import KGlobal
import udpLeser

class WetterThread(QThread):
    def __init__(self,parent):
        QThread.__init__(self, parent)
        self.ende=False
        self.leser=udpLeser.WetterLeser()

    def __del__(self):
        self.ende=True
        self.wait()

    def run(self):
        while not self.ende:
            w=self.leser.lesen(False)
            print w

class NfixWetter(plasmascript.Applet):
    def __init__(self,parent,args=None):
        plasmascript.Applet.__init__(self,parent)
        self.werte=None
        self.anzeigetext='Es wurden noch keine Werte eingelesen ...'

    def init(self):
        self.setHasConfigurationInterface(False)
        self.Bild=QImage(self.package().path()+"contents/images/wetter.png")
        self.resize(400, 120)
        self.thread=WetterThread(self)
        self.thread.start()
        self.connectToEngine()
 
    def paintInterface(self, painter, option, rect):
        painter.save()
        r=QRect(rect);
        r.setHeight(60)
        r.setWidth(60)
        rect.setLeft(r.right()+5)
        painter.setPen(Qt.yellow)
        painter.drawImage(r,self.Bild)
        painter.drawText(rect, Qt.AlignVCenter , self.anzeigetext)
        painter.restore()
        del(r)

    def connectToEngine(self):
        self.timeEngine = self.dataEngine("time")
        self.timeEngine.connectSource("Local",self, 500)

    @pyqtSignature("dataUpdated(const QString &, const Plasma::DataEngine::Data &)")
    def dataUpdated(self, sourceName, data):
        self.update()
 
def CreateApplet(parent):
    return NfixWetter(parent)
In der run-Methode des Threads lese ich die Werte in w ein. Das 'print w' zeigt auch an, das die Werte angekommen sind.
Wie sage ich aus dem Thread dem Plasmoid bescheid, das neue Werte da sind und übertrage sie?

Bernd
BlackJack

@bernd59: Du könntest Deinem Thread ein Signal verpassen und das mit einem Slot auf der GUI verbinden, und darüber dann die Daten weiter geben.

Programmierst Du sonst in einer anderen Sprache? Einige Sachen sehen nämlich etwas „unpythonisch“ aus. Zum Beispiel ``for j in range(len(data))`` ist ein „anti-pattern“. Man kann in Python nicht nur direkt über Elemente iterieren, statt den Umweg über einen Index zu gehen, sondern man kann die Schleife in diesem Fall sogar durch ein einfaches ``data = map(int, data)`` ersetzen.

*Keine* Stilfrage ist die `__del__()`-Methode. Das ist kein deterministischer Destruktor. Alleine das vorhandensein der Methode kann Konsequenzen haben („Speicherlecks“) die man nicht haben möchte. Verbunden damit das nicht garantiert ist, wann die Methode aufgerufen wird, oder ob sie *überhaupt* aufgerufen wird, sollte man diese Methode nicht implementieren.

Das lesen/auswerten der Pakete erscheint mir nicht besonders robust. Pakete können zum Beispiel verloren gehen, oder in einer anderen Reihenfolge ankommen als sie abgeschickt wurden. Es ist auf jeden Fall ziemlich unübersichtlich so.

'%3.3d' als Platzhalter für die Zeichenkettenformatierung ist seltsam. Ganze Zahlen, und dafür steht das 'd', haben keinen Dezimalpunkt, also ist es eigenartig den in der Formatierung anzugeben. Zumal die führende 3 keinen weiteren Effekt hat. Der Platzhalter würde normalerweise so aussehen '%03d'. Von hinten gelesen: ganze Zahl, drei Stellen, links mit 0en auffüllen.
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

Hallo,
ja ich programmiere sonst 8 Stunden am Tag eine andere Programiersprache(Delphi). Dort muss ich immer selbst auf die Freigabe des Speichers achten. In Python bin ich noch ein Anfänger

Aber jetzt zur Frage, Wie müste ich solch ein Signal/Slot machen.

Das mit dem __del__ habe ich aus einem Beispiel, wo ich auch den QThread herhabe.
Das auslesen der Pakete ist schon robust genug. Es kommen sogar immer verschiedene. Ich erkenne den Typ und parse dann die Werte. Wenn ganze Meßreihen verlorengehen gehen, ist es nicht so schlimm. Deswegen setze ich ja den UDP - Brodcast ein. Ohne Einstellungen im ganzen Netzt die Werte bei Bedarf zur Anzeige auslesen. Die konkreten Meßergebnisse werden im Android auf eine SD geschrieben.

Bernd
BlackJack

@bernd59: Ist das Datenformat der Pakete selbst entworfen? Probleme sehe ich zum Beispiel wenn zwei Paketserien sich überlappen. Deswegen haben UDP-Protokolle bei denen die Pakete nicht wirklich komplett losgelöst von anderen interpretiert werden können, eine Sequenznummer integriert.

Was passiert wenn ein 'T'-Paket kommen sollte, das aber verloren geht? Dann wird das ``break`` in der Behandlung des `E`-Pakets nicht ausgeführt.

Das *eine* ID als Folge von ASCII-kodierten Bytewerten, die durch Kommata getrennt werden, übermittelt werden, finde ich auch ein wenig ungewöhnlich.
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

Hallo,

wen ein T-Paket verlorengeht, dann funktioniert es trotzdem. Und ob und wie viel Pakete ankommen, ist völlig unbestimmt. Es kommt u.A. auf die Anzahl der Sensoren an. Das funktioniert schon ausreichend sicher. Die Kommandozeilenversion funktioniert schon seit Tagen ohne Probleme. Der Aufbau der ID ist dem schwachen und begrenzten Arduino geschuldet. Das will ich aber alles nicht ändern. Und mir geht es dabei auch nur um einen Test.

Es würde mir helfen, wenn mir einer meine Frage beantworten könnte:

Wie müsste ich solch ein Signal/Slot machen.

Bernd
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

Inzwischen habe ich die Lösung gefunden

Code: Alles auswählen

from PyQt4.QtCore import QObject,Qt,QRect,pyqtSignature,QThread,SIGNAL
from PyQt4.QtGui import QImage
from PyKDE4.plasma import Plasma
from PyKDE4 import plasmascript
import udpLeser

class WetterThread(QThread):
    def __init__(self,parent):
        QThread.__init__(self, parent)
        self.ende=False
        self.leser=udpLeser.WetterLeser()

    def __del__(self):
        self.ende=True
        self.wait()

    def run(self):
        while not self.ende:
            w=self.leser.lesen(False)
            self.emit(SIGNAL("wetter(PyQt_PyObject)"),w)
 
class NfixWetter(plasmascript.Applet):
    def __init__(self,parent,args=None):
        plasmascript.Applet.__init__(self,parent)
        self.werte=None
        self.anzeigetext='Es wurden noch keine Werte eingelesen ...'

    def init(self):
        self.setHasConfigurationInterface(False)
        self.Bild=QImage(self.package().path()+"contents/images/wetter.png")
        self.resize(400, 120)
        self.thread=WetterThread(self)
        QObject.connect(self.thread,SIGNAL("wetter(PyQt_PyObject)"),self.update_temperatur)
        self.thread.start()
 
    def paintInterface(self, painter, option, rect):
        painter.save()
        r=QRect(rect);
        r.setHeight(60)
        r.setWidth(60)
        rect.setLeft(r.right()+5)
        painter.setPen(Qt.yellow)
        painter.drawImage(r,self.Bild)
        painter.drawText(rect, Qt.AlignVCenter , self.anzeigetext)
        painter.restore()
        del(r)

    def update_temperatur(self,w):
        self.werte=w
        self.anzeigetext=str(w['zeit'])
        self.update()

def CreateApplet(parent):
    return NfixWetter(parent)

lunar

@bernd59: "del(r)" ist vollkommen überflüssig, da die Referenz "r" unmittelbar nach dieser Anweisung ohnehin durch den Rücksprung aus der Methode "paintInterface()" gelöscht wird.

Die Implementierung von "WetterThread.__del__()" ist ebenso überflüssig, da sie im gezeigten Quelltext auch niemals aufgerufen wird. Der Aufruf von "__del__()" ist sowieso nicht garantiert, was immer Du also in "__del__()" tun wolltest, diese Methode ist mit Sicherheit der falsche Ort dafür. Wenn Du den Thread von außen beenden möchtest (danach sieht "__del__()" zumindest aus), dann geht das zuverlässig nur mit einer expliziten ".stop()"-Methode.

Bitte lies auch die Dokumentation zu PyQt4, insbesondere den Abschnitt über Signal- und Slot-Unterstützung. Es gibt seit geraumer Zeit eine Python-Syntax für Signal-Slot-Verbindungen, die zu verwenden ich Dir dringend rate. Sie kommt ohne C++-Signaturen aus, ist mithin lesbarer, und zudem weniger fehleranfällig, da bei nicht existierenden Signalen oder Slots Ausnahmen ausgelöst werden.
bernd59
User
Beiträge: 13
Registriert: Sonntag 8. Januar 2012, 00:31

@lunar
Ist das jetzt besser?

Code: Alles auswählen

from PyQt4.QtCore import QObject,Qt,QRect,QThread,pyqtSignal,pyqtSlot
from PyQt4.QtGui import QImage
from PyKDE4.plasma import Plasma
from PyKDE4 import plasmascript
import udpLeser

class WetterThread(QThread):
    sender=pyqtSignal(dict)
    
    def __init__(self,parent):
        QThread.__init__(self, parent)
        self.leser=udpLeser.WetterLeser()
        self.sender.connect(parent.update_temperatur)

    def run(self):
        while True:
            w=self.leser.lesen(False)
            self.sender.emit(w)
 
class NfixWetter(plasmascript.Applet):
    def __init__(self,parent,args=None):
        plasmascript.Applet.__init__(self,parent)
        self.werte=None
        self.anzeigetext='Es wurden noch keine Werte eingelesen ...'

    def init(self):
        self.setHasConfigurationInterface(False)
        self.Bild=QImage(self.package().path()+"contents/images/wetter.png")
        self.resize(400, 120)
        self.thread=WetterThread(self)
        self.thread.start()
 
    def paintInterface(self, painter, option, rect):
        painter.save()
        r=QRect(rect);
        r.setHeight(60)
        r.setWidth(60)
        rect.setLeft(r.right()+5)
        painter.setPen(Qt.yellow)
        painter.drawImage(r,self.Bild)
        painter.drawText(rect, Qt.AlignVCenter , self.anzeigetext)
        painter.restore()
        
    @pyqtSlot(dict)
    def update_temperatur(self,w):
        self.werte=w
        self.anzeigetext=str(w['zeit'])
        self.update()

def CreateApplet(parent):
    return NfixWetter(parent)
lunar

@bernd59: Im Bezug auf meine Anmerkung durchaus. Dafür sind einige andere Sachen unschön. Der Name des Signals ist irreführend, nutze besser etwas aussagekräftiges wie "wetterAktualisiert". Die Verbindung des Signals mit dem Slot gehört in "NfixWetter.init()", nicht in "WetterThread.__init__()".

Der Mischmasch aus deutschen und englischen Bezeichnern ist meines Erachtens auch nicht so schön, aber das ist eher nebensächlich.
Antworten