Tkinter threading probleme

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
StolleJay
User
Beiträge: 3
Registriert: Montag 27. Januar 2020, 20:40

Hallo,

Ich bin dabei mir ein kleines Programm für den Raspberry Pi zu schreiben mit dem ich WS2812 LED Streifen Steuern kann und wollte mir hierfür ein paar Effekte selbst Schreiben.
Die Effekte soll man dann ganz einfach über das GUI Aktivieren und Deaktivieren können.

Mein Problem hierbei war das meine Tkinter GUI sich bei einer immer wieder wiederholenden Schleife Aufhängt.
Das habe ich soweit mit threading erst mal beheben können doch jetzt hängt sich das ganze Programm auf sobald ich den thread "beended" habe und erneut auf irgendeinen Button klicke.

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

import time
import threading
import board
import neopixel
try:
    # for Python2
    from Tkinter import *   ## notice capitalized T in Tkinter 
except ImportError:
    # for Python3
    from tkinter import *   ## notice lowercase 't' in tkinter here
import RPi.GPIO as GPIO

ORDER = neopixel.GRB

ledMode = 0

num_pixels = 60

pixels = neopixel.NeoPixel(board.D18, num_pixels, brightness=0.2, auto_write=False, pixel_order=ORDER)

def mode_off():
    global ledMode
    
    ledMode = 0
    
    modeOffButton['state'] = NORMAL
    modeStaticButton['state'] = NORMAL
    modeSnakeButton['state'] = NORMAL
    
    pixels.fill((0, 0, 0))
    pixels.show()
def mode_static():
    global ledMode
    
    if(ledMode != 1):
        ledMode = 1
        
        modeOffButton['state'] = NORMAL
        modeStaticButton['state'] = DISABLED
        modeSnakeButton['state'] = NORMAL
        
        pixels.fill((int(staticColorR), int(staticColorG), int(staticColorB)))
        pixels.show()
    
def mode_snake_do():
    global ledMode
    
    while True:
        if(ledMode == 2):
            for x in range(0, num_pixels):
                pixels[x] = (0, 32, 0)
                pixels.show()
                time.sleep(0.025)
            for x in range(0, num_pixels):
                pixels[x] = (0, 0, 0)
                pixels.show()
                time.sleep(0.025)
            continue #Restart while loop
def mode_snake():
    global ledMode
    
    ledMode = 2
    
    modeOffButton['state'] = NORMAL
    modeStaticButton['state'] = NORMAL
    modeSnakeButton['state'] = DISABLED
    
    t = threading.Thread(target=mode_snake_do)
    t.start()
    
#GUI Window
app = Tk()

modesGrid = Frame(app)
modesGrid.pack(side=TOP, anchor=W, fill=X, pady=2)

modeSnakeButton = Button(modesGrid, text="Snake", fg="black", borderwidth=2, relief=GROOVE, command=mode_snake)
modeSnakeButton.pack(side=LEFT)
Ich Teste und Google schon seit knapp 3 Tagen komme aber mehr oder weniger immer wieder auf das gleiche Problem hinaus deshalb hoffe ich nun das ihr mir hier weiter helfen könnt.
Benutzeravatar
__blackjack__
User
Beiträge: 13115
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@StolleJay: Für Python 2 braucht man keinen neuen Code mehr schreiben. Die She-Bang-Zeile sollte also für Python 3 sein und `Tkinter` fliegt raus.

Sternchen-Importe sind Böse™. Bei `tkinter` holt man sich da hunderte Namen ins Modul von denen nur ein Bruchteil verwendet wird. Und nicht nur Namen die in `tkinter` definiert werden, sondern auch solche die `tkinter` seinerseits aus anderen Modulen importiert.

``as`` ist bei Importen zum umzubennenen beim Importieren da. `GPIO` wird aber gar nicht umbenannt.

`ORDER` ist ein ziemlich allgemeiner Name. `PIXEL_ORDER` wäre ein besserer Name.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Also `led_mode` statt `ledMode`.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. ``global`` ist ein Schlüsselwort das in sauberen Programmen nichts zu suchen hat. Bei Threads kann man sich auch gleich in den Fuss schiessen.

Jede nicht-triviale GUI braucht objektorientierte Programmierung.

Da sind so einige Sachen undefiniert und die GUI-Hauptschleife wird nirgends aufgerufen. Der Code läuft also so *gar nicht*.

Statt die Button-Zustände selbst zu Regeln würde man `tkinter.Radiobutton` nehmen um sicherzustellen das immer nur ein Modus gleichzeitig gedrückt sein kann.

Der Snake-Modus läuft bis zu 3 Sekunden (2 * 60 * 0.025) weiter nachdem `ledMode` geändert wurde und nichts stellt sicher das in der Zeit die Pixel von einem anderen Thread aus gleichzeitig geschaltet werden. Potentiell ein Problem.

