Konzeptfrage zu Endlosschleife und Queue

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
kaloschke
User
Beiträge: 7
Registriert: Sonntag 25. Februar 2018, 11:32

Hallo,
für ein kleines Radioprojekt (RPi Zero W2 mit 240x240 Display) benötige ich einerseits eine Laufschrift und andererseits die Möglichkeit auf die Eingabe einer Fernbedienung zu reagieren.
Ich habe für jeden dieser Bereiche sehr gut funktionierenden Code gefunden.

Für die Laufschrift:

Code: Alles auswählen

size_x= img_draw.textlength(MESSAGE, font)
text_x = 240
text_y = 15
t_start = time.time()
while True:
    x = (time.time() - t_start) * 25
    x %= (size_x + disp.width)
    draw.rectangle((0, 0, 240, 60), (226, 226, 226))  #draw ist
    draw.text((int(text_x - x), text_y), MESSAGE, font=font, fill=(0, 0, 0))
    time.sleep(0.1)
    img_bg.paste(img_scroll,(0,179))
    disp.display(img_bg)
Für die Oberfläche wird PIL benutzt.

Für die Eingaben würde ich gerne Queues benutzen, um "in Ruhe" auf die Ereignisse reagieren zu können.

Code: Alles auswählen

def send_key(queue, thread_event):
    while not thread_event.is_set():
        for key, mask in selector.select():  
            device = key.fileobj
            for event in device.read():
                if event.type == 1:
                    if event.value == 0:
                       rf_key = event.code
                       queue.put(rf_key)

def get_key(queue, thread_event):
    while not thread_event.is_set() or not queue.empty():
        rf_key = queue.get()
        match rf_key:
            case 115:   # Volume up
                .
                .
                .
if __name__ == "__main__":
    pipeline = queue.Queue(maxsize=10)
    thread_event = threading.Event()
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        executor.submit(send_key, pipeline, thread_event)
        executor.submit(get_key, pipeline, thread_event)
Auch dieser Teil funktioniert gut.

Mein Problem ist nun, wie ich diese Teile zusammen bekomme. Ich hatte hierzu schon eine nicht stabile C++ Lösung mit Timern, bei der ich die Ursache der Instabilität nicht herausfinden konnte.
Ich hoffe nun auf diesem Weg erfolgreicher zu sein.

Viele Grüße
Sirius3
User
Beiträge: 17759
Registriert: Sonntag 21. Oktober 2012, 17:20

Welches Framework benutzt Du denn für die Darstellung?
Einen ThreadPool benutzt man nur, wenn man auch den Pool nutzt. Du hast aber nur zwei einzelne Threads.
Warum hast Du zwei Threads? Du benutzt viele globale Variablen in Deinen Threads, das darf so nicht bleiben.

match ist nicht dafür da, einfache if-Abfragen zu ersetzen. Die while-Schleife sieht komisch aus, zum einen hast Du ein blockierendes select/get zum anderen wartest Du auf irgendwelche Events, bzw. wartest Du ja nicht.

So lange Du nicht wirklich Rechnungen hast, die lange dauern, würde ich das mit den Threads wieder vergessen.

Das ganze in einer Schleife:

Code: Alles auswählen


VOLUME_UP_KEY = 115

class Ticker:
    def __init__(self, message, font, text_x, text_y, draw, img_bg, img_scroll):
        self.message = message
        self.font = font
        self.draw = draw
        self.img_bg = img_bg
        self.img_scroll = img_scroll
        self.size_x = img_draw.textlength(message, font)
        self.text_x = text_x
        self.text_y = text_y
        self.start = time.time()
        self.last_x = None
    
    def update(self, disp):
        new_x = int((time.time() - self.start) * 25)
        new_x = text_x - new_x % (self.size_x + disp.width)
        if new_x != self.last_x:
            self.draw.rectangle((0, self.text_y - 15, disp.width, self.text_y + 45), (226, 226, 226))
            self.draw.text(new_x, text_y), self.message, font=self.font, fill=(0, 0, 0))
            self.img_bg.paste(self.img_scroll, (0, 179))
            self.disp.display(self.img_bg)
            self.last_x = new_x


def get_key(device):
    event = device.read()
    if event.type == 1 and event.value == 0:
        return event.code
    return None


