verzögerte, parallele Ausführung in Schleife (threading.Timer??)

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Brathorst
User
Beiträge: 3
Registriert: Mittwoch 27. April 2016, 21:16

Hallo Zusammen,

zuerst mal muss ich mich im Vorfeld entschuldigen, falls man meiner Ausdrucksweise erkennt, dass ich noch ganz schön grün hinter den Ohren bin, was Python betrifft, aber ich habe erst angefangen mich fürs Programmieren & Co. zu interessieren, nachdem ich mir einen Himbeerkuchen geholt habe.

Mein Problem ist folgendes:

Ich habe ein Python Script (welches prinzipiell perfekt läuft), das ich nun allerdings so erweitern möchte, dass ein weiterer Befehl zeitgleich (also das Script soll nicht warten, bis er fertig ist) und mit einer gewissen Verzögerung ausgeführt werden soll.

Hintergrund ist eine Foto-Box, die ich mir gebastelt habe, und in die ich nun noch einen LED-Lampen-Blitz einbauen möchte.

Da die Auslösung des Fotos (mittels gphoto) ca. 1,5sek dauert, wollte ich eben um diese 1,5 sek zeitverzögert den Befehl absetzen, dass die LED-Lampen für ca. 0,5sek auf volle Helligkeit gehen.

Bisher hab ich mit "threading.Timer" genau das geschafft, was ich wollte, allerdings hab ich dabei das Problem, dass der Threading-Befehl nur einmal ablaufen kann, ich aber am Ende des Scripts dieses per Knopfdruck wieder ablaufen lasse...

Hat einer eine Idee, wie ich evtl weiterkomme? Ist Threading hier richtig? Gibt es eventuell eine andere Möglichkeit einen definierten Befehl mit einer Verzögerung von X Sekunden ablaufen zu lassen, ohne dass das Skript auf das Beenden des Befehls wartet?

Code-Schnipsel kann ich natürlich gerne durchgeben, falls das alles zu chaotisch war...

Der Befehl den ich für threading.Timer definiert hatte lautet:

>def blitz():
> SPOT_1.ChangeDutyCycle(100)
> SPOT_2.ChangeDutyCycle(100)
> time.sleep(0.5)
> SPOT_1.stop()
> SPOT_2.stop()

>t = threading.Timer(1,5, blitz)

gestartet wurde der Thread dann in der entsprechenden Stelle im Skript mit

>t.start()

Da es aber wie gesagt in SChleife läuft, bekomm ich nach dem ersten (geglückten) Durchlauf eben die Fehlermeldung
"RuntimeError: threads can only be started once"

Vielleicht hat jemand eine Idee und versteht was ich vorhabe...Bin leider noch zu doof um die Möglichkeiten von Python komplett zu verstehen, aber ich bin ehrgeizig und hungrig :D

Vielen Dank und schönen Abend noch!
BlackJack

@Brathorst: Statt das bestehende `Thread`-Exemplar erneut zu starten, kannst Du doch einfach ein neues `Thread`-Exemplar erstellen und das starten.
Brathorst
User
Beiträge: 3
Registriert: Mittwoch 27. April 2016, 21:16

Hallo BlackJack,

vielen Dank erstmal für deine Antwort, auch wenn ich gleich schon wieder eine Rückfrage hätte:

Das Skript läuft theoretisch in einer endlos Schleife und startet immer wieder, wenn ein Knopf gedrückt wird. Bedeutet es müssten bei jedem Durchlaug ein neuer Thread erstellt werden. Kann das zu Problemen führen?

Kann ich einfach die Zeile

t = threading.Timer

weglassen und das nach dem = in die Zeile .start() mit reinpacken?? Bedeutet das, dass jedes mal ein neuer Thread erstellt wird, oder ändert das gar nix?

Is bestimmt ne doofe Frage, aber bei dem Python Grundverständis hapert es noch etwas... Mein Skript hab ich durch viel probieren aus verschiedensten QUellen zusammengewürfelt und versteh auch was dadrin passiert, nur mit dem selbst entwerfen und schreiben wenn ich was bestimmtes möchte wird es schon eher schwer für mich...

Grüßle
Sirius3
User
Beiträge: 18227
Registriert: Sonntag 21. Oktober 2012, 17:20

@Brathorst: ohne das ganze Skript zu kennen, kann man natürlich nicht sagen, ob das ändern von ein paar Zeilen nicht irgendwo zu Problemen führt. Aber prinzipiell ist es die einzig sinnvolle Möglichkeit, wiederholt einen Thread zu starten, in dem man ihn neu erzeugt. Du solltest Dir aber gedanken machen, was passiert, wenn man schnell hintereinander diesen Knopf drückt.
BlackJack

@Brathorst: Ja, das könntest Du in einem Ausdruck machen, das erstellen des `Timer`-Objekts und das starten des Threads. Das startet jedes mal einen Thread, aber das sollte kein Problem sein.

@Sirius: Ich hatte das so verstanden, dass danach das Foto mit `gphoto` geschossen wird und dass das dieser Aufruf 1,5 Sekunden (mindestens) blockiert. Weshalb dieses parallele Warten bis zum Blitz nötig wurde.
Brathorst
User
Beiträge: 3
Registriert: Mittwoch 27. April 2016, 21:16

Guten Morgen zusammen!

Es läuft, es läuft! :D Danke BlackJack für den Hinweis mit dem neuen Thread und dass es dafür reicht, dass ich vorher "t" nicht definiere (bzw. das 'Timer'-Objekt nicht definiere).

Wie gesagt jetzt läuft das Skript wieder perfekt durch und die LED's als Blitz können mit der Verzögerung auch an das tatsächliche Auslösen der Digicam angepasst werden!

@Sirius3:
Du hast natürlich recht, dass man vieles erst im Zusammenhang des ganzen Skripts erkennt. Daher folgt hier noch mein "Photobox-Skript", welches auf Knopfdruck erst einen Countdown auf einer 8x8 LED Matrix (MAX7219) ablaufen lässt, dann den Thread für die 2 Blitz-LED's (mittels PWM über ein ULN2003A) startet, bevor über gphoto das Foto gemacht wird und anschließend die Bildbearbeitung und den Ausdruck über ein Bash-Skript anstößt und zeitgleich (Daher über 'subprocess.Popen und .call') eine Warteschleifenanzeige auf der LED Matrix ablaufen lässt bis das Foto (Thermal- Etikettendrucker) gedruckt ist.
Danach gehts bei Knopfdruck wieder von vorne los...:

Code: Alles auswählen

#!/usr/bin/python
# Einzelfoto und LED Matrix

import RPi.GPIO as GPIO, time, os, subprocess
import max7219.led as led
import threading
from subprocess import Popen

# LED Matrix Setup
device = led.matrix(cascaded=1)

# GPIO setup
GPIO.setmode(GPIO.BCM)
BUTTON = 24
GPIO.setup(BUTTON, GPIO.IN)
PRINT_LED = 22
READY_LED = 23
GPIO.setup(READY_LED, GPIO.OUT)
GPIO.setup(PRINT_LED, GPIO.OUT)

GPIO.setup(18, GPIO.OUT)
GPIO.setup(25, GPIO.OUT)
SPOT_1 = GPIO.PWM(18, 100)	
SPOT_2 = GPIO.PWM(25, 100)

def blitz():
  SPOT_1.ChangeDutyCycle(100)
  SPOT_2.ChangeDutyCycle(100)
  time.sleep(0.5)
  SPOT_1.ChangeDutyCycle(0)
  SPOT_2.ChangeDutyCycle(0)
  #SPOT_1.stop()
  #SPOT_2.stop()
  

SPOT_1.start(0)
SPOT_2.start(0)

GPIO.output(READY_LED, True)
GPIO.output(PRINT_LED, False)

try:
  while True:
    if GPIO.input(BUTTON) == GPIO.HIGH:
      snap = 0
      while snap < 1:
        print('go')
        GPIO.output(READY_LED, False)
        SPOT_1.ChangeDutyCycle(5)
        SPOT_2.ChangeDutyCycle(5)
        device.brightness(4)
        # Helligkeit 1 bis 15