Ich würde da auch keinen externen Thread für bemühen, sondern das mit der `after()`-Methode auf Widgets lösen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
StolleJay
User
Beiträge: 3
Registriert: Montag 27. Januar 2020, 20:40

Erst ein mal danke für deine Antwort.

Zur GUI-Hauptschleife... Ja die letzte Zeile (app.mainloop()) muss ich beim Kopieren wohl versehentlich raus gelassen haben. Die ist also drin ;)

Das mit den Namen werde ich dann ändern. Danke dafür.
So lange bin ich noch nicht bei Python.

Das mit dem Schalten der LEDs etc. ist so auch noch nicht komplett. Ist erst mal nur test weise so wie es jetzt ist da mein Hauptproblem erst mal das ist das ich die GUI weiterhin bedienen kann auch wenn ein "Effekt" aktiv ist. Später will ich dann auch auf Radio Buttons und Slider umsteigen aber das nach und nach ;)

Ich habe es wie oben im Beispiel mit threading und auch mit multiprocessing sowie mit der `after()`-Methode probiert doch bekomme ich es einfach nicht hin. Irgend etwas hängt sich dann immer auf so das ich ggf. das Programm via "kill -9 PID" killen muss.
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

multiprocessing ist dafür der komplette Overkill. After oder NUR EIN THREAD der dann verschiedene Aufgaben per Queue bekommt & abarbeitet. Aber wenn es mit after geht ( und das sollte es) ist das die beste Wahl.
Benutzeravatar
__blackjack__
User
Beiträge: 13115
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@StolleJay: Da fehlt mehr als der Aufruf von `mainloop()`. Mindestens zwei Buttons und die `staticColor*`-Namen sind undefiniert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
StolleJay
User
Beiträge: 3
Registriert: Montag 27. Januar 2020, 20:40

Da muss irgend etwas mit dem Copy&Paste nicht funktioniert haben...

Hier noch mal der ganze Code jetzt.

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import os
import time
import threading
import board
import neopixel
try:
    # for Python2
    from Tkinter import *   ## notice capitalized T in Tkinter 
except ImportError:
    # for Python3
    from tkinter import *   ## notice lowercase 't' in tkinter here
import RPi.GPIO as GPIO
import configparser

path = "/home/myApps/"

config = configparser.ConfigParser()
config.read('%srpiCLS.ini' %path)

ORDER = neopixel.GRB

ledCountGet = config.getint('strip', 'ledcountval')
ledModeGet = config.getint('strip', 'ledMode')

#Variables
appName = "RPi CLS (Custom RGB LED Strip)"
appVersion = "v1.0"
num_pixels = ledCountGet
ledMode = ledModeGet
staticColorR = config.getint('static', 'red')
staticColorG = config.getint('static', 'green')
staticColorB = config.getint('static', 'blue')

pixels = neopixel.NeoPixel(board.D18, num_pixels, brightness=0.2, auto_write=False, pixel_order=ORDER)

def restart_program():
	python = sys.executable
	os.execl(python, python, * sys.argv)
    
def mode_off():
    global ledMode
    
    ledMode = 0

    if(ledMode == 2):
        mode_snake_do()
        
    modeOffButton['state'] = NORMAL
    modeStaticButton['state'] = NORMAL
    modeSnakeButton['state'] = NORMAL
    
    pixels.fill((0, 0, 0))
    pixels.show()
def mode_static():
    global ledMode
    
    if(ledMode != 1):
        ledMode = 1
        
        modeOffButton['state'] = NORMAL
        modeStaticButton['state'] = DISABLED
        modeSnakeButton['state'] = NORMAL
        
        pixels.fill((int(staticColorR), int(staticColorG), int(staticColorB)))
        pixels.show()

def mode_snake_do():
    global ledMode
    
    while True:
        if(ledMode == 2):
            for x in range(0, num_pixels):
                pixels[x] = (0, 32, 0)
                pixels.show()
                time.sleep(0.025)
            for x in range(0, num_pixels):
                pixels[x] = (0, 0, 0)
                pixels.show()
                time.sleep(0.025)
            continue #Restart while loop
def mode_snake():
    global ledMode
    
    ledMode = 2
    
    modeOffButton['state'] = NORMAL
    modeStaticButton['state'] = NORMAL
    modeSnakeButton['state'] = DISABLED
    
    t = threading.Thread(target=mode_snake_do)
    t.start()

def settingsWindow():
    def saveConfig():
        config.set('strip', 'ledcountval', ledCountSet.get())
        config.set('static', 'red', staticColorRVal.get())
        config.set('static', 'green', staticColorGVal.get())
        config.set('static', 'blue', staticColorBVal.get())
        with open('%srpiCLS.ini'  %path, 'w') as configfile:
            config.write(configfile)
            
        restart_program()
    
    self = Toplevel()
    self.title("RPi CLS Settings")
    self.minsize(300, 0)
    self.geometry("+500+200")
    self.resizable(width=False, height=False)
    
    LEDCountGrid = Frame(self)
    LEDCountGrid.pack(side=TOP, anchor=W, fill=X, pady=2)
    staticColorGrid = Frame(self)
    staticColorGrid.pack(side=TOP, anchor=W, fill=X, pady=2)
    
    hrLine2 = Frame(self, bg="gray", height=1)
    hrLine2.pack(side=TOP, anchor=W, fill=X, pady=4)
    
    gridBottomSettings = Frame(self)
    gridBottomSettings.pack(side=BOTTOM, anchor=W, fill=X)
    
    #LED Settings
    LEDCLabel = Label(LEDCountGrid, text="LED Count:").pack(side=LEFT, padx=(5, 5))
    ledCountSet = Entry(LEDCountGrid, width=2)
    ledCountSet.pack(side=LEFT)
    ledCountSet.insert(0, num_pixels)
    LEDCLabel2 = Label(LEDCountGrid, text="(1 - 60), 0 = None").pack(side=LEFT, padx=(5, 0))
    
    staticCLabel = Label(staticColorGrid, text="Static Color:").pack(side=LEFT, padx=(5, 5))
    staticColorRLabel = Label(staticColorGrid, text="R:").pack(side=LEFT)
    staticColorRVal = Entry(staticColorGrid, width=3)
    staticColorRVal.pack(side=LEFT)
    staticColorRVal.insert(0, staticColorR)
    staticColorGLabel = Label(staticColorGrid, text="G:").pack(side=LEFT)
    staticColorGVal = Entry(staticColorGrid, width=3)
    staticColorGVal.pack(side=LEFT)
    staticColorGVal.insert(0, staticColorG)
    staticColorBLabel = Label(staticColorGrid, text="B:").pack(side=LEFT)
    staticColorBVal = Entry(staticColorGrid, width=3)
    staticColorBVal.pack(side=LEFT)
    staticColorBVal.insert(0, staticColorB)
    staticCLabel2 = Label(staticColorGrid, text="(0 - 255)").pack(side=LEFT, padx=(5, 0))
    
    #Other Settings
    closeButton = Button(gridBottomSettings, text="Close", command=self.destroy).pack(side=RIGHT)
    saveButton = Button(gridBottomSettings, text="Save", command=saveConfig).pack(side=RIGHT)

#GUI Window
app = Tk()
app.title(appName)
app.minsize(500, 0)
app.geometry("+400+200")
app.resizable(width=False, height=False)
#img = Image("photo", file="rpiCLS_icon.png")
#app.call('wm', 'iconphoto', app._w, img)

#Grids
modesGrid = Frame(app)
modesGrid.pack(side=TOP, anchor=W, fill=X, pady=2)

hrLine = Frame(app, bg="gray", height=1)
hrLine.pack(side=TOP, anchor=W, fill=X, pady=4)

gridBottom = Frame(app)
gridBottom.pack(side=BOTTOM, anchor=W, fill=X)

#Modes & Settings
modeOffLabel = Label(modesGrid, text="Modes:").pack(side=LEFT, padx=(5, 0))
modeOffButton = Button(modesGrid, text="Off", fg="black", borderwidth=2, relief=GROOVE, command=mode_off)
modeOffButton.pack(side=LEFT)
modeStaticButton = Button(modesGrid, text="Static", fg="black", borderwidth=2, relief=GROOVE, command=mode_static)
modeStaticButton.pack(side=LEFT)
modeSnakeButton = Button(modesGrid, text="Snake", fg="black", borderwidth=2, relief=GROOVE, command=mode_snake)
modeSnakeButton.pack(side=LEFT)

#Other Settings
vLabel = Label(gridBottom, text=appVersion, fg="darkgray").pack(side=LEFT)
quitButton = Button(gridBottom, text="Quit", command=quit).pack(side=RIGHT)
settingsButton = Button(gridBottom, text="Settings", command=settingsWindow).pack(side=RIGHT)

app.mainloop()
Ich habe es eben noch ein mal mit der after()-Methode versucht aber da hängt das ganze Programm wieder sobald der Effekt aktiv ist.
Benutzeravatar
__blackjack__
User
Beiträge: 13115
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@StolleJay: Dann machst Du was falsch. Was weiss keiner, weil keiner weiss was Du machst. Und da sind ja noch ein Haufen andere Sachen die Du angehen solltest. ``global`` und kein Code auf Modulebene der da nicht hin gehört als erstes.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten