Falscher und verzögerter Rückgabewert

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
MTC
User
Beiträge: 1
Registriert: Mittwoch 15. Mai 2019, 07:03

Es geht um Folgendes. Ich greife über ein Python-Skript auf den DALI-Bus zu um einen Dimmer zu konfigurieren und im Anschluss die Konfiguration zu überprüfen. Dazu nutze ich ein exe-Datei, die ich über „subprocess.Popen()“ aufrufe und deren Aufruf ich ausblende. Die exe-Datei baut die Kommunikation zum Bus auf, sendet die Befehle und gibt die Rückgabewerte vom Bus zurück Der Kontakt zum Bus wird über eine USB-DALI-Maus aufgebaut. Die Stromversorgung des Dimmers steuere ich, indem ich mit pyserial über einen USB-RS485-Connector (ftdi) ein Relais ansteuere. Die Konfigurationsdaten lese ich aus einer TXT-Datei ein und die Konfiguration dokumentiere ich in einem Log.

Das Skript macht, was es soll. Ich kann Befehle und Anfragen an den DALI-Bus senden, ich erhalte die DALI-Antworten, die Parameter werden eingelesen, die Testergebnisse gespeichert und das Relais lässt sich auch ansteuern. Ich stoße aber leider immer wieder einmal auf ein Problem. Lese ich zum Beispiel über das Skript die ersten vier Stellen der GTIN des Dimmers über DALI aus. Kann es passieren, dass ich statt (3,17,175,18) ein (4294967295,17,175,18) oder ein (4294967295, 4294967295, 42949672953,3) zurückbekomme und beim nächsten Mal (4294967295, 4294967295, 3, 42949672953) erhalte.

Ich habe also das Problem, dass statt dem empfangenen Wert ein 32-Bit-Wert mein Skript erreicht oder aber 32-Bit-Werte das Skript erreichen und der erwartete Wert verzögert das Skript erreicht. Bei konfigurierten Werten passiert das Gleiche. Lese ich die aus kann es sein, dass ein 32-Bit-Wert beim 3. Parameter erscheint und der Wert des dritten Parameters dann beim vierten erscheint und so weiter, während beim Auslesen über ein anderes Skript/Aufrufen der exe-Datei in der Kommandozeile/DALI-Tools die Korrektheit der Konfiguration bestätigt wird. Auf dem Bus-Monitor kann ich erkennen, dass die korrekten Werte vom Bus kommen. Verzichte ich darauf die Anzeige der exe-Datei auszublenden, kann ich sehen, dass auch von der exe-Datei die korrekten Werte übergeben werden. Ich nutze die exe-Datei auch in anderen Skripten, um etwa DALI-Testsequenzen nachzubauen, und bin dabei nicht auf dieses Problem gestoßen.

Das Problem tritt, sofern es auftritt, gleich beim ersten Start des Skripts auf. Ist das Problem aufgetreten, hilft nur ein Neustart des Rechners, danach tritt der Fehler nicht mehr auf. Neustarten oder Abstecken der genutzten DALI- Hardware und so weiter bringt nichts. Läuft das Skript beim ersten Starten, läuft es ohne Probleme bis in alle Ewigkeit. Von 10 Starts des Skript habe ich den Fehler vielleicht einmal. Da ein Neustart des Rechners nötig ist das Problem zu beheben und das Skript ansonsten ohne Probleme funktioniert, bin ich in Sachen Fehlerquelle identifizieren ein wenig überfragt.

Am Anfang hatte ich mir für die ersten Versuche einen älteren DALI-Schaltaktor ausgeliehen und mit dem hatte ich das Problem nicht. Den mußte ich aber aufgeben, weil ich, als ich die Konfiguration ausgebaut habe, Befehle nutzen mußte, die der Aktor als Schaltbefehle versteht, und der Dimmer keinen unandressierten Broadcast unterstützt. Der USB-RS485-Connector könnte also eine Rolle spielen. Aber wie und wieso er mein Skript beeinflussen könnte und wieso nur manchmal, weiß ich nicht. Änderungen am Zugriff auf den COM-Port haben nichts gebracht und nach jedem Schaltbefehl wird die Kommunikation gleich wieder geschlossen, um zu verhindern, daß nach Abbrechen des Skripts der COM-Port belegt bleibt.

