Anfänger - Problem mit Kodierung: UnicodeDecodeError

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.
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hallo,

ich beschäftige mich seit quasi gestern Abend mit Python [...].
Ich arbeite seit einigen Wochen an batteriebetriebenen Funk-Temperatursensoren auf ATTiny84-Basis, die ihre Daten an einen zentralen Arduino per 433 Mhz funken.
Bisher war das ein kleiner Arduino Ethernet, der die Daten direkt an ein PHP-Webscript übergeben hat - da ich aber dadurch "Kabelgebunden" bin (da gibts kein WLan...), wollte ich mich mal versuchen einen einfachen Arduino an einen Raspberry Pi zu klemmen: Der läuft mit einem WLan-Stick und frisst die Arduino-Daten per COM.

Okay, soviel dazu: Mein Arduino spuckt die Daten am Raspberry Pi aus. Ich habe es auch geschafft, per python3-serial die Daten auszulesen, und per urllib.request an mein Webscript zu übergeben (GET).

Nachfolgend mal der Code:

Code: Alles auswählen

# Benötigt: python3-serial

import serial
import urllib.request
import sys
import datetime

#sys.stdout = open('/dev/null', 'w')
#sys.stdout = open('/var/log/serialread.log', 'w')

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

while 1:
        time = datetime.datetime.now()
        rs232 = ser.readline().decode("ascii")
        print("Uhrzeit: ", time, end="\n")
        print("Empfangene Daten: ", rs232, end="\n")
        url = 'http://www.xxx.de/attiny/parse.php?r=1&key=xxxxx&' + rs232 #.decode("ascii")
        urlrequest = urllib.request.urlopen(url).read()
        urlrequest = urlrequest.decode("ascii")
        print("URL: ", url, end="\n")
        print("Ergebnis: ", urlrequest, end="\n")
        print("--------", end="\n")
Okay, mein Problem ist nun folgendes: Anscheinend gibt der Arduino den Datenstring (gleich vorformatiert, kommt so an: s=3&t=1990&h=6810&v=4615) Zeichen für Zeichen aus und hängt ein \n o.ä. an.

Manchmal macht das - aus welchem Grund auch immer - Probleme, den manchmal kommt beim Raspberry nicht der komplette Datenstring an, sondern nur ein Teil - oder ein kauderwelsch aus zwei Datenstrings, Beispiel: s=1s=3&t=1990&h=6840&v=4615.
Der Part "s=1" gehört zum vorherigen Datenpaket - allerdings fängt das damit an; wo der Rest dessen geblieben ist, weiß ich nicht.

Kurzum: Kommt nicht alles an, spuckt er mir folgende Meldung aus:

Code: Alles auswählen

pi@raspi01 ~/serialread $ python3 serialread.py
Uhrzeit:  2013-12-28 20:51:16.024846
Empfangene Daten:  ss=3&t=1979&h=6830&v=4635

Traceback (most recent call last):
  File "serialread.py", line 21, in <module>
    urlrequest = urlrequest.decode("ascii")
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 226: ordinal not in range(128)
Starte ich das Script und es wirft nicht gleich diesen Fehler, läuft es Stundenlang ohne Fehler durch. Vermutlich passiert der Fehler, wenn ich das Script starte während der Arduino gerade Daten an den Raspberry übergibt. Dann kommt nur die Hälfte an...

Was ich aber nicht verstehe: Die Daten die von der seriellen Schnittstelle gelesen werden MÜSSEN doch ASCII sein; schließlich klappts ja - wenn der Fehler nicht gleich auftaucht - ohne Probleme.

Wie kann ich soetwas abfangen oder verhindern? Später soll das Script als Daemon oder per Cronjob täglich gestartet werden - wenn's einmal aussteigt, kriege ich das vermutlich nicht mit, und mir fehlen dann (im schlimmsten Fall) einen ganzen Tag Messwerte.


*Update*
Ich habe mal von ASCII auf UTF-8 umgeschwenkt; wenn ich mich richtig erinnere, sind alle Standard-Strings in Python 3 UTF-8 kodiert - kann also nicht schaden.
Anscheinend passiert obiger Fehler auch vereinzelt bei der Antwort der WebRequest:

