Exeption-Handling beim lesen von OBD-Daten über Bluetooth

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

Hallo zusammen,

um die Daten über den Aktuellen Ladezustand vom BMS über die OBD-Schnittstelle auszulesen verwende ich ein Python-Sycript auf einem PI-Zero W.
Als Oberfläche für gewisse Handeingaben verwende ich Kivy.
Das ganze funktioniert recht gut, aber ab und zu bekomme ich beim auslesen der Schnittstelle eine exeption:

Code: Alles auswählen

Exception in thread Thread-12:
 Traceback (most recent call last):
   File "/usr/lib/python3/dist-packages/serial/serialposix.py", line 490, in read
     'device reports readiness to read but returned no data '
 serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)
 
 During handling of the above exception, another exception occurred:
 
 Traceback (most recent call last):
   File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
     self.run()
   File "/usr/lib/python3.5/threading.py", line 862, in run
     self._target(*self._args, **self._kwargs)
   File "./ConsumptionMonitorApp.py", line 154, in OBD_thread
     obddata=self.get_bms_data("2101","PID 2101 Abfragen",Dongel)
   File "./ConsumptionMonitorApp.py", line 356, in get_bms_data
     reading = OBD_Dongel.read()
   File "/usr/lib/python3/dist-packages/serial/serialposix.py", line 497, in read
     raise SerialException('read failed: {}'.format(e))
 serial.serialutil.SerialException: read failed: device reports readiness to read but returned no data (device disconnected or multiple access on port?)
Hier mal der relevante Code-Schnipsel:

Code: Alles auswählen

    def get_bms_data(self,command,AusgabeText,OBD_Dongel):
        self.SetToast(AusgabeText)        
        local_data="Unknown command"
        if command=="2105":
            OBD_Dongel.write(b'2105\r\n')
        else:
            OBD_Dongel.write(b'2101\r\n')
        OBD_Dongel.flush()
        seq = []
        joineddata=''
        while True:
            reading = OBD_Dongel.read()
            seq.append(reading)
            joineddata = ' '.join(str(v) for v in seq).replace(' ', '')
            err = re.search('ERROR', joineddata)
            if err:
                self.SetToast("Befehl " + command + "ist Fehlgeschlagen")
                local_data="Fehler"
                break
            Unbekannt = re.search('\?', joineddata)
            if Unbekannt:
                self.SetToast("Befehl " + command + "ist Unbekannt") 
                local_data="Unknown command"
                break 
            m = re.search('>', joineddata)            
            if m:
                tmpdata0=bytes('-',"utf-8").join(seq)
                tmpdata1=tmpdata0.decode("utf-8")
                tmpdata2=tmpdata1.replace(' ', '')
                tmpdata3=tmpdata2.replace('-', '')
                local_data=tmpdata3.replace('\r', '')
                self.SetToast("Befehl " + command + " ist Abgearbeitet")
                break       
        return local_data
Das ganze Projekt habe ich unter Github abgelegt:
https://github.com/olsvse/GUIConsumptionMonitorEV
Die Zeilen sind dort um 1 zu der Fehlermeldung verschoben.

Wie müsste ich jetzt einen try-Exeption-Block machen, damit ich entweder die Anfrage noch einer kurzen Zeit nochmals Wiederhole oder ich initialisiere mir nochmals die Verbindung mit "init_Dongel".
Momentan gehe ich davon aus, dass das Warten von einer Sekunde und dann ein neuer Read funktionieren müsste.

PS.: ich weis, dass meine Variablen-Namen nicht ganz Python-Like sind.

Gruß,
OlSvSe
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@olsvse: Wenn ich die Ausnahme so lese und sehe dass das `threading`-Modul beteiligt ist, kommt natürlich als allererstes die Frage ob Du sichergestellt hast, dass immer nur ein Thread auf die serielle Verbindung zugreift‽

