Anfängerfrage

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
felline
User
Beiträge: 4
Registriert: Samstag 12. Oktober 2019, 16:50

Hallo zusammen,

ich habe bisher nicht in Python programmiert dafür jede Mende in Visual Basic.

War natürlich damals alles unter Windows.

Ich habe nun ein wohl ziemlich dummes Problem:

ich habe aus einem Buch ein RFID-Türschloss-Projekt nachgebaut. Im Code (konnte ich runterladen) sollten keine Fehler sein aber dennoch habe ich en interessante Phänomen:

Starte ich das Python Script über mu, so funktioniert alles bestens. Sobald ich es in einem Shell-Fenster über das Kommando

python main.py starte, kommen nur Fehlermeldungen und das Script bricht ab.

Muss ich für den Start des Scripts direlt aus einer Shell heraus andere Bibliotheken includen als für mu notwendig? Ich war der Annahne dass die Bibliothek sys eigentlich ausreichen müsste, um die Print-Befehle etc. richtig auszugeben.
Ich steige da nicht ganz durch.

Mier mal die Listings (main.py und funkctions.py), vielleicht hat jemand einen Tipp:

#!/usr/bin/python3
# Datei main.py

import RPi.GPIO as GPIO # Laden der GPIO-Funktionen
from functions import * # import aller Funktionen
import time.sys # Laden der Zeitstempel-Funktionen

GPIO.setmode(GPIO.BOARD) # GPIO Parameter setzen
GPIO.setup(13,GPIO.OUT) # GPIO-Pin13 als Ausgang
db = DB() # die DB mit den RFIDs
time_open = 10 # Dauer der Tueroeffnung, hier 10 Sekunden

try:
while True:
print ("Warte auf Transponder...\n") # Aufforderung, einen RFID-Tag anzumelden
id = read_rfid() # Lesen was der RFID-Reader bekommen hat
check = db.mysql_read(id) # Test, ob die eingelesene ID in der RFID-Datenbank drin ist
if check[1] != 0: # Falls ID gefunden wurde, wird ein Array mit allen Daten der ID kommen
print ("Hallo", check[1],"\n") # Begruessung des gefundenen Users
print ("Tuer ist ",time_open," Sekunden offen") # Meldung, dass die Tuer 10 Sekunden geoeffnet wird
db.update_timestamp(id) # Zeitstempel neu holen
GPIO.output(13, True) # GPIO-Pin13 bekommt ein true (Relais zieht an)
time.sleep(time_open) # 10 Sekunden warten
GPIO.output(13, False) # GPIO-Pin13 bekommt ein False (Relais faellt ab)
else: # unbekannten RFID-Tag erkannt
print ("Transpondernummer ", id, " unbekannt!")
print ("Kein Zutritt zum Vereinsheim!")
continue # weiter machen
except KeyboardInterrupt: # z.B. CTRL-C
db.close_db() # Datenbank schliessen
GPIO.cleanup() # GPIO deaktivieren
sys.exit() # beenden

#!/usr/bin/python3
# coding=utf8
# Datei functions.python3

import serial
import pymysql
import sys

# Auslesen der RFID-Transponder

def read_rfid():
ser = serial.Serial("/dev/serial0")
ser.baudrate = 9600
daten = ser.read(14)
ser.close()
daten = daten.replace(b'\x02', b'')
daten = daten.replace(b'\x03', b'')
return daten

class DB():
def __init__(self):
db = pymysql.connect(host="localhost", user="pi",
passwd="Klabautermann", db="rfid")
db.autocommit(True)
self.cur = db.cursor()

def close_db( self ):
self.cur.close()

def add_user(self,rfid_id, name):
add=self.cur.execute("INSERT INTO user (rfid, name) " +
"VALUES (%s,%s)", (rfid_id, name))

def mysql_read(self, rfid_id):
a=self.cur.execute("SELECT id, name, timestamp, rfid " +
"FROM user WHERE rfid = %s" ,rfid_id)
id = 0
name = 0
timestamp = 0
rfid = 0
for row in self.cur.fetchall():
id = int(row[0])
name = str(row[1])
timestamp = str(row[2])
rfid = str(row[3])
return id, name, timestamp, rfid