Code: Alles auswählen

Traceback (most recent call last):
  File "serialread.py", line 23, in <module>
    urlrequest = urllib.request.urlopen(url).read().decode("utf-8") # Web-Parse-Request absetzen und Statusausgabe in UTF-8 konvertieren
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe4 in position 226: invalid continuation byte
Ich glaube, ich mache da einen grundlegenden Fehler :/
BlackJack

@neovanmatix: In beiden gezeigten Beispielen kommt der Fehler von der Webanfrage und nicht vom Arduino.

Wenn vom Arduino verstümmelte oder unvollständige Daten kommen kann das auch an einer schlechten seriellen Verbindung liegen. Du müsstest die Daten halt prüfen ob die in sich stimmig sind und so als Webanfrage weitergereicht werden dürfen.

Gleiches gilt für die Antwort von der Webanfrage wenn da auch Unsinn zurück kommen kann.

Das einfachste wäre erst einmal Ausnahmebehandlung an beiden Stellen zu machen und zu protokollieren was da jeweils an falschen Daten kam. An der Stelle mit der `ascii()`-Funktion ausgeben lassen, damit man auch tatsächlich jeden Bytewert identifizieren kann.
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hi,

ich habe noch ein wenig experimentiert.
Zum einen eine sehr einfache Prüfung der erhaltenen Daten eingepflegt - die prüft zwar nur die ersten 2 Zeichen und eines in der Mitte; zu 95% filtert mir das aber schonmal fehlerhafte Datensätze weg.

Weiterhin habe ich eine Exception eingebaut, damit zumindest das Script nicht abbricht und weiterläuft.

Du hast vermutlich recht; meine bisherigen Fehlermeldungen haben mit der Antwort des Webservers zu tun, genauer mit der Dekoderiung dessen.

Hier mal ein Output:

Code: Alles auswählen

pi@raspi01 ~/serialread $ python3 serialread-test.py
UnicodeDecodeError
rs232: s=s=3&t=1990&h=7150&v=4635

urlrequest: b'<br />\n<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/www/htdocs/w01081de/web/xxx/attiny/parse.php</b> on line <b>26</b><br />\nKeine SensorID gesetzt oder Sender nicht dem Empf\xe4nger zugeordnet'
Ich vermute mal er bemängelt das \xe4 im Wort Empfänger.

Ich bin, wie gesagt, keine 5 Stunden mit Python vertraut, vielleicht könnt ihr mir unter die Arme greifen.
Ich vermute mein Fehler ist, dass ich nicht weiß, mit welchem Format die Webserver-Antwort kodiert ist. Wie kann ich das prüfen, wenn ich z.B. nicht weiß, worauf \xe4 schließen lässt?

Mal das aktuelle Script:

Code: Alles auswählen

# Benoetigt: python3-serial

import serial
import urllib.request
#import sys
import datetime

ser = serial.Serial('/dev/ttyACM0', 9600) # Serielle Schnittstelle öffnen

while 1:
        time = datetime.datetime.now()
        rs232 = ser.readline().decode("utf-8") # Daten von COM abgreifen und in UTF8 formatieren

        if (rs232[0:2] == 's=') & (rs232[-8] == 'v'):
            log = open("serial.log", "a")
            logerror = open("serialerror.log", "a")

            print("Uhrzeit:          ", time, file=log)
            print("Empfangene Daten: ", rs232, end="", file=log)

            try:
                url = 'http://www.xxx/attiny/parse.php?r=1&key=xxxxxx&' + rs232 # Web-Parse-URL generieren
                urlrequesttemp = urllib.request.urlopen(url).read()
                urlrequest = urlrequesttemp.decode("utf-8") # Web-Parse-Request absetzen und Statusausgabe in UTF-8 konvertieren
#               print("URL: ", url, file=log)
                print("Ergebnis:         ", urlrequest, file=log)
                print("-", file=log)
            except UnicodeDecodeError:
                print("UnicodeDecodeError")
                print("rs232:", rs232)
                print("urlrequest:", urlrequesttemp)

            log.close()
        else:
             print("Daten nicht OK:", rs232)
             print("0:2", rs232[0:2])
             print("-6", rs232[-6])
             print("0:", rs232[0])
             print("len:", len(rs232))
             print("-1", rs232[-1])
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

