Events auslösen und darauf reagieren

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
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Hallo allerseits,


nachdem das mein erster Post hier ist, ganz kurz:
Nach einigen Jahren mit GNU/Linux, vor allem Debian, einigen Shell/Bash Skripten und einigen sehr einfachen C-Programmen bin ich durch einige Anwendungen, die mir sehr gefallen und die in python geschrieben sind (zB der Audioplayer Quodlibet), auf python gekommen. Zum Basteln habe ich mir einen Cubietruck (ähnlich dem Raspberry Pi) besorgt, ein paar LEDs und Taster drangehängt und ein lächerlich simples Pythonskript geschrieben, das in einer Endlosschleife die Taster abfragt, damit den mpd (Music Player Daemon) steuert und auch die LEDs entsprechend einiger Funktionen steuert.
Gemeinsam mit einem Freund will ich das ganze jetzt etwas eleganter lösen und weil ich eigentlich keine Ahnung von python habe, komme ich nicht so recht weiter.

Dank des sehr netten und hilfsbereiten gkoala aus dem Cubieboardforum, der WiringPi auf den Cubietruck portiert hat [1], kann ich zB py-gaugette auf dem Cubietruck verwenden um einen Drehimpulsgeber auszulesen. Da ist ein Beispielpropgramm dabei, das mit zwei Schleifen funktioniert:
  • einem Workerthread der die Pins ausliest, an denen der Drehimpulsgeber angeschlossen ist und der laufend die Änderungen der Stellung des Drehknopfes berechnet.
  • der Hauptschleife, die die Änderungen abfragt und ausgibt.
Soweit verstehe ich das ganze einigermaßen und es funktioniert, aber ich habe mir das anders vorgestellt :D Also habe ich aus py-gaugette einen einfachen Thread angepaßt, der jetzt in einer Schleife die Änderungen ausgibt und ungefähr so aussieht:

Code: Alles auswählen