def main():
    ...
    ticker = Ticker(message, font, 240, 15, draw, img_bg, img_scroll)
    while True:
        ticker.update()
        if selector.select(timeout=0.1):
            key = get_key(device)
            if key == VOLUME_UP_KEY:
                ...

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kaloschke: Beides sieht unvollständig aus.

Beim zweiten macht es keinen Sinn zwei Sachen als Threads zu starten um dann im Hauptthread *nichts* zu machen. Wenn man `get_key()` mit in eine Hauptschleife integriert, wo man dann auch die Laufschrift aktualisieren kann, wäre das Problem gelöst. Ein Schritt der ``while``-Schleife aus dem ersten Quelltext sollte in einer Funktion oder Methode stecken, die man dann in dieser neuen Hauptschleife aufruft.

Bei `time.time()` muss die Zeit nicht vorwärts laufen. Wenn man diese Eigenschaft braucht, nimmt man `time.monotonic()`.

Es sieht so aus als würde ``match …:`` als einfaches ``switch`` aus anderen Programmiersprachen missbraucht, statt da einfach ``if``/``elif``(/``else``) zu verwenden. Wenn man mit den ``case``-Anweisungen keine Werte an Namen zuweist, dann ist dass nicht das richtige Sprachmittel IMHO.

`thread_event` ist als Namen zu allgemein. Als Leser möchte man wissen was dieses Ereignis *bedeutet*.

`send_key()` macht im Grunde genau das Gegenteil vom Namen. In dieser Funktion werden die Tastendrücke von der Fernbedienung *empfangen* und nicht gesendet.

Für beide Programme: Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherwesie in einer Funktion die `main()` heisst.

`selector` ist nirgends definiert. Das müsste als Argument in `send_key()` kommen und in der Hauptfunktion definiert werden.

Wenn `selector` das ist was ich denke was es ist, hat der Code an der Stelle ein Problem: da ist ein blockierender Aufruf der dafür sorgt das die Schleife erst abgebrochen werden kann wenn irgendeine Taste gedrückt wird, weil sonst die äussere Schleifenbedingung gar nichtb geprüft wird.

`concurrent.futures` macht hier keinen Sinn. Das Benutzt man wenn man an Ergenissen aus den Threads interessiert ist. Nicht wenn man da als Aufgaben *Endlosschleifen* startet. Dafür nimmt man einfach `threading.Thread`.

Für magische Zahlen wie 115 sollte man sinnvoll benannte Konstanten definieren, statt als Kommentar dran zu schreiben was sie bedeuten. Es gibt da ein `enum`-Modul in der Standardbibliothek. Was man mehr oder weniger benutzen *muss* wenn man das tatsächlich mit ``match``/``case`` machen will, was wie gesagt nicht einfach ein Ersatz für ``switch`` aus beispielsweise C oder C++ ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
kaloschke
User
Beiträge: 7
Registriert: Sonntag 25. Februar 2018, 11:32

Hallo
und vielen Dank für die schnellen und ausführlichen Antworten.
Ich hielt es für sinnvoll, nur den meiner Meinung nach wesentlichen Code zu posten, sehe jetzt aber ein, dass es so nicht möglich ist, eine vollständig geeignete Antwort zu erstellen.
Daher einige zusätzliche Informationen:

Für das oben angesprochene Radioprojekt soll auf dem Display einerseits das Logo des aktuellen Senders dargestellt werden und darunter in einem Laufband Infos zum aktuellen Titel.
Das Display spreche ich über einen Treiber direkt im Programm an. So erspare ich mir X11 und kann das Script direkt über die SSH-Kommandozeile starten. Für das GUI benutze ich wie oben angegeben das Modul PIL. Da ich aufgrund der Einbaus in ein Gehäuse den Inhalt des Displays drehen muss, werden die beiden o.a. Fenster in ein Backgroundimage kopiert - anderenfalls erfolgt eine fehlerhafte Darstellung.
Meine RF-Fernbedienung sendet ihre Inhalte nicht nur über /dev/input/event0, sondern auch über .../event1 bis .../event 3. Dies kann ich mit der Library "evdev" gut behandeln.