In welchem Encoding liefert denn 'http://www.xxx/attiny/parse.php?r=1&key=xxxxxx' die Antwort zurück? Offenbar *nicht* als UTF-8...
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hyperion hat geschrieben:In welchem Encoding liefert denn 'http://www.xxx/attiny/parse.php?r=1&key=xxxxxx' die Antwort zurück? Offenbar *nicht* als UTF-8...
Hi,

ich habe es mal wie folgt versucht:

Code: Alles auswählen

pi@raspi01 ~/serialread $ python3
Python 3.2.3 (default, Mar  1 2013, 11:53:50)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> url = b'<br />\n<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/www/htdocs/w01081de/web/xxx/attiny/parse.php</b> on line <b>26</b><br />\nKeine SensorID gesetzt oder Sender nicht dem Empf\xe4nger zugeordnet'
>>> url.decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe4 in position 226: invalid continuation byte
>>> url.decode("ascii")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 226: ordinal not in range(128)
>>> url.decode("latin-1")
'<br />\n<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/www/htdocs/w01081de/web/xxx/attiny/parse.php</b> on line <b>26</b><br />\nKeine SensorID gesetzt oder Sender nicht dem Empfänger zugeordnet'
Es scheint also, als sei der Web-Parse mit latin-1 kodiert :)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

neovanmatix hat geschrieben:Es scheint also, als sei der Web-Parse mit latin-1 kodiert :)
Du hättest ja auch einfach in der HTML-Ausgabe der Seite gucken können... eigentlich sollte da eine Angabe des Encodingas im Header zu finden sein. Z.B. hier im Forum zu finden in Zeile 7:

Code: Alles auswählen

<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Jaein.
Das Webscript zum Parsen der Daten ist sehr einfach gestrickt; da gibt es keinen Header im Quelltext:

Code: Alles auswählen

<br />
<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/www/htdocs/w01081de/web/xxx/attiny/parse.php</b> on line <b>26</b><br />
Keine SensorID gesetzt oder Sender nicht dem Empfänger zugeordnet
Aber wieder was gelernt ;)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

neovanmatix hat geschrieben: Das Webscript zum Parsen der Daten ist sehr einfach gestrickt; da gibt es keinen Header im Quelltext:
Hm... dann ist das HTML eigentlich kaputt - denn wenn Du kein ASCII überträgst, kann die Empfängerseite das nicht sinnvoll interpretieren. Alternativ kann natürlich auch der HTTP-Header die Kodierung enthalten; insbesondere, wenn kein HTML übertragen werden soll, sondern z.B. JSON. (Oder hat JSON eine Standard-Kodierung?)

Ohne das Wissen über eine Zeichenkodierung kommt es schlicht zu solchen Problemen, wie Du sie gerade hattest :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hyperion hat geschrieben:
neovanmatix hat geschrieben: Das Webscript zum Parsen der Daten ist sehr einfach gestrickt; da gibt es keinen Header im Quelltext:
Hm... dann ist das HTML eigentlich kaputt - denn wenn Du kein ASCII überträgst, kann die Empfängerseite das nicht sinnvoll interpretieren. Alternativ kann natürlich auch der HTTP-Header die Kodierung enthalten; insbesondere, wenn kein HTML übertragen werden soll, sondern z.B. JSON. (Oder hat JSON eine Standard-Kodierung?)

Ohne das Wissen über eine Zeichenkodierung kommt es schlicht zu solchen Problemen, wie Du sie gerade hattest :-)
Naja, ich gebe dir ja grundsätzlich recht. Die Antwort des PHP-Scripts ist standardmäßig nur ein "OK" mit den Werten hinten dran, die per GET übermittelt wurden - quasi als Antwort zum Debuggen, das alles OK war.
Einen wirkliche Nutzen sollte das nicht haben und ist mehr eine Programmierhilfe - zumindest beim Arduino gab es beim Parsen dieses Outputs keine Probleme; nur Python hat eben rumgezickt :)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