def run(self):
	while True:
		delta = self.encoder.get_delta()
		with self.lock:
			self.delta += delta
			if abs(self.delta) >= 4:
				print "hi there: %d" % (self.delta//4)
				self.delta = self.delta%4
		time.sleep(0.004)
Statt dem "Hi there: %d", das mir die Änderung ausgibt, würde ich gerne eine Art Event auslösen, auf den ich im Hauptprogramm reagieren kann und der mir in irgendeiner Form das delta mit übergeben muss. Kann mir vielleicht jemand auf die Sprünge helfen?


vielen Dank und viele Grüße,
smutbert

[1] http://www.forum-cubieboard.de/Thread-w ... cubietruck
Benutzeravatar
MagBen
User
Beiträge: 799
Registriert: Freitag 6. Juni 2014, 05:56
Wohnort: Bremen
Kontaktdaten:

Ein Event ist nichts anderes als ein Aufruf einer Callback Funktion. Verschiedene GUI Toolkits haben verschiedene Namen dafür, in Java sind es die EventListener und in Qt sind es die Slots und Signals. Du könntest an die run-Funktion eine Callback-Funktion übergeben, die als Parameter das Delta erwartet. Wie Du die Callback-Funktion an die run-Funktion übergeben kannst, das hängt davon ab, wie Du die run-Funktion in Dein Programm eingebaut hast, d.h. vorallem wie Du die run-Funktion in den Thread eingebaut hast.
a fool with a tool is still a fool, www.magben.de, YouTube
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich denke, eine Queue löst das Problem hier besser als ein Event-System. Dazu erstellt man in dem Thread, von wo man den oder die weiteren Thread-Objekte istanziieren möchte, ein Queue-Objekt. Dieses kann man dann an den Thread, der im Eingangsbeitrag gezeigt wurde, übergeben - am Besten als zusätzlichen Parameter seiner ``__init__``-Methode. Daten, die außerhalb dieses Threads verarbeitet werden sollen, legt man dann innerhalb der schon vorhandenen Endlos-Schleife einfach mittels ``self.queue.put(meine_daten)`` in die Queue ab. Anschließend muss man nur noch mit dem selben Queue-Objekt an der Stelle, wo die Daten ausgelesen werden sollen, das Gegenstück - nämlich ``queue.get()`` in einer Schleife ausführen. Letzteres blockiert in seiner Grundeinstellung, falls gerade mal keine Elemente in der Queue vorliegen, weil das Auslesen schneller ging als das Ablegen. Je nachdem, was man genau vor hat, kann dieses Verhalten gut oder schlecht sein. Es lässt sich wahlweise auch auf nicht-blockierend oder mit einem Timeout konfigurieren. Damit sollte man sich als Anfänger aber IMHO erst befassen, wenn der eigentliche Code für das Herumreichen der Informationen lauffähig ist.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Ok, ich sehe schon, ganz so einfach wie ich gedacht habe, wird das wohl nicht. (ich wollte schon früher antworten, aber mein Browser stürzt in diesem Forum reproduzierbar ab, wenn ich Code posten will.)

Es gibt noch ein weiteres Beispiel, diesmal mit Callback Funktionen, die mich etwas verwirren und das ich gerne 100%ig verstehen würde, weil es näher an dem dran ist, was ich will (glaube ich zumindest). Ein gewisser Bob Rathbone [1] hat, offensichtlich ebenfalls ausgehend von py-gaugette, eine Klasse und ein Testprogramm geschrieben:

Code: Alles auswählen

#!/usr/bin/env python
#
# Raspberry Pi Rotary Test Encoder Class
#
# Author : Bob Rathbone
# Site   : http://www.bobrathbone.com
#
# This class uses a standard rotary encoder with push switch
#

import sys
import time
from rotary_class import RotaryEncoder

# Define GPIO inputs
PIN_A = 16       
PIN_B = 20      
BUTTON = 21     

# This is the event callback routine to handle events
def switch_event(event):
        if event == RotaryEncoder.CLOCKWISE:
                print "Clockwise"
        elif event == RotaryEncoder.ANTICLOCKWISE:
                print "Anticlockwise"
        elif event == RotaryEncoder.BUTTONDOWN:
                print "Button down"
        elif event == RotaryEncoder.BUTTONUP:
                print "Button up"
        return

# Define the switch
rswitch = RotaryEncoder(PIN_A,PIN_B,BUTTON,switch_event)

while True:
        time.sleep(0.5)
Dient hier die Schleife nur dazu, dass das Programm weiterläuft (und könnte man das auch anders lösen - mir erscheint es so irgendwie merkwürdig)?

Mir ist auch nicht ganz klar, wofür ich eine Queue brauchen könnte — ich dachte, ich könnte einfach noch ein paar weitere Absätze mit Callback Routinen für die Events hinzufügen (def switch_event(event):…).
Ich habe auch noch das grundsätzliche Problem, dass ich mir für diese Zwecke keine (elegante) Programmstruktur vorstellen kann, weil es zumindest 2 unterschiedliche Modi geben soll. in denen die Bedienelemente unterschiedliche Funktionen haben sollen.

Es gibt zwar genug Leute, die schon Internetradios und dergleichen mit dem Raspberry Pi gebaut haben, aber ich habe bis jetzt nichts gefunden, was mich von der Software her überzeugen würde (und wo ich abkupfern könnte :))


[1] http://www.bobrathbone.com/raspberrypi_rotary.htm
BlackJack

@smutbert: Die Klasse benutzt die Ereignisse von GPIO und die sind dort als Threads implementiert. Das läuft nur solange wie das Hauptprogramm läuft. Darum braucht man halt irgendetwas was das Hauptprogramm am leben erhält — notfalls eine Endlosschleife mit einem `sleep()` darin.

Was bei der Klasse etwas abschreckt sind die Klassenvariablen die der da benutzt und die eigentlich gleich auf das Exemplar gehören. Beziehungsweise teilweise auch überhaupt nicht als Attribute an irgend ein Objekt gehören weil das eigentlich nur lokale Namen innerhalb einer Methode sein müssten.

