Musikspieler mit Display und Bedienelementen

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

Hallo,

vor Jahren habt ihr mir in diesem Forum schon einmal sehr geholfen („Events auslösen und darauf reagieren“). So habe ich es immerhin zu zwei funktionierenden Geräten geschafft, das letzte namens orpheus sieht so aus:

Bild

Nun habe ich nach Jahren endlich beschlossen weiterzumachen.

Es gibt folgende Bedienelemente (von links nach rechts)
  • Taster zum Teil mit LEDs
    1. Ein-/Aus ("switch_power") mit ringförmiger LED (lässt sich ebenfalls steuern kommt bis jetzt aber im Programm nicht vor)
    2. Play-/Pause ("switch_play") mit LED darüber ("led_play")
    3. Shuffle-Taster ("switch_random") mit LED darüber ("led_random")
    4. Internetradio ("switch_radio") mit LED darüber ("led_x")
    5. Playlist löschen ("switch_clear")
  • Display
  • Drehimpulsgeber
    1. voriges/nächstes Stück ("encoder_control") mit eingebautem Taster ("switch_restart")
    2. Lautstärke ("encoder_volume") mit eingebautem Taster ("switch_mute")
(ich erzähle auch gern mehr über die Hardware, wenn es jemanden interessiert oder poste einen Link zu einem Vorstellungsthread im Debianforum, aber nachdem es hier in erster Linie um python geht, habe ich gedacht, ich beschränke mich erst einmal auf das notwendigste)

Auf dem System laufen neben mpd zur Musikwiedergabe auch mpdlcd und lcdproc/LCDd zum Ansteuern des Displays, wobei ich das eigentlich gern direkt in meinem Programm machen würde. Momentan gebe ich in meinem Programm lediglich als Client von lcdproc/LCDd Rückmeldungen über die Tastendrücke aus.

In erster Linie ist mein Programm ein mpd-Client, der entsprechend der Tastendrücke und Drehimpulsgeber mpd bedient und es sieht aktuell so aus

Code: Alles auswählen

#!/usr/bin/env python

import math
import threading
import time
import random
import mpd
import Queue
import subprocess
import socket
import wiringpi2

#1 und 2 fuer Pull-up und -down sind beim Cubietruck vertauscht
PULLUP=1

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def lcd_open(host, port):
        s.connect(("localhost", 13666))
        lcdproc_version = lcd_cmd('hello')
        lcd_cmd('client_set name orpheus')

def lcd_close():
        lcd_cmd('screen_set ' + 'commando' + ' backlight off')
        s.close()

def lcd_cmd(lcdproc_cmd):
        s.send(lcdproc_cmd + '\n')

def lcd_text(msg_text):
        lcd_cmd('screen_add ' + 'commando')
        lcd_cmd('screen_set ' + 'commando' + ' -priority alert -heartbeat off')
        lcd_cmd('screen_set ' + 'commando' + ' -timeout 16')
        lcd_cmd('widget_add ' + 'commando' + ' anzeige' + ' string')
        lcd_cmd('widget_set ' + 'commando' + ' anzeige' + ' 1 1 ' + '"' + msg_text + '"')

wiringpi2.wiringPiSetup()

class LED:
	def __init__(self, pin):
		self.pin = pin
		self.state = 0
		wiringpi2.pinMode(self.pin, 1)
		wiringpi2.digitalWrite(self.pin, 0)

	def getState(self):
		return self.state

	def setState(self, state):
		wiringpi2.digitalWrite(self.pin, state)
		self.state = state