Zu meinen - beschränkten - Fähigkeiten:
Ich bin relativ neu in Python und habe nicht den Anspruch alles umfänglich zu beherrschen. Jedoch erwarte ich keine fertigen Lösungen und bin bereit mich in das Thema zu vertiefen, soweit es das Projekt erfordert. So habe ich mich durchaus mit dem Thema Threads und Asyncio beschäftigt, jedoch gelingt mir hier ein übergreifendes Verständnis nur schwer. Aber bisher ist es mir immer ganz gut gelungen, im Netz gefundene Teile zu einem gewünschten Ganzen zusammenzufügen.

@Sirius3:
So habe ich mir eine Lösung im Prinzip gewünscht, vielen Dank dafür. Leider ist es so, dass die Laufschrift jetzt nicht mehr ruckelfrei läuft, so wie in meiner angegebenen reinen Schleife. Die Ursache vermute ich in der Zeile

Code: Alles auswählen

if selector.select(timeout=0.1):
Vielleicht ist Python dafür doch nicht die geeignete Wahl. Weiterhin verhält sich die - von mir in deinem Code angepasste - Funktion getkey() nicht mehr wie erwartet. In meinem oben geposteten Code wird nur auf KeyUp reagiert

Code: Alles auswählen

   if event.type == 1:
       if event.value == 0:
um die von der FB gesendeten Mehrfach-Events zu unterdrücken.
Wenn ich diesen Code in deinen einbringe, wird ein Code für einen Key erst bei mehrfacher Betätigung erzeugt, ansonsten erscheint immer nur der Wert 4. Hier mögliche Interferenzen verschiedener Eventbehandlungen überschaue ich leider nicht.
Hier nun mein gesamter Code, mit deinem integriert und an manchen Stellen anpasst.

Code: Alles auswählen

#!/usr/bin/env python3
import time
import os
import ST7789
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from evdev import InputDevice
from selectors import DefaultSelector, EVENT_READ

message = "Hello World! How are you today?"

RF_KEY_RIGHT = 106

selector = DefaultSelector()

keybd0 = InputDevice('/dev/input/event0')
keybd1 = InputDevice('/dev/input/event1')
keybd2 = InputDevice('/dev/input/event2')
keybd3 = InputDevice('/dev/input/event3')

# This works because InputDevice has a `fileno()` method.
selector.register(keybd0, EVENT_READ)
selector.register(keybd1, EVENT_READ)
selector.register(keybd2, EVENT_READ)
selector.register(keybd3, EVENT_READ)

# Create ST7789 LCD display class.
disp = ST7789.ST7789(
    height=240,
    width=240,
    rotation=270,
    port=0,
    cs=ST7789.BG_SPI_CS_FRONT,  # BG_SPI_CS_BACK or BG_SPI_CS_FRONT
    dc=9,
    backlight=19,               # 18 for back BG slot, 19 for front BG slot.
    spi_speed_hz=80 * 1000 * 1000,
    offset_left=0,
    offset_top=0
)
# Initialize display.
disp.begin()

img_bg     = Image.new('RGB', (240,240), color=(180, 180, 180))
img_scroll = Image.new('RGB', (240,60))
img_draw   = ImageDraw.Draw(img_scroll)

img_station = Image.open("e_ndr2.png")
img_bg.paste(img_station,(0,0))

sfont = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 40)
    
class Ticker:
    def __init__(self, message, font, text_x, text_y, draw, img_bg, img_scroll):
        self.message = message
        self.font = font
        self.img_draw = img_draw
        self.img_bg = img_bg
        self.img_scroll = img_scroll
        self.size_x = img_draw.textlength(message, font)
        self.size_y = 30
        self.text_x = text_x
        self.text_y = text_y
        self.start = time.time()
        self.last_x = None
    
    def update(self, disp):
        new_x = int((time.time() - self.start) * 25)
        new_x = self.text_x - new_x % (self.size_x + disp.width)
        if new_x != self.last_x:
            self.img_draw.rectangle((0, self.text_y - 15, disp.width, self.text_y + 45), (226, 226, 226))
            self.img_draw.text((new_x, self.text_y), self.message, font=self.font, fill=(0, 0, 0))
            self.img_bg.paste(self.img_scroll, (0, 179))
            disp.display(self.img_bg)
            self.last_x = new_x