Meine Frage wäre nun, ob jemand eine Idee hat, wie der Fehler entsteht und woher der 32-Bit-Wert kommen könnte und wie er sich lösen lassen könnte. Mit ist klar, daß das jetzt keine alltägliche Frage ist, aber vielleicht hatte ja schon jemand ein ähnliches Problem.

Ich verwende das aktuelle Anaconda und Python 3.7 auf Windows 7 32-Bit und 10 64-Bit.


Code: Alles auswählen


#Methode zum Auslesen von DALI-Werten
def Read(Verzeichnis):  
    time.sleep(.300)    
    ans=subprocess.Popen(Verzeichnis, stdout=subprocess.PIPE, shell=True)
    ans.wait()
    ans.communicate()[0]
    Ausgabe=ans.poll()    
    return Ausgabe

# Methode zum Ausführen von Befehlen
def Com(Verzeichnis):
    # os.system(Verzeichnis)
    time.sleep(.300)
    subprocess.Popen(Verzeichnis, stdout=subprocess.PIPE, shell=True)

#Auslesen der GTIN
while True:  
    #Spannung aus
    Uof()
    time.sleep(1)
    #Spannung an
    Uon()
    #Reset
    com=loc+str(adr)+"d 32d"+do
    Com(com)
    time.sleep(0.5)  
  
    print("Test startet ... ")   
   


    print("Gerät identifizieren...")
    
    for i in range(3,7,1):
    	#DTR1 setzen
        com=loc+"C3h 0h"
        Com(com)     
        time.sleep(0.5)
        #DTR0 setzen
        com=loc+"A3h "+str(i)+"h"
        Com(com)   
        time.sleep(0.5)
        com=loc+str(adr)+"d C5h"
        #Read Memory at DTR1 und DTR0
        ans=Read(com)
        print(ans)
        time.sleep(0.5)
        #print(ans)
        
        GTIN += [ans]

	# ... 
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich sehe da nichts, dass Python implizieren wuerde, da die gesamte Kommunikation durch die EXE erfolgt. Auch die Problematik mit dem Neustart etc. deutet da eher auf ein Problem auf einer Hardware/Treiber-Ebene hin, die von Python so nicht ohne weiteres zu beeinflussen ist.
Benutzeravatar
__blackjack__
User
Beiträge: 14044
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MTC: Was auffällt ist das `Com()` nicht wartet bis das externe Programm fertig ist. Da kann es also zumindest theoretisch passieren das dieses externe Programm mehr als einmal gleichzeitig läuft. Ausserdem leitest Du die Ausgabe in eine `PIPE` um die niemals ausgelesen wird. Sollte das externe Programm da etwas ausgeben was den Pufferspeicher zwischen den Prozessen übersteigt, dann hängt das externe Programm ewig, weil es sich erst beenden kann wenn Du genug von der Ausgabe abgenommen hast. Du fragst auch den Rückgabecode nicht ab, was zu Zombie-Prozessen führen kann.

Sonstige Anmerkungen: Um binäre Operatoren und Gleicheitszeichen ausserhalb von Argumentlisten erhöhen Leerzeichen die Lesbarkeit. Das gleiche gilt für ein Leerzeichen nach Kommas.

Namen schreibt man in Python klein_mit_unterstrichen. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Man sollte keine Abkürzungen verwenden die nicht allgemein bekannt sind. Was soll `Com` heissen? COM-Port? Command? Communicate? Was ganz anderes? Und da könnte ich noch erfolgreich raten – bei `loc`, `GTIN`, und `do` habe ich keine Ahnung was das jeweils bedeuten mag.

`Com()` ist zudem noch problematisch weil das eine Funktion ist die eigentlich `com()` geschrieben werden müsste, dann aber mit dem `com`-Wert kollidiert. Darum benennt man Funktionen und Methoden üblicherweise nach der Tätigkeit die sie ausführen, damit man sie leichter von ”passiven” Werten unterscheiden kann. Also beispielsweise statt `Com()` und `com` besser `execute_command()` und `command`.

Wenn man nicht abkürzt kann man sich auch Kommentare sparen. Bei `Uof()` und `Uon` bräuchte man keine Kommentare das dort die Spannung aus- und angeschaltet wird, wenn die Funktionen `turn_voltage_off()` und `turn_voltage_on()` hiessen.

``shell=True`` sollte man bei `subprocess` nicht verwenden. Da fängt man sich die gleichen Probleme ein wegen denen man `os.system()` nicht verwenden sollte.