#        device.show_message('ready...', delay=0.05)
	device.pixel(1, 1, 1, redraw=True)
	device.pixel(2, 1, 1, redraw=True)
	device.pixel(5, 1, 1, redraw=True)
	device.pixel(6, 1, 1, redraw=True)
	device.pixel(0, 2, 1, redraw=True)
	device.pixel(3, 2, 1, redraw=True)
	device.pixel(4, 2, 1, redraw=True)
	device.pixel(7, 2, 1, redraw=True)
	device.pixel(0, 3, 1, redraw=True)
	device.pixel(4, 3, 1, redraw=True)
	device.pixel(7, 3, 1, redraw=True)
	device.pixel(0, 4, 1, redraw=True)
	device.pixel(2, 4, 1, redraw=True)
	device.pixel(3, 4, 1, redraw=True)
	device.pixel(4, 4, 1, redraw=True)
	device.pixel(7, 4, 1, redraw=True)
	device.pixel(0, 5, 1, redraw=True)
	device.pixel(3, 5, 1, redraw=True)
	device.pixel(4, 5, 1, redraw=True)
	device.pixel(7, 5, 1, redraw=True)
	device.pixel(1, 6, 1, redraw=True)
	device.pixel(2, 6, 1, redraw=True)
	device.pixel(5, 6, 1, redraw=True)
	device.pixel(6, 6, 1, redraw=True)

        time.sleep(2)

        
	print("3")
	device.letter(0, ord('3'))
	time.sleep(1)
	device.clear()
	time.sleep(0.4)
	
	print("2")
	device.letter(0, ord('2'))
	time.sleep(1)
	device.clear()
	time.sleep(0.4)

	print("1")
	device.letter(0, ord('1'))
	time.sleep(1)
	device.clear()
	time.sleep(0.4)

	print("Smile!")
	device.letter(0, 1)
	#time.sleep(1)
	
	threading.Timer(1.5, blitz).start()
 
 
        print("SNAP") 
        gpout = subprocess.check_output("gphoto2 --capture-image-and-download --filename /home/pi/photobox/photobox%H%M%S.jpg", stderr=subprocess.STDOUT, shell=True)
        print(gpout)
        if "ERROR" not in gpout:
          snap += 1
        device.clear()
	
		
      print("please wait while your photos print...")
      # build image and send to printer
      GPIO.output(PRINT_LED, True)
      subprocess.Popen("sudo sh /home/pi/scripts/photobox/jonas/assemble_and_print_1", shell=True)
      subprocess.call("sudo python /home/pi/scripts/photobox/wait.py", shell=True)

      time.sleep(2)
      # Wie lange dauert der Druckvorgang tatsaechlich?
      print("ready for next round")
      device.clear()
      GPIO.output(PRINT_LED, False)
      GPIO.output(READY_LED, True)

except KeyboardInterrupt:
  GPIO.cleanup()
  device.clear()
 
Es schwimmt auf jeden Fall noch ein bisschen Altlast mit von Trial & Error Versuchen und älteren Skript-Ständen... Und bestimmt fallen euch ein paar Python Sünden ein die man besser machen könnte...
Beispielsweise stammt das 'snap' noch aus dem Beginn des Skripts, da es hier noch 4 Fotos hintereinander gemacht hat. Mittlerweile wird nur noch eins gemacht und das direkt gedruckt und gespeichert.

Ach ja: Ich hab jetzt für den das Threading.Timer überhaupt kein '.stop()' mehr drin, weil da der 2.Durchlauf wieder nicht ging. Is das ein Problem? Sonst müsst ich doch wahrscheinlich den .start() mit in die 'while true'-Schleife legen, oder?

Ich bin für alle Tipps und Tricks auch dankbar, da ich wie gesagt recht motiviert bin Python irgendwann mal "richtig" zu verstehen ;)
Aber dafür dass ich Ende letzten Jahres (vor dem Kauf des Pi) noch ein klassicher Windoof-Endnutzer war, bin ich mit dem Ergebnis recht zufrieden!
Und für die Hochzeit meines Kumpels wirds auch reichen ;)

Also vielen Dank nochmal für eure Tipps und frohes Schaffen heut!
Grüße
Brathorst
BlackJack

@Brathorst: Also ein offensichtliches Problem bei dem Quelltext ist die Einrückung. Du hast da Tabulatorzeichen und Leerzeichen gemischt was in Deinem Editor mit Deinen Einstellungen richtig aussehen mag, hier im Forum aber beispielsweise nicht. Wobei das eventuell auch vom Browser anhängen kann. Ausserdem weicht die Einrückung von den im Style Guide for Python Code angegebenen vier Leerzeichen pro Ebene ab.