class RotaryEncoder(threading.Thread):
	def __init__(self, a_pin, b_pin, pin_pud, callback):
		threading.Thread.__init__(self)
		self.daemon = True
		self.a_pin = a_pin
		self.b_pin = b_pin
		self.pin_pud = pin_pud
		self.steps_per_indent = 4
		self.callback=callback
		wiringpi2.pinMode(self.a_pin, 0)
		wiringpi2.pinMode(self.b_pin, 0)
		wiringpi2.pullUpDnControl(self.a_pin, self.pin_pud)
		wiringpi2.pullUpDnControl(self.b_pin, self.pin_pud)

	def run(self):
		last_direction = 0
		delta_sum = 0
		a_state = wiringpi2.digitalRead(self.a_pin)
		b_state = wiringpi2.digitalRead(self.b_pin)
		last_rotation_sequence = (a_state ^ b_state) | b_state << 1
		while True:
			a_state = wiringpi2.digitalRead(self.a_pin)
			b_state = wiringpi2.digitalRead(self.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
				delta_sum += delta
				delta = delta_sum // self.steps_per_indent
				if delta != 0:
					self.callback(delta)
					delta_sum %= self.steps_per_indent
			time.sleep(0.004)

class Switch(threading.Thread):
	def __init__(self, pin, pin_pud, debounce, callback):
		threading.Thread.__init__(self)
		self.daemon = True
		self.pin = pin
		self.pin_pud = pin_pud
		self.debounce = debounce
		self.callback = callback
		wiringpi2.pinMode(self.pin, 0)
		wiringpi2.pullUpDnControl(self.pin, self.pin_pud)

	def run(self):
		last_state = wiringpi2.digitalRead(self.pin)
		while True:
			state = wiringpi2.digitalRead(self.pin)
			if state != last_state:
				self.callback(state - last_state)
				time.sleep(self.debounce)
			last_state = state
			time.sleep(0.08)

localmpd = mpd.MPDClient(use_unicode=True)
localmpd.connect("localhost",6600)

def rotated_volume(event):
	if event == -1:
		commands.put("quieter")
	elif event == 1:
		commands.put("louder")
	return
def rotated_control(event):
	if event == -1:
		commands.put("previous")
	elif event == 1:
		commands.put("next")
	return
def pressed_power(event):
	if event == 1:
		commands.put("shutdown")
def pressed_play(event):
	if event == 1:
		commands.put("play")
def pressed_random(event):
	if event == 1:
		commands.put("random")
def pressed_radio(event):
	if event == 1:
		commands.put("radio")
def pressed_clear(event):
	if event == 1:
		commands.put("clear")
def pressed_restart(event):
	if event == 1:
		commands.put("restart")
def pressed_mute(event):
	if event == 1:
		commands.put("mute")

commands=Queue.Queue()

encoder_volume = RotaryEncoder(10, 8, PULLUP, rotated_volume)
encoder_control = RotaryEncoder(11, 9, PULLUP, rotated_control)
switch_power = Switch(20, PULLUP, 0.1, pressed_power)
switch_play = Switch(17, PULLUP, 0.1, pressed_play)
switch_random = Switch(19, PULLUP, 0.1, pressed_random)
switch_radio = Switch(16, PULLUP, 0.1, pressed_radio)
switch_clear = Switch(18, PULLUP, 0.1, pressed_clear)
switch_restart = Switch(15, PULLUP, 0.1, pressed_restart)
switch_mute = Switch(12, PULLUP, 0.1, pressed_mute)

encoder_volume.start()
encoder_control.start()
switch_power.start()
switch_play.start()
switch_random.start()
switch_radio.start()
switch_clear.start()
switch_restart.start()
switch_mute.start()

led_power = LED(28)
led_play = LED(22)
led_random = LED(21)
led_x = LED(26)

lcd_open("localhost", 13666)
led_power.setState(0)

while True:
	try:
		command=commands.get(1, 1)
	except:
		command=99

	if command == "play":
		if localmpd.status()['state'] == 'play':
			localmpd.pause()
			lcd_text('pause')
		else:
			if localmpd.status()['playlistlength'] == '0':
				albums = localmpd.list("album")
				albumindex=0
				lcd_text(albums[albumindex].encode('ascii', 'ignore'))
				while True:
					navigate=commands.get(1)
					if navigate == "next":
						albumindex = (albumindex + 1) % len(albums)
					elif navigate == "previous":
						albumindex = (albumindex - 1) % len(albums)
					elif navigate == "louder":
						albumindex = (albumindex + 10) % len(albums)
					elif navigate == "quieter":
						albumindex = (albumindex - 10) % len(albums)
					lcd_text(albums[albumindex].encode('ascii', 'ignore'))
					if navigate == "random":
						localmpd.findadd("album", albums[albumindex])
						break
					if navigate == "play":
						localmpd.clear()
						localmpd.findadd("album", albums[albumindex])
						break
			else:
				lcd_text('play')
			localmpd.play()
	elif command == "next":
		localmpd.next()
		try:
			lcd_text('%3.0f: ' % float(localmpd.status()['song']) + localmpd.currentsong()['name'])
		except:
			lcd_text('next ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
	elif command == "previous":
		localmpd.previous()
		try:
			lcd_text('%3.0f: ' % float(localmpd.status()['song']) + localmpd.currentsong()['name'])
		except:
			lcd_text('previous ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
	elif command == "random":
		random = (1 - int(localmpd.status()['random']))
		localmpd.random(random)
		if int(localmpd.status()['random']) == 1:
			lcd_text('randomize')
		else:
			lcd_text('random off')
	elif command == "quieter":
		volume = int(localmpd.status()['volume']) - 1
		if volume < 0:
			volume = 0
		localmpd.setvol(sorted([0,  volume, 100 ])[1])
		lcd_text('volume %1.2f' % (float(volume)/100))
	elif command == "louder":
		volume = int(localmpd.status()['volume']) + 1
		if volume > 100:
			volume = 100 
		localmpd.setvol(sorted([0,  volume, 100 ])[1])
		lcd_text('volume %1.2f' % (float(volume)/100))
	elif command == "clear":
		localmpd.clear()
		lcd_text('clear playlist')
	elif command == "radio":
		localmpd.clear()
		localmpd.load('radio')
		localmpd.play()
		lcd_text('internet radio')
	elif command == "restart":
		lcd_text('stopping network')
		subprocess.call(["systemctl", "stop", "ssh.service"])
		subprocess.call(["systemctl", "stop", "systemd-timesyncd.service"])
		subprocess.call(["systemctl", "stop", "systemd-resolved.service"])
		subprocess.call(["systemctl", "stop", "systemd-networkd.service"])
		subprocess.call(["systemctl", "stop", "wpa_supplicant@wlan0.service"])
		lcd_text('done')
		#lcd_text('restart ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
	elif command == "mute":
		lcd_text('stopping network')
		subprocess.call(["systemctl", "start", "wpa_supplicant@wlan0.service"])
		subprocess.call(["systemctl", "start", "systemd-resolved.service"])
		subprocess.call(["systemctl", "start", "systemd-networkd.service"])
		subprocess.call(["systemctl", "start", "systemd-timesyncd.service"])
		subprocess.call(["systemctl", "start", "ssh.service"])
		lcd_text('done')
		#subprocess.call(["amixer", "-D", "hw:U24XL", "set", "PCM", "1+", "toggle"])
		#lcd_text('toggle mute')
	elif command == "shutdown":
		lcd_text('shutting down')
		subprocess.call(["systemctl", "poweroff"])
	if localmpd.status()['state'] == 'play':
		led_play.setState(1)
	else:
		led_play.setState(0)
	led_random.setState(int(localmpd.status()['random']))
	command = 99
(Dass die Taster "switch_mute" und "switch_restart" momentan das Netzwerk (de)aktivieren liegt an einem kleinen Problem, das ich mit dem WLAN habe.)

Mir geht es vor allem um
  1. allgemeine Hinweise
    (ich weiß, dass ich vieles ungünstig/patschert mache oder mich bei Variablennamen blöd anstelle, aber oft fehlt mir auch die Idee wie es besser ginge)
  2. ich versuche gerade eine Möglichkeit zu schaffen am Gerät selbst ein Album (oder auch mehrere Musikstücke nach einem anderen Kriterium) zur Wiedergabe auszuwählen. Momentan steht das alles in der Hauptschleife hinter dem else bei command == "play":
    Falls die Wiedergabeliste leer ist kann man mit den Drehimpulsgebern die Alben einzeln oder 10er-Schritten durchgehen und dann mit "switch_random" oder "switch_play" abspielen. Allerdings würde ich da gerne auch andere Tags verwenden und habe nicht die geringste Idee wie ich das anstelle, weil python-mpd offensichtlich nicht direkt das bietet was ich dazu bräuchte. Ich würde beispielsweise gerne die Musik nach
    Dirigent → Komponist → Album
    oder
    Interpret → Album
    also recht flexibel auswählen können, aber ich scheitere schon daran zu einer nach Interpret gefilterten Albenliste zu kommen...
  3. Threads/Prozesse
    Hier bin ich komplett ahnungslos ;).

    Zum Beispiel habe ich den Eindruck, dass mir das Multithreading nicht viel bringt und die mpd-Befehle die Abfrage der Drehimpulsgeber stören/lahmlegen/unterbrechen (eigentlich nicht sehr störend, aber ich habe das Gefühl, dass das etwas besser gehen müsste).
    Schließlich möchte ich wie erwähnt auch das Display direkt aus meinem Programm (ohne lcdproc/LCDd und ohne mpdlcd) ansprechen und dann steh ich komplett an - immerhin will ich gleichzeitig
    • das Display ansteuern, dass entweder das aktuelle Album mit Titel angezeigt wird oder im "Musikauswahlmodus" eben nacheinander die Interpreten, Komponisten, Alben,...
    • die LEDs so ansteuern, dass sie den Wiedergabestatus und den Zufallswiedergabemodus richtig anzeigen. Eine LED sollte oder könnte dann später auch anzeigen ob man sich im "Musikauswahlmodus" befindet.
    • die mpd-Befehle abarbeiten so dass sie möglichst nicht den Bedienfluss aufhalten. Ich fände es zum Beispiel beim Regeln der Lautstärke nett, wenn das Display ohne Verzögerung die "Ziel-Lautsärke" anzeigt, man also nicht darauf warten muss, dass die vorigen mpd-Befehle abgearbeitet wurden, aber dazu müsste ich wohl parallel zu mpd im Programm einen eigenen Lautstärkewert speichern, den ich dann irgendwie (in beide Richtungen) mit dem Lautstärkewert von mpd abgleichen muss.
    • Nachdem sich mpd auch über das Netzwerk bedienen lässt, wäre es nett, wenn man einige Befehle, die über das Netzwerk gegeben wurden, auch am Display sehen kann, zum Beispiel Lautstärkeänderungen.
Danke und lg, smutbert
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Was optisch sofort auffällt ist das nicht alles vier Leerzeichen pro Ebene eingerückt ist.

Das `random`-Modul wird importiert, aber nicht verwendet. `led_x` wird definiert aber nirgends verwendet.

`wiringpi2` ist veraltet, und sowohl die Github-Seite als auch der Package-Index verweisen auf `wiringpi` ohne die 2 vom selben Autor.

Als nächstes sollte man das dringend auf Python 3 portieren, denn Python 2 gibt's nicht mehr lange: https://pythonclock.org/

Für Python 2 hätte man bei den Klassen die von nichts erben von `object` erben sollen, damit das „new style“-Klassen sind, und so Sachen wie Properties wie erwartet funktionieren.

Dann sollte aller Code der nicht Konstanten, Funktionen, oder Klassen definiert von Modulebene verschwinden. Du versteckst da immer mal wieder ein paar Zeilen Code vom Hauptprogramm zwischen Funktions- und Klassendefinitionen – das ist sehr unübersichtlich.

Wenn man das Hauptprogramm in eine Funktion verschiebt (die üblicherweise `main()` heisst), fallen die globalen Variablen `s` und `commands` auf. Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. `s` ist auch kein guter Name und schon gar nicht für eine globale Variable.

`lcd_open()` verwendet die Argumente gar nicht. Bei den `lcd_*()`-Funktionen fällt das sinnlose dynamische zusammensetzen von statischen Zeichenketten auf.

`lcd_cmd()` hat keinen Rückgabewert, also ein impizites `None`, das an den Namen `lcdproc_version` gebunden wird‽

`lcd_cmd()` verwendet `socket.send()` was *falsch* ist! Die Methode garantiert *nicht* das alles was da übergeben wurde, auch tatsächlich gesendet wird. Dafür muss man `sendall()` verwenden.

Das Socket für das LCD kapselt man am besten in einer Klasse um das globale `s` los zu werden.

`localmpd` würde ich `mpd_client` nennen. Nichts an dem Ding *muss* lokal sein und wenn man das mal ändern sollte, muss man unnötigerweise den Namen ändern.

Einiges ist Yoda-mässig benannt. `switch_power` ist der Name einer Funktion mit der man ein Gerät ein-/ausschalten kann, oder die Kraft die zum Schalten nötig ist. `power_switch` ist der Name für einen Ein/Aus-Schalter. `encoder_volume` ist die Lautstärke eines Encoders, `volume_encoder` ist der Encoder für die Lautstärke, und so weiter.

Das `RotaryEncoder` und `Switch` von `Thread` erben ist unschön, weil das mit dem Thread ja eigentlich unabhängig von der Funktionalität ist. Zudem sind das im Grunde ja nur zwei Funktionen, die man gar nicht in Klassen stecken muss. Man könnte also zwei Funktionen schreiben und eine Funktion die mittels `Thread` Funktionen asynchron ausführt.

Die Rückruffunktionen für die Drehregler haben ein unnötiges ``return`` am Ende.

Bei den Rückruffunktionen gibt es ein klares immer wiederkehrendes Muster das man nicht immer und immer wieder hinschreiben müssen sollte.

Bei der `LED`-Klasse verstösst die Schreibweise von `getState()` und `setState()` gegen die Namenskonventionen und die beiden Methoden sind auch ein Fall für ein Property.

Ist es notwendig den Zustand dort noch einmal als Attribut vorzuhalten? Man kann den doch auch einfach von der Hardware auslesen‽

``command = 99`` ist schräg. Warum nicht 42? Oder 23? Gültige Werte sind ja Zeichenketten, warum also irgendeine Zahl an diesen Namen binden? Man könnte eine leere Zeichenkette verwenden, oder `None`. Ein willkürliche Zahl ist eher verwirrend.

Das der Wahrheitswert bei `commands.get()` als 1 angegeben ist, verwirrt auch. Ich würde den Defaultwert an der Stelle gar nicht angeben, sondern die Zeitüberschreitung explizit als Schlüsselwortargument.

Man sollte keine nackten ``except``\s ohne konkrete Ausnahme verwenden. Bei der Queue ist doch ganz klar, das man da auf ein `queue.Empty` reagieren will, dann sollte man das auch so schreiben um nicht *alle* Ausnahmen so zu behandeln als wäre eine Zeitüberschreitung aufgetreten.

Die Kommandos 'next' und 'previous' sind nahezu gleich, die sollte man zusammenfassen.

Das zusammenstückeln von Zeichenkettenliteralen und Werten per `str()` und ``+`` ist eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten.

Ich verstehe nicht so ganz warum Du die Songnummer in eine Gleitkommazahl umwandelst um die dann ohne Nachkommastellen in eine Zeichenkette zu formatieren‽

'quieter' und 'louder' haben auch fast exakt den gleichen Code – auch hier sollte man zusammenfassen.

Was Du da mit `sorted()` machst ist komisch. Da würde man einfach mit `min()`/`max()` dafür sorgen das `volume` innerhalb von 0 bis 100 liegt.

Die System-D-Dienste die da gestartet und gestoppt werden können, sollten nicht zweimal im Quelltext stehen sondern einmal, am besten als Konstante definiert.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
import math
import queue
import socket
import subprocess
import threading
import time
from functools import partial

import mpd
import wiringpi

# 1 und 2 fuer Pull-up und -down sind beim Cubietruck vertauscht.
PULLUP = 1

SYSTEMD_SERVICE_NAMES = [
    'wpa_supplicant@wlan0',
    'systemd-networkd',
    'systemd-resolved',
    'systemd-timesyncd',
    'ssh',
]

def call_async(function, *arguments):
    thread = threading.Thread(target=function, args=arguments)
    thread.daemon = True
    thread.start()


class LCD:
    
    def __init__(self, host, port):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))
        self.send_command(b'hello')
        self.send_command(b'client_set name orpheus')

    def __enter__(self):
        return self
    
    def __exit__(self, *args):
        self.close()

    def send_command(self, command):
        self.socket.sendall(command + b'\n')

    def display(self, text):
        self.send_command(b'screen_add commando')
        self.send_command(b'screen_set commando -priority alert -heartbeat off')
        self.send_command(b'screen_set commando -timeout 16')
        self.send_command(b'widget_add commando anzeige string')
        self.send_command(
            'widget_set commando anzeige 1 1 "{}"'
                .format(text)
                .encode('ascii', 'ignore')
        )

    def close(self):
        self.send_command('screen_set commando backlight off')
        self.socket.close()


def watch_rotary_encoder(a_pin, b_pin, pin_pud, callback):
    wiringpi.pinMode(a_pin, 0)
    wiringpi.pinMode(b_pin, 0)
    wiringpi.pullUpDnControl(a_pin, pin_pud)
    wiringpi.pullUpDnControl(b_pin, pin_pud)
    steps_per_indent = 4
    last_direction = 0
    delta_sum = 0
    a_state = wiringpi.digitalRead(a_pin)
    b_state = wiringpi.digitalRead(b_pin)
    last_rotation_sequence = (a_state ^ b_state) | b_state << 1
    while True:
        a_state = wiringpi.digitalRead(a_pin)
        b_state = wiringpi.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
            delta_sum += delta
            delta = delta_sum // steps_per_indent
            if delta != 0:
                callback(delta)
                delta_sum %= steps_per_indent
        time.sleep(0.004)


def watch_switch(pin, pin_pud, debounce, callback):
    wiringpi.pinMode(pin, 0)
    wiringpi.pullUpDnControl(pin, pin_pud)
    last_state = wiringpi.digitalRead(pin)
    while True:
        state = wiringpi.digitalRead(pin)
        if state != last_state:
            callback(state - last_state)
            time.sleep(debounce)
        last_state = state
        time.sleep(0.08)


class LED:
    
    def __init__(self, pin):
        self.pin = pin
        self._state = 0
        wiringpi.pinMode(self.pin, 1)
        wiringpi.digitalWrite(self.pin, 0)
    
    @property
    def state(self):
        # 
        # TODO Is `self._state` really neccessary?
        #   Can't it be read from the pin?
        # 
        return self._state
    
    @state.setter
    def state(self, value):
        wiringpi.digitalWrite(self.pin, value)
        self._state = value


def rotary_encoder_callback(commands, left_command, right_command, event):
    if event == -1:
        commands.put(left_command)
    elif event == 1:
        commands.put(right_command)


def switch_callback(commands, command, event):
    if event == 1:
        commands.put(command)