`verzeichnis` ist als Name falsch. Selbst wenn man ``shell=True`` verwendet, ist das ja kein Verzeichnis, denn Verzeichnisse kann man nicht ausführen. Es sollte eine Liste mit dem Namen/Pfad des externen Programms und Argumenten sein wenn ``shell=True`` nicht verwendet wird.

In `Read()` ist der `wait()`-Aufruf falsch. Wieder das gleiche Problem wie bei `Com()` – es wird die Ausgabe umgeleitet und dann wartest Du mit `wait()` auf das Ende des Prozesses, der aber blockieren kann wenn zu viel Ausgabe erzeugt wird, womit man dann einen schönen „deadlock” hat. Beide Prozesse warten aufeinander.

Du musst die `communicate()`-Methode zuerst aufrufen. Das ``[0]`` macht keinen Sinn, weil Du mit dem Ergebnis dann nichts machst. Im Grunde ist damit die Umleitung der Ausgabe mit `PIPE` sinnlos, denn Du machst damit überhaupt gar nichts.

Warum das `Popen`-Objekt in der Funktion `ans` heisst, ist mir auch nicht klar. `answer` kann es ja nicht sein, denn der Name würde nicht zu einem Prozess passen.

Im Grunde kann man die ganze `Read()`-Funktion auf das hier zusammenstreichen:

Code: Alles auswählen

def read(command):  
    time.sleep(0.3)
    return subprocess.Popen(command).wait()
Wobei das `sleep()` da nicht reingehört.

Und die `Com()` bräuchte man auch nicht, denn da kann man einfach auch die `read()`-Funktion verwenden und einfach den Rückgabewert ignorieren.

Das ein Programm eine Zahl als Ergebnis über den Rückgabecode kommuniziert erscheint mir komisch, denn da kann man nicht auf jeder Plattform beliebige Werte liefern.

Die Kommentare bei den beiden Funktionen sind inhaltlich falsch, weil es keine Methoden sondern Funktionen sind.

Zeichenketten und Werte mit `str()` und ``+`` zusammenstückeln ist eher BASIC als Python. In Python gibt es dafür die `format()`-Methode auf Zeichenketten und ab Python 3.6 f-Zeichenkettenliterale.

Die Umwandlung von `i` in ein Argument für das externe Programm sieht fehleranfällig aus wenn das 'h' für Hexadezimal steht, denn dann funktioniert das nur solange `i` kleiner als 16 ist/bleibt. Kann man da kein 'd' anhängen? Alternativ sollte man `i` als Hexadezimaldarstellung formatieren.

``+=`` bei einer Liste mit einer Liste die grundsätzlich nur ein Element enthält erstellt nur um ein Element anzuhängen eine unnötige temporäre Liste mit diesem einen Element. Um ein Element an eine Liste anzuhängen gibt es die `append()`-Methode.

Zwischenergebnis (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time

COMMAND = ['/path/to/external.exe', '--option', 'spam']


def execute_command(command):  
    return subprocess.Popen(command).wait()


def main():
    address = 42
    do = ['no', 'idea', 'what', 'this', 'is']
    
    # Auslesen der GTIN.
    while True:  
        switch_voltage_off()
        time.sleep(1)
        switch_voltage_on()
        # Reset.
        command = COMMAND + [str(address) + 'd', '32d'] + do
        execute_command(command)
        time.sleep(0.5)  
      
        print('Test startet...')
        print('Gerät identifizieren...')
        for i in range(3, 7):
            # DTR1 setzen.
            execute_command(COMMAND + ['C3h', '0h'])     
            time.sleep(0.5)
            # DTR0 setzen.
            execute_command(COMMAND + ['A3h', f'{i:x}h'])   
            time.sleep(0.5)
            # Read Memory at DTR1 und DTR0.
            answer = execute_command(COMMAND + [f'{address}d', 'C5h'])
            time.sleep(0.5)
            print(answer)
            gtin.append(answer)  # TODO Better name for `gtin`.

        # ... 


if __name__ == '__main__':
    main()
Falls `do` auch nur aus Zahlen besteht, würde ich die API von `execute_command()` so ändern, dass man dort tatsächlich eine Liste mit Zahlen oder ein `bytes`-Objekt übergeben kann und die Funktion das dann in Argumente für das externe Programm umwandelt.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Antworten