Motivation

Alles fing damit an, dass unser Kombi-Verstäker im Wohnzimmer langsam verstarb. Erst versagte der DVD-Laser aufgrund der Temperatur im Gerät und dann fing eine Endstufe das Spinnen an. Es war also an der Zeit einen neuen Verstäker zu kaufen. Nach längerer Recherche überzeugte der NAD 7050D. Das Problem lag nur darin, dass der Verstäker voll digital arbeitet und keinen Analogeingang für ein UKW-Radio bietet.
Gut, was nun. Neben dem Fernsehton ist Radio hören die wesentliche Nutzung. Da war guter Rat teuer.
Zu dem Zeitpunkt dachte ich das erste Mal daran einen Raspberry Pi einzusetzen und den NAD als USB Sound Device zu nutzen. Mein VDR-Server mit vier USB-SAT-Receivern bietet live Streaming auch von SAT-Radio und der MPD kann das abspielen. Internet Radio schied bei 1,2MBit Downstream aus. Die Lösung mit einem Drehknopf Überzeugte meine Familie nicht wirklich. Es musste eine andere Lösung her.

Das die neue Lösung eine universelle Touch Oberfläche für Kommandozeilen wurde, ist ein angenehmer Nebeneffekt für weitere Ideen.

Hardware

Bild 00
Um eine gute Bedienbarkeit zu erreichen, habe ich mich für ein kleines Gehäuse mit integriertem Touch Display entschieden.

Tontec® 3.5 Zoll Raspberry Pi Touchscreen Display Monitor 480x320 LCD Bildschirm mit transparentem Gehäuse


Damit komme ich mit drei Kabeln ohne extra Netzteil aus und der Raspberry Pi nur läuft nur wenn der Verstärker auch an ist.
  • 1x USB-A-Stecker auf Micro-B-Stecker Kabel als Stromversorgung durch den NAD Verstärker
  • 1x USB-A-Stecker auf USB-B-Stecker um den NAD Verstärker als Sound Device zu verwenden
  • 1x LAN Kabel für die Verbindung zum VDR-Server
Bild 01
In der Anleitung des Herstellers habe ich dieses Bild vermisst. Es zeigt die Position des Steckverbinders auf der Pfostenleiste und die Position der beiliegenden Unterlegscheibe zwischen Platine und Schraubbuchse.

Applikation

Erst einmal habe ich ganz normal ein aktuelles Raspbian Image auf die Micro-SD-Karte gespielt und aktualisiert:

sudo apt-get update sudo apt-get upgrade sudo reboot
Anschließend wird die SPI Kommunikation zum Display aktiviert. Entgegen der Anleitung des Herstellers ist beim Raspbian Jessie vom Mai 2016 der richtige Treiber bereits im Image enthalten.

sudo nano /boot/config.txt
dtparam=spi=on dtoverlay=mz61581

Als nächstes ist die Anzeige auf das Touch Display umzustellen:

sudo nano /usr/share/X11/xorg.conf.d/99-fbturbo.conf
Option "fbdev" "/dev/fb1"

Zum Abschluss muss der Touch Sensor noch kalibriert werden. Bei mir haben diese Parameter funktioniert. Die Anleitung zum Kalibrieren ist Standard und hängt nicht vom Display ab.

sudo nano /usr/share/X11/xorg.conf.d/99-calibration.conf
Section "InputClass" Identifier "calibration" MatchProduct "ADS7846 Touchscreen" Option "Calibration" "192 3914 251 3784" Option "SwapAxes" "0" EndSection

Jetzt ist noch neu zu booten und alles sollte laufen.

sudo reboot
Zu diesem Zeitpunkt ist es gut eine Maus angeschlossen zu haben. Dann lässt sich die Startzeile aufräumen (Mathematica, Wolfram und Bluetooth entfernen), der SSH-Server aktivieren und der Samba-Server abschalten.

Als nächstes sind der Mediaplayer Daemon und Client zu installieren

sudo apt-get install mpd mpc
Dann wird eine Playlist erstellt. In meinem Fall enthält sie Links zum VDR Streaming Server. Ich bitte den Servernamen <server> durch den Namen des eigenen Servers zu ersetzen.

sudo nano /var/lib/mpd/playlists/sender.m3u
#EXTM3U #EXTINF:1,Antenne Bayern http://<server>:3000/ES/S19.2E-133-7-170 #EXTINF:2,NDR 2 http://<server>:3000/ES/S19.2E-1-1093-28437 #EXTINF:3,BAYERN 3 http://<server>:3000/ES/S19.2E-1-1093-28402 #EXTINF:4,Fritz http://<server>:3000/ES/S19.2E-1-1093-28457 #EXTINF:5,PULS http://<server>:3000/ES/S19.2E-1-1093-28406 #EXTINF:6,hr3 http://<server>:3000/ES/S19.2E-1-1093-28421 #EXTINF:7,SWR3 http://<server>:3000/ES/S19.2E-1-1093-28468 #EXTINF:8,MDR JUMP http://<server>:3000/ES/S19.2E-1-1093-28432 #EXTINF:9,MDR SPUTNIK http://<server>:3000/ES/S19.2E-1-1093-28433 #EXTINF:10,N-JOY http://<server>:3000/ES/S19.2E-1-1093-28440 #EXTINF:11,1LIVE http://<server>:3000/ES/S19.2E-1-1093-28475 #EXTINF:12,1LIVE diGGi http://<server>:3000/ES/S19.2E-1-1093-28481 #EXTINF:13,DASDING http://<server>:3000/ES/S19.2E-1-1093-28471 #EXTINF:14,YOU FM http://<server>:3000/ES/S19.2E-1-1093-28423

Nun fehlt noch die Umstellung auf das USB-Audio Gerät. Dazu sind die vorhandenen audio_output Bereiche vollständig als Kommentar zu setzen und das USB-Gerät als Ausgabe am Ende einzutragen.

sudo nano /etc/mpd.conf
audio_output { type "alsa" name "NAD USB Audio" device "hw:1,0" # optional }

Mit den folgenden Kommandos wird der Daemon zum Einlesen der geänderten Konfiguration neu gestartet und in meinem Fall Bayer3 mit voller Lautstärke ausgegeben.

sudo service mpd restart mpc load sender mpc playlist mpc play 3
Jetzt sollte das laufende Programm des Senders zu hören sein.

Nun wird es spannend. David Hunts Projekt Lapse-Pi Touch hat mich inspiriert.
Er verwendet zur Bedienung seiner Anwendung mit einem Touch Display die Python-Bibliothek PyGame.

Das mache ich mit dem folgenden Python Script auch. Ich habe sein Script genommen und geringfügig angepasst. Erst einmal werden Bibliotheken importiert, die so gebraucht werden. Dann definiere ich ein paar Parameter für den Bildschirm.
Das Entscheidende ist seine Klasse Button. Sie macht alles, was für die Bedienung wichtig ist. Die Methode selected wertet die Touch Bedienung aus und startet die Kommandos über shlep. Mit der Funktion SetCaption vereinfache ich mir auch das Leben. Der entscheidende Punkt ist die Initialisierung des buttons Arrays. Hier werden für jeden Knopf Position und Größe, die anzuzeigende Grafik sowie die ausführende Callback Funktion definiert. Die Grafik für die Buttons ist jeweils ein transparentes PNG mit 80x80 Pixel.
Mit shlep wird der MPC ordentlich gestartet.
Das Backlight wird ohne Warnungen eingeschaltet. Dann wird die Spielebiliothek initialisiert, die Button Liste durchgelesen und die Maus unsichtbar gemacht.
Die While-Schleife läuft endlos, bearbeitet die gedrückten Buttons und schaltet das Backlight nach 30s ab. Das war's.
Um beim Einschalten des Backlights sicher nichts zu bewirken, sollte die Applikationsfläche mit den Buttons getroffen werden. Trifft an die Caption (Titelzeile) so geht das Backlight nicht an und das Fenster wird verschoben.

Update 18.12.2021 "Rasbian OS Buster"

  1. shlepResult wird neu ermittelt
  2. die Initalisierung des MPD mit shlep hat sich geändert
nano /home/pi/pygameradio/pygameradio.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
# UI to use Icons to control MPC

import pygame, sys, shlex, time
import RPi.GPIO as GPIO
from subprocess import Popen, PIPE
from pygame.locals import *

WINDOWWIDTH  = 480 # size of window's width in pixels
WINDOWHEIGHT = 320 # size of windows' height in pixels
WINDOWBORDER = 12 # border if each button
BOTTONSIZEX  = WINDOWWIDTH/6
BOTTONSIZEY  = WINDOWHEIGHT/4
BOTTONSTEPX  = BOTTONSIZEX+WINDOWBORDER
BOTTONSTEPY  = BOTTONSIZEY+WINDOWBORDER

#            R    G    B
WHITE   = (255, 255, 255)
BGCOLOR = WHITE

BACKLIGHTTIMEOUT = 30
BacklightTimer   = BACKLIGHTTIMEOUT

# Button is a simple tappable screen region.  Each has:
#  - bounding rect ((X,Y,W,H) in pixels)
#  - background Icon, always centered
#  - single callback function
#  - string value passed to callback
# Occasionally Buttons are used as a convenience for positioning Icons
# but the taps are ignored.  Stacking order is important; when Buttons
# overlap, lowest/first Button in list takes precedence when processing
# input, and highest/last Button is drawn atop prior Button(s).  This is
# used, for example, to center an Icon by creating a passive Button the
# width of the full screen, but with other buttons left or right that
# may take input precedence (e.g. the Effect labels & buttons).
# After Icons are loaded at runtime, a pass is made through the global
# buttons[] list to assign the Icon objects (from names) to each Button.

class Button:

        def __init__(self, rect, picture, callback, command):
          self.rect     = rect                       # Bounds
          self.bg       = pygame.image.load(picture) # Background of the button
          self.callback = callback                   # Callback function
          self.cmd      = command                    # Value passed to callback

        def selected(self, pos):
          x1 = self.rect[0]
          y1 = self.rect[1]
          x2 = x1 + self.rect[2] - 1
          y2 = y1 + self.rect[3] - 1
          if ((pos[0] >= x1) and (pos[0] <= x2) and
              (pos[1] >= y1) and (pos[1] <= y2)):
            pygame.display.set_caption("")
            if self.callback:
              if self.cmd is None: self.callback()
              else:                self.callback(self.cmd)
            return True
          return False

        def draw(self, screen):
          if self.color:
            screen.fill(self.color, self.rect)
          if self.iconBg:
            screen.blit(self.iconBg.bitmap,
              (self.rect[0]+(self.rect[2]-self.iconBg.bitmap.get_width())/2,
               self.rect[1]+(self.rect[3]-self.iconBg.bitmap.get_height())/2))
          if self.iconFg:
            screen.blit(self.iconFg.bitmap,
              (self.rect[0]+(self.rect[2]-self.iconFg.bitmap.get_width())/2,
               self.rect[1]+(self.rect[3]-self.iconFg.bitmap.get_height())/2))

        def setBg(self, name):
          if name is None:
            self.iconBg = None
          else:
            for i in icons:
              if name == i.name:
                self.iconBg = i
                break

def shlep(cmd):
    '''shlex split and popen
    '''
    parsed_cmd = shlex.split("/bin/bash -c '" + cmd + "'")
    proc = Popen(parsed_cmd, stdout=PIPE, stderr=PIPE)
    out, err = proc.communicate()
    return (proc.returncode, out, err)

def SetCaption():
    shlepResult = shlep('mpc | head -n 1 | cut -d\: -f1')
    sender = shlepResult[1].rstrip('\n')
    if sender[:6] == "volume":
        pygame.display.set_caption("Aus")
    else:
        pygame.display.set_caption("Aktuell läuft:   " + sender)

buttons = [
    Button((WINDOWBORDER,                 WINDOWBORDER,                 BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/antenne.png',     shlep, 'mpc play 1'),
    Button((WINDOWBORDER+BOTTONSTEPX,     WINDOWBORDER,                 BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/ndr2.png',        shlep, 'mpc play 2'),
    Button((WINDOWBORDER+(2*BOTTONSTEPX), WINDOWBORDER,                 BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/bayern3.png',     shlep, 'mpc play 3'),
    Button((WINDOWBORDER+(3*BOTTONSTEPX), WINDOWBORDER,                 BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/fritz.png',       shlep, 'mpc play 4'),
    Button((WINDOWBORDER+(4*BOTTONSTEPX), WINDOWBORDER,                 BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/puls.png',        shlep, 'mpc play 5'),
    Button((WINDOWBORDER,                 WINDOWBORDER+BOTTONSTEPY,     BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/hr3.png',         shlep, 'mpc play 6'),
    Button((WINDOWBORDER+BOTTONSTEPX,     WINDOWBORDER+BOTTONSTEPY,     BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/swr3.png',        shlep, 'mpc play 7'),
    Button((WINDOWBORDER+(2*BOTTONSTEPX), WINDOWBORDER+BOTTONSTEPY,     BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/jump.png',        shlep, 'mpc play 8'),
    Button((WINDOWBORDER+(3*BOTTONSTEPX), WINDOWBORDER+BOTTONSTEPY,     BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/sputnik.png',     shlep, 'mpc play 9'),
    Button((WINDOWBORDER+(4*BOTTONSTEPX), WINDOWBORDER+BOTTONSTEPY,     BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/n-joy.png',       shlep, 'mpc play 10'),
    Button((WINDOWBORDER,                 WINDOWBORDER+(2*BOTTONSTEPY), BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/1live.png',       shlep, 'mpc play 11'),
    Button((WINDOWBORDER+BOTTONSTEPX,     WINDOWBORDER+(2*BOTTONSTEPY), BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/1live-diggi.png', shlep, 'mpc play 12'),
    Button((WINDOWBORDER+(2*BOTTONSTEPX), WINDOWBORDER+(2*BOTTONSTEPY), BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/dasding.png',     shlep, 'mpc play 13'),
    Button((WINDOWBORDER+(3*BOTTONSTEPX), WINDOWBORDER+(2*BOTTONSTEPY), BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/youfm.png',       shlep, 'mpc play 14'),
    Button((WINDOWBORDER+(4*BOTTONSTEPX), WINDOWBORDER+(2*BOTTONSTEPY), BOTTONSIZEX, BOTTONSIZEY), '/home/pi/pygameradio/schalter.png',    shlep, 'mpc stop')
]

# Initalize the MPD
shlep('sleep 2; mpc stop; mpc clear; mpc load sender; sleep 2; mpc play 3')
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(18,GPIO.OUT)

pygame.init()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
DISPLAYSURF.fill(BGCOLOR)
for item in buttons:
    DISPLAYSURF.blit(item.bg,(item.rect[0],item.rect[1]))
pygame.mouse.set_visible(False)
pygame.display.update()
SetCaption()

while True: # main game loop
    for event in pygame.event.get():
        if(event.type is MOUSEBUTTONDOWN):
          if BacklightTimer < 0:
            BacklightTimer = BACKLIGHTTIMEOUT
            # Backlight on
            GPIO.output(18,0)
          else:
            pos = pygame.mouse.get_pos()
            for b in buttons:
              if b.selected(pos):
                BacklightTimer = BACKLIGHTTIMEOUT
                SetCaption()
                break
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    if BacklightTimer > 0:
      BacklightTimer = BacklightTimer -1
    elif BacklightTimer == 0:
      BacklightTimer = -1
      # Backlight off
      GPIO.output(18,1)
    time.sleep(1)

Nun muss die Radio Oberfläche noch automatisch gestartet werden. Dazu wird die Autostart Datei um eine Zeile ergänzt.

Update 18.12.2021 "Rasbian OS Buster"

  • der Autostart erfolgt jetzt anders
sudo nano /etc/xdg/autostart/piradio.desktop
[Desktop Entry] Type=Application Name=PiRadio Comment=Touch Radio NoDisplay=true Exec=/usr/bin/python /home/pi/pygameradio/pygameradio.py NotShowIn=GNOME;KDE;XFCE;

Mit einem Reboot sollte nun alles ordnungsgemäß arbeiten.

sudo reboot
Wenn jemand Anregungen für mein SAT-Radio mit Touch Oberfläche hat, bin ich über eMail zuerreichen.
Ich freue mich auch über eine eMail die mir nur von einem erfolgreichen Nachbau berichtet.

Nun wünsche viel Erfolg!