Über USB-seriell empfangene Daten in MySQL schreiben

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
saschaw
User
Beiträge: 2
Registriert: Samstag 4. Januar 2020, 09:44

Hallo zusammen,

ich habe einen Arduino welcher über USB mit meinem Raspberry verbunden. Der Arduino sendet mir in zeitlich unregelmäßigen Abständen eine "O" oder eine "1" über die serielle Schnittstelle an den RaspPi. Diese Werte sollen dann in eine MySQL-DB geschrieben werden.
Ich bin absoluter Newbie in Sachen Python, das ist mein erstes (und vermutlich auch letztes Projekt mit Python) und habe mir ein Script zusammen gebaut, mit dem das soweit auch funktioniert.
Rufe ich das Script im Terminal auf, empfange ich die Daten vom Arduino und schreibe sie in die Datenbank.
Da ich aber nicht weiß wann die Daten kommen, muss mein Script ja ständig auf der Schnittstelle lauschen, was ja scheinbar auch funktioniert.
Über einen Cronjob starte ich beim booten vom RaspPi das Script: @reboot python /home/pi/pythonscript.py
Und hier kommt mein Problem: es werden nach dem booten ein paar Datensätze gespeichert und dann passiert nichts mehr.
Wie gesagt, ich habe keine Ahnung von Python und will auch ehrlich gesagt keine Programmiersprache erlernen, welche ich nie mehr benutze, nur um eine Funktion zu verwirklichen.

Hier mal mein zusammen gewürfeltes Script:

Code: Alles auswählen

import time
import serial
import MySQLdb as mdb

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

db_host = 'localhost'
db_user = 'user'
db_pass = 'passwort'
db_name = 'test_db'

ser.isOpen()

while(1==1):
   # Vom Arduino empfangen
  out = ''
  while ser.inWaiting() > 0:        
     out += ser.read()

     if out != '':
       #Daten in Datenbank schreiben
       try:
           con = mdb.connect(db_host, db_user, db_pass, db_name);
           cur = con.cursor()
           cur.execute("INSERT INTO test_table (datumzeit, to_arduino, from_arduino, status) VALUES (NOW(),'', %s, 1)", out)
           con.commit()
#           print "Daten empfangen"
       except mdb.Error, e:
           if con:
               con.rollback()
           #print "Error %d: %s" % (e.args[0],e.args[1])
           sys.exit(1)

     time.sleep(0.5)

Vielleicht kann sich jemand das mal ansehen und hat spontan eine Idee was falsch ist.
Irgendwo habe ich was von einem Timeout gelesen, ob das hier eine Rolle spielt?
Wäre echt froh wenn mir jemand helfen würde.

VG
Sascha
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@saschaw: Wenn Du kein Python lernen willst, nur für diese eine Sache, dann schreib das doch in einer Programmiersprache die Du schon kennst.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da ist vieles falsch. Es werden CPU Zyklen in einer nutzlosen äusseren Schleife verschwendet, potentiell mehrere Bytes eingelesen, aber dann als Ganzes weggeschrieben, viel zu oft die Datenbankverbindung geöffnet, dann aber noch nicht mal sauber geschlossen, und zu guter letzt Fehler einfach geschluckt, womit man dann einem problem natürlich noch schwerer auf die Schliche kommt. Nichts davon hat übrigens mit Python zu tun.

Schmeiß die Fehlerbehandlung weg. Öffne die DB EINMAL zu Beginn des Programmes. Lies immer genau 1 Zeichen ein. Und weil das blockiert solange keines da ist, kannst du eine while Schleife und das sleep weglassen. Stell sicher, das die Fehlerausgabe auch beim automatischen start sichtbar wird. Nutz dazu eine systemd Unit statt dem veralteten cron Ansatz.
Benutzeravatar
noisefloor
User
Beiträge: 4191
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

außerdem verwendest du Python 2, was seit dem 1.1.2020 ohne Support durch Python-Entwickler ist. Heißt: du möchtest dein Programm auf Python3 portieren, was im gegebenen Fall kein großes Problem sein sollte.

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

@saschaw: So ganz spontan würde ich ja sagen das läuft an der Stelle wo Du es sowieso beenden willst, beim ``sys.exit(1)`` in einen `NameError` weil es `sys` nicht gibt wenn man es nicht importiert.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Das `serial`-Modul hält sich da mittlerweile auch dran, so dass man nicht mehr die alten, falsch geschriebenen Namen wie `isOpen()` oder `inWaiting()` verwenden sollte. Der ``ser.isOpen()``-Aufruf macht so auch gar keinen Sinn. Erstens wird der immer `True` ergeben, zweitens wird mit diesem Ergebnis ja auch überhaupt gar nichts gemacht.

Man sollte bei Dateien, also auch bei der seriellen Verbindung immer dafür sorgen das die auch wieder geschlossen werden, egal was passiert.

`ser` ist kein besonders guter Name. `serial_connection` beschreibt den Wert deutlich besser.

``1 == 1`` ergibt immer `True`, das sollte man dann auch direkt hinschreiben. Und ohne Klammern und nicht so als wäre ``while`` eine Funktion.

