Python3 QThread, subprocess und ein C-Programm

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

Hallo,

ich habe folgendes Problem:

Ich möchte mit python3 in einem Thread ein C-Programm starten und während dieses läuft dessen Ausgabe (stdout) auswerten und ggf. Eingaben per Script an das Programm senden (stdin).
Das C-Programm steuert ein RFID-Lesegerät und liest dort die ID von den Tags aus. Ich habe mich schon auf anderen Webseiten umgesehen (stackoverflow, python3 doku, etc.), konnte aber nichts dazu finden was mir weiterhilft.
Die meisten Code-Snippets die ich so gefunden habe, gehen davon aus, dass man subprocess.communicate() benutzt. Das geht hier aber nicht, da die Ausgabe vom C-Programm nie abgeschlossen ist (es soll quasi für immer laufen, solange das python Programm läuft). Da die Anwendung auch eine QT GUI besitzt, habe ich hier QThread verwendet.

Hier ist der Code vom Thread, soweit wie ich gekommen bin:

Code: Alles auswählen

...
class scanThread(QtCore.QThread):
    def __init__(self):
        QtCore.QThread.__init__(self)
        self.pause = False
        self.exitthread = False

    def run(self):
        logging.debug("scanThread: started!")
        try:
            f = subprocess.Popen('bin/nfc-poll-command', stdin=subprocess.PIPE, 
                                 stdout=subprocess.PIPE, universal_newlines=True)
        except IOError as e:
            logging.debug("scanThread: cannot open poller:".format(e.errno, e.strerror))
        
        while(self.exitthread != True):
            line = f.stdout.readline().decode("utf-8").rstrip()
            logging.debug(line)
            while(line != "WAITING FOR COMMAND"):
                line = f.stdout.readline().decode("utf-8").rstrip()
            
            #send scan command and read output
            #signal with id
            
            self.setpause()
            while(self.pause == True):
                time.sleep(1)
                pass
        
        f.close()
                   
    def setpause(self):
        logging.debug("scanThread: setpause")
        self.pause = True
    
    def unsetpause(self):
        logging.debug("scanThread: unsetpause")
        self.pause = False
...
Das Problem ist, dass das Programm bei logging.debug(line) gar nichts ausgibt (Es hängt einfach nach der Ausgabe von "scanThread: started!"). Wenn ich das Popen() ohne stdin=subprocess.PIPE aufrufe geht das zwar, aber dann kann ich ja auch nichts über stdin eingeben.
Dann soll das Programm solange die Zeilen von stdout lesen, bis die Zeile "WAITING FOR COMMAND" erscheint, und dann "scan" auf stdin ausgeben und die nächsten 4 Zeilen auslesen. Danach soll wieder auf "WAITING FOR COMMAND" gewartet werden.

Es wäre auch eine Option was am C-Programm zu ändern, wenn jemand eine bessere Lösung hat. Aber es wäre toll wenn sich das über python realisieren lässt.
Das Ganze läuft auf einem Raspberry Pi unter Raspbian/Debian Linux.

Gruß
bitstacker
BlackJack

@bitstacker: Bist Du denn sicher dass das externe Programm diese Ausgaben überhaupt über sein `stdout` tätigt und nicht vielleicht über `stderr`? Und ist beim C-Programm sichergestellt, dass es seine Ausgaben nicht puffert?
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

BlackJack hat geschrieben:@bitstacker: Bist Du denn sicher dass das externe Programm diese Ausgaben überhaupt über sein `stdout` tätigt und nicht vielleicht über `stderr`? Und ist beim C-Programm sichergestellt, dass es seine Ausgaben nicht puffert?
Da ich das C-Programm selber geschrieben habe, kann ich das ja beeinflussen. Die Ausgaben werden da alle über printf() gemacht. Das müsste ja dann defaultmäßig stdout sein. Wie das mit dem Puffern ist, kann ich leider nicht sagen, da ich nicht weiss in wiefern C printf() puffert. Ich könnte da aber ein Flush einbauen.
Das komische ist halt, dass wenn ich in python beim Popen() das stdin=subprocess.PIPE weglasse (also nur stdout) dann geht das mit stdout.readline() (Also nur lesen geht).
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Ich weiß jetzt nicht, ob readline versucht ganze Blöcke zu lesen und dann hängen bleibt. Aber für das, was Du hier versuchst, gibt es Module, wie z.B. pexpect, die ein paar Feinheiten glattbügeln.
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