neovanmatix hat geschrieben: Einen wirkliche Nutzen sollte das nicht haben und ist mehr eine Programmierhilfe - zumindest beim Arduino gab es beim Parsen dieses Outputs keine Probleme; nur Python hat eben rumgezickt :)
Python hat deswegen rumgezickt, weil Du die Bytes nehmen und in eine interne Struktur (Unicode) umwandeln wolltest (sehr sinnvoll!). Wenn man dafür nun eine falsche Kodierung angibt, *kann* Python ja nur meckern, sofern eine Byte-Folge ungültig ist bezüglich der angenommenen Codierung. Was sollte es sonst tun? Hättest Du die Bytes unverändert weiterverarbeitet (was man natürlich nicht sollte!), gäbe es auch keine Probleme bezüglich der Interpretation der Zeichen ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Naja, sei's drum. Bin froh das das jetzt soweit klappt - lasse das Script beim Booten autom. mitstarten und allen Output in ein Log schreiben.
Hier mal der aktuelle Stand:

Code: Alles auswählen

# Benoetigt: python3-serial
# Im cron eintragen, damit es bei jedem Boot/Reboot gestartet wird:
# @reboot python3 /home/pi/serialread/serialread.py &

import serial
import urllib.request
import datetime

url = 'http://www.xxx/attiny/parse.php?r=1&key=xxx&'    # URL zum Webscript inkl. Secret und Receiver-ID

ser = serial.Serial('/dev/ttyACM0', 9600) # Serielle Schnittstelle öffnen

while 1:
        time = datetime.datetime.now()                  # Zeit für Log in eine Variable schreiben
        rs232raw = ser.readline().decode("utf-8")       # Daten von COM abgreifen und in UTF8 formatieren
        rs232 = rs232raw[0:len(rs232raw)-2]             # Die letzten beiden Zeichen abschneiden; es handelt sich um Leerzeichen

        log = open("serial.log", "a")                   # Log-Datei zum Anhängen öffnen

        if (rs232[2].isdigit()) & (rs232[4] == 't'):    # Wenn die 3. Stelle eine Zahl ist, und die 6. letzte ein "v"...

            print("-- Daten:         ", rs232, file=log)
            print("Uhrzeit:          ", time, file=log)
#            print("Empfangene Daten: ", rs232, end="", file=log)

            parseurl = url + rs232 # Web-Parse-URL generieren

            try:
                urlrequesttemp = urllib.request.urlopen(parseurl).read()        # Web-Parse-Request absetzen und Statusausgabe in UTF-8 konvertieren
                urlrequest = urlrequesttemp.decode("latin-1")                   # Parse-Antwort mit latin-1 dekodieren
                print("Ergebnis:         ", urlrequest.replace("<br>", " "), file=log) # Parse-Antwort ins Log schreiben, <br> durch Leerzeichen ersetzen
            except urllib.error.URLError:
                print("!! Exception:      ", end="", file=log)
                print("urllib.error.URLError: Host nicht gefunden, DNS-Fehler oder URL falsch?", file=log)

            print("-- / Daten\n", file=log)

        else:
             print("\n-- Daten nicht OK:", rs232, file=log)
             print("Pos 2 keine Zahl:", rs232[2], end="", file=log)
             print(" Pos 4 kein t:", rs232[-8], end="", file=log)
             print(" Länge:", len(rs232), file=log)
             print("-- / Daten\n", file=log)

        log.close()     # Log-File schließen
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hui... da sind ja eine Menge Sachen drin, die ziemlich unpythonisch und allgemein unschön sind! Hier mal einige Dinge, die mir sofort aufgefallen sind:

- sämtlicher Code ist auf Modulebene und nicht in Funktionen unterteilt. Das macht es auf Dauer unwartbar, weil einfach viel zu viel in einer *Einheit* passiert.

- Es gibt einen Boolschen Typen und auch die beiden Lietrale ``True`` und ``False`` in Python. Anstelle von ``while 1:`` solltest Du besser ``while True:`` schreiben. Das ist viel lesbarer.

- Kommentare sind nur dann sinnvoll, wenn sie Informationen geben, die man *nicht* sofort aus dem Code ableiten kann. Du hast viel zu viele "trivial"-Kommentare in Deinem Code. Viele (z.B. Unlce Bob) betrachten Kommentare an sich als Code Smell, weil sie bedeuten, dass man seinen Code unverständlich geschrieben hat. Zudem muss man bei einem Update des Codes darauf achten, die Kommentare ebenfalls anzupassen; das ist Dir z.B. bei der Umstellung des Encodings zum Decodieren der Webserver-Response entgangen. Nun hast Du einen lügenden Kommentar, dass etwas in "UTF-8 umgewandelt wird"... das verwirrt viel mehr, als es nützt.
Zudem sollte man keine Inline-Kommentare in Python benutzen.

- Konstanten sollte man GROß schreiben; also ``URL = '...'``.

- Du überschreibst durch den Namen ``time`` ein Objekt der Standard-Lib; dies sollte man wenn es geht vermeiden. Da ``time`` eh sehr allgemein ist, wäre ``current_time`` imho viel aussagekräftiger an der Stelle. Schließlich willst Du Dir ja die aktuelle Zeit merken - nicht irgend eine.

- Dateien solltest Du immer mit folgendem Pattern öffnen:

Code: Alles auswählen

with open(...) as f:
    # ``f`` ist in diesem Block als File-Objekt verfügbar
Damit ist sicher gestellt, dass die Datei auch sauber geschlossen wird - auch bei Ausnahmen!

- Strings setzt man in Python mittels der ``"".format()``-Methode auf Strings oder dem ``%``-Operator zusammen. Benutze kein ``+`` dafür.

- Python bietet in der Standard-Lib ein Modul für das Loggen an; imho ist das schöner zu benutzen als Dein Verbiegen der ``print``-Nachrichten. Schau Dir das doch mal an!

- Einige Namen sind nicht so aussagekräftig. ``rs232`` bedeutet was genau? Ich würde es für ein Objekt halten, welches eine Schnittstelle zu einem Bus anbietet. Hier ist es aber eine *Nachricht*, die "zufälliger weise" aus der Schnittstelle kommt. Woher das stammt, spielt aber ja für den weiteren Ablauf bzw. für die Verwendung keine Rolle. Ich würde daher etwas mit "message" kombinieren, damit klar wird, dass es sich um eine spezielle Nachricht handelt.
Bei ``urlrequest`` verhält es sich analog... dahinter verbirgt sich eigentlich der *Inhalt der Antwort*. Der Name verrät das leider nicht.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

(Antwort lag eine Weile rum weil ich etwas anderes gemacht habe, darum sind einige Sachen schon mal von lunar angemerkt worden. :-))

@neovanmatix: Wenn weder im HTML selbst noch im Header eine Kodierung angegeben wird, dann kann man bei HTML von Latin-1 ausgehen.

Einen Header *muss* es übrigens geben, denn sonst bekommst Du vom Webserver eine Fehlermeldung. Der ist aber nicht Teil der Daten die man mit `read()` bekommt. An der Stelle möchte ich mal das `requests`-Modul empfehlen was diesen ganzen `urllib`-Murks hinter einer wesentlich schöneren API versteckt. Und zum Beispiel auch das dekodieren schon übernimmt und dabei selber schaut ob in den Headern oder dem HTML Angaben darüber stehen.

Noch ein paar Anmerkungen:

Du verwendest an zwei Stellen Zahlen als Wahrheitswerte wo man das in Python nicht machen würde. Einmal in der Endlosschleife wo man statt der 1 besser `True` schreiben würde und dann beim ``if`` die bitweise Und-Verknüpfung wo eigentlich ein ``and`` hingehört. Die Klammern um die beiden Teilausdrücke der Bedingung würde ich auch weg lassen.

Bei der Bedingung selbst ist der zweite Teil vielleicht zu zerbrechlich, denn der Test geht ja davon aus, dass sich die Länge der letzten Zahl nicht ändern kann.

Robuster dürfte es sein die Daten tatsächlich zu parsen und zu prüfen ob das was drin steht so passt. Also ob die Anzahl und die Namen der Parameter passen und eventuell auch ob die Werte jeweils als ganze Zahlen interpretiert werden können.

Die Endlosschleife mit ``while`` kann man auch durch eine mit ``for`` ersetzen, denn letztendlich möchte man ja über die Zeilen iterieren die von der seriellen Schnittstelle rein kommen. Und `Serial`-Objekte sind dateiähnliche, iterierbare Objekte.

`rs232` ist ein unpassender Name. Das ist der Name unter dem bestimmte Standards zur seriellen Übertragung von Daten zusammen gefasst sind. An den Namen gebunden ist aber eine Zeile mit Daten. Das die mal über eine serielle Schnittstelle kam, die vielleicht etwas mit RS-232 zu tun hatte, hat mit diesen Werten überhaupt nichts zu tun. Die hätten genau so gut über eine TCP-Verbindung oder Bluetooth gelesen werdne können, ohne dass sich an ihrere Bedeutung für das Programm etwas ändert.

Ebenfalls unpassend ist `urlrequest` was ja gar keine URL und auch keine Anfrage enthält sondern an die *Antwort* der Webanfrage gebunden ist.

Das Dateiobjekt welches an `logerror` gebunden wird, wird geöffnet aber weder benutzt noch wieder geschlossen.

Dateien sollte man mit der ``with``-Anweisung öffnen, damit man sicher ist, dass sie auch in jedem Fall wieder geschlossen werden. Ebenso sollte man sicherstellen, dass die serielle Verbindung wieder geschlossen wird.

Bei der Protokolldatei für die Ergebniswerte sollte man vielleicht auch ein Format wählen, welches sich später einfach wieder auswerten lässt. CSV zum Beispiel. Und andere Protokollausgaben könnte man mit dem `logging`-Modul erledigen.

Das aufteilen des Schreibens von den Daten bis zum 'Empfangene Daten: ' um *danach* erst die Daten tatsächlich auszuwerten finde ich etwas unübersichtlich. Ich würde einen Datensatz erst wegschreiben wenn er komplett ist. Dann könnte man das auch das empfangen, auswerten, und wegschreiben auch viel besser trennen und zum Beispiel in jeweils eigene Funktionen auslagern.

Ich komme dann vorläufig bei so etwas hier raus (ungetestet):

Code: Alles auswählen

from contextlib import closing
from datetime import datetime as DateTime
from serial import Serial
from urllib.parse import parse_qsl
import requests

# TODO Find better names.
URL = 'http://www.xxx/attiny/parse.php'
KEY = 'xxxxxx'


def parse_line(line):
    expected_keys = 'sthv'
    result = parse_qsl(line, keep_blank_values=True, strict_parsing=True)
    if len(result) != len(expected_keys):
        raise ValueError(
            'Wrong length, expected {} got {}'.format(
                len(expected_keys), len(result)
            )
        )
    for key, value in result:
        if key not in expected_keys:
            raise ValueError('Unexpected key {!r}'.format(key))
        try:
            int(value)
        except ValueError:
            raise ValueError(
                'Value for key {!r} is not a valid number: {!r}'.format(
                    key, value
                )
            )
    return result


def main():
    with closing(Serial('/dev/ttyACM0', 9600)) as serial:
        for line in serial:
            now = DateTime.now()
            try:
                parameters = parse_line(line.decode('ascii'))
            except (UnicodeDecodeError, ValueError) as error:
                print('Daten nicht OK:', ascii(line))
                print('Fehler:', error)
            else:
                with open('serial.log', 'a') as log_file:
                    print('Uhrzeit:          ', now, file=log_file)
                    print(
                        'Empfangene Daten: ',
                        ascii(line),
                        end='',
                        file=log_file
                    )
                    try:
                        parameters = [('r', '1'), ('key', KEY)] + parameters
                        response = requests.get(URL, params=parameters)
                        print(
                            'Ergebnis:         ', response.text, file=log_file
                        )
                        print('-', file=log_file)
                    except UnicodeDecodeError as error:
                        print(error)
                        print('line:', ascii(line))
                        print('url:', response.url)
                        print('response content:', ascii(response.content))