def main():
    # 
    # TODO This function is way too big.  The command handling should be in a
    #   class with a method per command.
    # 
    wiringpi.wiringPiSetup()
    mpd_client = mpd.MPDClient(use_unicode=True)
    mpd_client.connect('localhost', 6600)

    commands = queue.Queue()

    call_async(
        watch_rotary_encoder,
        10,
        8,
        PULLUP,
        partial(rotary_encoder_callback, commands, 'quieter', 'louder'),
    )
    call_async(
        watch_rotary_encoder,
        11,
        9,
        PULLUP,
        partial(rotary_encoder_callback, commands, 'previous', 'next'),
    )
    
    for pin, command in [
        (12, 'mute'),
        (15, 'restart'),
        (16, 'radio'),
        (17, 'play'),
        (18, 'clear'),
        (19, 'random'),
        (20, 'shutdown'),
    ]:
        call_async(
            watch_switch,
            pin,
            PULLUP,
            0.1,
            partial(switch_callback, commands, command),
        )

    power_led = LED(28)
    play_led = LED(22)
    random_led = LED(21)

    with LCD('localhost', 13666) as lcd:
        power_led.state = False

        while True:
            try:
                command = commands.get(timeout=1)
            except queue.Empty:
                command = None

            if command == 'play':
                if mpd_client.status()['state'] == 'play':
                    mpd_client.pause()
                    lcd.display('pause')
                else:
                    if mpd_client.status()['playlistlength'] == '0':
                        albums = mpd_client.list('album')
                        album_index = 0
                        while True:
                            lcd.display(albums[album_index])
                            navigate_command = commands.get()
                            if navigate_command == 'next':
                                album_index = (album_index + 1) % len(albums)
                            elif navigate_command == 'previous':
                                album_index = (album_index - 1) % len(albums)
                            elif navigate_command == 'louder':
                                album_index = (album_index + 10) % len(albums)
                            elif navigate_command == 'quieter':
                                album_index = (album_index - 10) % len(albums)
                            
                            if navigate_command == 'random':
                                mpd_client.findadd('album', albums[album_index])
                                break
                            
                            if navigate_command == 'play':
                                mpd_client.clear()
                                mpd_client.findadd('album', albums[album_index])
                                break
                    else:
                        lcd.display('play')
                    
                    mpd_client.play()
            
            elif command in ['next', 'previous']:
                getattr(mpd_client, command)()
                status = mpd_client.status()
                try:
                    text = '{0[song]:>3}: {1[name]}'.format(
                        status, mpd_client.currentsong()
                    )
                except:
                    #
                    # FIXME This naked ``except`` really shouldn't be here.
                    # 
                    text = '{0} {1[song]}/{1[playlistlength]}'.format(
                        command, status
                    )
                lcd.display(text)
            
            elif command == 'random':
                random = (1 - int(mpd_client.status()['random']))
                mpd_client.random(random)
                if int(mpd_client.status()['random']) == 1:
                    lcd.display('randomize')
                else:
                    lcd.display('random off')
            
            elif command in ['quieter', 'louder']:
                offset = {'quieter': -1, 'louder': 1}[command]
                volume = min(
                    100, max(0, int(mpd_client.status()['volume']) + offset)
                )
                mpd_client.setvol(volume)
                lcd.display('volume {:1.2f}'.format(volume / 100))
            
            elif command == 'clear':
                mpd_client.clear()
                lcd.display('clear playlist')
            
            elif command == 'radio':
                mpd_client.clear()
                mpd_client.load('radio')
                mpd_client.play()
                lcd.display('internet radio')
            
            elif command == 'restart':
                lcd.display('stopping network')
                for service_name in reversed(SYSTEMD_SERVICE_NAMES):
                    subprocess.call(
                        ['systemctl', 'stop', service_name + '.service']
                    )
                lcd.display('done')
                # lcd.display(
                #     'restart {0[song]}/{0[playlistlength]}'.format(
                #         mpd_client.status()
                #     )
                # )
            
            elif command == 'mute':
                lcd.display('stopping network')
                for service_name in SYSTEMD_SERVICE_NAMES:
                    subprocess.call(
                        ['systemctl', 'start', service_name + '.service']
                    )
                lcd.display('done')
                # subprocess.call(
                #     ['amixer', '-D', 'hw:U24XL', 'set', 'PCM', '1+', 'toggle']
                # )
                # lcd.display('toggle mute')
            
            elif command == 'shutdown':
                lcd.display('shutting down')
                subprocess.call(['systemctl', 'poweroff'])
            
            play_led.state = mpd_client.status()['state'] == 'play'
            random_led.state = int(mpd_client.status()['random'])


if __name__ == '__main__':
    main()
Die beiden dringensten Baustellen sind IMHO das aufteilen der viel zu grossen `main()`-Funktion und statt selber Threads zu starten die Verwendung von der `wiringpy`-Funktion die Callbacks aufruft, wenn sich der Zustand eines Pins geändert hat.
„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

Herzlichen Dank!
Das muss ich erst einmal alles verdauen und durchblicken (ich melde mich dann in 3 Jahren wieder :wink: :D ).


Bei einigen wenigen Punkten habe ich aber eine Rechtfertigung parat:
__blackjack__ hat geschrieben: Donnerstag 20. Juni 2019, 10:38 Ist es notwendig den Zustand dort noch einmal als Attribut vorzuhalten? Man kann den doch auch einfach von der Hardware auslesen‽
Ursprünglich habe ich nicht wiringPi verwendet sondern die gpios über das sysfs gesteuert. Das war allerdings im Vergleich sehr langsam. An Drehimpulsgeber war nicht zu denken und ich wollte die Zugriffe auf das sysfs möglichst reduzieren.

wiringpi2 verwende ich, weil das eine inoffizielle Portierung eines ebenfalls wohl höchstens halboffiziellen wiringpi2 für das Cubieboard 2 auf den Cubietruck ist, den ich verwende. Die Portierung stammt natürlich nicht von mir sondern von einem Mitglied des nicht mehr existierenden Cubieboard-Forums. Dadurch erklärt sich auch das
__blackjack__ hat geschrieben: Donnerstag 20. Juni 2019, 10:38 [...] statt selber Threads zu starten die Verwendung von der `wiringpy`-Funktion die Callbacks aufruft, wenn sich der Zustand eines Pins geändert hat.
dazu bräuchte ich Interrupts, von denen soweit ich das verstanden habe, schon hardwaremäßig an den gpios nur einer zur Verfügung steht und der lässt sich mit der WiringPi2-Portierung nicht nutzen.
Ob es eine softwaremäßige "Interrupt-Emulation" gibt, ähnlich dem Soft-PWM-Feature, das problemlos funktioniert, habe ich nicht verlässlich herausfinden können, aber ich habe den Eindruck, dass es das nicht gibt. Was das außerdem für die Verwendung von python3 bedeutet, weiß ich nicht, aber es stört mich nicht besonders, wenn ich auf dem cubietruck bei python 2 bleiben muss – wenn ich das nächste Mal bastle werde ich halt wahrscheinlich eher zum Raspberry Pi greifen, damit es gar nicht erst zu solchen Schwierigkeiten kommt.


Bei den restlichen Punkte weiß ich keine Rechtfertigung und kann höchstens erklären wie es dazu gekommen ist, zum Beispiel
__blackjack__ hat geschrieben: Donnerstag 20. Juni 2019, 10:38 ``command = 99`` ist schräg. Warum nicht 42? Oder 23? Gültige Werte sind ja Zeichenketten, warum also irgendeine Zahl an diesen Namen binden? Man könnte eine leere Zeichenkette verwenden, oder `None`. Ein willkürliche Zahl ist eher verwirrend.
Ursprünglich waren es keine Zeichenketten sondern Zahlen, weil ich Schwierigkeiten im Umgang mit Strings hatte. Ich könnte dir die erste Version zeigen die gelaufen ist, aber das möchte ich diesem netten Forum und dir nicht antun...
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@smutbert: Das mit `wiringpi2` habe ich nicht so ganz verstanden – der Autor davon verlinkt sowohl auf der Github-Seite als auch im Python Package Index auf `wiringpi` – was auch von dem selben Autor ist.

`wiringpi` hat eine `wiringPiISR()`-Funktion um asynchron Callbacks aufrufen zu lassen.

Statt einfacher Zahlen oder Zeichenketten könnte man für die Kommandos auch ein `enum.Enum` definieren. Das macht das ganze etwas sicherer gegen fipptehler und das man irgendwo Äpfel mit Birnen vergleicht.

Wenn man die grosse Schleife in der alles verarbeitet wird in Methoden auf einer Klasse aufteilt, kann man aber auch gleich die Methoden selbst in die Queue stecken. Eventuell auch mit Argumenten, dann kann man sich das eine oder andere `partial()`/`partialmethod()` sparen.
„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

Das mit wiringpi ist einfach, wenn man sich nicht so kompliziert ausdrückt wie ich vorher:
Das originale WiringPi ist für den Raspberry Pi geschrieben, aber ich habe einen Cubietruck. Auf dem Cubietruck steht mir nur ein leicht angestaubter Port von wiringpi2 zur Verfügung, der python3 noch gar nicht zu kennen scheint.

wiringPiISR nutzt Interrupts, die mir soweit ich weiß der Cubietruck bzw. dessen SoC nicht zur Verfügung stellen.


Jetzt suche ich nach Wegen trotzdem python3 verwenden zu können. zum Beispiel indem ich versuche den python-Wrapper von wiringpi auf einen aktuelleren Stand zu bringen (das wäre allerdings ein Glückstreffer, wenn es funktionieren sollte) oder indem ich wiringpi durch eine andere Bibliothek ersetze.
(Nachdem mir wiringpi2 als selbst gebautes Debianpaket vorliegt, könnte es sogar sein, dass ich beim Paketbau die ursprünglich vielleicht sogar bereits vorhandene python3-Unterstützung „vergessen“ habe, aber das werde ich mir auch noch einmal genauer ansehen.)
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Laut github gibt’s das für python3. Ältere Version, aber könnte trotzdem klappen. https://github.com/gootoomoon/WiringCB-python