Das `GPIO.setmode()` gehört auch nicht in die `__init__()` von so einer Klasse weil das Auswirkungen auf das gesamte Programm hat. Solche Funktionen die globalen Zustand ändern gehören in die Hauptfunktion oder zumindest sehr nahe dort hin, und nicht in irgendwelche Bibliotheken versteckt.

Eine Queue würde man anstelle einer Rückruffunktion verwenden. Also statt die Rückruffunktion zu verwenden, würde man das Ereignis entsprechend kodiert in diese Queue stecken und das Hauptprogramm würde dann nicht aus der Endlosschleife die nichts tut bestehen, sondern aus einer Schleife die nacheinander alle Elemente aus der Queue holt und darauf irgendwie reagiert.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Danke euch allen.
So sieht das aus, was ich bis jetzt mit Hilfe von Teilen von py-gaugette zustande gebracht habe und immerhin läuft es:

Code: Alles auswählen

import math
import threading
import time
import wiringpi2

class GPIO:
	def __init__(self):
		self.gpio = wiringpi2.GPIO(wiringpi2.GPIO.WPI_MODE_PINS)
		self.setup = self.wiringpi2_setup
		self.output = self.gpio.digitalWrite
		self.input = self.gpio.digitalRead
		self.OUT = self.gpio.OUTPUT
		self.IN = self.gpio.INPUT
		self.HIGH = self.gpio.HIGH
		self.LOW = self.gpio.LOW
		self.PUD_UP = self.gpio.PUD_UP
		self.PUD_DOWN = self.gpio.PUD_DOWN
		self.PUD_OFF = self.gpio.PUD_OFF
 
	def wiringpi2_setup(self, channel, direction, pull_up_down=None):
		self.gpio.pinMode(channel, direction)
		if pull_up_down is None: pull_up_down = self.gpio.PUD_OFF 
		self.gpio.pullUpDnControl(channel, pull_up_down)


class RotaryEncoder:

	def __init__(self, a_pin, b_pin):
		self.a_pin = a_pin
		self.b_pin = b_pin

		self.gpio = GPIO()

		self.gpio.setup(self.a_pin, self.gpio.IN, self.gpio.PUD_DOWN)
		self.gpio.setup(self.b_pin, self.gpio.IN, self.gpio.PUD_DOWN)

		self.last_delta = 0
		self.r_seq = self.rotation_sequence()

	def rotation_sequence(self):
		a_state = self.gpio.input(self.a_pin)
		b_state = self.gpio.input(self.b_pin)
		r_seq = (a_state ^ b_state) | b_state << 1
		return r_seq

	def get_delta(self):
		delta = 0
		r_seq = self.rotation_sequence()
		if r_seq != self.r_seq:
			delta = (r_seq - self.r_seq) % 4
			if delta==3:
				delta = -1
			elif delta==2:
				delta = int(math.copysign(delta, self.last_delta))
			self.last_delta = delta
			self.r_seq = r_seq
		return delta

	class Worker(threading.Thread):
		def __init__(self, a_pin, b_pin, callback):
			threading.Thread.__init__(self)
			self.lock = threading.Lock()
			self.callback = callback
			self.encoder = RotaryEncoder(a_pin, b_pin)
			self.daemon = True
			self.delta = 0

		def run(self):
			while True:
				event = 0
				delta = self.encoder.get_delta()
				with self.lock:
					self.delta += delta
					if abs(self.delta) >= 4:
						event = self.delta//4	
						#print "hi there: %d" % (self.delta//4)
						self.delta = self.delta%4
				if event != 0:
                        		self.callback(event)
				time.sleep(0.003)

		def get_delta(self):
			with self.lock:
				delta = self.delta//4
				self.delta = self.delta%4
			return delta



def rotary_event(event):
        if event == -2:
                print "zwei nach links"
        elif event == -1:
                print "nach links"
        elif event == 1:
                print "nach rechts"
        elif event == 2:
                print "2 nach rechts"
        return


A1_PIN  = 28
B1_PIN  = 29
 