if __name__ == '__main__':
    main()
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:(Antwort lag eine Weile rum weil ich etwas anderes gemacht habe, darum sind einige Sachen schon mal von lunar angemerkt worden. :-))
So eine Frechheit von ihm, sich mit meinem Login zu tarnen... 8)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@Hyperion: :oops: Ich bin heute nicht nur langsam sondern kann auch keine Nutzernamen lesen. Sorry. :-)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:@Hyperion: :oops: Ich bin heute nicht nur langsam sondern kann auch keine Nutzernamen lesen. Sorry. :-)
NP :-) Meinen Beitrag für einen von lunar zu halten ehrt mich ja eher :mrgreen:
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Also zum einen finde ich es interessant, wie qualitiativ hochwertig hier Feedback gegeben wird - und ich habe nicht einmal darum gebeten.
Nein ernsthaft, kein Sarkasmus: Ungefragt so ausführliche Antworten zu schreiben ist mir bisher auch nicht untergekommen, und dann noch gleich zweimal :)

Ich fühle mich im übrigen geehrt, dass man meinen nicht-pythonhaften-Programmierstil herausliest. Wie gesagt, ich wollte lediglich ein kleines Script in einer mir unbekannten Sprache erstellen, dass Daten von einer seriellen Schnittstelle liest und an ein Webscript übergibt. Das obige Ergebnis entstand nach gefühlt 3h Beschäftigung - das es da Verbesserungspotential gibt, war mir natürlich klar - verwundert war ich, dass das quasi auf Anhieb funktioniert hat :)

An meiner Arduino-Klasse, die eigentlich das gleiche getan hat, habe ich ein ganzes Wochenende verbraten...

Für die Kritik Danke ich euch - einiges davon werde ich auch sicherlich umsetzen, schon allein um ein wenig tiefer in Python einzusteigen. Die meisten Punkte sind ja auch richtig; das mit den Kommentaren ist mir ebenfalls schon bei meinen eigenen Scripten aufgefallen - man schreibt halt lieber mehr, als zu wenig...

Was ich allerdings schwerlich zugeben muss: Wenn ich meinen Code zu mit dem von BlackJack vergleiche, leuchtet mir meiner doch ein wenig mehr ein :)

Ursache für die meisten eurer Kritikpunkte wird im übrigen sein, dass ich's nicht besser wusste ;) Ich dachte mir schon, dass es quasi für vieles eine eigene Klasse oder Lib gibt; jedoch dachte ich, weshalb mit einer Lib beschäftigen, wenn ich nur drei Zeilen in eine Textdatei klopfen wollte. Mit Sicherheit nicht der beste Ansatz für größere Projekte - jedoch, wie gesagt, wird das Script nie sonderlich mehr können müssen als das, was es jetzt kann.

Auf jeden Fall schonmal vielen Dank für das Feedback; sobald ich einige davon umgesetzt habe, aktualisiere ich das Script hier :)
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hallo,

ich habe noch nicht alle Ratschläge umgesetzt (nur die, die ich auch verstanden habe :)) aber möchte euch trotzdem mein aktuelles Script anhängen.
Ich habe, glaube ich, alles umgesetzt - bis auf die Exceptions:

Code: Alles auswählen

# Benoetigt: python3-serial

# Im cron eintragen, damit es bei jedem Boot/Reboot gestartet wird:
# @reboot python3 /home/pi/serialread/serialread.py &
#
# Empfängt Daten über seriellen Port und übergibt sie an ein PHP-Webscript, dass diese in eine SQL-DB schreibt

# Die empfangenen Daten haben folgendes Format: s=3&t=2000&h=7160&v=4635

# Die komplette URL, die das PHP-WebScript aufruft, sieht wie folgt aus:
# http://www.xxx.de/parse.php?r=1&key=xxx&s=3&t=2000&h=7160&v=4635

