Musikspieler mit Display und Bedienelementen

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

Sonntag 29. September 2019, 14:03

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

Mittwoch 9. Oktober 2019, 15:43

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: 17
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Donnerstag 10. Oktober 2019, 08:35

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: 17
Registriert: Samstag 5. Juli 2014, 17:22
Wohnort: Graz

Montag 14. Oktober 2019, 22:30

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
Antworten