Sirius3 hat geschrieben:Ich weiß jetzt nicht, ob readline versucht ganze Blöcke zu lesen und dann hängen bleibt. Aber für das, was Du hier versuchst, gibt es Module, wie z.B. pexpect, die ein paar Feinheiten glattbügeln.
Ah, danke, ich guck mir das mal an. Hätt ich mir auch denken können das es sowas schon gibt, aber ohne den namen weiss man nicht was man googlen muss :D

Dann erstmal danke für eure Hilfe, ich werd das mal mit pexpect versuchen und mein Ergebnis hier posten ;)

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

@bitstacker:
Mit der PIPE-Angabe hängt subprocess eine echte OS-Pipe dazwischen, was dazu führt, dass ziemlich aggressiv gepuffert wird (bei Linux sind Pipes bis zu 2^16 Bytes groß). Hinzu kommt noch der FILE*-Puffer, der bei allen C/C++-Lib basierten Calls aktiv ist:

Code: Alles auswählen

SUBPROCESS                              KERNEL                             PARENT PROCESS
printf() -> FILE* stdout    --->    write (Pipe) read    --->    FILE* (in CPython) -> readline()
Ausgabepuffer                            Pipepuffer                          Eingabepuffer
Macht 3 Puffer, die Du überwinden musst:
- Ausgabepuffer: fflush (in C) ist Dein Freund und wahrscheinlich Ursache des Problems [1]
- Pipepuffer: ist ein Durchschreibepuffer - heisst, sobald Daten vorliegen, blockiert read nicht mehr (kannst Du mit `select` beim Kernel anfragen)
- Eingabepuffer: ist nicht so leicht auszutricksen (flush funktioniert hier nicht!), allerdings öffnet `readline` die "Datei" im Zeilenmodus, dadurch blockiert der Call nur bis zum nächsten EOL

[1] Normalerweise zeigen STDIN/STDOUT auf ein Terminal, was dazu führt, dass die CLib diese im Zeilenmodus verarbeitet (Zwischenpufferung erfolgt bis zum nächsten EOL). Das Verhalten kann man in C z.B. mit `setbuf` kontrollieren. Falls es auch damit nicht funktionieren will, hilft es, Ausgaben immer zu flushen und die Eingaben auf der anderen Seite zeichenweise zu lesen. Das gilt in beide Richtungen - obiges Bsp. für die stdout-Pipe gilt analog umgekehrt für die stdin-Pipe.

expect brauchst Du nur, wenn Du in Deinem Subprogramm ein Terminal erwartest (z.B. ncurses verwendest).

Übrigens bietet QThread im gezeigten Code keine Vorteile ggü threading. Das Dein Subprogramm Qt nutzt, ist unerheblich, da es ein getrennter Process ist.
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

Also mit expect habe ich ähnliche probleme wie mit subprocess.

Gibt es denn noch eine andere Möglichkeit mit einem C-Programm zu kommunizieren? Ich hatte das auch schonmal so, dass das C-Programm nach dem Start nur einen Tag liest, und sich dann wieder beendet. Allerdings gab es da probleme wenn das Gerät, welches das C-Programm anspricht nicht verfügbar ist (/dev/ttyUSB0).

Dann habe ich das C-Programm so umgebaut, wie es jetzt ist. Also das das Gerät dauerhaft vom Programm benutzt wird.

Ich habe mir auch schonmal die python.h für C angeguckt, allerdings setzt das auch vorraus das aufgerufene Funktionen irgendwann mit einem Ergebnis zurrückkehren und nicht für immer laufen.

Danke für die Hilfe!

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

@bitstacker:
Zeig doch mal, was Du auf der C-Seite mit STDIN/STDOUT machst.
BlackJack