Was ich heute das erste mal sehe ist das jemand anscheinend eine Allergie gegen `True` und `False` hat. ;-) Warum steht da überall ``bool(1)`` und ``bool(0)`` im Programm? Und genau wie bei `True` und `False` gilt auch hier (denn letztendlich wird das ja zu `True` und `False` ausgewertet), dass man damit keine Vergleiche macht, denn ``irgendwas == True`` gibt ja nur wieder `True` wenn `irgendwas` `True` war oder eben `False` wenn `irgendwas` schon `False` war. Da kann man auch gleich `irgendwas` verwenden.

Reguläre Ausdrücke für einfach enthaltensein Tests von Teilzeichenketten sind mit Kanonen auf Spatzen geschossen. Statt `if re.search('needle', haystack):`` ist ein einfaches ``if 'needle' in haystack:`` weniger komplex. Und man muss auch nicht auf besondere Bedeutungen von Zeichen in 'needle' aufpassen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

In `get_bms_data` bekommen `local_data` und `joineddata` Werte zugewiesen, die nicht benutzt werden. Die String-Representation von Bytes sollten nicht zur Weiterverarbeitung verwendet werden.

Mal ein bißchen aufgeräumt:

Code: Alles auswählen

    def get_bms_data(self, command, AusgabeText, OBD_Dongel):
        self.SetToast(AusgabeText)
        OBD_Dongel.write(b'{}\r\n'.format(b"2105" if command=="2105" else b"2101"))
        OBD_Dongel.flush()
        seq = []
        while True:
            reading = OBD_Dongel.read()
            seq.append(reading)
            if b'ERROR' in reading:
                verb = "Fehlgeschlagen"
                local_data="Fehler"
                break
            elif b'?' in reading:
                verb = "Unbekannt"
                local_data="Unknown command"
                break 
            elif b'>' in reading:
                local_data = b' '.join(seq).decode("utf-8")
                local_data = local_data.replace(' ', '').replace('-', '').replace('\r', '')
                verb = "Abgearbeitet"
                break
        self.SetToast("Befehl {} ist {}".format(command, verb))
        return local_data[/python|
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

__blackjack__ hat geschrieben: Sonntag 9. Dezember 2018, 20:36 @olsvse: Wenn ich die Ausnahme so lese und sehe dass das `threading`-Modul beteiligt ist, kommt natürlich als allererstes die Frage ob Du sichergestellt hast, dass immer nur ein Thread auf die serielle Verbindung zugreift‽
Es greift immer nur ein thread auf den OBD-Task zu.
Damit da was schief läuft müsste der alte OBD-Task sich nicht beenden und der Start eines neuen Tasks schon freigegeben sein.
Und dass ein OBD-Task läuft, kann ich an der Oberfläche sehen bevor ich den neuen Starte.
Normalerweise sollte das 2. klicken auf den Connect-Button den aktuellen Task beenden.
__blackjack__ hat geschrieben: Sonntag 9. Dezember 2018, 20:36 Was ich heute das erste mal sehe ist das jemand anscheinend eine Allergie gegen `True` und `False` hat. ;-) Warum steht da überall ``bool(1)`` und ``bool(0)`` im Programm? Und genau wie bei `True` und `False` gilt auch hier (denn letztendlich wird das ja zu `True` und `False` ausgewertet), dass man damit keine Vergleiche macht, denn ``irgendwas == True`` gibt ja nur wieder `True` wenn `irgendwas` `True` war oder eben `False` wenn `irgendwas` schon `False` war. Da kann man auch gleich `irgendwas` verwenden.
Da gebe ich Dir recht. Normalerweise bevorzuge ich TRUE/FALSE. Allerdings habe ich da (wenn ich mich recht erinnere) immer wieder fehler-Meldungen belommen, so dass ich zu dem Cast gegriffen habe.
An einer stelle Brauche ich den cast, wenn ich von Bit gesetzt/nicht-Gesetzt auf TRUE/FALSE caste.

Werde ich auf alle fälle überarbeiten.
__blackjack__ hat geschrieben: Sonntag 9. Dezember 2018, 20:36 Reguläre Ausdrücke für einfach enthaltensein Tests von Teilzeichenketten sind mit Kanonen auf Spatzen geschossen. Statt `if re.search('needle', haystack):`` ist ein einfaches ``if 'needle' in haystack:`` weniger komplex. Und man muss auch nicht auf besondere Bedeutungen von Zeichen in 'needle' aufpassen.
Danke werde das auch so verbessern.
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