`os` und `Popen` werden importiert, aber nicht verwendet.

Namen komplett in Grossbuchstaben sind per Konvention für Konstanten vorgesehen. `SPOT_1` und `SPOT_2` sind allerdings Objekte deren Zustand im Laufe des Programms verändert wird, damit also nicht Konstant. Bei den beiden Objekten hast Du die Pin-Nummern als Zahlen angegeben anstatt wie bei den anderen Pin-Nummern Konstanten zu definieren.

Auf Modulebene sollten nur Konstanten, Funktionen, und Klassen definiert werden. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Die `blitz()`-Funktion ist zudem mitten im Hauptprogramm an einer willkürlich erscheinenden Stelle definiert, was das Lesen an der Stelle etwas erschwert.

Wenn man das Hauptprogramm in eine Funktion verschiebt, fällt auf das `blitz()` Variablen benutzt die nicht als Argumente übergeben werden, sondern einfach so ”magisch” aus der Umgebung verwendet werden. Werte, ausser Konstanten, sollten Funktionen als Argumente betreten und als Rückgabewerte verlassen.

`device` ist als Name ein bisschen zu generisch für eine LED-Matrix.

Was im ``except``-Block steht, sollte in ein ``finally`` verschoben werden, denn auch wenn das Programm aus anderen Gründen abbricht, beispielsweise ein Programmierfehler, möchte man, dass die Aufräumarbeiten durchgeführt werden.

Das Programm steckt solange der Auslöseknopf nicht gedrückt wird in einer Endlosschleife die den Prozessor unnötig belastet. Dieses „busy waiting“ ist deshalb keine gute Idee. Das GPIO-Modul hat eine Funktion um auf eine Flanke zu warten die intern effizienter implementiert sein sollte als so eine Schleife. Damit spart man praktischerweise dann auch eine Einrück-Ebene ein.

`snap` ist eigentlich nur ein Flag und kein Zähler. Das würde deutlicher wenn man den Code entsprechend formuliert. Ausserdem ist ”Schnapp”/schnappen/einrasten vielleicht kein so passender Name für das was der Wert eigentlich bedeutet. Wenn man eine ”Endlosschleife” daraus macht, die im Erfolgsfall durch ein ``break`` abgebrochen wird, kann man sich den Namen komplett sparen.

Laut erster Zeile handelt es sich um Python 2, da ist ``print`` allerdings eine Anweisung und keine Funktion. Also sollte man entweder durch den entsprechenden `__future__`-Import dafür sorgen, dass es eine Funktion ist, oder es nicht so schreiben als wäre es eine.

In der inneren ``while``-Schleife werden Sachen mehrfach gemacht die man *einmal* davor erledigen könnte. Zum Beispiel die Bereitschafts-LED ausschalten oder den Blitz leicht anschalten. Die Helligkeit der LED-Matrix wird sogar nur in dieser Schleife immer wieder auf den gleichen konstanten Wert gesetzt. Das könnte man aus allen Schleife heraus ziehen.

Bei den ganzen `pixel()`-Aufrufen könnte man die Unterschiede in eine Datenstruktur herausziehen und eine Schleife darüber schreiben. Auch den Countdown kann man mit einer Schleife kürzer schreiben.

``shell=True`` sollte man beim `subprocess`-Modul nicht verwenden sofern man das nicht *wirklich* braucht. Und selbst dann sollte man versuchen eine Lösung ohne eine zusätzliche Shell zwischen dem Python-Programm und dem gestarteten Prozess zu finden. Die Pfade kann man als Konstanten herausziehen, damit sich das besser anpassen lässt wenn man das an eine andere Stelle verschiebt.

Warum werden die externen Programme mit ``sudo`` aufgerufen? Hat das laufende Programm selbst nicht schon diese Rechte um auf die GPIOs zugreifen zu können?

Aus einem Python-Programm heraus ein anderes Python-Modul als Programm zu starten ist nicht die erste Wahl. Was macht `wait.py` denn?