@bitstacker: Die einfachste Kommunikation ist schon über die Ein-/Ausgabekanäle. Wie jerch schon sagte dürfte das `fflush()` beim C-Programm die Lösung sein. Denn der Pipe-Buffer blockiert nicht und `subprocess` öffnet sein Ende der Verbindungen per Default ungepuffert. Da muss man also manchmal den umgekehrten Weg gehen und puffern explizit aktivieren wenn man zum Beispiel zu wenig Datendurchsatz hat, und dem Programm das Puffern nichts ausmacht.

Ohne Code zu sehen können wir allerdings ab hier nur raten. Schmeiss doch erst mal alles unnötige raus, also zum Beispiel den Thread. Bekomm das erst mal ohne Thread zum laufen. Dann könntest Du den Rest noch auf ein minimal lauffähiges Beispiel mit dem Problem zusammenstreichen, möglichst auch ohne das man spezielle Hardware benötigt um das laufen zu lassen, und das könntest Du dann mal zeigen, damit wir uns ein besseres Bild machen können. Vielleicht findest Du das konkrete Problem bei diesem Vorgang sogar selber.
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

Also hier erstmal das C-Programm:
http://www.python-forum.de/pastebin.php?mode=view&s=396

Das ist im Prinzip eins der Beispiele aus der libnfc, ich hab das so umgebaut das es mit den Commands "scan" und "exit" gesteuert werden kann. Ich bin absolut kein C-Experte, ich hab da bestimmt ne ganze Menge falsch gemacht :D

Also ich würde da jetzt erstmal die fflush() einbauen und dann guck ich mal ob ich es mit subprocess zum laufen kriege.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@bitstacker:
Füg mal in `main` als erstes hinzu:

Code: Alles auswählen

setvbuf(stdout, NULL, _IOLBF, BUFSIZ);
Das schaltet für STDOUT den Zeilenmodus an, dann musst Du nicht jedes `printf` separat flushen.

Und auf Pythonseite musst Du beachten, auch jedes Kommando mit '\n' abzuschliessen, sonst blockiert `fgets` weiterhin.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@bitstacker:
Übrigens ist Dein Problem wahrscheinlich einfacher mit ctypes lösbar. Hätte den Vorteil, dass Du nicht einen Subprozess starten und die Steuerung durch die Ein-/Ausgabe quetschen musst. Für ctypes reicht es, alle benötigte Funktionalität in Funktionen zu abstrahieren und in eine Lib zu packen, was recht überschaubar dem C-Schnipsel nach ist. Von Python aus kannst Du dann die Funktionen direkt aufrufen.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@bitstacker: auch C-Programme sollten in Funktionen strukturiert werden und nicht alles in eine main gepackt werden. Statt Kommandos zu senden, ließe sich der C-Teil auch einfacher über Signale steuern.
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

Habe das ganze jetzt mit subprocess und dem Tip von jerch mit setvbuf() realisiert:

Code: Alles auswählen

import subprocess
def main():
    
    print("start")
    try:
        f = subprocess.Popen('bin/nfc-poll-command', stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, universal_newlines=True)
    except IOError as e:
        print("Kaputt: " + e)

    line = None
    while(True):
        while(line != "WAITING FOR COMMAND"):
            line = f.stdout.readline().rstrip()
            print(line)
        f.stdin.write("scan\n")
        for x in range(0, 5):
            line = f.stdout.readline().rstrip()
            print(line)
        tag = f.stdout.readline()
        tag = tag.rstrip()
        tag = tag.replace(" ", "")
        tag = tag.replace("UID(NFCID1):", "")
        print("Der Tag ist: " + tag)
    f.terminate()

if __name__ == '__main__':
    main()    
Danke für die Hilfe :)
BlackJack

@bitstacker: Das sieht aber alles wenig robust aus.

Warum heisst der Prozess `f`? Das ist ein sehr nichtssagender Name.

Die Fehlerbehandlung mit dem ``except`` ist falsch weil danach einfach weitergemacht wird als wäre nichts passiert, und das endet dann unweigerlich in Zeile 14 in einem `NameError` weil im Fehlerfall `f` ja gar nicht an das `Popen`-Exemplar gebunden wurde.

