pyserial CPU Last + globale Variablen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Hallo,

als Python Anfänger versuche ich gerade meine Arduino an meinen Pi via Python anzukoppeln.
pyserial läuft - allerdings ist die CPU Last recht hoch. Ca 30% - nur fürs Lesen des Ports. Da mache ich bestimmt was falsch!?
Außerdem komme ich mit den globalen Variablen noch nicht klar. Zumindest wenn ein Teil des scripts importiert wird.

Es wäre super wenn mich jemand erhellen könnte. :wink:

Robert

Hier der Code:

gVar.py

Code: Alles auswählen

GlobalSerIn = ""
th_serial.py

Code: Alles auswählen

import serial
import time
import gVar

ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0)
ser.close()
ser.open()
ser.flush()

def SerialThread():
    buffer = ''    
         
    while True:
        #ser.open()
        buffer += ser.read(ser.inWaiting() or 1)
        if '\n' in buffer:
            #print(buffer)
            gVar.GlobalSerIn = buffer.rstrip()
            buffer = ""
        time.sleep(0.005)
main.py

Code: Alles auswählen

import time
import thread

import gVar
import th_serial

thread.start_new_thread(th_serial.SerialThread())
    
if __name__ == '__main__':
    time.sleep(1)
    if gVar.GlobalSerIn != "":
        print(gVar.GlobalSerIn)
        gVar.GlobalSerIn = ""
BlackJack

@robvoi: `thread` sollte man nicht mehr verwenden. Das Modul gibt es in Python 3 auch nicht mehr und die Dokumentation von Python 2 verweist schon seit einer halben Ewigkeit auf das `threading`-Modul.

Wie auch immer: Du verwendest das sowieso falsch weil Du gar keinen Thread startest. Du rufst `SerialThread` auf statt es nur zu übergeben. Da die Funktion aber aus einer Endlosschleife besteht kehrt der Aufruf nie zurück und damit wird vom `main`-Modul natürlich nichts nach der 7. Zeile jemals ausgeführt und in der 7. Zeile auch der `start_new_thread()`-Aufruf niemals. Die Funktion ist fehlerhaft weil alles nach einem '\n' verworfen wird. Da kann ja durchaus mindestens der Anfang einer folgenden Zeile vorhanden sein.

Das mit den globalen Variablen möchtest Du gar nicht weiterverfolgen. Lös das besser gleich ordentlich. Auch eine Sekunde warten ist schlicht unsauber und „gefährlich”. Dafür verwendet man die entsprechenden Typen aus dem `threading`-Modul um Codeabschnitte gegeneinander zu sperren oder gezielt darauf zu warten das ein bestimmtes Ereignis eintritt.

Wegen der Namensschreibweisen könntest Du mal einen Blick in den Style Guide for Python Code werfen. Und Abkürzungen die nicht allgemein bekannt sind, sollte man vermeiden. Funktionen sollten nach der Tätigkeit benannt werden, die sie durchführen, denn sie stehen für Tätigkeiten und nicht für „Dinge”.

Ebenso Code auf Modulebene der verhindert, dass man es ohne Seiteneffekte importieren kann. Wenn man ein Modul importiert sollte alleine das Importieren keine serielle Verbindung aufbauen. So etwas lässt sich unheimlich schlecht testen. Und es behindert auch andere Werkzeuge die Module importieren, zum Beispiel Analysewerkzeuge und Dokumentationssysteme.

Ich würde diese drei Module erst einmal zu einem zusammenfassen und dort sauber ohne (modul)globalen Zustand arbeiten.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Danke für die schnelle Antwort. Ich werde mir die einzelnen Aussagen nochmal durchlesen und zu Herzen nehmen.
Generell bleibt dann aber die Frage:

Ich möchte, dass die serielle Kommunikation - und später andere Teile - nicht das main script blockieren. Trotzdem möchte ich zwischen den Teilen Informationen austauschen.

Wie wäre hier eine saubere Herangehensweise?
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Da helfen dir das threading- und das queue-Modul weiter.
Das Leben ist wie ein Tennisball.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Danke - werde ich mir anschauen.

Es wäre sehr hilfreich, wenn jemand mein Beispiel etwas aufarbeiten könnte.
Ich habe mir das aus den Referenzen zusammengestückelt. Offensichtlich nicht erfolgreich. :oops:
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

In specifications, Murphy's Law supersedes Ohm's.
BlackJack

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from Queue import Queue
from threading import Thread
from serial import Serial


def read_lines(lines, process):
    try:
        for line in lines:
            process(line)
    finally:
        process(None)


def main():
    connection = Serial('/dev/ttyACM0', 115200, timeout=0)
    incoming_lines = Queue()
    Thread(target=read_lines, args=(connection, incoming_lines.put)).start()
    line = incoming_lines.get()
    print line


if __name__ == '__main__':
    main()
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Danke für den ganzen Input.
Auch wenn der Code noch nicht sauber ist funktioniert er schonmal.
Den Style guide schaue ich mir an - genauso wie ein paar grundlegende Python Tutorials. Ist doch schon etwas anders - und wirklich mächtig. Ich bin beeindruckt wie einfach manche Sachen teilweise sind.