Hallo zusammen,
Sirius3 hat geschrieben: Sonntag 9. Dezember 2018, 20:56 In `get_bms_data` bekommen `local_data` und `joineddata` Werte zugewiesen, die nicht benutzt werden. Die String-Representation von Bytes sollten nicht zur Weiterverarbeitung verwendet werden.

Mal ein bißchen aufgeräumt:

Code: Alles auswählen

    def get_bms_data(self, command, AusgabeText, OBD_Dongel):
        self.SetToast(AusgabeText)
        OBD_Dongel.write(b'{}\r\n'.format(b"2105" if command=="2105" else b"2101"))
        OBD_Dongel.flush()
        seq = []
        while True:
            reading = OBD_Dongel.read()
            seq.append(reading)
            if b'ERROR' in reading:
                verb = "Fehlgeschlagen"
                local_data="Fehler"
                break
            elif b'?' in reading:
                verb = "Unbekannt"
                local_data="Unknown command"
                break 
            elif b'>' in reading:
                local_data = b' '.join(seq).decode("utf-8")
                local_data = local_data.replace(' ', '').replace('-', '').replace('\r', '')
                verb = "Abgearbeitet"
                break
        self.SetToast("Befehl {} ist {}".format(command, verb))
        return local_data
Vielen dank für's aufräumen. Werde das im ganzen Projekt noch nachziehen.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@olsvse: In dem gesamten Quelltext habe ich nicht einen `bool()`-Aufruf gefunden der nicht eine literale 1 oder 0 als Argument hat. Die kann man also alle durch `True` und `False` ersetzen ohne das sich irgendetwas ändert, denn diese Aufrufe ergeben ja `True` und `False`:

Code: Alles auswählen

In [1]: bool(1), bool(0)
Out[1]: (True, False)
Es gibt also keinen Grund das da irgendwelche Fehlermeldungen kommen. Du hättest besser die tatsächliche Ursache für eventuelle Fehler identifizieren sollen.

Ich würde das übrigens auch nicht als „cast“ bezeichnen, denn das ist im Grunde ein ganz normaler Funktionsaufruf.

Und wie gesagt, auch bei `True`/`False` sind die Vergleiche mit ``==`` in Bedingungen in aller Regel überflüssig und sollten weggelassen werden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

Hallo zusammen,
__blackjack__ hat geschrieben: Donnerstag 13. Dezember 2018, 16:55 @olsvse: In dem gesamten Quelltext habe ich nicht einen `bool()`-Aufruf gefunden der nicht eine literale 1 oder 0 als Argument hat. Die kann man also alle durch `True` und `False` ersetzen ohne das sich irgendetwas ändert, denn diese Aufrufe ergeben ja `True` und `False`:

Code: Alles auswählen

In [1]: bool(1), bool(0)
Out[1]: (True, False)
Es gibt also keinen Grund das da irgendwelche Fehlermeldungen kommen. Du hättest besser die tatsächliche Ursache für eventuelle Fehler identifizieren sollen.

Ich würde das übrigens auch nicht als „cast“ bezeichnen, denn das ist im Grunde ein ganz normaler Funktionsaufruf.

Und wie gesagt, auch bei `True`/`False` sind die Vergleiche mit ``==`` in Bedingungen in aller Regel überflüssig und sollten weggelassen werden.
Danke.

Hab mir gerade die Stelle angeschaut, wo ich den Literal vermutet hatte.
Da habe ich wirklich ganz profan das mit dem "(bool)" gemacht.

Das habe ich wirklich viel Verbesserungs-Potential, das es zu heben gilt. :)

Wie ist der Folgende Code?
(Habe mal auf der Bereinigung von @Sirius3 aufgesetzt.)

