Mit Taster vom RasPi Stoppuhr stoppen

Fragen zu Tkinter.
Antworten
Sn0w3y
User
Beiträge: 5
Registriert: Montag 17. Oktober 2016, 02:01

Hallo Leute, habe eine Stoppuhr und will mit dem Taster des RasPis der über den GPIO Eingang angeschlossen ist Starten bzw. stoppen.

Habt Ihr vielleicht ein paar gute Tipps.. im Anhang meine mittlerweile funktionierende Stoppuhr..

Allerdings muss man zuerst eben den Start Button drücken und dann frägt er erst den Taster ab. Soll aber ohne die Buttons auch funktionieren..

Bitte helft mir.. :K :K :K

http://pastebin.com/EHubsRJf
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sn0w3y: wenn Du keinen Start-Knopf haben willst, mußt Du eben die ganze Zeit auf Deinen GPIO-Eingang warten. Am besten schreibst Du das wait_for_edge in einen eigenen Thread.

Allgemein: vergiss, dass es "global" gibt. Für zustandsbehaftete GUI-Programmierung braucht man Klassen. Die vielen Leerzeilen innerhalb der Funktionen erschweren die Lesbarkeit mehr, als dass sie helfen. Durch die Einrückung wird das Auge schon genug geführt. Die Klammern um die if-Bedingungen sind überflüssig. Statt den Timer händisch zu zählen würde ich auf time.time zurückgreifen. Die Zeitintervalle der after-Methode sind viel zu ungenau für eine Stoppuhr. Die Funktion wird auch dann aufgerufen, wenn gar keine Stoppuhr läuft.

Zeile 43: der Funktionsname exist sagt nicht wirklich, was diese Funktion macht.
Zeile 51: repr ist nur für Debug-Ausgaben gedacht. Statt + solltest Du .format verwenden.
Ab Zeile 58: alle Anweisungen (außer Definitionen) gehören in eine Funktion, üblicherweise main genannt und wird über das Konstrukt "if __name__ == '__main__': main()" aufgerufen.
Zeile 81: and ist eine logische Verknüpfung, das was Du da denkst, was es tut, funktioniert so nicht; schau Dir mal in einer interaktiven Konsole an, was "stop and zeiten" wirklich ergibt.
BlackJack

@Sn0w3y: Kommentare sollten dem Leser einen Mehrwert gegenüber dem Code liefern und nicht einfach nur wiederholen was im Code sowieso schon da steht. Bei einem ``setwarnings(False)`` ist es unnötig noch einmal zu kommentieren, dass dort die Warnungen ausgeschaltet werden. Faustregel: Nicht kommentieren was der Code macht, denn das kann man ja am Code selbst ablesen, sondern *warum* er das so macht. Natürlich nur falls das nicht offensichtlich ist, oder einfach nachgelesen werden kann.

Schlimm ist wenn der Kommentar dem Code widerspricht. Denn dann weiss der Leser nicht was falsch ist, der Code oder der Kommentar. Wenn man also kommentiert, dass irgend etwas mit Pin 23 gemacht wird, dann sollte der Code stattdessen nichts mit Pin 22 machen. Das ist hier nochmal verwirrend weil man nicht weiss ob der Kommentar und Code nicht vielleicht doch zusammen passen weil in der Informatik oft bei 0 angefangen wird zu zählen, so dass man hier auch denken könnte 22 steht tatsächlich für Pin 23 weil 0 für Pin 1 stehen könnte.

Warum schaltest Du die Warnungen aus? Die gibt es ja nicht ohne Grund. Wenn die etwas ausgeben, dann in aller Regel weil man nicht sauber programmiert hat, also zum Beispiel nicht dafür sorgt, dass `GPIO.cleanup()` aufgerufen wird, bevor das Programm verlassen wird. Statt Warnungen zu deaktivieren sollte man besser die Ursache der Warnungen beseitigen.

Für das Rechnen mit Zeiten bietet sich das `datetime`-Modul an.

``self = root`` ist eine eigenartige Zeile. Der Name `self` hat in Python eine besondere Bedeutung. Unnötig ist die Zeile zu dem auch, denn man kann doch einfach `root` verwenden.

Dateien die man öffnet, sollte man auch wieder schliessen. Die ``with``-Anweisung ist da sehr praktisch.

Werte die sich im Code wiederholen, aber auch solche die zwar für den Programmlauf fest sind, die man aber vielleicht irgendwann einmal ändern können möchte, sollte man am Anfang des Programms als Konstanten festlegen. Das betrifft zum Beispiel Pin-Nummern und Dateinamen.

Die Programmlogik und die GUI sollte man sauber trennen, so das man die Programmlogik auch ohne die GUI verwenden und testen kann.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import Tkinter as tk
from datetime import datetime as DateTime, timedelta as TimeDelta
from itertools import count
from RPi import GPIO