# r = Receiver-ID (dieser Empfänger), key = Secret zum Auth am PHP-Script
# s = Sender-ID, t = Temperatur, h = Luftfeuchtigkeit, v = Spannung Stromversorgung

import serial
import urllib.request
from datetime import datetime as DateTime

RID = '1'                                               # ID des Receivers
URL = 'http://www.xxx/attiny/parse.php'
KEY = 'xxx'
LOGFILE = '/home/pi/serialread/serial.log'

serialcom = serial.Serial('/dev/ttyACM0', 9600)

serialdata_previous = ""

def format_received_data(rawdata):
    sdatatemp = rawdata.decode("utf-8")
    sdata = sdatatemp[0:len(sdatatemp)-2]
    return sdata

def check_received_data(received_data):
    if received_data[2].isdigit() and received_data[4] == 't':
        return True
    else:
        writelog("Daten haben falsches/fehlerhaftes Format: {0!r}".format(received_data))
        return False

def transfer_received_data(received_data, previous_received_data, RID, URL, KEY):
    if received_data == previous_received_data:
        return False

    urltoopen = "{0}?r={1}&key={2}{3}".format(URL, RID, KEY, received_data)

    try:
        transmit_result = urllib.request.urlopen(urltoopen).read().decode("latin-1").replace("<br>", " ")
        writelog("Daten wurden an WebScript übergeben: {0!r}, Status: {1!r}".format(received_data, transmit_result))
        return True
    except urllib.error.URLError:
        writelog("urllib.error.URLError: Host nicht gefunden, DNS-Fehler oder URL falsch?")
        return False

def writelog(text, logfilepath=LOGFILE, logfilemodi="a"):
    with open(logfilepath, logfilemodi) as log:
        current_time = DateTime.now().strftime("%d.%m.%Y %H:%M:%S")
        log.write("{0} {1}".format(current_time, text))


for line in serialcom:
        serialdata = format_received_data(line)

        if check_received_data(serialdata):
            transfer_received_data(serialdata, serialdata_previous, RID, URL, KEY)
            serialdata_previous = serialdata
Zuletzt geändert von neovanmatix am Donnerstag 2. Januar 2014, 19:34, insgesamt 1-mal geändert.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Mal abgesehen vom logging-Modul fällt aber sofort auf, dass Du Boolesche-Typen immer noch nicht verwendest! Nimm doch ``return True``, wenn etwas wahres zurückgegeben werden soll und eben ``False`` für etwas falsches. Das ist doch viel aussagekräftiger als ``1`` und ``None`` (Was so gar nicht zueinander passt - dann schon eher ``1`` und ``0`` ;-) )

Wieso öffnest Du ein Dateiobjekt, nutzt aber gar nicht die Methoden darauf, sondern gehst den Umweg über ``print``?

Den Rest habe ich mir (noch) nicht en detail angeguckt.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
neovanmatix
User
Beiträge: 19
Registriert: Samstag 28. Dezember 2013, 20:52

Hi :)
Diesmal bin ich besser vorbereitet!
Mal abgesehen vom logging-Modul fällt aber sofort auf, dass Du Boolesche-Typen immer noch nicht verwendest! Nimm doch ``return True``, wenn etwas wahres zurückgegeben werden soll und eben ``False`` für etwas falsches. Das ist doch viel aussagekräftiger als ``1`` und ``None`` (Was so gar nicht zueinander passt - dann schon eher ``1`` und ``0`` ;-) )
*hust*, komische Sache... Ich habe das tatsächlich umgesetzt, bevor ich den Code in Funktionen ausgelagert habe. Dabei habe ich 1-2 Fehler erhalten, und wieder zurückgerudert.
Ich habe es gerade noch einmal versucht - mit True und False - und es klappt. Ich vermute jetzt mal, "true" und "TRUE" sind nicht gleich "True"?
Wieso öffnest Du ein Dateiobjekt, nutzt aber gar nicht die Methoden darauf, sondern gehst den Umweg über ``print``?
Weil ich bis gerade garnicht wusste, dass es Methoden für das File-Objekt gibt. Aber dank deines Tipps kurz gegoogelt und implementiert.

Den obigen Code habe ich mal aktualisiert :)
Antworten