Code: Alles auswählen

    def get_bms_data(self, command, AusgabeText, OBD_Dongel):
        self.SetToast(AusgabeText)
        OBD_Dongel.write(b'{}\r\n'.format(b"2105" if command=="2105" else b"2101"))
        OBD_Dongel.flush()
        seq = []
        counter=0
        while True:
            try:
                reading = OBD_Dongel.read()
            except serial.serialutil.SerialException:
                counter=counter+1
                time.sleep(1)
            else:
                seq.append(reading)
                if b'ERROR' in reading:
                    verb = "Fehlgeschlagen"
                    local_data="Fehler"
                    break
                elif b'?' in reading:
                    verb = "Unbekannt"
                    local_data="Unknown command"
                    break 
                elif b'>' in reading:
                    local_data = b' '.join(seq).decode("utf-8")
                    local_data = local_data.replace(' ', '').replace('-', '').replace('\r', '')
                    verb = "Abgearbeitet"
                    break
            if counter>=5:
                verb="Lesen nicht möglich"
                local_data="Lese-Fehler"
                break
        self.SetToast("Befehl {} ist {}".format(command, verb))
        return local_data
Bei der Rückgabe von "Lese-Fehler" würde ich so als erste Idee die aktuellen Daten sichern und dann neu mit dem init Starten.
Dabei den Thread laufen lassen, da ich meinen GPIO-Thrad ja aus diesem Thread heraus gestartet habe.

Lässt sich die exception irgend wie auf die genaue Fehlermeldung einschränken?
oder ist bei der allgemeinen Fehler-Abhandlung mit keinen Problemen zu rechnen?

Gruß,
Oliver
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

Hallo zusammen,

ich bin gerade bei der Bereinigung und Kommentierung des Scripts.

Sirius3 hat geschrieben: Sonntag 9. Dezember 2018, 20:56

Code: Alles auswählen

        OBD_Dongel.write(b'{}\r\n'.format(b"2105" if command=="2105" else b"2101"))
        OBD_Dongel.flush()
@Sirius3:
Du hast ja meinen Verhau schön aufgeräumt.

Bei einer anderen Funktion, die ähnlich aufgebaut ist will ich das gearde auch machen.
Allerdings Verarbeitet diese nicht nur zwei Befehle, sondern gleich 12 verschiedene Befehle.

Code: Alles auswählen

    def init_Dongel(self,command,AusgabeText,OBD_Dongel):
        self.SetToast(AusgabeText)
        local_data="Unknown command"        
        ByteBefehl=bytes(command, 'utf-8')
        OBD_Dongel.write(ByteBefehl)
        OBD_Dongel.flush()
        seq = []
        joineddata=''
        while True:
              .
              Hier steht noch der gleiche Quark wie beim meinem alten get_bms_data
              .
        return local_data
ich könnte mir vorstellen, dass ich den format-Befehl mittels elif noch mehr verlängere.
Aber das sieht mir nach einem Monster aus:

Code: Alles auswählen

OBD_Dongel.write(b'{}\r\n'.format(b"ATZ" if command=="ATZ" elif  command=="ATLP"  b"ATLP" elif command=="ATL0"  b"ATLP0" elif ......... else b"ATE0" ))
Gibt es da eine elegantere Lösung oder ist ausnahmsweise mein original Konstrukt das richtige?

Vielen Dank im voraus.

Gruß,
Oliver
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ist das immer nur die Wandlung von “XXX” nach b”XXX”? Dann kannst du dir das gesamte geraffel sparen, und einfach command.encode(“ascii”) machen - da sollte dann der gleiche Inhalt in Form einen Bytestrings bei rumkommen.
olsvse
User
Beiträge: 8
Registriert: Dienstag 13. November 2018, 19:39

__deets__ hat geschrieben: Samstag 15. Dezember 2018, 16:49 Ist das immer nur die Wandlung von “XXX” nach b”XXX”? Dann kannst du dir das gesamte geraffel sparen, und einfach command.encode(“ascii”) machen - da sollte dann der gleiche Inhalt in Form einen Bytestrings bei rumkommen.
Vielen Dank, das hört sich gut an.
Es ist immer die Umwandlung von "XXX\r\n" nach b"XXX\r\n".

Gruß,
Oliver
Antworten