START_PIN = 22
LOG_FILENAME = 'log.txt'


class Stopwatch(object):

    def __init__(self, log_filename):
        self.log_filename = log_filename
        self.part_counter = count()
        self.start_time = None
        self._elapsed_time = TimeDelta()

    def __str__(self):
        minutes, seconds = divmod(self.elapsed_time.total_seconds(), 60)
        hours, minutes = divmod(minutes, 60)
        return '{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds)

    @property
    def is_running(self):
        return self.start_time is not None

    @property
    def elapsed_time(self):
        if self.is_running:
            return DateTime.now() - self.start_time
        else:
            return self._elapsed_time

    def start(self):
        if not self.is_running:
            self.start_time = DateTime.now()

    def stop(self):
        if self.is_running:
            self._elapsed_time = self.elapsed_time
            self.start_time = None

            with open(self.log_filename, 'a') as log_file:
                log_file.write(
                    'Teil #{0} {1}\n'.format(next(self.part_counter), self)
                )


class StopwatchUI(tk.Frame):

    def __init__(self, parent, stopwatch):
        tk.Frame.__init__(self, parent)
        self.stopwatch = stopwatch
        self.time_label = tk.Label(parent, font=('Helvetica', 150))
        self.time_label.pack()

        tk.Button(parent, text='Start', command=self.stopwatch.start).pack()
        tk.Button(parent, text='Stop', command=self.stopwatch.stop).pack()
        tk.Button(parent, text='Quit', command=self.quit).pack()
        self._update_display()

    def _update_display(self):
        self.time_label['text'] = str(self.stopwatch)
        self.after(10, self._update_display)

  
def main():
    try:    
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(START_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

        root = tk.Tk()
        root.overrideredirect(True)
        width, height = root.winfo_screenwidth(), root.winfo_screenheight()
        root.geometry('{0}x{1}+0+0'.format(width, height))
        root.wm_title('Stoppuhr')

        stopwatch = Stopwatch(LOG_FILENAME)
        GPIO.add_event_detect(START_PIN, GPIO.FALLING, stopwatch.start)

        stopwatch_ui = StopwatchUI(root, stopwatch)
        stopwatch_ui.pack()

        root.mainloop()
    finally:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Wobei ich nicht explizit selbst einen Thread starte der auf die fallende Flanke an dem GPIO-Pin wartet, sondern das der Funktion `add_event_detect()` aus dem `GPIO`-Modul überlasse. Und das funktioniert auch nur deswegen so einfach, weil der Rest des Programms so geschrieben ist, wie er das im Moment ist. Das Thema nebenläufige Programmierung, und die ganzen Probleme die dabei auftreten können, ist recht komplex. Wenn man beispielsweise die Aktualisierung der Anzeige effizienter machen will, so dass die nur stattfindet wenn die Uhr auch tatsächlich läuft, dann wird das alles ein wenig komplizierter.
Sn0w3y
User
Beiträge: 5
Registriert: Montag 17. Oktober 2016, 02:01

Hallo, erst einmal ein mega großes Danke für die hilfreichen Antworten.

Leider bekomme ich bei deinem Script Fehlermeldungen..

Traceback (most recent call last):
File "python.py", line 93, in <module>
main()
File "python.py", line 84, in main
stopwatch_ui = StopwatchUI(root, stopwatch)
File "python.py", line 63, in __init__
self._update_display()
File "python.py", line 66, in _update_display
self.time_label['text'] = str(self.stopwatch)
File "python.py", line 24, in __str__
return '{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds)
ValueError: Unknown format code 'd' for object of type 'float'


Was könnte das nun wieder sein :K :K
BlackJack

Das könnte daran liegen das es ungetestet ist, und das `timedelta.total_seconds()` eine Gleitkommazahl liefert, damit die ganzen zwischenergebnisse Gleitkommazahlen sind, und man da deshalb in Zeile 22 die Zahl in eine ganze Zahl umwandeln muss:

Code: Alles auswählen

        minutes, seconds = divmod(int(self.elapsed_time.total_seconds()), 60)
Womit ich allerdings keine Garantie übernehme das es jetzt funktioniert, weil wie gesagt, ungetestet.
Sn0w3y
User
Beiträge: 5
Registriert: Montag 17. Oktober 2016, 02:01

Jetzt funktioniert alles so ziemlich - wenn ich allerdings den Taster drücke, wirft er mir einen TypeError aus..

TypeError: start() takes exactly 1 argument (2 given)

aber wo :cry:
BlackJack

@Sn0w3y: Der Rückruf vom GPIO-Modul übergibt da anscheinend ein Argument das wir nicht erwarten. Ein Blick in die Dokumentation bestätigt das: Die ”Kanalnummer”, sprich der Pin. Kann man mit einem ``lambda``-Ausdruck leicht beheben:

Code: Alles auswählen

        GPIO.add_event_detect(
            START_PIN, GPIO.FALLING, lambda _: stopwatch.start()
        )
Sn0w3y
User
Beiträge: 5
Registriert: Montag 17. Oktober 2016, 02:01

@BlackJack sorry für die Fehlinfo - hab die Sicherungsdatei editiert :roll: welch dummer Fehler von mir.

Jetzt gibt er mir keinen Fehler mehr aus, allerdings schreibt er auch bei erneutem Tastendruck keine weiteren Zeiten in die log.txt Datei - warum ?

Der Zähler sollte sich praktisch zurücksetzen und bei 00:00:00 anfangen und dann die gezählte zeit beim Tastendruck wieder in die log.txt Datei schreiben.

Also:
1.Taster
2.Stoppuhr (Start)
2.Taster
3.Stoppuhr (Stop + schreibe in log.txt + Reset auf 00:00:00 + starte von vorne)
Sn0w3y
User
Beiträge: 5
Registriert: Montag 17. Oktober 2016, 02:01

Sn0w3y hat geschrieben:@BlackJack sorry für die Fehlinfo - hab die Sicherungsdatei editiert :roll: welch dummer Fehler von mir.

Jetzt gibt er mir keinen Fehler mehr aus, allerdings schreibt er auch bei erneutem Tastendruck keine weiteren Zeiten in die log.txt Datei - warum ?

Der Zähler sollte sich praktisch zurücksetzen und bei 00:00:00 anfangen und dann die gezählte zeit beim Tastendruck wieder in die log.txt Datei schreiben.

Also:
1.Taster
2.Stoppuhr (Start)
2.Taster
3.Stoppuhr (Stop + schreibe in log.txt + Reset auf 00:00:00 + starte von vorne)
Hab es mittlerweile geschafft, dass der Taster die Stoppuhr startet und beim drücken auf 0 setzt und sie von vorne anfängt.
Aber wieso schreibt er mir dazwischen Werte rein, welche überhaupt nicht gedrückt wurden ? :?: :idea:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import Tkinter as tk
from datetime import datetime as DateTime, timedelta as TimeDelta
from itertools import count
from RPi import GPIO
 
START_PIN = 22
LOG_FILENAME = 'log.txt'
 
 
class Stopwatch(object):
 
    def __init__(self, log_filename):
        self.log_filename = log_filename
        self.part_counter = count()
        self.start_time = None
        self._elapsed_time = TimeDelta()
 
    def __str__(self):
        minutes, seconds = divmod(self.elapsed_time.total_seconds(), 60)
        hours, minutes = divmod(minutes, 60)
        return '{0:02}:{1:02}:{2:02}'.format(hours, minutes, seconds)
 
    @property
    def is_running(self):
        return self.start_time is not None
 
    @property
    def elapsed_time(self):
        if self.is_running:
            return DateTime.now() - self.start_time
        else:
            return self._elapsed_time
 
    def start(self):
        if not self.is_running:
            self.start_time = DateTime.now()
 
    def stop(self):
        if self.is_running:
            self._elapsed_time = self.elapsed_time
            self.start_time = None
 
            with open(self.log_filename, 'a') as log_file:
                log_file.write('Teil #{0} {1}\n'.format(next(self.part_counter), self._elapsed_time))
        if not self.is_running:
            self.start_time = DateTime.now()
 
 
class StopwatchUI(tk.Frame):
 
    def __init__(self, parent, stopwatch):
        tk.Frame.__init__(self, parent)
        self.stopwatch = stopwatch
        self.time_label = tk.Label(parent, font=('Helvetica', 150))
        self.time_label.pack()
 
        tk.Button(parent, text='Quit', command=self.quit).pack()
        self._update_display()
 
    def _update_display(self):
        self.time_label['text'] = str(self.stopwatch)
        self.after(10, self._update_display)
 
 
def main():
    try:    
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(START_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
        root = tk.Tk()
        root.overrideredirect(True)
        width, height = root.winfo_screenwidth(), root.winfo_screenheight()
        root.geometry('{0}x{1}+0+0'.format(width, height))
        root.wm_title('Stoppuhr')
 
        stopwatch = Stopwatch(LOG_FILENAME)
        GPIO.add_event_detect(START_PIN, GPIO.FALLING, lambda _: stopwatch.stop())
 
        stopwatch_ui = StopwatchUI(root, stopwatch)
        stopwatch_ui.pack()
 
        root.mainloop()
    finally:
        GPIO.cleanup()
 
 
if __name__ == '__main__':
    main()
Antworten