def update_timestamp(self, rfid_id):
a = self.cur.execute("UPDATE user SET timestamp = NOW() " +
"WHERE rfid = %s" ,rfid_id)
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Bitte die Code Tags benutzen. Und die vollständigen Fehlermeldung posten.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Du versuchst ein python3-Programm mit Python2 auszuführen und wahrscheinlich sind dafür nicht alle Bibliotheken installiert. Ohne Fehlermeldung kann man dazu aber nichts definitiv sagen.
Mich wundert auch, dass das Programm überhaupt funktioniert bei den Fehlern.
felline
User
Beiträge: 4
Registriert: Samstag 12. Oktober 2019, 16:50

das Problem habe ich selbst lösen können.

Also im Code hatte ich jede Menge Kommentare hinter den Codezeilen drin. Macht man ja schließlich so. Nun ist es wohl so, dass in mu alles nach einem # ignoriert wird, beim Aufruf in der Shell wird das aber scheinbar nicht "überlesen".

In einigen kommentaren waren Umlaute enthalten, das hat wohl die Fehler verursacht. Habe die Umlaute durch die entsprechenden Zeichenketten ersetzt und schon funktionierte alles.

Gruß

felline
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@felline: Jede Menge Kommentare hinter den Codezeilen macht man in der Regel nicht. Zum einen nicht *hinter* den Code-Zeilen, und zum anderen nicht jede Menge. Normalerweise sollte der Code selbst ausreichen um dem Leser zu vermitteln was er wissen muss. Insbesondere kommentiert man nicht *was* der Code macht, denn das steht da ja bereits als Code, sondern nur *warum* er etwas macht, und das auch nur wenn das nicht offensichtlich ist.

``#`` ist in Python das Zeichen mit dem Kommentare gekennzeichnet werden, und Python ignoriert alles danach bis zum Zeilenende. Das macht Python egal wie/wo man es startet (was auch immer ”mu” ist).

Es gibt einen besonderen Kommentar der von Python nicht komplett ignoriert wird, und das ist der Kodierungskommentar der in der ersten oder zweiten Zeile steht, und in dem man angeben kann welche Kodierung in der Datei benutzt wird: https://docs.python.org/3.6/reference/l ... clarations

Wenn Umlaute in der Quelltextdatei Probleme beim compilieren verursachen, dann passt die im Kodierungskommentar angegebene Kodierung nicht mit der tatsächlich verwendeten zusammen, oder es gibt keinen solchen Kommentar und es wurde eine andere Kodierung als UTF-8 verwendet. Wenn so etwas passiert ist es *keine* Lösung keine Umlaute in Kommentaren zu verwenden, sondern dafür zu sorgen, dass die Kodierung und der (eventuell implizite) Kodierungskommentar sich nicht widersprechen. Am besten man verwendet UTF-8 als Kodierung, selbst wenn man einen expliziten Kodierungskommentar verwendet. Der dann natürlich auch UTF-8 lauten sollte.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@felline: Noch ein paar Anmerkungen zum Quelltext: `functions` ist kein guter Modulname. *Was* für Funktionen? Wobei beide Module auch so kurz sind, das IMHO keine zwei Module nötig sind. Und *wenn* man ein Programm auf mehrere Module aufteilt, sollte man die auch in einem Package zusammenfassen, so das nur *ein* Name auf oberster Ebene in Konkurrenz zu allen anderen Modulen und Packages steht.

Die Baudrate kann man auch schon beim erstellen des `Serial()`-Objektes angeben.

`Serial`-Objekte sind Kontextmanager, lassen sich also mit ``with`` verwenden.

Die Funktion ist also letztlich ein Zweizeiler:

Code: Alles auswählen

def read_rfid():
    with serial.Serial("/dev/serial0", 9600) as connection:
        return connection.read(14).replace(b'\x02', b'').replace(b'\x03', b'')
Wobei ich mir relativ sicher bin, dass die `replace()`-Aufrufe falsch sind und eventuell sogar Daten kaputt machen.

