RFID UART - Lesezyklus

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

Hallo in die Runde,
ich bin neu hier im Forum und auch noch relativ frisch im Thema Raspberry und Python.
Nach dem ich mich eine Weile mit Python und Rasp Grundlagen beschäftigt habe.
Arbeite ich nun an meinem ersten kleinen "Projekt". Sicherlich schon oft gehört hier im Forum
geht es um das Thema RFID lesen, Zugangskontrolle usw.

Der bisherige Code funktioniert auch eigentlich so wie er soll. Sicherlich kann man diesen noch
besser strukturieren und in mehrere Module packen, aber ich bin halt noch in den Anfängen.

Hier der Code:

Code: Alles auswählen

# !/bin/bash/python
#  -*- coding: utf8 -*-
# V0.01
 
# imports
import serial
import sys
import time
import pygame
import RPi.GPIO as GPIO

from operator import xor
from sys import exit
from time import sleep, clock

pygame.init()
 
# UART
ID = ""
Zeichen = 0
 
Checksumme = 0
Tag = 0
opentime = 3  # Zeit in der die Tuer geoeffnet bleibt
name1 = "Martin"
name2 = "Jessi"
my_sound = pygame.mixer.Sound('/home/pi/Music/keypad_fail.wav')
my_sound2 = pygame.mixer.Sound('/home/pi/Music/keypad_ok.wav')

# Flags
Startflag = "\x02"
Endflag = "\x03"
 
# UART oeffnen
UART = serial.Serial("/dev/ttyAMA0", 9600)
 
while True:
    print("")
    print("Warte auf Transponder....... ")
    
    # Variablen loeschen
    Checksumme = 0
    Checksumme_Tag = 0
    ID = ""
    print(str(ID))
 
    # Zeichen einlesen
    Zeichen = UART.read()

    # Uebertragungsstart signalisiert worden?
    if Zeichen == Startflag:
 
        # ID zusammen setzen
        for Counter in range(13):
            Zeichen = UART.read()
            ID = ID + str(Zeichen)
 
        # Endflag aus dem String loeschen
        ID = ID.replace(Endflag, "" );
 
        # Checksumme berechnen
        for I in range(0, 9, 2):
            Checksumme = Checksumme ^ (((int(ID[I], 16)) << 4) + int(ID[I+1], 16))
        Checksumme = hex(Checksumme)
 
        # Tag herausfiltern
        Tag = ((int(ID[1], 16)) << 8) + ((int(ID[2], 16)) << 4) + ((int(ID[3], 16)) << 0)
        Tag = hex(Tag)
 
        print ID[4:10]
        
        # Erlaubte RFID Chips
        a = "581585"  # Key 1
        b = "551A10"  # Key 2
        if ID[4:10] == a:
            my_sound2.play()
            sleep(1)
            print("Guten Tag " + str(name1) + " !")
            print("Tuer offen fuer " + str(opentime) + " Sek!")
            GPIO.setwarnings(False)
            GPIO.setmode(GPIO.BCM)
            GPIO.setup(21, GPIO.OUT)
            GPIO.output(21, True)
            sleep(opentime)
            GPIO.output(21, False)
            GPIO.cleanup(21)
            print("Tuer wieder verriegelt")
            sleep(1)
        elif ID[4:10] == b:
            my_sound2.play()
            sleep(1)
            print("Guten Tag " + str(name2) + " !")
            print("Tuer offen fuer " + str(opentime) + " Sek!")
            GPIO.setwarnings(False)
            GPIO.setmode(GPIO.BCM)
            GPIO.setup(21, GPIO.OUT)
            GPIO.output(21, True)
            sleep(opentime)
            GPIO.output(21, False)
            GPIO.cleanup(21)
            print("Tuer wieder verriegelt")
            sleep(1)
        else:
            print("Transpondernummer nicht gefunden!")
            my_sound.play()
            sleep(2)
Problem :?: :!: :
Wie kann ich erreichen das UART nur einmal liest und nicht dauerhaft.
Ich schalte nach dem Einlesen ein Relais und lasse einen Sound abspielen.
Je nach dem wie lange ich aber den RFID Chip über den Reader halte, schaltet er das Relais bis zu 10 mal und mehr ohne das der Chip überhaupt in der Nähe des Readers ist.