Und ich sehe keinen Grund, nicht die ISRs zu nutzen, die werden im Linux User Space sowieso “nur” per Thread abgeliefert. Das es da nur einen echten interrupt pro GPIO Bank gibt ist völlig irrelevant - das ist beim PI nicht anders.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Das alles ist ja schon eine Zeit lang her, aber wiringCB hat zumindest auf dem Cubietruck nicht richtig funktioniert (obwohl es sich um denselben SoC handelt - ich meine mich dunkel zu erinnern, dass derjenige, der das auf den Cubietruck portiert hat gemeint hätte, dass wiringCB überhaupt ziemlich fehlerhaft war/ist).
wiringPiISR hat ebenfalls nicht funktioniert, weder mit wiringCB noch mit der Portierung auf den Cubietruck. Ich kann mich aber nur noch vage an die wenig hilfreiche Fehlermeldung "unable to setup isr" erinnern.

Immerhin gibt es zu später Stunde noch einen Teilerfolg zu vermelden: Der wiringpi2-Port unterstützt tatsächlich auch bereits python3 und dein Zwischenstand lief auf Anhieb (getestet habe ich erst play/pause und die Lautstärkeregelung).

Allerdings habe ich beim Versuch eine python3-Version des wiringpi2-Debianpakets zu erstellen ziemlich viel kaputt gemacht und trotzdem kein Debianpaket zustande gebracht. Das muss ich noch einmal von vorne angehen.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Darf ich wieder um Hilfe bitten?
Endlich wollte ich die große main()-Funktion angehen, aber nach einigen fruchtlosen Herumprobieren habe ich festgestellt, dass ich nicht so recht weiß wie. Am Code hat sich seit dem Zwischenstand von __blackjack__ nicht viel getan, es sind nur ein paar Sachen rausgeflogen, die ich nur zu Testzwecken drin hatte, aber die Änderungen sind so minimal, dass ich deswegen nicht den ganzen Code von neuem poste.



Mein größtes Problem ist, dass ich keine Idee habe wie ich das Zerteilen der main-Funktion angehen soll. Sollte meine Hauptschleife dann in der main-Funktion bleiben?

Noch hilfloser fühle ich mich angesichts meines Wunsches mehrere Bedienmodi zu machen, aber möglicherweise löst sich das ja, wenn ich begriffen habe wie das Zerteilen der main-Funktion am besten funktioniert.
Für den Anfang möchte ich zum Beispiel
  • den „normalen“ Modus in dem alles in etwa so läuft wie jetzt
  • einen Auswahlmodus, in dem ich eine Liste von Interpreten/Alben/... durchgehen kann. Ganz rudimentär habe ich das ja schon beim letzten Mal mit einer eigenen Schleife, mit der ich die Albenliste durchgehen kann umgesetzt (bei »if command == 'play':« im Falle einer leeren Wiedergabewarteschlange).
Geht das ohne eine weitere Unterschleife pro Modus?
Ich denke mir würde eine Art Grundgerüst helfen, mit einem Standardmodus, der zwei einfache Befehle kennt

Code: Alles auswählen

if command == "play":
    if localmpd.status()['state'] == 'play':
        localmpd.pause()
        lcd_text('pause')
    else:
        lcd_text('play')
        localmpd.play()

elif command == 'clear':
    mpd_client.clear()
    lcd.display('clear playlist')
und dem Minimalauswahlmodus

Code: Alles auswählen

while True:
    lcd.display(albums[album_index])
    navigate_command = commands.get()
    if navigate_command == 'next':
        album_index = (album_index + 1) % len(albums)
    elif navigate_command == 'previous':
        album_index = (album_index - 1) % len(albums)
    elif navigate_command == 'louder':
        album_index = (album_index + 10) % len(albums)
    elif navigate_command == 'quieter':
        album_index = (album_index - 10) % len(albums)
                           
    if navigate_command == 'random':
        mpd_client.findadd('album', albums[album_index])
        break
                            
    if navigate_command == 'play':
        mpd_client.clear()
        mpd_client.findadd('album', albums[album_index])
        break
Selbstverständlich wäre ich auch für jede andere Art der Hilfe dankbar und bemühen es hinzubekommen werde ich mich auch ohne Hilfe – auch wenn ich mich dann wahrscheinlich auf einen Irrweg begebe :)
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python
import math
import queue
import socket
import subprocess
import threading
import time
from functools import partial

import mpd
import wiringpi

# 1 und 2 fuer Pull-up und -down sind beim Cubietruck vertauscht.
PULLUP = 1

SYSTEMD_SERVICE_NAMES = [
    "wpa_supplicant@wlan0",
    "systemd-networkd",
    "systemd-resolved",
    "systemd-timesyncd",
    "ssh",
]


def call_async(function, *arguments):
    thread = threading.Thread(target=function, args=arguments)
    thread.daemon = True
    thread.start()


class LCD:
    def __init__(self, host, port):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))
        self.send_command(b"hello")
        self.send_command(b"client_set name orpheus")

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def send_command(self, command):
        self.socket.sendall(command + b"\n")

    def display(self, text):
        self.send_command(b"screen_add commando")
        self.send_command(
            b"screen_set commando -priority alert -heartbeat off"
        )
        self.send_command(b"screen_set commando -timeout 16")
        self.send_command(b"widget_add commando anzeige string")
        self.send_command(
            'widget_set commando anzeige 1 1 "{}"'.format(text).encode(
                "ascii", "ignore"
            )
        )

    def close(self):
        self.send_command("screen_set commando backlight off")
        self.socket.close()


def watch_rotary_encoder(a_pin, b_pin, pin_pud, callback):
    wiringpi.pinMode(a_pin, 0)
    wiringpi.pinMode(b_pin, 0)
    wiringpi.pullUpDnControl(a_pin, pin_pud)
    wiringpi.pullUpDnControl(b_pin, pin_pud)
    steps_per_indent = 4
    last_direction = 0
    delta_sum = 0
    a_state = wiringpi.digitalRead(a_pin)
    b_state = wiringpi.digitalRead(b_pin)
    last_rotation_sequence = (a_state ^ b_state) | b_state << 1
    while True:
        a_state = wiringpi.digitalRead(a_pin)
        b_state = wiringpi.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
            delta_sum += delta
            delta = delta_sum // steps_per_indent
            if delta != 0:
                callback(delta)
                delta_sum %= steps_per_indent
        time.sleep(0.004)


def watch_switch(pin, pin_pud, debounce, callback):
    wiringpi.pinMode(pin, 0)
    wiringpi.pullUpDnControl(pin, pin_pud)
    last_state = wiringpi.digitalRead(pin)
    while True:
        state = wiringpi.digitalRead(pin)
        if state != last_state:
            callback(state - last_state)
            time.sleep(debounce)
        last_state = state
        time.sleep(0.08)


class LED:
    def __init__(self, pin):
        self.pin = pin
        self._state = 0
        wiringpi.pinMode(self.pin, 1)
        wiringpi.digitalWrite(self.pin, 0)

    @property
    def state(self):
        #
        # TODO Is `self._state` really neccessary?
        #   Can't it be read from the pin?
        #
        return self._state

    @state.setter
    def state(self, value):
        wiringpi.digitalWrite(self.pin, value)
        self._state = value


def rotary_encoder_callback(commands, left_command, right_command, event):
    if event == -1:
        commands.put(left_command)
    elif event == 1:
        commands.put(right_command)


def switch_callback(commands, command, event):
    if event == 1:
        commands.put(command)


def on_play(mpd_client, lcd, commands):
    if mpd_client.status()["state"] == "play":
        mpd_client.pause()
        lcd.display("pause")
    else:
        if mpd_client.status()["playlistlength"] == "0":
            albums = mpd_client.list("album")
            album_index = 0
            while True:
                lcd.display(albums[album_index])
                navigate_command = commands.get()
                if navigate_command == "next":
                    album_index = (album_index + 1) % len(albums)
                elif navigate_command == "previous":
                    album_index = (album_index - 1) % len(albums)
                elif navigate_command == "louder":
                    album_index = (album_index + 10) % len(albums)
                elif navigate_command == "quieter":
                    album_index = (album_index - 10) % len(albums)

                if navigate_command == "random":
                    mpd_client.findadd("album", albums[album_index])
                    break

                if navigate_command == "play":
                    mpd_client.clear()
                    mpd_client.findadd("album", albums[album_index])
                    break
        else:
            lcd.display("play")

        mpd_client.play()