Hier der jetzige Stand des Codes. Cie CPU des PI idled vor sich hin und der main loop wird nicht blockiert. So wie es sein sollte.

Ich werde bestimmt noch mehr Fragen haben. Ich post den Code nur, damit andere Python Anfänger auch ein Funktionierendes Ergebnis im Thread finden.

Kritik ist willkommen ...

Robert

Code: Alles auswählen

import time
import serial
import threading


ser = serial.Serial('/dev/ttyACM0', 115200)

class SerialThread(threading.Thread): 
        
    def run(self): 
        data = ""
        while 1:
            readnext=ser.read(1)
            if readnext != "\n":
                data = data + readnext
            else:
                self.analyseData(data.rstrip())
                data=""

    def analyseData(self,inString):
        print(inString)


thread = SerialThread() 
thread.daemon = True
thread.start()


while 1:
    time.sleep(1.00)
    ser.write("test")
BlackJack

@robvoi: Warum liest Du byteweise und verwendest nicht einfach den Umstand das so ein `Serial`-Exemplar ein iterierbares Objekt ist was die Zeilen liefert? Die Klasse könnte dann so aussehen:

Code: Alles auswählen

class SerialThread(threading.Thread):
    def __init__(self, connection):
        threading.Thread.__init__(self)
        self.connection = connection

    def run(self):
        for line in self.connection:
            self.analyse_data(line.rstrip())
 
    @staticmethod
    def analyse_data(string):
        print string
So wie es da steht ist das allerdings kaum eine Klasse wert. Das geht auch prima ohne.

Die serielle Verbindung ist immer noch global. Funktionen und Methoden sollten auf nichts zugreifen was keine Konstante ist und weder als Argument übergeben wurde, noch Attribut auf dem Objekt ist, auf dem die Methode aufgerufen wird.

Endlosschleifen würde ich als ``while True:`` schreiben, weil das IMHO deutlicher ist als 0 und 1 als Wahrheitswerte zu verwenden.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

BlackJack hat geschrieben:@robvoi: Warum liest Du byteweise und verwendest nicht einfach den Umstand das so ein `Serial`-Exemplar ein iterierbares Objekt ist was die Zeilen liefert?
Mangels Wissen :)
Deine Klasse funktioniert super. - Danke
Die serielle Verbindung ist immer noch global.
Ja. Dadurch kann ich von jeder Funktion aus auf den seriellen Port schreiben. Ideal ist das aber in der Tat nicht.

Alternativ könnte ich noch eine Queue füllen, die dann vom "Serial Thread" gelesen und verarbeitet wird. Dann greift nur eine Instanz auf den seriellen Port zu.
Aber wieder verschiedene Funktionen auf die Queue ...

Was wäre denn ein Sinnvoller Ansatz?
BlackJack

@robvoi: Den sinnvollen Ansatz habe ich doch mit der Klasse schon gezeigt. Da greift die Methode dann nicht mehr auf ein globales Objekt zu sondern auf das welches beim erstellen übergeben wurde.

Ob Du eine Queue brauchst oder nicht hängt davon ab was wo ausgeführt werden soll. In Deinem letzten Beispiel wird sowohl das lesen als auch das analysieren/ausgeben in dem selben Thread gemacht. Da braucht man dann keine Queue. Die ist ja für die Kommunikation über Threads hinweg.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Ah, dass mit dem Übergeben auch eine Instanz des Objektes erstellt wird war mir nicht klar. Ich was davon ausgegangen, dass der serielle Port geblockt ist, wenn ein Thread darauf zugreift - und ich deshalb nicht mehrere Objekte für den Seriellen Port erstellen kann.

Ich hoffe die Tutorials erhellen mich noch etwas. :-)
BlackJack

@robvoi: Da wird keine neue Instanz erstellt sondern die vorhandene übergeben. Ob lesen und schreiben gleichzeitig klappt hängt wahrscheinlich von der Implementierung ab. Ob so ein serieller Port wirklich vollduplex ist. Ich denke ja erst einmal ja, das sollte er sein. Wenn das nicht funktioniert, muss man das lesen und das schreiben entsprechend gegeneinander ausschliessen.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

OK - verstanden.
Die Übergabe ist aber nicht exklusiv, oder? Ich kann noch immer vom main loop aus das serial Objekt nutzen. Wo ist dann der Vorteil?

Sorry, wenn ich dich jetzt als Lehrer missbrauche :oops:
BlackJack

@robvoi: Der Vorteil ist, dass man nicht mehr auf irgendein Objekt ausserhalb zugreifen muss was einfach da sein muss, sondern das man das übergeben bekommt. Damit ist die Klasse unabhängig von dem Modul. Man könnte sie in ein anderes Modul verschieben. Und man kann zum Testen auch etwas anderes als ein `Serial`-Objekt übergeben. Zum Beispiel eine Liste mit Zeilen zum Testen. Und man kann den Code dann auch so schreiben das sich das Modul seiteneffektfrei importieren kann. Wie schon gesagt: Das reine importieren sollte keine serielle Verbindung erstellen.
robvoi
User
Beiträge: 8
Registriert: Montag 21. Oktober 2013, 13:23

Verstanden. Danke!
Antworten