Klammern um Bedingungen bei ``while``/``if``/… sind nicht nötig und auch nicht üblich. Ausserdem sieht es ohne ein Leerzeichen so aus wie ein Funktionsaufruf, was verwirren kann, wenn zum Beispiel der Quelltext ohne Syntaxhervorhebung gelesen wird.

`readline()` wird eigentlich nicht verwendet weil Dateiobjekte Iteratoren über die Zeilen in der Datei sind, und man deshalb das flexiblere `next()` verwenden kann.

Mit der Schleife die 5 Zeilen überliest und dann das Tag ausliest ist die IMHO brüchigste Stelle erreicht. Du machst Dich da völlig von einer allgemeinen, nicht formal spezifizierten Ausgabe von dem Programm abhängig die für Menschen gedacht ist und nicht für Programme. Das Format kann sich mit jeder neuen Version der Bibliothek ändern, hängt vielleicht sogar vom Chiptyp oder vom konkreten Treiber oder ähnlichem ab. Man sollte da vielleicht selber ein Protokoll spezifizieren das man dann implementiert und das ”maschinenlesbar” ist. Man könnte auch über das `stdout` des C-Programms nur die Kommunikation zum externen Programm laufen lassen, und andere Ausgaben wie Bibliotheksversion und so Debugausgaben auf `stderr` ausgeben. Dann braucht sich das Python-Programm darum gar nicht erst kümmern.

``f.terminate()`` wird niemals erreicht, weil die Endlosschleife davor nicht verlassen wird.
bitstacker
User
Beiträge: 7
Registriert: Montag 28. Juli 2014, 13:32

BlackJack hat geschrieben:@bitstacker: Das sieht aber alles wenig robust aus.

Warum heisst der Prozess `f`? Das ist ein sehr nichtssagender Name.

Die Fehlerbehandlung mit dem ``except`` ist falsch weil danach einfach weitergemacht wird als wäre nichts passiert, und das endet dann unweigerlich in Zeile 14 in einem `NameError` weil im Fehlerfall `f` ja gar nicht an das `Popen`-Exemplar gebunden wurde.

Klammern um Bedingungen bei ``while``/``if``/… sind nicht nötig und auch nicht üblich. Ausserdem sieht es ohne ein Leerzeichen so aus wie ein Funktionsaufruf, was verwirren kann, wenn zum Beispiel der Quelltext ohne Syntaxhervorhebung gelesen wird.

`readline()` wird eigentlich nicht verwendet weil Dateiobjekte Iteratoren über die Zeilen in der Datei sind, und man deshalb das flexiblere `next()` verwenden kann.

Mit der Schleife die 5 Zeilen überliest und dann das Tag ausliest ist die IMHO brüchigste Stelle erreicht. Du machst Dich da völlig von einer allgemeinen, nicht formal spezifizierten Ausgabe von dem Programm abhängig die für Menschen gedacht ist und nicht für Programme. Das Format kann sich mit jeder neuen Version der Bibliothek ändern, hängt vielleicht sogar vom Chiptyp oder vom konkreten Treiber oder ähnlichem ab. Man sollte da vielleicht selber ein Protokoll spezifizieren das man dann implementiert und das ”maschinenlesbar” ist. Man könnte auch über das `stdout` des C-Programms nur die Kommunikation zum externen Programm laufen lassen, und andere Ausgaben wie Bibliotheksversion und so Debugausgaben auf `stderr` ausgeben. Dann braucht sich das Python-Programm darum gar nicht erst kümmern.

``f.terminate()`` wird niemals erreicht, weil die Endlosschleife davor nicht verlassen wird.
Danke, das sind alles echt gute Tipps :) Also meine Lösung, die ich da oben gepostet habe, war auch nur als kurzer Test gedacht (Ob das mit dem flushen funktioniert).
Das mit den Klammern kommt wohl davon, wenn man dauernd zwischen so vielen Sprachen hin und her hüpft :D Bin wohl noch nicht so ganz python3 sicher.
Das sich da das C-Programm bzw. die Library ändert ist nicht vorgesehen, das ist alles auf einem Embedded System ohne Internetanschluss. Aber das ich da die unnötigen Ausgaben rausschmeisse ist ebenfalls eine gute Idee.

Nochmals Vielen Dank für die Hilfe!

Gruß
bitstacker
Antworten