def on_track_change(command, mpd_client, lcd, _commands):
    getattr(mpd_client, command)()
    status = mpd_client.status()
    try:
        text = "{0[song]:>3}: {1[name]}".format(
            status, mpd_client.currentsong()
        )
    except:
        #
        # FIXME This naked ``except`` really shouldn't be here.
        #
        text = "{0} {1[song]}/{1[playlistlength]}".format(command, status)

    lcd.display(text)


def on_random(mpd_client, lcd, _commands):
    random = 1 - int(mpd_client.status()["random"])
    mpd_client.random(random)
    if int(mpd_client.status()["random"]) == 1:
        lcd.display("randomize")
    else:
        lcd.display("random off")


def on_volume_change(command, mpd_client, lcd, _commands):
    offset = {"quieter": -1, "louder": 1}[command]
    volume = min(100, max(0, int(mpd_client.status()["volume"]) + offset))
    mpd_client.setvol(volume)
    lcd.display("volume {:1.2f}".format(volume / 100))


def on_clear(mpd_client, lcd, _commands):
    mpd_client.clear()
    lcd.display("clear playlist")


def on_radio(mpd_client, lcd, _commands):
    mpd_client.clear()
    mpd_client.load("radio")
    mpd_client.play()
    lcd.display("internet radio")


def on_restart(_mpd_client, lcd, _commands):
    lcd.display("stopping network")
    for service_name in reversed(SYSTEMD_SERVICE_NAMES):
        subprocess.call(["systemctl", "stop", service_name + ".service"])
    lcd.display("done")
    # lcd.display(
    #     "restart {0[song]}/{0[playlistlength]}".format(mpd_client.status())
    # )


def on_mute(_mpd_client, lcd, _commands):
    lcd.display("stopping network")
    for service_name in SYSTEMD_SERVICE_NAMES:
        subprocess.call(["systemctl", "start", service_name + ".service"])
    lcd.display("done")
    # subprocess.call(["amixer", "-D", "hw:U24XL", "set", "PCM", "1+", "toggle"])
    # lcd.display("toggle mute")


def on_shutdown(_mpd_client, lcd, _commands):
    lcd.display("shutting down")
    subprocess.call(["systemctl", "poweroff"])


def main():
    wiringpi.wiringPiSetup()
    mpd_client = mpd.MPDClient(use_unicode=True)
    mpd_client.connect("localhost", 6600)

    commands = queue.Queue()

    call_async(
        watch_rotary_encoder,
        10,
        8,
        PULLUP,
        partial(rotary_encoder_callback, commands, "quieter", "louder"),
    )
    call_async(
        watch_rotary_encoder,
        11,
        9,
        PULLUP,
        partial(rotary_encoder_callback, commands, "previous", "next"),
    )

    for pin, command in [
        (12, "mute"),
        (15, "restart"),
        (16, "radio"),
        (17, "play"),
        (18, "clear"),
        (19, "random"),
        (20, "shutdown"),
    ]:
        call_async(
            watch_switch,
            pin,
            PULLUP,
            0.1,
            partial(switch_callback, commands, command),
        )

    power_led = LED(28)
    play_led = LED(22)
    random_led = LED(21)

    with LCD("localhost", 13666) as lcd:
        power_led.state = False

        commmand_to_func = {
            "play": on_play,
            "next": partial(on_track_change, "next"),
            "previous": partial(on_track_change, "previous"),
            "random": on_random,
            "quieter": partial(on_volume_change, "quieter"),
            "louder": partial(on_volume_change, "louder"),
            "clear": on_clear,
            "radio": on_radio,
            "restart": on_restart,
            "mute": on_mute,
            "shutdown": on_shutdown,
        }

        while True:
            try:
                command = commands.get(timeout=1)
            except queue.Empty:
                command = None

            commmand_to_func[command](mpd_client, lcd, commands)

            play_led.state = mpd_client.status()["state"] == "play"
            random_led.state = int(mpd_client.status()["random"])


if __name__ == "__main__":
    main()
„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

Ok, da habe ich wohl etwas zu kompliziert gedacht. Danke!
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Juhuu, einen Bug habe ich erfolgreich beseitigt (ich verrate nicht wie lange ich gebraucht habe um das offensichtliche Problem zu verstehen). Die Hauptschleife sieht jetzt so aus

Code: Alles auswählen

…
        while True:
            try:
                command = commands.get(timeout=1)
                commmand_to_func[command](mpd_client, lcd, commands)
            except queue.Empty:
                command = None
                
            # in try-Block verschoben   
            #commmand_to_func[command](mpd_client, lcd, commands)
…
damit es mit command=None nicht fehlschlägt.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Was setzt Du `command` auf None? Wird doch nicht mehr gebraucht.
Besser ist es, den try-Block so klein wie möglich zu halten:

Code: Alles auswählen

        while True:
            try:
                command = commands.get(timeout=1)
            except queue.Empty:
                pass
            else:
                commmand_to_func[command](mpd_client, lcd, commands)
Schreibfehler in Variablennamen sind schlecht, weil man immer wieder den selben Fehler machen muß.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Danke für den Hinweis.
Wenn ich so genau wüßte was ich warum tue und was man besser nicht macht, gäbe es diesen Thread nicht. :wink:


Ich stehe außerdem eh schon wieder vor dem nächsten Problem. Wenn ich die Musikdatenbank zum Beispiel nach den Beatles durchsuche, erhalte ich eine Liste aus dict-Elementen. Soweit so gut...

Code: Alles auswählen

>>> type(m.search("performer", "beatle"))
<class 'list'>
>>> m.search("performer", "beatle")[357]
{'file': 'flac/The Beatles/2009 Revolver/01 04.flac', 'last-modified': '2017-12-02T11:45:22Z', 'format': '44100:16:2', 'time': '180', 'duration': '179.826', 'album': 'Revolver', 'composer': 'George Harrison', 'title': 'Love You To', 'artist': 'The Beatles', 'date': '2009', 'performer': 'The Beatles', 'genre': 'Pop', 'track': '4', 'disc': '1'}
>>> m.search("performer", "beatle")[357]['album']
'Revolver'
..aber daran mir alle (unterschiedlichen) Werte für ''album' aus der gesamten Liste ausgeben zu lassen, scheitere ich grandios.
(Ich muss dazu doch bestimmt nicht die alle dict-Elemente der Liste in einer Schleife durchgehen?)
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Doch. Genau das musst du. Und warum ist das ein Problem?
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das ganze dann in ein `set()` damit man keine Doubletten hat und sortiert:

Code: Alles auswählen

sorted(set(song["album"] for song in m.search("performer", "beatle")))
„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

Ok, danke. Ist natürlich kein Problem (ich habe wohl ein bisschen zu viel an matlab/octave gedacht).
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Zwischendurch ein großes Dankeschön!
Es funktioniert schon fast alles so wie ich es mir vorstelle, aber bevor ich den kompletten Code noch einmal poste, habe ich noch einiges zu tun :)


Mein größtes Problem ist momentan die Anzeige von Umlauten auf dem Display. Zu Beginn hatte ich mpdlcd ([1], noch mit python2) zur Anzeige des aktuellen Titels verwendet. Nun mache ich alles im eigenen Programm, verwende aber für die Anzeige statt dem eigenen Code das Modul python-lcdproc [2] für die Interaktion mit LCDd.
Zum Testen verwende ich gerade das hier

Code: Alles auswählen

#!/usr/bin/env python3
import time
import musicpd as mpd
from lcdproc.server import Server as lcdd

mpd_client = mpd.MPDClient()
mpd_client.connect('localhost', 6600)

now_playing = mpd_client.currentsong()

oled = lcdd('localhost', debug=False)
oled.start_session()

screen_now_playing = oled.add_screen('now_playing')
screen_now_playing.set_priority('foreground')
screen_now_playing.set_timeout(0)
now_playing_line1 = screen_now_playing.add_scroller_widget('now_playing_line1', left=1, top=1, right=28, bottom=1, speed=5, text='')
now_playing_line2 = screen_now_playing.add_scroller_widget('now_playing_line2', left=1, top=2, right=20, bottom=2, speed=5, text='')

line1 = now_playing['title']
line2 = now_playing['artist']

now_playing_line1.set_text(line1)
now_playing_line2.set_text(line2)

time.sleep(10)
Spiele ich nun beispielsweise ein Lied von Björk erscheint im Display BjAπrk (ich weiß nicht ob ich die genau richtigen Zeichen erwischt habe, ich kann die Zeichen ja nicht aus dem Display kopieren).
Grundsätzlich sind Umlaute mit dem Display und LCDd/lcdproc aber kein Problem und mit mpdlcd haben sie funktioniert ohne dass ich etwas unternehmen mußte.


[1] https://github.com/rbarrois/mpdlcd
[2] https://github.com/jinglemansweep/lcdproc
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Vielleicht habe ich etwas gefunden:

mpdlcd hat lcdproc eingebaut, allerdings wurde dort lcdproc angepasst. Die entscheidenden Änderungen könnten die hier sein: https://github.com/rbarrois/mpdlcd/comm ... 7b3bf88561
Wenn mir hier niemand einen besseren Rat gibt, werde ich versuchen statt lcdproc, das in mpdlcd eingebaute lcdproc in mein Programm zu importieren.
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Die Probleme mit Umlauten bin ich mit dem Umstieg auf das lcdproc aus mpdlcd los geworden und sonst habe ich mich bemüht nicht zu viel Dummes anzustellen, allerdings mit mäßigem Erfolg, befürchte ich. Wenn ich noch einmal den kompletten Code posten darf:

Code: Alles auswählen

#!/usr/bin/env python3

import math
import queue
import socket
import subprocess
import threading
import time
import datetime

from functools import partial
from mpdlcd.vendor.lcdproc.server import Server as lcdd
import musicpd as mpd
import wiringpi2 as wiringpi

# 1 und 2 fuer Pull-up und -down sind beim Cubietruck vertauscht.
PULLUP = 1

def call_async(function, *arguments):
	thread = threading.Thread(target=function, args=arguments)
	thread.daemon = True
	thread.start()
	return thread


def watch_rotary_encoder(a_pin, b_pin, pin_pud, callback):
	wiringpi.pinMode(a_pin, 0)
	wiringpi.pinMode(b_pin, 0)
	wiringpi.pullUpDnControl(a_pin, pin_pud)
	wiringpi.pullUpDnControl(b_pin, pin_pud)
	steps_per_indent = 4
	last_direction = 0
	delta_sum = 0
	a_state = wiringpi.digitalRead(a_pin)
	b_state = wiringpi.digitalRead(b_pin)
	last_rotation_sequence = (a_state ^ b_state) | b_state << 1
	while True:
		a_state = wiringpi.digitalRead(a_pin)
		b_state = wiringpi.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 last_direction == 0:
				if delta == 3:
					delta = -1
				elif delta == 2:
					delta = 0
			elif last_direction * delta < 0:
				delta = math.copysign(4 - delta, last_direction)
			last_direction = delta
			last_rotation_sequence = rotation_sequence
			delta_sum += delta
			delta = delta_sum // steps_per_indent
			if delta != 0:
				callback(delta)
				delta_sum %= steps_per_indent
		else:
			last_direction = 0
		time.sleep(0.005)


def watch_switch(pin, pin_pud, debounce, callback):
	wiringpi.pinMode(pin, 0)
	wiringpi.pullUpDnControl(pin, pin_pud)
	last_state = wiringpi.digitalRead(pin)
	while True:
		state = wiringpi.digitalRead(pin)
		if state != last_state:
			callback(state - last_state)
			time.sleep(debounce)
		last_state = state
		time.sleep(0.05)


class LED:
	def __init__(self, pin):
		self.pin = pin
		wiringpi.pinMode(self.pin, 1)
		wiringpi.digitalWrite(self.pin, False)
	
	@property
	def state(self):
		return wiringpi.digitalRead(self.pin)
	
	@state.setter
	def state(self, value):
		wiringpi.digitalWrite(self.pin, value)


def rotary_encoder_callback(commands, left_command, right_command, event):
	if event == -1:
		commands.put(left_command)
	elif event == 1:
		commands.put(right_command)


def switch_callback(commands, command, event):
	if event == 1:
		commands.put(command)


		
def on_power(_mpd_client, _commands, _feedback_line1, _feedback_line2):
	subprocess.call(['shutdown', '-h', 'now'])


def on_play(mpd_client, _commands, feedback_line1, feedback_line2):
	now_status = mpd_client.status()
	line2 = ''
	if int(now_status['playlistlength']) == 0:
		line1 = 'EMPTY QUEUE'
	elif mpd_client.status()['state'] == 'play':
		mpd_client.pause()
		line1 = 'PAUSE ||'
	else:
		mpd_client.play()
		line1 = 'PLAY >'
	feedback_line1.set_text(line1)
	feedback_line2.set_text(line2)


def on_mode1(mpd_client, _commands, feedback_line1, _feedback_line2):
	mpd_client.clear()
	feedback_line1.set_text('CLEAR QUEUE')


def on_mode2(mpd_client, _commands, feedback_line1, feedback_line2):
	random = (1 - int(mpd_client.status()['random']))
	mpd_client.random(random)
	line1 = 'RANDOM:'
	if random == 0:
		line2 = 'OFF'
	else:
		line2 = 'ON'
	feedback_line1.set_text(line1)
	feedback_line2.set_text(line2)


def on_radio(mpd_client, _commands, feedback_line1, feedback_line2):
	mpd_client.clear()
	mpd_client.load('radio')
	mpd_client.play()
	now_song = mpd_client.currentsong()
	line1 = 'INTERNET RADIO:'
	line2 = '{0:3d}: {1}'.format(int(now_song['pos']) + 1, now_song['name'])
	feedback_line1.set_text(line1)
	feedback_line2.set_text(line2)


def on_track_change(command, mpd_client, _commands, feedback_line1, feedback_line2):
	now_status = mpd_client.status()
	if now_status['state'] != 'play' and now_status['state'] != 'pause':
		line1 = 'NOT PLAYING'
		line2 = ''
	else:
		now_length = int(now_status['playlistlength'])
		now_position = int(now_status['song'])
		if command == 'next':
			line1 = 'NEXT >>|'
			new_position = (now_position + 1) % now_length
		elif command == 'previous':
			if float(now_status['elapsed']) < 5:
				line1 = 'PREV |<<'
				new_position = (now_position - 1) % now_length
			else:
				new_position = now_position
				line1 = ('|<')
		mpd_client.play(new_position)
		line2 = '{0:3d}/{1:3d}'.format(new_position + 1, now_length)
	feedback_line1.set_text(line1)
	feedback_line2.set_text(line2)


def on_volume_change(command, mpd_client, _commands, feedback_line1, feedback_line2):
	delta = {'down': -1, 'up': 1}[command]
	# TODO
	# rethink calculation of new_volume
	new_volume = 2 * min(50, max(0, int(float(mpd_client.status()['volume'])/2) + delta))
	mpd_client.setvol(new_volume)
	line1 = 'VOLUME:'
	line2 = '{0:1.2f}'.format(new_volume / 100)
	feedback_line1.set_text(line1)
	feedback_line2.set_text(line2)


def on_enter(mpd_client, commands, feedback_line1, feedback_line2):
	outputs = mpd_client.outputs()
	number_of_outputs = len(outputs)
	items = []
	for i in range(number_of_outputs):
		items.append(outputs[i]['outputname'])
	while True:
		command, index = select_from_list(mpd_client, commands, 'Output', items, feedback_line1, feedback_line2)
		if command == 'enter':
			line1 = '{0}:'.format(outputs[index]['outputname'])
			if int(outputs[index]['outputenabled']) == 0:
				line2 = 'ON'
			else:
				line2 = 'OFF'
			feedback_line1.set_text(line1)
			feedback_line2.set_text(line2)
			mpd_client.toggleoutput(outputs[index]['outputid'])
			return
		elif command == 'select':
			line1 = '{0}:'.format(outputs[index]['outputname'])
			if int(outputs[index]['outputenabled']) == 0:
				line2 = 'OFF TO EXCL. ON'
				mpd_client.toggleoutput(outputs[index]['outputid'])
			else:
				line2 = 'ON TO EXCL. ON'
			feedback_line1.set_text(line1)
			feedback_line2.set_text(line2)
			for i in range(number_of_outputs):
				if i != index:
					mpd_client.disableoutput(outputs[i]['outputid'])
			return
		else:
			return