Freue mich über Beiträge :wink:
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

Konnte das Problem selbstständig lösen.

Hab als ersten Punkt in der While-Schleife folgenden Befehl eingefügt:

Code: Alles auswählen

UART.flushInput()  # alle bisher aufgesammelten Informationen löschen.
Jetzt funktioniert es auch das er nicht tausendfach ausliest 8)
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hmjaneenichtsogeil. UART ist ein Strom von Daten, von dem du ueberhaupt nicht weisst, wann der angefangen hat. Es kann also sein, dass du munter die Haelfte einer wichtigen Nachricht wegflushst.

Stattdessen musst du dir einfach nur merken, wann du welchen RFID-Tag das letzte mal gesehen hast, und eine Sperrfrist gegen Mehrfachausloesung einbauen.
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

__deets__ hat geschrieben: Freitag 15. März 2019, 10:57 Hmjaneenichtsogeil. UART ist ein Strom von Daten, von dem du ueberhaupt nicht weisst, wann der angefangen hat. Es kann also sein, dass du munter die Haelfte einer wichtigen Nachricht wegflushst.

Stattdessen musst du dir einfach nur merken, wann du welchen RFID-Tag das letzte mal gesehen hast, und eine Sperrfrist gegen Mehrfachausloesung einbauen.
oh das war so nicht bewusst, funktioniert aber dafür dennoch ganz schön gut :D :D
Weitere Dinge werden über den UART von mir ja nicht abgefragt.

Wie könnte so eine Lösung mit der Sperrfrist gegen Mehrfachauslösung aussehen?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mk_500: Die erste Zeile ist falsch. Da gehört kein Leerzeichen zwischen # und !. Dann gibt es ganz sicher kein Programm hinter ``/bin/bash/python``. Dazu müsste man da ja ein *Verzeichnis* mit dem Namen ``/bin/bash/`` haben, was bedeutet das es kein ausführbares Programm mit dem Namen ``/bin/bash`` geben kann.

Man sollte eigentlich nichts neues mehr mit Python 2 anfangen. Diese Python-Versions-Linie ist am Ende und wird ab Ende des Jahres nicht mehr mit Bugfixes oder Sicherheitsupdates versorgt.

Solange Du nicht auf Python 3 umsteigst oder `print_function` aus dem `__future__`-Modul importierst, hast Du bei Deinen ``print``-Anweisungen fast überall falsche Klammern gesetzt.

Auf mehrere Module Aufteilen lohnt sich bei dem bisschen Code sicher nicht. Funktionen wären vielleicht sinnvoll. Also mindestens eine muss sein, denn auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Kommentare sollten dem Leser einen Mehrwert über den Code liefern. Ein Kommentar ``# imports`` vor den Zeilen mit den ``import``-Anweisungen tut das ganz sicher nicht. Auch ist da eine Menge total offensichtliches kommentiert. Faustregel: Ein Kommentar beschreibt nicht *was* gemacht wird, denn das steht da ja bereits als Code, sondern *warum* der Code das (so) macht. Sofern das nicht offensichtlich ist.

Da wird so einiges importiert was überhaupt gar nicht benutzt wird. Zudem sind die Importe recht ungeordnet.

``as`` wird beim Importieren zum Umbenennen verwendet. Beim GPIO-Import wird ja aber gar nichts umbenannt.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Der `my_`-Präfix ist sinnlos und man nummeriert keine Namen. Das ist in der Regel ein Zeichen das man entweder keine guten Namen gewählt hat, oder das man eigentlich eine Datenstruktur statt einzelner Namen verwenden will. Bei `name*` ist es eine Datenstruktur, und bei `my_sound*` sind es schlechte Namen.

Der Name soll dem Leser vermitteln was der Wert bedeutet. Statt also nummerierter Töne bei denen sinnloserweise noch mal gesagt wird das die Dir gehören (wem sonst?), sollte man da Namen wählen die auch verraten was der Ton bedeutet. Also beispielsweise `ok_sound` und `fail_sound`.