encoder = RotaryEncoder.Worker(A1_PIN, B1_PIN, rotary_event)
encoder.start()

while 1:
	time.sleep(10)
aber irgendwie kommt mir das mit den Callbackfunktionen noch verkehrt herum vor. ich hätte mir vorgestellt, dass der Workerthread einfach die Events abliefert und dann das Hauptprogramm, je nachdem wie es ihm gerade in den Kram paßt darauf reagiert oder nicht, aber hier muss ich ja schon im Vorhinein die Routinen festlegen, die aufgerufen werden sollen.

Jetzt wäre ich sehr daran interessiert zu hören, was ich falsch oder schlecht gemacht habe (bzw. Guy Carpenter, der py-gaugette geschrieben hat und auf den ich mich ausreden werde :mrgreen:). Zum Beispiel die self.lock Geschichte kann ich mir vermutlich, so wie ich das geändert habe, vermutlich ersparen?
(Original von Guy Carpenter)

Wenn ihr darüber hinaus noch Vorschläge hättet, wie ein Programm aufgebaut sein könnte, das in unterschiedlichen Situationen/Modi unterschiedlich auf Tastendrücke oder den Drehimpulsgeber reagieren soll, wäre das noch großartiger. Dabei soll eigentlich eine richtige (rudimentäre) Menüführung herauskommen.
(wenn ich mit lauter/leiser, nächstes/voriges Stück und Play/Pause zufrieden wäre, wäre ich an dieser Stelle ja schon so gut wie fertig)

lg smutbert
BlackJack

@smutbert: Eine Klasse als ein Attribut einer anderen Klasse zu definieren ist eher ungewöhnlich in Python. Das bringt keine Vorteile und den Nachteil das die Klasse intern trotzdem dem Modul zugeordnet wird, dort aber nicht gefunden werden kann.

Wenn ich jetzt nichts übersehen habe brauchst Du das Lock nur wenn die `get_delta()`-Methode auf dem `Worker` auch tatsächlich irgendwo aus einem anderen Thread aufgerufen würde. Das wird aber *gar* nicht aufgerufen. Damit ist die ganze `Worker`-Klasse überflüssig denn eine Klasse die nur aus `__init__()` und *einer* anderen Methode besteht kann man ja einfacher als Funktion schreiben.

Warum der Wechsel zu `wiringpi` und einer Schleife die ständig abfragt statt wie vorher die Hardwareinterrupts zu verwenden?
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Mein erster Versuch den Thread in eine einfache Funktion zu packen ist kläglich gescheitert, aber das werde ich noch einmal etwas hartnäckiger versuchen und bei Schwierigkeiten hier wieder um Hilfe bitten.

Für WiringPi und die Schleife gibt es mehrere Gründe:
  • RPi.GPIO gibt es nicht für den Cubietruck, daher WiringPi. (Ich habe zu Beginn gedacht RPi.GPIO wäre Teil von WiringPi, weil sie doch recht ähnlich sind und beide oft in ein und demselben Artikeln, Blogeinträgen,… behandelt werden.)
  • Hardwareinterrupts lassen sich am Cubietruck nicht ganz so einfach nutzen, zumindest noch (?) nicht über WiringPi und wenn doch gäbe es vermutlich nur wenige Pins an denen man sie nutzen könnte.
  • Eigentlich will ich mir die Entprellung der Taster und Drehimpulsgeber in Hardware ersparen und angeblich (und naheliegenderweise) ist es keine gute Idee nicht entprellte Schalter an einen Interrupt zu hängen.
  • In einem Elektronikforum [1] habe ich gelesen, dass die Abfrage eines Drehimpulsgebers mit Interrupts sowieso nicht das gelbe vom Ei ist und tatsächlich hat in meinen Tests bis jetzt py-gaugette besser funktioniert als die rotary_class mit den Interrupts (das kann aber natürlich auch andere Ursachen gehabt haben). Vielleicht ist diese Variante auch nicht der Weisheit letzter Schluss, aber soweit ich das bis jetzt beurteilen kann funktioniert sie sehr gut und sie läuft auf dem Cubietruck.