def get_key():
    for key, mask in selector.select():
        device = key.fileobj
        for event in device.read():
            print(event)   #Test
            if event.code != 4:
                if event.type == 1:       # Keypress
                    if event.value == 0:  # reagiert auf Up-Key um Mutlikey zu vermeiden
                       return event.code
            return None          #bzw. return event.code  zur Testausgabe

def main():     
    ticker = Ticker(message, sfont, 240, 15, img_draw, img_bg, img_scroll)
    while True:
        ticker.update(disp)
        if selector.select(timeout=0.1):
            key = get_key()
            if key == RF_KEY_RIGHT: 
                print(key)   #Test

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kaloschke: Das Filtern nach „key up“ ist in dem Code von Sirius3 grundsätzlich auch drin. Allerdings wird das auf dem falschen Objekt getestet. Und bei Deinem Code können Ereignisse verlorengehen/unbearbeitet bleiben, weil Du nach dem ersten Ereignis auf den das ”Muster” zutrifft, abbrichst.

Aber von vorne: `os` wird importiert, aber nirgends verwendet.

Da ist viel zu viel Zeug auf Modulebene was da nicht hingehört. Dort sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert.

Wenn man das da alles weg räumt, fällt das bereits erwähnte `selector` auf, was in `get_key()` auf magische Weise verwendet wird, und das `Ticker.__init__()` das Argument `draw` nicht verwendet, dafür aber auf ein globales `img_draw` zugreift.

Man nummeriert keine Namen. Man will sich dann entweder bessere Namen überlegen, oder gar keine Einzelnamen und -werte, sondern eine Datenstruktur verwenden. Oft eine Liste. Oder wie hier: gar keine Namen. Die werden alle nur einmal verwendet, das kann man einfach mit einer Schleife erledigen.

Namen sollten keine kryptsichen Abkürzungen enthalten, oder gar nur daraus bestehen. Wenn man `display` meint, sollte man nicht nur `disp` schreiben. Und das `s` in `sfont` wirft Fragen auf, statt zu klären was diesen `font` von anderen unterscheidet.

Erst auf den `event.code` und dann erst `type` und `value` zu testen ist falsch. So werden *alle* Typen ignoriert die als Code eine 4 haben.