Bei den beiden Namen will man eigentlich eine Datenstruktur verwenden die IDs auf Namen abbildet. Dann hat man später weder die superschlechten Namen `a` und `b` für die IDs, noch die Codewiederholung bei der alles gleich ist, bis auf den ausgegebenen Namen. Und man kann das dann auch ganz einfach um weitere IDs/Namen erweitern ohne da immer Code kopieren und anpassen zu müssen. Zudem liesse sich die Datenstruktur in eine Konfigurationsdatei oder Datenbank auslagern, so das man das Programm gar nicht mehr anpassen müsste um IDs/Namen zu löschen/ändern/hinzuzufügen.

Namen sollten erst dort definiert werden, wo sie auch tatsächlich benötigt werden. Nicht irgendwo am Anfang alle. Das macht es schwerer den Code zu verstehen und Code-Teile in eigene Funktionen heraus zu ziehen. Zudem passiert es bei der Entwicklung so leichter das man unbenutze Namen drin lässt, weil man vergisst die Initialisierung weiter oben im Code zu entfernen wenn man einen Namen da wo er tatsächlich benutzt wird ändert oder entfernt. `Checksumme_Tag` wird definiert aber nirgends verwendet.

`ID`, `Zeichen`, `Checksumme`, und `Tag` werden am Anfang mit Werten initialisiert, die nie irgendwo verwendet werden. `Zeichen` zudem mit einem Typ (`int`) der nicht dem entspricht was dann später tatsächlich sinnvoll an diesen Namen gebunden wird (`bytes`).

Die serielle Schnittstelle wird nicht geschlossen. Dafür sollte man auch bei Endlosschleifen sorgen, denn wirklich endlos sind Programme ja nicht. Wenn eine Ausnahme in der Schleife auftritt oder man das Programm mit Strg+C abbricht, sollte ordentlich aufgeräumt werden.

`str()` wird an zwei Stellen auf Werte angewendet die garantiert schon Zeichenketten *sind*. Die werden dadurch nicht irgendwie ”zeichenkettiger”.

Und an einer Stelle ist der `str()`-Aufruf in Python 2 überflüssig und in Python 3 falsch: Beim einlesen eines einzelnen Zeichens vom der seriellen Schnittstelle. Da sollte man mit `decode()` arbeiten und explizit `ASCII` als Kodierung angeben.

Das Einlesen kann ohne eine nicht verwendete Zählvariable gemacht werden, in dem man sich mit `iter()` aus der `uart.read()`-Methode einen Iterator erstellt, aus dem man dann mit `itertools.islice()` die gewünschte Anzahl von Bytes liest.

Weder `Checksumme` noch `Tag` werden irgendwo verwendet. Der Code der die beiden Werte berechnet kann also raus.

Zeichenketten und Werte mit `str()` und ``+`` zusammensetzen ist eher BASIC als Python. Python hat dafür Zeichenkettenformatierung mit der `format()`-Methode oder ab Python 3.6 auch f-Zeichenkettenliterale.

Die Einstellungen der GPIOs gehören weiter an den Anfang, die müssen ja nur einmal gemacht werden. Analog gehört das Aufräumen ans Ende, nach dem die Pins nicht mehr verwendet werden. Und man sollte sicherstellen das `cleanup()` auch in jedem Fall aufgerufen wird.

Die Warnungen sollten nicht abgestellt werden, sondern wenn Warnungen kommen, die Ursachen dafür beseitigt werden.

Beim öffnen der Tür sollte man auch sicherstellen, dass die auf jeden Fall auch wieder geschlossen wird.

Auch Pygame sollte ordentlich beendet werden.

Im Quelltext ist ein überflüssiges Semikolon am Ende einer Anweisung/Zeile.

Ungetesteter Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf8 -*-
# 
# FIXME Port to Python 3.
# 
from __future__ import absolute_import, division, print_function
from itertools import islice
from time import sleep

import pygame
import serial
from RPi import GPIO

__version__ = '0.01'

ID_TO_NAME = {
    '581585': 'Martin',
    '551A10': 'Jessi',
}
DOOR_PIN = 21
OPEN_TIME = 3  # Zeit in der die Tuer geoeffnet bleibt in Sekunden.

START_FLAG = '\x02'
END_FLAG = '\x03'