[1] http://www.mikrocontroller.net/articles ... auswertung
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Falls man das am Ende doch lieber mit Warteschlangen lösen möchte, kann übrigens statt der Queue auch eine Deque verwenden. Dies hätte den Vorteil, dass man eine Maximalgröße an Elementen angeben kann.* Falls diese Größe überschritten wird, dann werden die ältesten Elemente automatisch rausgeschmissen. Somit könnte man dauerhaft reinschreiben, aber müsste nicht dauerhaft auslesen. Da wird also kein Callback "gezwungen", etwas zu tun, sondern der verarbeitende Code kann selbst entscheiden, wann er neue Events abholen möchte, ohne dass er sich mit Altlasten rumplagen muss. Je nachdem, um was es geht, möchte man vielleicht nur die 10 neuesten Events im "Cache" haben. Das wäre wie gesagt leicht realisierbar. Und es ist threadsafe.

* Geht bei Queues zwar auch, aber wenn die voll sind, schmeißen sie alte Elemente nicht von selbst raus.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Danke, an Queues komme ich vielleicht nicht vorbei. Fülle ich die Queue(s) direkt aus den Funktionen heraus oder mache ich das dann immer noch mit den Callback-Funktionen und soll ich versuchen möglichst mit einer Queue auszukommen?
(es geht ja nur darum auf die Bedienelemente zu reagieren und die Reihenfolge sollte dabei beibehalten werden, weil der Plan vorsieht, dass sich die Funktion der Bedienelemente ändert)


Nach viel Gewürge — mit python, aber mehr noch mit WiringPi und dem Setup der Pins — habe ich endlich das ganze in eine einfache Funktion gepackt. Ich habe es zwar noch nicht sehr intensiv getestet, aber es scheint zu funktionieren.

Code: Alles auswählen

import math
import threading
import time
import wiringpi2

#1 und 2 fuer Pull-up und -down sind vertauscht
PUD_UP=1
PUD_DOWN=2
PUD_OFF=0

wiringpi2.wiringPiSetup()

def rotary_encoder(a_pin, b_pin, pin_pud, callback):
	MODE_IN=0
	MODE_OUT=1

	wiringpi2.pinMode(a_pin, MODE_IN)
	wiringpi2.pinMode(a_pin, MODE_IN)
	wiringpi2.pullUpDnControl(a_pin, pin_pud)
	wiringpi2.pullUpDnControl(b_pin, pin_pud)

	last_direction = 1
	delta = 0
	new_delta = 0
	last_rotation_sequence = 0
	
	while True:
		a_state = wiringpi2.digitalRead(a_pin)
		b_state = wiringpi2.digitalRead(b_pin)
		rotation_sequence = (a_state ^ b_state) | b_state << 1
		
		if rotation_sequence != last_rotation_sequence:
			delta = (rotation_sequence - last_rotation_sequence) % 4
			if delta == 3:
				delta = -1
			elif delta == 2:
				delta = int(math.copysign(delta, last_direction))
			last_direction = delta
			last_rotation_sequence = rotation_sequence
			new_delta += delta
		if abs(new_delta) >= 4: #4 steps per indent
			callback(new_delta // 4)
			new_delta = new_delta % 4
		time.sleep(0.002)


def rotary_event(event):
        if event == -2:
                print "zwei nach links"
        elif event == -1:
                print "nach links"
        elif event == 1:
                print "nach rechts"
        elif event == 2:
                print "2 nach rechts"
        return


t = threading.Thread(target=rotary_encoder, args=(28, 29, PUD_UP, rotary_event,))
t.setDaemon(True)
t.start()

while True:
	time.sleep(1)
Hätte ich den Thread besser direkt in der Funktion gestartet — die Funktion ist ja sinnlos, wenn sie nicht in einem Daemonthread gestartet wird (das habe ich aber nicht hinbekommen)?
Fällt euch vielleicht sonst noch etwas auf oder besser: Was fällt euch sonst noch auf?
Antworten