Eingerückt wird mit vier Leerzeichen pro Ebene.

`out` kann niemals eine leere Zeichen- oder Bytekette sein solange man keinen Timeout setzt, der Test ist also überflüssig.

`con` und `cur` sind wieder schlechte Namen die besser `db_connection` und `cursor` heissen würden.

Das Semikolon ist in Python dazu da um mehr als eine Anweisung in eine Zeile schreiben zu können. Ein Semikolon am Zeilenende macht also keinen Sinn wenn danach keine Anweisung mehr kommt beziehungsweise nur noch die Leeranweisung. Mehrere Anweisungen in eine Zeile quetschen macht man in Python aber konventionell sowieso nicht.

Die Fehlerbehandlung ist fehlerhaft, denn wenn die Ausnahme schon beim Verbinden mit der Datenbank auftritt, dann ergibt entweder das ``if con:`` einen `NameError` weil es den Namen beim ersten Schleifendurchlauf noch gar nicht gibt, oder aber das im ``if``-Zweig stehende ``con.rollback()`` bezieht sich auf das Verbindungsobjekt aus dem *vorherigen* Schleifendurchlauf.

``%`` zum formatieren von Werten in Zeichenketten würde ich nicht mehr verwenden. Die `format()`-Methode auf Zeichenketten oder ab Python 3.6 auch f-Zeichenkettenliterale sind besser.

Das vorhandene mal überarbeitet:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
from contextlib import closing

import MySQLdb
import serial

DB_HOST = "localhost"
DB_USER = "user"
DB_PASS = "passwort"
DB_NAME = "test_db"


def main():
    with serial.Serial("/dev/ttyACM0", 9600) as serial_connection:
        while True:
            for value in map(int, iter(serial_connection.read, None)):
                try:
                    db_connection = MySQLdb.connect(
                        DB_HOST, DB_USER, DB_PASS, DB_NAME
                    )
                    with closing(db_connection):
                        try:
                            cursor = db_connection.cursor()
                            cursor.execute(
                                "INSERT INTO test_table"
                                " (datumzeit, to_arduino, from_arduino, status)"
                                " VALUES (NOW(), '', %s, 1)",
                                [value],
                            )
                            db_connection.commit()
                        except Exception:
                            db_connection.rollback()
                            raise
                except MySQLdb.Error as error:
                    sys.exit("Error {0[0]}: {0[1]}".format(error.args))


if __name__ == "__main__":
    main()
Für meinen Geschmack ist das deutlich zu tief verschachtelt und ich würde deshalb Ein- und Ausgabe, also lesen von den Werten und schreiben in die Datenbank trennen und jeweils in eine Funktion auslagern.

Ich verstehe die besondere Behandlung von Datenbankfehlern nicht. Warum wird das bei Datenbankfehlern mit einer verkürzten Ausgabe der Ausnahme explizit abgebrochen, während es bei Fehlern der seriellen Verbindung implizit mit einem kompletten Traceback abgebrochen wird?
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
saschaw
User
Beiträge: 2
Registriert: Samstag 4. Januar 2020, 09:44

Hallo,

vielen Dank für eure Rückmeldungen.
Nachdem __deets__ mir schon so viele Tipps gegeben hatte, habe ich mich gestern mal hingesetzt und mein Script versucht um zu schreiben. Hier mein Ergebnis:

Code: Alles auswählen

#!/usr/bin/python

import time
import serial
import MySQLdb as mdb

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

db_host = 'localhost'
db_user = 'user'
db_pass = 'passwort' 
db_name = 'test_db'

con = mdb.connect(db_host, db_user, db_pass, db_name);

while True:
    out = ser.read()

    if out == '1':
      cur = con.cursor()
      cur.execute("INSERT INTO test_table (datumzeit, to_arduino, from_arduino, status) VALUES (NOW(),'', %s, 1)", out)
      con.commit()
 
Jetzt wird nur noch beim Empfang einer "1" diese in die DB geschrieben.
Ich denke jetzt bin ich auf dem richtigen Weg, __deets__ ?

@ __blackjack__:
du hast mir ja mal einen schnellen Überblick über Python gegeben :) Vielen Dank dafür.
Ich werde versuchen deine Hinweise zu verstehen und einzuarbeiten.

Sobald ich "fertig" bin werde ich es euch zeigen und wäre froh, wenn ihr es dann auch kommentiert.

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

Fast richtig. Ein Ding fehlt: das read kann beliebig viele Zeichen lesen. Weil du aber davon ausgehst immer nur eines zu bekommen, muss es read(1) sein.

Und wie ist jetzt das Verhalten?
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: `Serial.read()` liest ohne Argumente genau ein Byte. Ist etwas unerwartet. Ums vorsichtig auszudrücken.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hah. Wo du Recht hast... https://pyserial.readthedocs.io/en/late ... erial.read

Dann bleibt die Frage, ob Fehler einsehbar sind (testen durch kuenstliches schmeissen eine Ausnahme, zB die DB mal runterfahren, oder test-code dafuer einbauen). Und dann schauen, ob und wo das im log landet.
Antworten