Seriell einlesen (Windows)

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
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Hallo Python-Experten,

ich möchte gerne mit einem Python-Programm (mit Tkinter) DAten von der seriellen Schnittstelle einlesen. Plattform ist Windows.
Die DAten kommen in der Form
$A123
$B555
$C333
$A123
$B555
$C333
..
(Jeweils CRLF am Ende jeder Zeile.
Im Terminalprogramm hterm sieht alles gut aus und im Prinzip bekomme ich die Daten auch im Python-Programm zu fassen.
Es wird jeweils "gefiltert", ob die Zeile zum Beispiel mit $A anfängt. Wenn ja, dann wird der String in eine Variable geschrieben, die mit einem LAbel verknüpft ist.
Im Prinzip funktioniert das, aber es gibt nach kurzer Zeit immer einen Überlauf oder so etwas:
Exception in Tkinter callback
Traceback (most recent call last):
File ..
return self.func(*args)
File ..
func(*args)
File ... line 44, in empfangen
IndexError: string index out of range

Hier das Programm:


Code: Alles auswählen

from Tkinter import *
import serial                      

fenster=Tk()

UART = serial.Serial('COM4', 9600, timeout=0)          
Activ = False
if(not(UART.isOpen())):
  Activ = True
  UART.open()
a_string=StringVar()
a_string.set("A Anfang")

def empfangen():
  daten = UART.readline()           
  if len(daten) > 0:
#       daten= daten.strip("\r")          # abschneiden   ??
       daten= daten.strip("\n")          # abschneiden    ???   <- meine Versuche, das zu beheben
       if daten[0]=="$":
         if daten[1] == "A":             # Wenn das erste Zeichen ein A ist
           a_string.set(daten[2:])      #   Variable in Label updaten
       UART.flushInput()  # Puffer leeren   #  <- Mein Versuch, das Problem zu beheben. Ohne Erfolg.
  fenster.after(100, empfangen)      # 100 Millisekunden
  
textfeld4=Label(fenster,textvariable=a_string)
textfeld4.grid(row=1,column=1)
fenster.mainloop()
Es kann ja nur eine Kleinigkeit sein. Die Daten werden empfangen. Das Filtern funktioniert. DAs Updaten des Variablen und damit des Labelfeldes funktioniert auch. Aber irgendwann crasht es.

Ich wäre dankvar für einen Tipp.
Zuletzt geändert von Anonymous am Freitag 22. Januar 2016, 12:12, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@pythonstefan: entweder gibt es Übertragungsfehler, oder die Daten liegen nicht in dem Format vor, wie Du sie erwartest. Ob ein String einen bestimmten Anfang hat, prüft man ja auch mit .startswith, dann kann es keinen IndexError geben. Statt der if-Abfragen sollte man auch ein Wörterbuch nehmen:

Code: Alles auswählen

[...]
string_vars = {
    '$A': StringVar(),
    '$B': StringVar(),
    '$C': StringVar(),
}
[...]
daten = UART.readline().strip()
if daten:
    try:
        string_vars[daten[:2]].set(daten[2:])
    except KeyError:
        print "Unerwartete Daten", daten
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Danke Sirius3 für die schnelle Antwort! Es gibt Fortschritte.

Das strip() bei daten = UART.readline().strip() hat bewirkt, dass es keine Fehlermeldung mehr gibt - auch wenn ich nicht verstehe, warum das so ist.
Der Kanal A funktioniert jetzt super. Reagiert auch sofort, wenn der Wert sich ändert.

Leider wird aber zuverlässig nur der $A Kanal empfangen. Die Kanäle $B und $C werden nur sporadisch richtig erkannt. Das verstehe ich noch gar nicht. Ich dachte übrigens, dass ich mit dem flushInput() nach jedem Empfang einfach alle bereinige. Ich weiß, dass dadurch empfangene Zeichen verloren gehen, aber sonst ist die Reaktion am PC nur Verzögert auf Änderungen vom Sender, weil wohl der Puffer schneller vollgeschrieben wird, als ich das am PC auslese.

Die Daten kommen m.E. sehr sauber an, wie der Snapshot aus hterm zeigt:

Code: Alles auswählen

$A712<\r><\n>
$B66.1<\r><\n>
$CAlarm<\r><\n>
$A712<\r><\n>
$B66.1<\r><\n>
$CAlarm<\r><\n>
$A712<\r><\n>
$B66.1<\r><\n>
$CAlarm<\r><\n>
....


Code: Alles auswählen

..
UART = serial.Serial('COM4', 9600, timeout=0)             # fuer Windows
...

def empfangen():
  daten = UART.readline().strip()           # liest Zeile bis Ende   LF bzw. CRLF
  if len(daten) > 0:
#       daten= daten.strip("\r")          # abschneiden    aendert nicht ?
#       daten= daten.strip("\n")          # abschneiden
       if daten[0]=="$":
         print 'Empfangen:', daten
         if daten[1] == "A":             # Wenn das erste Zeichen ein A ist
           print daten[2:]             #   Kontrollausgabe
           a_string.set(daten[2:])      #   Variable in Label updaten
         if daten[1] == "B":            # Wenn das erste Zeiche ein B ist
           print daten[2:]
           b_string.set(daten[2:])
         if daten[1] == "C":            # Wenn das erste Zeiche ein B ist
           print daten[2:]
           c_string.set(daten[2:])
       UART.flushInput()  # Puffer leeren   # sonst verzoegerte Reaktion. Da ist wohl der Sender schneller als der PC das verarbeitet.
  fenster.after(100, empfangen)      # 100 Millisekunden
Zuletzt geändert von Anonymous am Freitag 22. Januar 2016, 12:26, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

Du kannst nicht Daten wegwerfen und Dich gleichzeitig wundern das Du Daten verlierst. Lass das mit dem `flushInput()` mal sein.

Das auslesen muss in einen eigenen Thread, wenn `readline()` blockiert solange bis tatsächlich eine komplette Zeile gelesen werden konnte. Sollte das mal nicht schnell geschehen, dann ist so lange Deine GUI ”tot” weil sie auf nichts reagieren kann. Lass die Daten in einem Thread ständig auslesen und stecke sie in eine Queue und mit `after()` verarbeitest Du dann regelmässig alles was in der Queue steht. So blockiert die serielle Verbindung die GUI nicht, und die GUI hält auch das auslesen nicht auf wenn das mal schneller sein sollte als alle 100 Millisekunden abgefragt wird.

Einrücktiefe ist per Konvention übrigens *vier* Leerzeichen pro Ebene und nicht zwei.

Dein Code hat immer noch das gleiche Problem wenn nur ein '$' in einer Zeile vorkommt. Digitale Kommunikation ist heutzutage ja ziemlich sicher, aber bei seriellen Schnittstellen kann das immer mal ein bisschen wackelig sein, und man sollte da auch mit unerwarteten Ergebnissen klar kommen.

Quelltext kopieren und leicht anpassen ist keine gute Programmiertechnik. Wiederholungen in Code und Daten vermeidet man als Programmierer so gut es geht, denn das ist fehleranfällig und schlecht wartbar. Du hast da beispielsweise vergessen bei 'C' den Kommentar anzupassen. Wenn der nicht so trivial wäre das man ihn sowieso weglassen sollte, wäre das eine Stelle an der ein anderer Leser, oder auch Du selbst in ein paar Monaten, erst einmal überlegen müsste wer Recht hat, der Kommentar, oder der Code. Und da Kommentare üblicherweise Informationen liefern die nicht einfach aus dem Code ersichtlich sind, misst man dem Kommentar normalerweise höheres Gewicht zu. Der sollte also besser korrekt sein.

Auf Modulebene gehören nur Definitionen von Konstanten, Funktionen, und Klassen. Das Hauptprogramm steht üblicherweise in einer `main()`-Funktion. Beides hat zur Folge das Funktionen und Methoden nur auf Werte (ausser Konstanten) zugreifen können die als Argumente übergeben wurden, und Ergebnisse gegebenenfalls als Rückgabewerte die Funktion/Methode verlassen. Bei GUI-Programmierung ist objektorientierte Programmierung (OOP) angesagt, weil GUI-Programmierung ereignisbasiert ist, und man sich über Aufrufe hinweg Zustände merken muss. Also Daten (Zustand) und Funktionen (Methoden) zu Objekten zusammen fasst. Das was Du da machst, Variablen und ”Funktions”definitionen auf Modulebene zu mischen so das letztlich jede Zeile Einfluss auf jede andere haben könnte, führt sehr schnell zu einem unüberschaubaren Chaos, weil man immer das gesamte Programm im Blick haben muss und sich nicht auf in sich abgeschlossene, kleine Teilbereiche beschränken kann, um verlässliche Aussagen über den Programmverlauf zu machen.


Im ersten Beitrag ist das `Activ` komisch — was soll das bedeuten? Ob die serielle Verbindung offen ist, kann man auch direkt von dem `Serial`-Exemplar abfragen. Wobei das an der Stelle wo es gemacht wird völlig unnötig ist, denn an der Stelle ist die Verbindung garantiert offen, denn sonst hätte das erstellen des Objekts schon nicht geklappt.

Beide Namen `Activ` und `UART` sind nicht mit dem Style Guide for Python Code konform. Namen durchnummerieren ist auch ein Warnzeichen. Entweder war man zu faul sich einen vernünftigen Namen zu überlegen, oder man möchte eigentlich keine Einzelnamen sondern eine Datenstruktur verwenden. Oft ist das dann eine Liste.
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Dank, BlackJack, für die ausführlichen Erläuterungen. Ich werde davon möglichst viel umsetzen. Einrückungen und NAmenszeichen ist ja schon einmal kein Problem. DAs mit Activ sehe ich ein. Das hatte ich tatsächlich irgendwo gelesen und jetzt habe ich es weggelassen.
Schwieriger ist es mit Queue und solchen SAchen, weil di eeben auch mehr Kenntnisse erfordern und in den Beispielen, die ich gesehen habe, sah es aus, als ginge es "für den Hausgebrauch" auch auf einfache Weise. Ist ja nur für mich.. Eigentlich funktioniert es ja auch alles schon sehr gut und am liebsten würde ich nicht alles komplett auf eine neue, schwierigere Ebene heben, sondern das, was ich bisher habe nur noch an einer "kleinen Ecke" verbessern und dann ist es erst einmal gut. Vielleicht hat ja jemand noch konkrete Tipps, wie sich mein Programm mit wenig Aufwand funktionsfähig machen lässt. Ich würde mich freuen.
Vielleicht hat sonst jemand einen Link für mich auf eine Seite, auf der genau so etwas beschrieben ist: Einlesen von der seriellen Schnittstelle ohne zu blockieren. Bitte Anfängertauglich. Die Beispiele, die ich gegoogelt bekommen hatte, gingen in die Richtung wie beschrieben.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@pythonstefan: die Daten kommen halt zu Zeitpunkten an, die Du nicht kontrollieren kannst. Willst Du das mit einer GUI kombinieren kommst Du nicht an Nebenläufigkeit vorbei. Es gibt sicher Beispiele, wie man Tk mit einer Queue kombinieren kann. Hier im Forum kann ich mich an mehrere erinnern. Um ein wenig Aufwand kommst Du nicht drumrum.
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Danke
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

So, ein neuer Versuch. Ich hoffe, es geht ein bisschen in die richtige Richtung - während ich es gleichzeitig noch selbst verstehe. Diesmal mit Queue.
Wieder geht es "im Prinzip".
Also:Es werden die Daten eingelesen und sie werden auch aus der Queue wieder herausgeholt und über die Labels ausgegeben.
Wenn ich per Poti am Sender die Werte verändere, dann reagiert das Programm zwar, aber nur mit Verzögerung.
Die Daten kommen übrigens so an:
$A123
$B555
$C333
#### 100 Millisekunden nichts ###
$A123
$B555
$C333
#### 100 Millisekunden nichts ###
..
Also alle 100 Millisekunden werden die drei Datensätze hintereinander gesendet.

Nun habe ich mit den Zeitwerten bei after() gespielt, aber das hat es auch nicht gelöst. Entweder es ist immer noch verzögert oder es kommt sogar - wenn ich zu niedrig gehe mit den Zeitwerten die Fehlermeldung:
....
if ausqueue[1] == "A":
IndexError: string index out of range

Wie kann ich das jetzt lösen? Durch die beiden after() habe ich doch auch jetzt eine Nebenläufigkeit und durch die Queue eine Unabhängigkeit, oder?

Code: Alles auswählen

#!/usr/bin/python
# Seriell empfangen
from Tkinter import *
import serial                       # Modul fuer serielle Verbindungen importieren
import Queue
q = Queue.Queue()                   # FIFO-Queue

fenster=Tk()

# ------- Seriellen Port oeffnen ------------------------------------------
meinUART = serial.Serial('COM4', 9600, timeout=0)             # fuer Windows
if(not(meinUART.isOpen())):
    meinUART.open()

a_string=StringVar()
a_string.set("-A-")
b_string=StringVar()
b_string.set("-B-")
c_string=StringVar()
c_string.set("-C-")

# ----- Prozeduren definieren --------------------------------------------
def seriell_empfangen():
    daten = meinUART.readline().strip()   # liest Zeile bis Ende   LF bzw. CRLF
#    print daten                          # zur Kontrolle
    q.put(daten)
    fenster.after(20, seriell_empfangen)  # Empfangsroutine aktivieren  inqueue_empfangen

def ausqueuelesen():
    ausqueue ="leer" 
    if not q.empty():
        ausqueue=q.get()
#        print ausqueue                   # zur Kontrolle
        if len(ausqueue) > 0:
            if ausqueue[0]=="$":
                if ausqueue[1] == "A":                # Wenn das erste Zeichen ein A ist
                    a_string.set(ausqueue[2:])     #   Variable in Label updaten
                if ausqueue[1] == "B":                # Wenn das erste Zeiche ein B ist
                    b_string.set(ausqueue[2:])
                if ausqueue[1] == "C":              # Wenn das erste Zeiche ein C ist
                    c_string.set(ausqueue[2:])
    fenster.after(20, ausqueuelesen)           # Nach n Millisekunden wieder aufrufen

#------- GUI definieren --------------------------------------------------------------
# Fensterueberschrift
textfeld0=Label(fenster,text="Sensordaten",font=("Helvetica",16),fg="blue")
textfeld0.grid(row=0,column=0,columnspan=2)      

# Feste Textfelder
textfeld1=Label(fenster,text="Sensor 1: ",fg="blue",font="Helvetica 16 bold")
textfeld1.grid(row=1,column=0)

textfeld2=Label(fenster,text="Sensor 2: ", fg="blue",font="Helvetica 16 bold")
textfeld2.grid(row=2,column=0)

textfeld3=Label(fenster,text="Sensor 3: ", fg="blue",font="Helvetica 16 bold")
textfeld3.grid(row=3,column=0)

# Variable Felder
textfeld4=Label(fenster,textvariable=a_string, fg="black",font="Helvetica 16 bold")
textfeld4.grid(row=1,column=1)

textfeld5=Label(fenster,textvariable=b_string, fg="black",font="Helvetica 16 bold")
textfeld5.grid(row=2,column=1)

textfeld6=Label(fenster,textvariable=c_string, fg="black",font="Helvetica 16 bold")
textfeld6.grid(row=3,column=1)
#---------------------------------------------------------------------

# Prozeduren starten
fenster.after(100, seriell_empfangen)    
fenster.after(100, ausqueuelesen)  

fenster.mainloop()
Zuletzt geändert von Anonymous am Sonntag 24. Januar 2016, 13:13, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@pythonstefan: Du hast das Problem zeitlich nur verschoben, es ist immer noch so, dass die GUI das Auslesen blockieren kann, und dass das Auslesen die GUI blockieren kann. Es läuft halt im selben Thread und damit kann immer nur entweder das eine oder das andere laufen, und dabei das jeweils andere aufhalten.
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Ach so. Mhh.
Kannst Du mir einen Anstupser (oder einen Link) geben, wie ich meine beiden Prozeduren zu Threads machen kann. Bitte nicht zu viel auf einmal.
Vom Prinzip ist doch die Idee, dass ich die GUI ganz normal habe, wie sie schon ist und dass meine beiden Prozeduren zu unabhängigen Threads werden müssen, richtig?
Der eine Thread liest dann immer eifrig von der seriellen Schnittstelle und schreibt in die Queue und der andere Thread holt sich etwas von der Queue und schreibt es "heimlich" in das jeweilige Label. Dann müssten doch eigentlich jetzt nur meine briden Prozeduren zu Threads gemacht werden? Nur wie?
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@pythonstefan: das Lesen muß ein einen von der GUI unabhängigen Thread. Das schreiben in die Labels kannst Du weiterhin per after antriggern. Allgemein solltest Du auf *-importe verzichten, if und not nicht wie Funktionen schreiben (also ohne Klammern), den GUI-Aufbau in Funktionen stecken und Variablen nicht durchnummerieren.

Code: Alles auswählen

#!/usr/bin/python
import Tkinter as tk
import threading
import serial
import Queue

def read_serial(queue):
    meinUART = serial.Serial('COM4', 9600, timeout=0)
    if not meinUART.isOpen():
        meinUART.open()
    while True:    
        daten = meinUART.readline().strip()
        queue.put(daten)

def build_gui():
    fenster = tk.Tk()
    l = tk.Label(fenster,text="Sensordaten",font=("Helvetica",16),fg="blue")
    l.grid(row=0,column=0,columnspan=2)      
     
    string_vars = {}
    for sensor, label in enumerate('ABC', 1):
        l = tk.Label(fenster,text="Sensor %d: " % sensor, fg="blue", font="Helvetica 16 bold")
        l.grid(row=sensor,column=0)
        stringvar = tk.StringVar()
        stringvar.set('-%s-' % label)
        string_vars['$%s' % label] = stringvar
        l = Label(fenster,textvariable=stringvar, fg="black", font="Helvetica 16 bold")
        l.grid(row=sensor,column=1)
    return fenster, string_vars

def ausqueuelesen(string_vars, queue):
    if not queue.empty():
        daten = queue.get()
        try:
            string_vars[daten[:2]].set(daten[2:])
        except KeyError:
            print "Unerwartete Daten", daten
    fenster.after(20, lambda: ausqueuelesen(string_vars, queue))

def main():
    queue = Queue.Queue()
    thread = threading.Thread(target=read_serial, args=(queue,))
    fenster, string_vars = build_gui()
    fenster.after(20, lambda: ausqueuelesen(string_vars, queue))
    fenster.mainloop()
    
if __name__ == '__main__':
    main()
BlackJack

Das testen ob die serielle Verbindung offen ist, ist wie gesagt unnötig. Wenn man `Serial()` den Port angibt, dann ist da ein `open()` implizit mit drin und falls das nicht klappt, gibt es eine Ausnahme.

Das ``lambda`` nicht nötig weil man `after()` weitere Argumente geben kann, die dann auf Aufruf der Rückruffunktion als Argumente übergeben werden.

Ich würde in einer Schleife so viel wie möglich aus der Queue lesen, sonst bekommt man Probleme wenn schneller empfangen wird als das Intervall in dem die Funktion aufgerufen wird, hergibt.
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

Danke Sirius3!
DAs ist eine tolle Vorlage. Ich habe davon auch ziemlich viel verstanden.
Bei einem LAbel (Zeile 27 habe ich noch ein tk. ergänzt - das war leicht.
Leider bekomme ich aber trotz längerer Anstrengung noch hartnäckige Fehlermeldungen.
Das Fenster öffnet sich zwar, aber dann schreibt er sofort:
NameError: global name 'ausqueuelesen' is not defined.
Er bezieht das auf fenster.after(20, lambda:ausqueuelesen(string_vars, queue))
Und da hört mein Verständnis dann auch (für heute) auf ... :-(

Wasa macht das
if __name__ == '__main__':
main()
?
Sieht für mich irgendwie komisch aus.
Klar: Irgendwie ruft das wohl main() auf.
Und in main() wird der Thread read_serial() gestartet. Sehr schön.
Nach after() wird in main() auch ausqueuelesen() gestartet, was sich mit after() wiederum alle 20 ms selbst wieder aufruft.
Der Aufruf von buid_gui() ist für mich dann noch etwas rätselhaft, aber es klappt ja.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@pythonstefan: Das "if __name__"... sorgt dafür, dass main nur aufgerufen wird, wenn das Programm auch als Skript gestartet wird. Auf diese Weise kann man das Programm auch als Modul importieren (entweder, weil man die Funktionen noch anderweitig verwenden will, oder weil man sie testen will). build_gui vermeidet doch einfach nur das mehrfache Kopieren des selben Codes mit einer for-Schleife. ausqueuelesen sollte eigentlich vorhanden sein; fenster war es nicht; hier also der Teil mit den Verbesserungen die BlackJack mir auferlegt hat:

Code: Alles auswählen

def ausqueuelesen(fenster, string_vars, queue):
    try:
        while True:
            daten = queue.get_nowait()
            try:
                string_vars[daten[:2]].set(daten[2:])
            except KeyError:
                print "Unerwartete Daten", daten
    except Queue.Empty:
        pass
    fenster.after(20, ausqueuelesen, fenster, string_vars, queue)
 
def main():
    queue = Queue.Queue()
    thread = threading.Thread(target=read_serial, args=(queue,))
    thread.daemon = True
    thread.start()
    fenster, string_vars = build_gui()
    fenster.after(20, ausqueuelesen, fenster, string_vars, queue)
    fenster.mainloop()
Du siehst, dass ausqueuelesen inzwischen schon eine ganze Menge Parameter mit sich herumschleppen muß. Da würde ich sagen, es ist Zeit, objektorientiert zu programmieren.
pythonstefan
User
Beiträge: 13
Registriert: Freitag 22. Januar 2016, 09:34

DANKE Sirius3!!!!!!

Nachdem ich das timeout=0 weggenommen habe, läuft es jetzt perfekt!!!
VIIIIEEEELEN DANK!!
Und ich habe viel gelernt!

(Vielleicht) letzte Fragen:
Was bewirken die folgenden drei Zeilen?
thread.daemon = True
thread.start()
fenster, string_vars = build_gui()

Muss man eigentlich die Datei nicht korrektermaßen noch beim Schließen des Fensters wieder schließen?
Antworten