Bei der `DB`-Klasse wird statt der Verbindung zur Datenbank ein Cursor als Zustand verwendet. Das ist falsch weil man auf diese Weise die Verbindung nicht wieder sauber schliessen kann, und weil Cursor nicht so langlebig sein sollten.

Wenn man `cursor` meint, sollte man nicht `cur` schreiben.

Bei `close_db()` ist das `_db()` überflüssig, denn der Datentyp auf dem das definiert ist, heisst ja bereits `DB`.

Wenn ein Datentyp eine `close()`-Methode hat, macht es in aller Regel Sinn auch einen Kontextmanager aus dem Datentyp zu machen.

`rfid_id` scheint mir irgendwie unnötig redundant zu sein. Das `ID` in `RFID` steht ja bereits für `ID`.

Der Rückgabewert von `Cursor.execute()` wird dauernd an Namen gebunden aber damit wird nirgends etwas gemacht. Warum?

`mysql_read()` ist als Name falsch. Da steckt völlig unnötigerweise der Name eines DBMS drin, dabei würde die Methode auch mit anderen DBMS funktionieren, denn da ist ja nicht wirklich etwas MySQL-spezifisches drin.

Dann ist `read` sehr allgemein. *Was* wird da gelesen? Die Methode sollte wohl eher `get_user()` heissen, denn sie liefert die Benutzerdaten zu einer gegebenen RFID. Kommentar und Name für den Rückgabewert beim Aufruf der Methode sind dementsprechend falsch. Da wird nichts geprüft und das Ergebnis ist keine Prüfung (`check`).

Die Abfrage ist falsch weil das zweite Argument von `execute()` nicht in einer Sequenz verpackt ist. Hier sollte eigentlich eine Ausnahme auftreten.

Wenn man nur ein Ergebnis von einer Datenbankabfrage erwartet — und die RFID-Spalte sollte ja UNIQUE sein – dann schreibt man keine sinnfreie Schleife über ”alle” Ergebnisse sondern fragt mit `fetchone()` genau den einen Datensatz ab.

Das man die Rückgabewerte aus der Datenbank in ganze Zahlen und Zeichenketten umwandeln muss ist komsisch bis falsch. Die ID sollte bereits eine Zahl sein, der Name eine Zeichenkette und so weiter. Beim Zeitstempel ist die Umwandlung dann sogar falsch, denn da sollte die Datenbank ein `datetime`-Objekt liefern und das sollte man nicht einfach so in eine Zeichenkette umwandeln. Das kann der Aufrufer machen, wenn er das will, der kann aber auch mit diesem Wert etwas machen wollen was mit einer Zeichenkette nicht geht. Und müsste die dann wieder in ein `datetime`-Objekt umwandeln.

Das falls kein Datensatz zur RFID gefunden wurde ein Tupel mit 0en zurückgegeben wird ist falsch. Da wo das aufgerufen wird, erkennt der Code daran das der Name keine Zeichenkette sondern die Zahl 0 ist, ob der Datensatz gefunden wurde oder nicht. Das ist eine ziemlich umständliche und damit schwer verständliche API. Man sollte hier einfach das Ergebnis von `fetchone()` zurückgeben. Also `None` oder eben der Datensatz. Oder eine Ausnahme auslösen falls der Datensatz nicht gefunden wurde.

Anstelle eines Tupels oder was auch immer die Datenbank als Ergebnis liefert, wäre ein eigener Datentyp für das Ergebnis auch vorstellbar. Beispielsweise ein `collections.namedtuple`. Dann hat man keine magischen Indexzugriffe im Code wo man sich merken muss, das die 1 für den Benutzernamen steht.

Zum Hauptmodul: Da wird versucht `time.sys` zu importieren was es nicht gibt. Der Punkt sollte wohl ein Komma sein. Es ist halt ein bisschen verwunderlich wie Du so etwas übersehen konntest, denn damit ist das Programm nicht ausführbar und bricht sehr früh ab. Importe von mehreren Toplevel-Modulen sollten auch nicht in *einer* Importanweisung stehen.

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

