lange und kurze Tastendrücke (python-evdev)

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Hallo liebe Leute,


ihr habt mir ja schon einige Male großartig geholfen (hier und hier), deshalb versuche ich es noch einmal :wink:


Es geht um an den Raspberry Pi angeschlossene Taster, die sich dank devicetree overlays und dem Kerneltreiber gpio-key wie eine Tastatur verhalten.
Bis jetzt verwende ich python-evdev um die Events mitzubekommen und das meiste funktioniert, allerdings schaffe ich es nicht lange Tastendrücke zu erkennen. Laut [1] sollten lange Tastendrücke einen Event mit value=2 erzeugen, aber wenn ich einen Taster lange drücke kommt kein solcher event

Code: Alles auswählen

# python3 -m evdev.evtest
ID  Device               Name                                Phys                                Uniq
-----------------------------------------------------------------------------------------------------
0   /dev/input/event0    button@19                           gpio-keys/input0                        
1   /dev/input/event1    button@18                           gpio-keys/input0                        
2   /dev/input/event2    soc:shutdown_button@17              gpio-keys/input0                        
3   /dev/input/event3    rotary@a                                                                    
4   /dev/input/event4    rotary@1b                                                                   

Select devices [0-4]: 1
Listening for events (press ctrl-c to exit) ...
time 1628979450.041453 type 1 (EV_KEY), code 59   (KEY_F1), value 1
time 1628979450.041453 --------- SYN_REPORT --------
time 1628979466.986278 type 1 (EV_KEY), code 59   (KEY_F1), value 0
time 1628979466.986278 --------- SYN_REPORT --------
Hier habe ich den Taster etwa 16 Sekunden gedrückt, aber es wurde das Drücken (value 1) und loslassen (value 0) erfasst.
Gibt es da vielleicht irgendeinen Trick, dass auch Events für lange Tastendrücke erzeugt werden – gefunden habe ich in der Dokumentation darüber nichts.

lg smutbert


[1] https://github.com/gvalkov/python-evdev/issues/70
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich glaube das ist ein Missverständnis. Das ist wenn ein repeat-Event. Siehe zb hier: https://stackoverflow.com/questions/209 ... put-device

Wenn dir das trotzdem reicht (du wirst aber dann viele davon bekommen), dann kann man das ggf Konfigurieren.

Ansonsten muss man für sowas eben einen Timer beim press starten, der dann vom Release gestoppt wird. Oder eben triggert, dann war es ein Long press.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Ohje, danke.
Leider habe ich schon einmal aus einem anderen Grund (vergeblich) versucht die "Tastaturwiederholungsrate" zu konfigurieren bzw. aktivieren – das scheint in Verbindung mit gpio-keys nicht möglich zu sein.
Da gibt es nicht zufällig ein fertiges python-Modul, das die Sache vereinfacht?

(gpiozero statt den Kerneltreibern habe ich schon ausprobiert. Das bietet für lange Tastendrücke zwar eigene Events/Callback-Funktionen, aber verglichen mit den Kerneltreibern treibt es meinen Beobachtungen nach die CPU-Last in die Höhe und es füht sich nicht so „flüssig“ an.)

Aktuell habe ich den Beispielcode in der python-evdev-Dokumentation [1] für meine Zwecke angepasst und diese Klasse geschrieben.

Code: Alles auswählen

#!/usr/bin/env python3

import threading
import queue
import evdev
import selectors

INPUT_DEVICES = [
                '/dev/input/event0',
                '/dev/input/event1',
                '/dev/input/event3',
                '/dev/input/event4',
]

INPUT_EVENTS = {
                59: { 1: 'play'},
                60: { 1: 'select'},
                5: { 1: 'right', -1: 'left'},
                6: { 1: 'up', -1: 'down'}
}


class Input:
	def __init__(self):
		self.queue = queue.Queue()
		self.selector = selectors.DefaultSelector()
		for i in INPUT_DEVICES:
			device = evdev.InputDevice(i)
			self.selector.register(device, selectors.EVENT_READ)
			device.grab()

		self.thread = threading.Thread(target=self.__keep_reading__)
		self.thread.daemon = True
		self.thread.start()

	def __input_to_command__(self, event):
		try:
			self.queue.put(INPUT_EVENTS[event.code][event.value])
		except KeyError:
			return

	def __keep_reading__(self):
		while True:
			for key, _mask in self.selector.select():
				for event in key.fileobj.read():
					self.__input_to_command__(event)

	def get(self, timeout):
		return self.queue.get(timeout=timeout)
Ich bin sicher, dass ich da (wieder) vieles suboptimal gemacht habe, aber außerdem scheint mir das denkbar ungünstig zu sein um da für jeden Taster einen Timer einzubauen?


[1] https://python-evdev.readthedocs.io/en/ ... -selectors
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man erfindet keine eigenen __dingsbums__-Methoden. Diese Namenskonvention dient dazu dem Leser mitzuteilen, dass da etwas besonderes passiert, das abhaengt von der Python-Semantik. ZB __init__, dass eben aufgerufen wird, wenn das Objekt initialisiert werden soll. Das stimmt fuer selbstausgedachte Sachen natuerlich nicht.

Man benutzt auch ohne Not keine __namen, die sind *nicht* als Ersatz fuer private Attribute gedacht, sondern zur Vermeidung von Namenskollisionen, und man handelt sich subtile Bugs ein, wenn man das Feature nicht gut versteht.

Zu deinem eigentlichen Problem: einen Timer pro Taste zu haben ist an sich ueberhaupt kein Problem. Da der evdev-Code basierend auf dem select/poll-File-Descriptor-Ansatz ist, kann man auch einfach Timer-File-Descriptoren machen, und dadurch bekommt man dann einen zentralen Mainloop, der einen wieder aufweckt, wenn irgendwas passiert ist. Timer oder Taste. Siehe zB https://abelbeck.wordpress.com/2014/01/ ... n-example/

Ich wuerde dir aber raten, auf gpiozero zu wechseln. Deine Beobachtungen in allen Ehren, aber ich sehe keinen technischen Grund dafuer, und solche Konzepte umzusetzen ist fuer Anfaeger oft eine Ueberforderung. Darum haben die sowas ja eingebaut. Ich vermute mal eher, du hast einen Fehler bei der Gestaltung der Hautpschleife gemacht, und dadurch eine Endlosschleife erzeugt, die dann das Verhalten erklaert. Das darf natuerlich nicht sein, gilt aber auch hier.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Ok verstanden und Dankeschön.
Also keine Methoden, die mit __ beginnen/aufhören. (Ich habe das im Netz an vielen Stellen anders gelesen und gesehen, aber das heißt ja nichts ☺)

Das mit der CPU-Last werde ich mir noch einmal genau ansehen, aber zuerst muss ich das mit den Callbacks hinbekommen. Eventuell bitte ich da noch einmal um Hilfe, wenn mir die Ideen ausgegangen sind...
Benutzeravatar
__blackjack__
User
Beiträge: 13102
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@smutbert: An vielen Stellen? Ich kenne nur recht wenige. Wo findet man so etwas?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

In meinem Browser sind die meiste Zeit viele Tabs offen und die zu dem Thema habe ich alle nach __deets__ Antwort geschlossen.
Bis jetzt habe ich davon kaum etwas wieder gefunden, aber ich habe noch von mindestens einer Frage auf stackoverflow und einem Blog die Erklärung (ok, das ist zugegebenermaßen noch nicht viel) in Erinnerung, dass _x_ (oder _x ?) ein leichter Hinweis sein soll, dass die Methode nur zur Verwendung innerhalb der Klasse gedacht ist und dass __x__ dasselbe in verschärfter Form bedeutet. Zumindest bei den beiden war nicht die Rede davon, dass man solche Namen selbst nicht verwenden soll.

Vielleicht habe ich nach der Lektüre der falschen Erklärungen auch viele richtige (vielleicht zum Teil etwas schwammige oder unvollständige) Erklärungen falsch verstanden – man (ich) versteht, sieht, liest ja oft eher das was man erwartet und nicht das was da wirklich steht. ;)
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Einfacher unterstrich ja. Doppelter nein, macht was anderes, als du denkst. Aber in beiden Fällen geht es dabei nur um den Anfang. Vorne & hinten ist “falsch” im Sinne von macht man nicht, weil es eine anders belegte Konvention ist.
Benutzeravatar
__blackjack__
User
Beiträge: 13102
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Um den Style Guide for Python Code zu zitieren:
__double_leading_and_trailing_underscore__: “magic” objects or attributes that live in user-controlled namespaces. E.g. __init__, __import__ or __file__. Never invent such names; only use them as documented.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Die dunders sind weg, aber jetzt habe ich bei gpiozero ein Problem, das ich nicht verstehe. Als Ersatz für die gepostete Input-Klasse mit evdev habe ich dasselbe mit gpiozero umgesetzt. Wenn ich eine gekürzte funktionierende Variante posten darf

Code: Alles auswählen

#!/usr/bin/env python3

import queue
import gpiozero

class Input:
        def __init__(self):
                self.queue = queue.Queue()

                self.setup_elements()
                self.set_callbacks()

        def setup_elements(self):
                self.select_encoder = gpiozero.RotaryEncoder(27, 22)
                self.volume_encoder = gpiozero.RotaryEncoder(10, 9)

        def set_callbacks(self):
                self.select_encoder.when_rotated_counter_clockwise = lambda: self.queue.put('left')
                self.select_encoder.when_rotated_clockwise = lambda: self.queue.put('right')
                self.volume_encoder.when_rotated_counter_clockwise = lambda: self.queue.put('down')
                self.volume_encoder.when_rotated_clockwise = lambda: self.queue.put('up')

        def get(self, timeout=None):
                return self.queue.get(block=True, timeout=timeout)
wenn ich das versuche die Drehimpulsgeber und deren Callbacks in Schleifen zu definieren funktioniert es nicht mehr – es verhält sich so als würde das Setzen des Callbacks des zweiten Drehimpulsgebers ('volume') das des ersten Überschreiben und in der queue landen nur mehr 'down' und 'up' Einträge unabhängig davon welchen Drehimpulsgeber ich betätige:

Code: Alles auswählen

#!/usr/bin/env python3

import queue
import gpiozero

ENCODERS = {
        'select': { 'pins': ( 27, 22 ), 'events': ( 'left', 'right' ) },
        'volume': { 'pins': ( 10, 9 ), 'events': ( 'down', 'up' ) },
}

class Input:
        def __init__(self):
                self.queue = queue.Queue()
                self.encoders = {}

                self.setup_elements()
                self.set_callbacks()

        def setup_elements(self):
                for encoder in ENCODERS.keys():
                        self.encoders[encoder] = gpiozero.RotaryEncoder(*ENCODERS[encoder]['pins'])

        def set_callbacks(self):
                for encoder in ENCODERS.keys():
                        self.encoders[encoder].when_rotated_counter_clockwise = lambda: self.queue.put(ENCODERS[encoder]['events'][0])
                        self.encoders[encoder].when_rotated_clockwise = lambda: self.queue.put(ENCODERS[encoder]['events'][1])

        def get(self, timeout=None):
                return self.queue.get(block=True, timeout=timeout)
ich finde da keinen prinzipiellen Fehler
Benutzeravatar
__blackjack__
User
Beiträge: 13102
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Die ``lambda``-Ausdrücke nehmen nur den Namen `encoder` mit, nicht dessen Wert. Erst wenn die ``lambda``-Funktion aufgerufen wird, wird der *aktuelle* Wert von `encoder` ermittelt, und das ist der vom letzten Schleifendurchlauf.

Schau Dir mal `functools.partial()` an. Und ich würde den Wert für die `put()`-Aufrufe auch schon in dieser Schleife auflösen.

Das sieht übrigens alles zu weit eingerückt aus. Konvention sind vier Leerzeichen pro Ebene.

Nachtrag: Wenn Du am Ende *immer* sowohl Schlüssel als auch Wert brauchst, mach gleich eine Schleife über Schlüssel/Wert-Paare, statt nur über die Schlüssel.

Die beiden Extra-Methoden sind auch ein bisschen Overkill.

Code: Alles auswählen

#!/usr/bin/env python3
import queue
from functools import partial

import gpiozero

ENCODERS = {
    "select": {"pins": (27, 22), "events": ("left", "right")},
    "volume": {"pins": (10, 9), "events": ("down", "up")},
}


class Input:
    def __init__(self):
        self.queue = queue.Queue()
        self.encoders = {}
        for name, data in ENCODERS.items():
            encoder = gpiozero.RotaryEncoder(*data["pins"])
            direction_a, direction_b = data["events"]
            encoder.when_rotated_counter_clockwise(
                partial(self.queue.put, direction_a)
            )
            encoder.when_rotated_clockwise(
                partial(self.queue.put, direction_b)
            )
            self.encoders[name] = encoder

    def get(self, timeout=None):
        return self.queue.get(timeout=timeout)
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Oh, an die functools.partial kann ich mich natürlich erinnern – das hast du mir in mein Hauptprogramm eingebaut und auch wenn ich sonst einiges geändert habe, steht das noch immer dort.
Meinst du die Zuordnung könnte/sollte etwa so aussehen?

Code: Alles auswählen

from functools import partial

encoder_to_event = {
    "select": {
        "pins": (27, 22),
        "events": (
            partial(queue.put, "left"),
            partial(queue.put, "right")
        )
    }
}
(für die Einrückungen mache ich zumindest zum Teil die Forensoftware verantwortlich, aber ich bemühe mich mich zu bessern ☺)

edit:
Hoppla, das hat sich jetzt etwas überschnitten. Du meinst es also nicht so!
__blackjack__ hat geschrieben: Freitag 20. August 2021, 20:59 Nachtrag: Wenn Du am Ende *immer* sowohl Schlüssel als auch Wert brauchst, mach gleich eine Schleife über Schlüssel/Wert-Paare, statt nur über die Schlüssel.
Dass solche Dinge in python möglich sind vergesse ich immer wieder.
__blackjack__ hat geschrieben: Freitag 20. August 2021, 20:59 Die beiden Extra-Methoden sind auch ein bisschen Overkill.
Das habe ich mir zuerst auch gedacht, aber insgesamt habe ich da in meiner Klasse am Ende, 2 Schalter mit je 2 Events (mit der Option auf bis zu 6 Taster mit dann insgesamt 12 Events), die beiden Drehimpulsgeber ebenfalls mit je zwei Events und zwei thematisch nur bedingt dazupassende LEDs.
Damit verdreifacht sich die Zahl der Schleifen immerhin schon einmal und ich habe es mit eigenen Methoden übersichtlicher gefunden.

wieder einmal Dankeschön!
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Jetzt muss ich noch einmal auf das Thema python-evdev oder python-gpiozero kommen. Ich habe jetzt nämlich zwei untereinander austauschbare Klassen, ähnlich der, die ich bereits gepostet habe, wobei eine davon gpiozero und die andere evdev nutzt und die hohe CPU-Last tritt nur mit gpiozero auf.
__deets__ hat geschrieben: Sonntag 15. August 2021, 12:42[...]
Ich wuerde dir aber raten, auf gpiozero zu wechseln. Deine Beobachtungen in allen Ehren, aber ich sehe keinen technischen Grund dafuer, und solche Konzepte umzusetzen ist fuer Anfaeger oft eine Ueberforderung. Darum haben die sowas ja eingebaut. Ich vermute mal eher, du hast einen Fehler bei der Gestaltung der Hautpschleife gemacht, und dadurch eine Endlosschleife erzeugt, die dann das Verhalten erklaert. Das darf natuerlich nicht sein, gilt aber auch hier.
Ich habe versucht das mit möglichst wenig Code zu provozieren und es passiert auch mit diesem übersichtlichen Progrämmchen

Code: Alles auswählen

#!/usr/bin/env python3

import gpiozero
import time

from functools import partial

ENCODERS = {
	'select': { 'pins': ( 27, 22 ), 'events': ( 'left', 'right' ) },
	'volume': { 'pins': ( 10, 9 ), 'events': ( 'down', 'up' ) },
}