def on_select(mpd_client, commands, feedback_line1, feedback_line2):
	filters = []
	tags = mpd_client.tagtypes()
	while True:
		command, index = select_from_list(mpd_client, commands, 'Tag', tags, feedback_line1, feedback_line2)
		selected_tag = tags[index]
		if command == 'select' or command == 'enter':
			items = mpd_client.list(selected_tag, *filters)
			command, index = select_from_list(mpd_client, commands, selected_tag, items, feedback_line1, feedback_line2)
			filters.append(selected_tag)
			filters.append(items[index])
		else:
			return
		if command == 'enter':
			continue
		else:
			break

	if command == 'play':
		mpd_client.clear()
		mpd_client.searchadd(*filters)
		mpd_client.play()
		return
	elif command == 'select':
		items = mpd_client.list('album', *filters)
		command, index = select_from_list(mpd_client, commands, 'Album', items, feedback_line1, feedback_line2)
		selected_album = items[index]
	else:
		return
	
	if command == 'play' or command == 'select':
		mpd_client.clear()
		mpd_client.findadd('album', selected_album)
		mpd_client.play()
		return
	elif command == 'enter':
		mpd_client.findadd('album', selected_album)
		return
		

def select_from_list(mpd_client, commands, name, items, feedback_line1, feedback_line2):
	index = 0
	number_of_items = len(items)
	if number_of_items > 10:
		big_step = int(number_of_items / 20)
	else:
		big_step = 1
	feedback_line1.set_text('SELECT {0}:'.format(name))
	while True:
		feedback_line2.set_text(items[index])
		command = commands.get()
		if command == 'next':
			index = (index + 1) % number_of_items
		elif command == 'previous':
			index = (index - 1) % number_of_items
		elif command == 'up':
			index = (index + big_step) % number_of_items
		elif command == 'down':
			index = (index - big_step) % number_of_items
		else:
			feedback_line1.set_text('')
			feedback_line2.set_text('      ...')
			return command, index
		feedback_line1.set_text('{0} {1}/{2}:'.format(name, index + 1, number_of_items))


def main():
	mpd_host = 'localhost'
	mpd_port = 6600
	lcdd_host = 'localhost'

	wiringpi.wiringPiSetup()
	mpd_client = mpd.MPDClient()
	mpd_client.connect(mpd_host, mpd_port)

	oled = lcdd(lcdd_host, debug=False, charset='iso-8859-1')
	oled.start_session()

	screen_now_playing = oled.add_screen('now_playing')
	screen_now_playing.set_heartbeat('off')
	screen_now_playing.set_priority('foreground')
	screen_now_playing.set_timeout(0)
	now_playing_line1 = screen_now_playing.add_scroller_widget('now_playing_line1', left=1, top=1, right=28, bottom=1, speed=5, text='')
	now_playing_line2 = screen_now_playing.add_scroller_widget('now_playing_line2', left=1, top=2, right=20, bottom=2, speed=5, text='')

	screen_feedback = oled.add_screen('feedback')
	screen_feedback.set_heartbeat('off')
	screen_feedback.set_priority('hidden')
	screen_feedback.set_timeout(0)
	feedback_line1 = screen_feedback.add_string_widget('feedback_line1', '', x=1, y=1)
	feedback_line2 = screen_feedback.add_string_widget('feedback_line2', '', x=1, y=2)

	commands = queue.Queue()

	call_async(
		watch_rotary_encoder,
		11,
		9,
		PULLUP,
		partial(rotary_encoder_callback, commands, 'previous', 'next'),
	)
	call_async(
		watch_rotary_encoder,
		10,
		8,
		PULLUP,
		partial(rotary_encoder_callback, commands, 'down', 'up'),
	)

	for pin, command in [
		(20, 'power'),
		(17, 'play'),
		(19, 'mode1'),
		(16, 'mode2'),
		(18, 'radio'),
	
		(15, 'select'),
		(12, 'enter'),
	]:
		call_async(
			watch_switch,
			pin,
			PULLUP,
			0.1,
			partial(switch_callback, commands, command),
		)

	play_led = LED(22)
	mode1_led = LED(21)
	mode2_led = LED(26)

	commmand_to_func = {
		"power": on_power,
		"play": on_play,
		"mode1": on_mode1,
		"mode2": on_mode2,
		"radio": on_radio,
			
		"select": on_select,
		"previous": partial(on_track_change, "previous"),
		"next": partial(on_track_change, "next"),
			
		"enter": on_enter,
		"down": partial(on_volume_change, "down"),
		"up": partial(on_volume_change, "up"),
	}

	while True:
		try:
			command = commands.get(timeout=1)
		except queue.Empty:
			now_playing = mpd_client.currentsong()
			now_status = mpd_client.status()
			screen_feedback.set_priority('hidden')
			feedback_line1.set_text('')
			feedback_line2.set_text('')
			# TODO
			# andere Taktik für die Titelanzeige überlegen
			try:
				line2 = now_playing['artist']
			except KeyError:
				try:
					line2 = now_playing['name']
				except KeyError:
					line1 = datetime.datetime.now().strftime('     %H:%M')
					line2 = datetime.date.today().strftime('   %Y-%m-%d')
				else:
					try:
						line1 = now_playing['title']
					except KeyError:
						line1 = '{0:3d}/{1:3d}'.format(int(now_status['song']) + 1, int(now_status['playlistlength']))
			else:
				line1 = now_playing['title']

			now_playing_line1.set_text(line1)
			now_playing_line2.set_text(line2)
		else:
			screen_feedback.set_priority('input')
			commmand_to_func[command](mpd_client, commands, feedback_line1, feedback_line2)	

		play_led.state = now_status['state'] == 'play'
		mode1_led.state = int(now_status['playlistlength']) > 0
		mode2_led.state = int(now_status['random'])

if __name__ == '__main__':
	main()

Ich habe zum Beispiel vergeblich versucht den Code für das Ansprechen von LCDd/lcdproc in eine Klasse zu packen, aber weil ich da so viele Handles oder Objekte (?) habe (zuerst LCDd (oled), darüber hinaus den Screen (screen_) und schließlich noch für jede einzelne Zeile ein Widget und dabei möchte ich ja irgendwann auf vier Zeilen aufstocken...) ist jeder Versuch in einem unübersichtlichen Chaos geendet.

Eine Möglichkeit die von call_async gestarteten Threads zu beenden oder zu pausieren wäre auch nett (aber nicht besonders wichtig – ich spiele nur mit dem Gedangen für eine Art Standbymodus möglichst alles was Resourcen und Strom verbraucht anzuhalten bzw. zu deaktiveren).

Dort wo TODO dabei steht habe ich wenigstens eine ungefähre Vorstellung wie ich es besser machen kann. Da hoffe ich also früher oder später auch ohne Hilfe zurechtzukommen.

Auch für Hinweise, was ich sonst noch besser nicht oder anders gemacht hätte, freue ich mich natürlich wieder.


lg smutbert
Benutzeravatar
smutbert
User
Beiträge: 31
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Habe ich (a) keine so dummen Fehler mehr gemacht, die einer Antwort würdig wären oder ist es (b) bei dem Umfang einfach schon ungehörig den kompletten Code zu posten.
Oder habe ich (c) dieses tolle Forum hier sowieso schon genug in Anspruch genommen?

Alle drei Möglichkeiten sind für mich vollkommen ok – alles andere wäre nach der großartigen Hilfe auch vollkommen unangebracht, aber wissen würde ich es gerne, weil ich mich im Falle von (b) bemühen würde nur meine wichtigsten Fragen in kleineren Portionen zu stellen :)
Antworten