def main():
    pygame.init()
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(DOOR_PIN, GPIO.OUT)
        
        fail_sound = pygame.mixer.Sound('/home/pi/Music/keypad_fail.wav')
        ok_sound = pygame.mixer.Sound('/home/pi/Music/keypad_ok.wav')

        with serial.Serial('/dev/ttyAMA0', 9600) as uart:
            while True:
                print()
                print('Warte auf Transponder....... ')
                zeichen = uart.read()
                if zeichen == START_FLAG:
                    user_id = (
                        b''.join(islice(iter(uart.read, None), 13))
                            .decode('ASCII')
                            .replace(END_FLAG, '')[4:10]
                    )
                    print(user_id)
                    
                    name = ID_TO_NAME.get(user_id)
                    if name:
                        ok_sound.play()
                        sleep(1)
                        print('Guten Tag {}!'.format(name))
                        print('Tuer offen fuer {} Sek!'.format(OPEN_TIME))
                        try:
                            GPIO.output(DOOR_PIN, True)
                            sleep(OPEN_TIME)
                        finally:
                            GPIO.output(DOOR_PIN, False)
                        print('Tuer wieder verriegelt')
                        sleep(1)
                    else:
                        print("Transpondernummer nicht gefunden!")
                        fail_sound.play()
                        sleep(2)
    finally:
        GPIO.cleanup()
        pygame.quit()


if __name__ == '__main__':
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

@blackjack

Vielen Dank für deine Antwort und den Einblick, wie man es richtig machen sollte.
Auch danke für das komplette Beispiel der "Code-Umarbeitung". Hut ab! :o :arrow: :idea:
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@mk_500: irgendwie komisch, dass mit einem Startflag angefangen wird, dann aber fix 13 Bytes gelesen werden und darin alle Endflags entfernt werden.
Ist es nicht so, dass man von Startflag bis Endflag lesen sollte, egal wieviele Bytes? Und so oder so nicht Byte 4 bis 10 ungesehen nehmen sollte, sondern prüfen, ob das, was da gelesen wurde auch dem Muster entspricht, das man erwartet?

Code: Alles auswählen

user_id = (
    b''.join(iter(uart.read, END_FLAG))
        .decode('ASCII')[4:10] # TODO: check pattern
)
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

Sirius3 hat geschrieben: Freitag 15. März 2019, 15:51 @mk_500: irgendwie komisch, dass mit einem Startflag angefangen wird, dann aber fix 13 Bytes gelesen werden und darin alle Endflags entfernt werden.
Ist es nicht so, dass man von Startflag bis Endflag lesen sollte, egal wieviele Bytes? Und so oder so nicht Byte 4 bis 10 ungesehen nehmen sollte, sondern prüfen, ob das, was da gelesen wurde auch dem Muster entspricht, das man erwartet?

Code: Alles auswählen

user_id = (
    b''.join(iter(uart.read, END_FLAG))
        .decode('ASCII')[4:10] # TODO: check pattern
)
Das kann gut sein. wie bereits erwähnt, bin absoluter Neuling.
Habe die o.g. Tipps verinnerlicht und angepasst.
Für die Testanwendung die ich aktuell benötige reicht und funktioniert es.

Wie aber sollte der Reader auf einmal etwas anderes lesen, was nicht dem Muster entspricht?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mk_500: Es kann Übertragungsfehler geben.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

__blackjack__ hat geschrieben: Freitag 15. März 2019, 18:09 @mk_500: Es kann Übertragungsfehler geben.
ah ok, dann danke für denk Hinweis.

@blackjack: Eine Frage habe ich noch, wie könnte ich zum Beispiel den von dir oben überarbeiteten Code in ein GUI einbinden?

Habe ja einmal die Python "Code-Schleife" und einmal die tkinter mainloop.
Wie rufe ich das Script in tkinter auf?
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mk_500: Man könnte das in einem Thread laufen lassen und dann mit einer `queue.Queue`, die aus der GUI mittels `after()`-Methode regelmässig geprüft wird, an den Hauptthread kommunizieren.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
mk_500
User
Beiträge: 7
Registriert: Freitag 15. März 2019, 08:51

@blackjack: danke für die schnelle Antwort. Könntest du mir dies in einem Beispiel Code zeigen, dann ist es vielleicht für mich etwas verständlicher.
Danke im Voraus.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wir diskutieren gerade hier viewtopic.php?f=18&t=45204#p343069 wie man auf Eingaben von Dateideskriptoren reagieren kann in tkinter. Das klappt auch mit der seriellen Verbindung.
Antworten