Wie schon in vorherigen Beitrag gesagt sind da viel zu viele Kommentare. Neben welchen die das offensichtlich noch mal wiederholen, sind welche dabei die man braucht weil der Code nicht klar genug ist. Wenn man beispielsweise die Bedeutung von Namen erklären muss, sollte man schauen ob man den Namen nicht selbsterklärender machen kann. Zum Beispiel müsste man beim Namen `db` nicht kommentieren dass es sich um die RFID-Datenbank handelt wenn es `rfid_db` heissen würde. Statt magischer Zahlen für Pin-Nummern, die man im Kommentar erklären muss, kann man eine aussagekräftige Konstante für die Zahl definieren. Beispielsweise `RELAIS_PIN` statt 13.

Ein ``continue`` am Ende einer Schleife ist sinnfrei. Mit diesem Schlüsselwort sollte man sowieso sparsam umgehen. Es gibt fast immer einen Weg das zu vermeiden.

`sys.exit()` sollte man nur aufrufen wenn man tatsächlich explizit einen Rückgabecode an den Aufrufer des Programms übermitteln will. Das ist hier nicht der Fall und man kann es ersatzlos streichen ohne das sich am Programmablauf etwas ändert.

Die `GPIO.cleanup()`-Funktion muss nicht nur bei einem `KeyboardInterrupt` aufgerufen werden, sondern *immer*. Das gehört also in einen ``finally``-Zweig.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
# Datei main.py
from contextlib import closing
import time

import pymysql
import RPi.GPIO as GPIO
import serial

DOOR_TIME_OPEN = 10  # in Sekunden.
RELAIS_PIN = 13


def read_rfid():
    """
    Lies RFID-Transponder.
    """
    with serial.Serial("/dev/serial0", 9600) as connection:
        return connection.read(14).replace(b'\x02', b'').replace(b'\x03', b'')


class DB:
    def __init__(self):
        self.connection = pymysql.connect(
            host="localhost", user="pi", passwd="Klabautermann", db="rfid"
        )
        self.connection.autocommit(True)

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def close(self):
        self.connection.close()

    def add_user(self, rfid, name):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute(
                "INSERT INTO user (rfid, name) VALUES (%s, %s)", (rfid, name)
            )

    def get_user(self, rfid):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute(
                "SELECT id, name, timestamp, rfid FROM user WHERE rfid = %s",
                (rfid,),
            )
            return cursor.fetchone()

    def update_timestamp(self, rfid):
        with closing(self.connection.cursor()) as cursor:
            cursor.execute(
                "UPDATE user SET timestamp = NOW() WHERE rfid = %s", rfid
            )


def main():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(RELAIS_PIN, GPIO.OUT)
    try:
        with DB() as rfid_db:
            while True:
                print("Warte auf Transponder...\n")
                rfid = read_rfid()
                user_data = rfid_db.get_user(rfid)
                if user_data:
                    print(f"Hallo {user_data[1]}\n")
                    print(f"Tür ist {DOOR_TIME_OPEN} Sekunden offen")
                    rfid_db.update_timestamp(rfid)
                    GPIO.output(RELAIS_PIN, True)
                    time.sleep(DOOR_TIME_OPEN)
                    GPIO.output(RELAIS_PIN, False)
                else:
                    print(f"Transpondernummer {rfid} unbekannt!")
                    print("Kein Zutritt zum Vereinsheim!")
    except KeyboardInterrupt:
        pass  # Just leave the ``try`` block.
    finally:
        GPIO.cleanup()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
felline
User
Beiträge: 4
Registriert: Samstag 12. Oktober 2019, 16:50

okay, das habe ich zwar nur teilweise verstanden aber ich denke mal was ich verstanden habe reicht. Dennoch einige syntaktische Kleinigkeiten, deren Bedeutung ich erfragen möchte:

if user_data:
print(f"Hallo {user_data[1]}\n")
print(f"Tür ist {DOOR_TIME_OPEN} Sekunden offen")
rfid_db.update_timestamp(rfid)

Wofzu braucht man das f in der Print-Anweisung? das kenne ich nicht. Macht das quasi automatisch ein Umwandlung von Zahlen in Text oder Uhrzeit nach Text?
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sind sogenannte Format-Strings. Das kannst du mal nachschlagen, das ist sonst etwas umfangreicher & ich tippe gerade auf dem Telefon 😬
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Antworten