Ich lande dann ungefähr bei dem hier (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
# Einzelfoto und LED Matrix
from __future__ import absolute_import, division, print_function
import os
import subprocess
import sys
import threading
import time

from max7219 import led
from RPi import GPIO

BASE_PATH = '/home/pi'
IMAGE_PATH = os.path.join(BASE_PATH, 'photobox/photobox%H%M%S.jpg')
PROGRAM_PATH = os.path.join(BASE_PATH, 'scripts/photobox')
PRINT_SCRIPT_PATH = os.path.join(PROGRAM_PATH, 'jonas/assemble_and_print_1')
WAIT_PROGRAM_PATH = os.path.join(PROGRAM_PATH, 'wait.py')

BUTTON_PIN = 24
PRINT_LED_PIN = 22
READY_LED_PIN = 23
SPOT_PINS = [18, 25]


def flash(pwms):
    for pwm in pwms:
        pwm.ChangeDutyCycle(100)
    time.sleep(0.5)
    for pwm in pwms:
        pwm.ChangeDutyCycle(0)


def main():
    led_matrix = led.matrix()
    led_matrix.brightness(4)
    try:
        GPIO.setmode(GPIO.BCM)
        
        GPIO.setup(BUTTON_PIN, GPIO.IN)
        GPIO.setup([READY_LED_PIN, PRINT_LED_PIN], GPIO.OUT)

        flash_pwms = list()
        for pin in SPOT_PINS:
            GPIO.setup(pin, GPIO.OUT)
            pwm = GPIO.PWM(pin, 100)
            pwm.start(0)
            flash_pwms.append(pwm)
        
        GPIO.output(READY_LED_PIN, True)
        GPIO.output(PRINT_LED_PIN, False)
        while True:
            GPIO.wait_for_edge(BUTTON_PIN, GPIO.RISING)
            GPIO.output(READY_LED_PIN, False)
            for pwm in flash_pwms:
                pwm.ChangeDutyCycle(5)
            while True:
                print('go')
                for x, y in [
                    (1, 1), (2, 1), (5, 1), (6, 1),
                    (0, 2), (3, 2), (4, 2), (7, 2),
                    (0, 3), (4, 3), (7, 3),
                    (0, 4), (2, 4), (3, 4), (4, 4), (7, 4),
                    (0, 5), (3, 5), (4, 5), (7, 5),
                    (1, 6), (2, 6), (5, 6), (6, 6),
                ]:
                    led_matrix.pixel(x, y, 1)
                time.sleep(1.6)

                for output, led_letter in [
                    ('3', '3'), ('2', '2'), ('1', '1'), ('Smile!', '\x01')
                ]:
                    time.sleep(0.4)
                    print(output)
                    led_matrix.clear()
                    led_matrix.letter(0, ord(led_letter))
                    time.sleep(1)

                threading.Timer(1.5, flash, [flash_pwms]).start()

                print('SNAP')
                gphoto_output = subprocess.check_output(
                    [
                        'gphoto2',
                        '--capture-image-and-download',
                        '--filename', IMAGE_PATH,
                    ],
                    stderr=subprocess.STDOUT,
                )
                print(gphoto_output)
                led_matrix.clear()
                if 'ERROR' not in gphoto_output:
                    break

            print('please wait while your photos print...')
            # 
            # Build image and send to printer.
            # 
            GPIO.output(PRINT_LED_PIN, True)
            subprocess.Popen(['sudo', 'sh', PRINT_SCRIPT_PATH])
            subprocess.call(['sudo', sys.executable, WAIT_PROGRAM_PATH])

            time.sleep(2)
            # 
            # TODO Wie lange dauert der Druckvorgang tatsaechlich?
            # 
            GPIO.output(PRINT_LED_PIN, False)
            print('ready for next round')
            GPIO.output(READY_LED_PIN, True)

    except KeyboardInterrupt:
        pass  # Intentionally ignored.
    finally:
        GPIO.cleanup()
        led_matrix.clear()


if __name__ == '__main__':
    main()
Die Hauptfunktion ist so deutlich zu lang. Das würde ich als nächstest in Angriff nehmen. Und wenn man dann beim Python lernen bei Klassen angekommen ist, kann man sicher auch das eine oder andere sinnvoll in einer Klasse kapseln.
Antworten