encoders = {}
for name, data in ENCODERS.items():
	encoder = gpiozero.RotaryEncoder(*data['pins'])
	encoder.when_rotated_counter_clockwise = partial(print, data['events'][0])
	encoder.when_rotated_clockwise = partial(print, data['events'][1])
	encoders[name] = encoder

while True:
	time.sleep(0.5)
in top sieht das so aus

Code: Alles auswählen

top - 12:15:18 up 48 min,  2 users,  load average: 0.89, 0.30, 0.10
Tasks: 138 total,   2 running, 136 sleeping,   0 stopped,   0 zombie
%Cpu(s):  8.5 us, 11.6 sy,  0.0 ni, 79.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :    909.9 total,    505.7 free,     81.9 used,    322.4 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    770.6 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  733 root      20   0   26884  10668   5496 S  42.5   1.1   0:27.34 python3
  [...]
Es scheint allerdings abhängig von einen der beiden Drehimpulsgebers zu passieren – dann ist die CPU-Last allerdings konstant hoch.

Der fragliche Drehimpulsgeber hat keine Rasterung und eine optische Abtastung – es könnte also sein, dass er in einigen Stellungen ständig zwischen zwei Zuständen hin und her schwingt/wackelt.

Mein Verdacht ist, dass das dem Kernel-Treiber (rotary-encoder) nichts ausmacht (da gibt es auch in den Kernelprozessen keine auffällige CPU-Last), während gpiozero unter diesen Umständen diese CPU-Last verursacht.
Kann ich irgendwie nachprüfen ob die Last in der RotaryEncoder-Klasse und in welcher Instanz dieser Klasse die Last verursacht wird?

Allerdings verursacht dieses Verhalten bis jetzt keine ernstzunehmenden Probleme, weder ist die Temperatur auffällig hoch noch läuft irgendetwas zäh.
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Also so fein Granular zu Profilen ist mE nicht möglich. Ich bin aber immer noch ein bisschen erstaunt, dass der soooo viel last erzeugt. Klar ist das im Kernel ein bisschen effizienter, aber das der da gleich auf 40% rumrödelt ist schon hart. Gibt der denn auch andauern was aus? Auch wenn du gar nichts machst?
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Nein, Ausgabe gibt es keine – bis auf die Last verhält sich alles wie erwartet.

Wenn ich dem fraglichen Drehimpulsgeber einen kleinen Ruck gebe (und das nötigenfalls ein paar Mal wiederhole) ist die CPU-Last wieder auf dem üblichen niedrigen Niveau. Deshalb auch mein Verdacht, dass der Drehimpulsgeber da gerade zwischen 2 Zuständen „zittert“ und gpiozero damit nicht so gut zurecht kommt.
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Aber sollte der dann nicht auch neue Werte liefern? Selbst wenn die immer nur +/- 1 schwanken?

Wenn das wirklich dein Problem ist, dann sollte da ggf. ein microcontroller oder ein Schmitt trigger zwischen.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Die beiden Ausgänge des Drehimpulsgeber beim Drehen insgesamt vier Zustände durchlaufen, im Uhrzeigersinn z. B.:
  1. 00
  2. 10
  3. 11
  4. 01
und das wiederholt sich dann. An der Abfolge der Zustände wird die Drehrichtung erkannt und meistens (auch bei mir) ist es so, dass 4 Zustandsänderungen in einer Richtung, also ein kompletter Zyklus dieser 4 Zustände, als ein „Impuls“ gewertet wird.

Ein Drehimpulsgeber könnte also ohne weiteres, etwa durch Vibrationen tatsächlich zwischen zwei Zuständen zittern ohne, dass es neue Werte gibt und bei 400 Impulsen, also 1600 Zustandsänderungen pro Umdrehung kommt mir das auch einigermaßen plausibel vor.

Bei nächster Gelegenheit werde ich die Zustände einmal überwachen und prüfen ob die sich bei erhöhter CPU-Last tatsächlich ändern und bei niedriger Last gleich bleiben.
Antworten