Zwischenergebnis (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import time
from selectors import EVENT_READ, DefaultSelector

import ST7789
from evdev import InputDevice, categorize
from evdev.events import KeyEvent
from PIL import Image, ImageDraw, ImageFont

MESSAGE = "Hello World! How are you today?"


class Ticker:
    def __init__(
        self, message, font, text_x, text_y, background_image, scroller_image
    ):
        self.message = message
        self.font = font
        self.img_draw = ImageDraw.Draw(scroller_image)
        self.background_image = background_image
        self.scroller_image = scroller_image
        self.size_x = self.img_draw.textlength(message, font)
        self.size_y = 30
        self.text_x = text_x
        self.text_y = text_y
        self.start = time.monotonic()
        self.last_x = None

    def update(self, disp):
        new_x = int((time.monotonic() - self.start) * 25)
        new_x = self.text_x - new_x % (self.size_x + disp.width)
        if new_x != self.last_x:
            self.img_draw.rectangle(
                (0, self.text_y - 15, disp.width, self.text_y + 45),
                (226, 226, 226),
            )
            self.img_draw.text(
                (new_x, self.text_y),
                self.message,
                font=self.font,
                fill=(0, 0, 0),
            )
            self.background_image.paste(self.scroller_image, (0, 179))
            disp.display(self.background_image)
            self.last_x = new_x


def get_keys(selector, timeout):
    for key, _ in selector.select(timeout=timeout):
        for event in map(categorize, key.fileobj.read()):
            print(event)  # Test
            if (
                isinstance(event, KeyEvent)
                and event.keystate == KeyEvent.key_up
            ):
                yield event.keycode


def main():
    selector = DefaultSelector()

    # This works because `InputDevice` has a `fileno()` method.
    for i in range(4):
        selector.register(InputDevice(f"/dev/input/event{i}"), EVENT_READ)

    display = ST7789.ST7789(
        port=0,
        cs=ST7789.BG_SPI_CS_FRONT,
        dc=9,
        rotation=270,
        backlight=19,  # 18 for back BG slot, 19 for front BG slot.
        spi_speed_hz=80_000_000,
    )
    display.begin()  # TODO Deprecated‽

    background_image = Image.new("RGB", (240, 240), color=(180, 180, 180))
    background_image.paste(Image.open("e_ndr2.png"), (0, 0))
    ticker = Ticker(
        MESSAGE,
        ImageFont.truetype(
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 40
        ),
        240,
        15,
        background_image,
        Image.new("RGB", (240, 60)),
    )

    while True:
        ticker.update(display)
        for key in get_keys(selector, 0.1):
            if key == "KEY_RIGHT":
                print(key)  # Test


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
kaloschke
User
Beiträge: 7
Registriert: Sonntag 25. Februar 2018, 11:32

:D Tja, was soll ich sagen? Bin begeistert! Es funktioniert fast (s.u.) alles wie erhofft und der Code ist wirklich schön kompakt.
Ich konnte den Timeout in 'get_keys(selector, 0.2)' wie gezeigt drastisch verringern und die Schrift läuft nun sehr geschmeidig.

Eine Frage bleibt: Der Wert von MESSAGE müsste in der 'for key ...' Schleife geändert werden. Wenn ich so ...

Code: Alles auswählen

   
   for key in get_keys(selector, 0.02):
          if key == "KEY_RIGHT":
                background_image.paste(Image.open("e_ffn.png"), (0, 0))
                MESSAGE = "Ich höre am liebsten FFN"
          if key == "KEY_LEFT":
                background_image.paste(Image.open("e_ndr2.png"), (0, 0))
                MESSAGE = "Ich höre am liebsten NDR2"
... vorgehe (am Anfang von 'def main' habe ich 'global MESSAGE' ergänzt), passiert nicht das erwünschte.
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kaloschke: Vergiss bitte ``global`` und die komische Idee *Konstanten* ändern zu wollen. Die Lösung ist im Grunde schon im Programm und heisst objektorientierte Programmierung. Der Text der da angezeigt wird, kommt ja nicht aus `MESSAGE` sondern wird von dort nur initialisiert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
kaloschke
User
Beiträge: 7
Registriert: Sonntag 25. Februar 2018, 11:32

Also so:

Code: Alles auswählen

          if key == "KEY_RIGHT":
                background_image.paste(Image.open("e_ffn.png"), (0, 0))
                ticker = Ticker("Ich höre FFN", ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 40),240,15,background_image,Image.new("RGB", (240, 60)),)
GROSSARTIG. Dieses Problem treibt mich nun schon seit zwei Wochen um und dieses Forum war - fast - meine letzte Hoffnung.

Vielen vielen Dank
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kaloschke: Kann man so machen, ist aber ein bisschen mit Kanonen auf Spatzen wenn man nur die Nachricht verändern will.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
kaloschke
User
Beiträge: 7
Registriert: Sonntag 25. Februar 2018, 11:32

Ja. Mir ist gestern noch im Bett 'ticker.message = ....' eingefallen. Wollte aber nicht mehr aufstehen :D
Allerdings musste ich in 'def update(...)' noch

Code: Alles auswählen

     self.size_x = self.img_draw.textlength(self.message, self.font)
einfügen, da bei neuem Text auch eine neue Textlänge entsteht.

Ich denke, jetzt ist alles gut.
Vielen Dank noch mal!
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@kaloschke: Naja, das wird jetzt dann aber *jedes mal* beim Update gemacht, obwohl das fast immer gleich bleibt. Da würde man eher eine Methode schreiben zum aktualisieren der Nachricht, oder `message` hinter einem `property()` ”verstecken” das bei der Zuweisung diese zusätzliche Arbeit verrichtet. Die Klasse `Ticker` ist wahrscheinlich sowieso etwas, das man noch einmal überarbeiten sollte. Sirius3 hatte da ja nur das minimale gemacht und ergänzt was aus der unvollständigen Schleife ersichtlich war.

Das Hintergrundbild würde man da auch eher nicht ausserhalb verändern, das würde in die Verantwortung der Klasse fallen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten