Raspberry Pi Wecker mit Webradio (Teil 5)

Webradio steuern

Wie versprochen geht es heute weiter mit der Steuerung des Webradios. Wir wollen uns heute ansehen, wie man den Raspberry Pi über den GPIO Port ansteuert und dort Taster abfragt und eine LED ansteuert.

Hierzu müssen wir zunächst ein wenig Software installieren. Zu aller erst benötigt man die WiringPi Bibliothek. Eine Anleitung, wie man diese herunterlädt und installiert gibt es hier.

Anschliessend muss man noch den Music Player Daemon installieren und dazu den passenden Client, das geschieht mittels

sudo apt-get install mpc mpd

Jetzt öffnet man die Datei /etc/mpd.conf mit einem beliebigen Editor wie z.B. nano oder vim und sucht sich die Zeile, in der steht "bind to address" hier nehmen wir das # am Anfang weg und starten den Daemon mit

sudo /etc/init.d/mpd restart

neu, damit die Änderungen übernommen werden.

Als nächstes benötigen wir eine Playlist, die eine Liste von URLs zu Radio-Streams enthält, die gibt es zuhauf im Internet. Als Beispiel mag diese hier dienen:

http://www.ndr.de/resources/metadaten/audio/m3u/n-joy.m3u
http://mp3-live.swr3.de/swr3_m.m3u
http://mp3-live.dasding.de/dasding_m.m3u
http://mp3-live.swr.de/swr1bw_m.m3u
http://www.ndr.de/resources/metadaten/audio/m3u/ndr2_sh.m3u
http://www.chromanova.de/chromanova.-.chill.high.pls

Speichert diese einfach als webradio.pls im Ordner /home/pi/webradio ab. Diese benötigen wir später.

Jetzt kommt zunächst die WiringPi Bibliothek ins Spiel. Damit wir das Webradio von aussen steuern können, benötigen wir wenigstens 3 Tasten und eine LED. Die drei Tasten bekommen die Funktionen Start/Stop und Next (Nächstes Element der Playlist), eine Taste halten wir uns frei, damit wir später die Weckerfunktionen steuern können, die wir in einer der nächsten Folgen behandeln.

Verdrahten wir also unseren Raspberry GPIO Port wie folgt:

Raspberry Buttons

Raspberry Buttons

Die Pins 1,3, und 5 vom Stecker SV1 verbinden wir noch mittels dreier 10kOhm WIderstände mit Plus. Ich habe dies über einen Wannenstecker gelöst, damit ich die Buttons auf einer externen Platine montieren kann, die an die Oberseite des Gehäuses befestigt wird. Dazu in einer der späteren Folgen mehr.

Wenden wir uns der Software zu und erstellen uns ein kleines C-Programm, daß die Tasten auswertet und die LED schaltet.

#include <stdio.h>
#include <wiringPi.h>
#include <stdlib.h>
#include <syslog.h>

#define DEBOUNCE_TIME   150

int debounceTime = 0;

int on = 0;

void startStopRadio(void) {

        if (millis () < debounceTime) {
                debounceTime = millis () + DEBOUNCE_TIME ;
                return ;
        }

        if (on) {
                on = 0;
                system("mpc stop");
                syslog(LOG_NOTICE,"On.\n");
        }
        else {
                on = 1;
                system("mpc play");
                syslog(LOG_NOTICE,"Off.\n");
        }

        digitalWrite(1,on);

    while (digitalRead (0) == LOW)
                delay(1) ;

    debounceTime = millis () + DEBOUNCE_TIME ;

}

void nextSong(void) {

        if (millis () < debounceTime) {
                debounceTime = millis () + DEBOUNCE_TIME ;
                return;
        }

        // action

        system("mpc next");
        syslog(LOG_NOTICE,"Next song.\n");

        while (digitalRead (3) == LOW)
                delay(1);

        debounceTime = millis () + DEBOUNCE_TIME;
}

int main (int argc, char** argv)
{
        setlogmask (LOG_UPTO (LOG_NOTICE));

        openlog (NULL, LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
        syslog (LOG_NOTICE, "Webradio control started by User %d", getuid ());

        int pin;

    if (wiringPiSetup () == -1) {
        syslog(LOG_NOTICE,"wiringPiSetup failed.\n");
                return 1 ;
        }

        system("gpio edge 0 falling");
        system("gpio edge 3 falling");

        pinMode(0,INPUT);
        pinMode(1,OUTPUT);
        pinMode(3,INPUT);

        if (wiringPiISR (0, INT_EDGE_FALLING, &startStopRadio) < 0) {
                syslog(LOG_NOTICE,"wiringPiISR for pin 0 failed.\n");
                return 1;
        }

        if (wiringPiISR (3, INT_EDGE_FALLING, &nextSong) < 0) {
                syslog(LOG_NOTICE,"wiringPiISR for pin 3 failed.\n");
                return 1;
        }

        for(;;){
                delay(100);
        }

        return 0;

}

Die beiden Funktionen startStopRadio und nextSong werden in Zeile 78 und 83 mit wiringPiISR an die fallende Flanke der jeweiligen Pins gebunden. Das heisst, daß jedesmal wenn der Pegel an den entsprechenden Pins auf LOW wechselt, wird ein Interrupt ausgelöst und die jeweilige Funktion aufgerufen.Wir erinnern uns, daß wir die Pins der Taster ja auf Masse gelegt haben und mit den Tastern auf Plus "ziehen".

Ansonsten läuft das Programm nun in einer Endlosschleife und wartet auf Ereignisse. In der Funktion startStopRadio wird auch noch die LED eingeschaltet, wenn das Webradio läuft.

Das Ganze speichern wir nun als webradio.c (auch wieder im Ordner /home/pi/webradio) und kompilieren mit

gcc -o webradio webradio.c -lwiringPi

Jetzt fehlt uns nur noch ein Skript, was das Programm beim Systemstart als Dienst startet:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          webradio
# Required-Start:    $all
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Webradio
# Description:       Webradio button control
### END INIT INFO

# Author: Matthias Pueski <matthias@pueski.de>
#

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Webradio"
NAME=webradio
DAEMON=/usr/sbin/$NAME
DAEMON_ARGS=""
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
        # Return
        #   0 if daemon has been started
        #   1 if daemon was already running
        #   2 if daemon could not be started
        start-stop-daemon -b --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
                || return 1
        start-stop-daemon -b --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
                $DAEMON_ARGS \
                || return 2
        # Add code here, if necessary, that waits for the process to be ready
        # to handle requests from services started subsequently which depend
        # on this one.  As a last resort, sleep for some time.

        mpc clear
        mpc load /home/pi/webradio/webradio.pls

}

#
# Function that stops the daemon/service
#
do_stop()
{
        # Return
        #   0 if daemon has been stopped
        #   1 if daemon was already stopped
        #   2 if daemon could not be stopped
        #   other if a failure occurred
        start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
        RETVAL="$?"
        [ "$RETVAL" = 2 ] && return 2
        # Wait for children to finish too if this is a daemon that forks
        # and if the daemon is only ever run from this initscript.
        # If the above conditions are not satisfied then add some other code
        # that waits for the process to drop all resources that could be
        # needed by services started subsequently.  A last resort is to
        # sleep for some time.
        start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
        [ "$?" = 2 ] && return 2
        # Many daemons don't delete their pidfiles when they exit.
        rm -f $PIDFILE
        return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
        #
        # If the daemon can reload its configuration without
        # restarting (for example, when it is sent a SIGHUP),
        # then implement that here.
        #
        start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
        return 0
}

case "$1" in
  start)
        [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
        do_start
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  stop)
        [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
        do_stop
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  status)
        status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
        ;;
  #reload|force-reload)
        #
        # If do_reload() is not implemented then leave this commented out
        # and leave 'force-reload' as an alias for 'restart'.
        #
        #log_daemon_msg "Reloading $DESC" "$NAME"
        #do_reload
        #log_end_msg $?
        #;;
  restart|force-reload)
        #
        # If the "reload" option is implemented then remove the
        # 'force-reload' alias
        #
        log_daemon_msg "Restarting $DESC" "$NAME"
        do_stop
        case "$?" in
          0|1)
                do_start
                case "$?" in
                        0) log_end_msg 0 ;;
                        1) log_end_msg 1 ;; # Old process is still running
                        *) log_end_msg 1 ;; # Failed to start
                esac
                ;;
          *)
                # Failed to stop
                log_end_msg 1
                ;;
        esac
        ;;
  *)
        #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
        echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
        exit 3
        ;;
esac

:

Dieses Skript liegt übrigens als Vorlage unter /etc/init.d/skeleton und kann für jeden beliebigen Zweck angepasst werden. Hierzu kopiert man sich einfach das Skript und passt die Variablen an seine Zwecke an. Man kopiert nun das Skript nach /etc/init.d und das Program nach /usr/sbin und führt folgenden Befehl aus:

update-rc.d webradio defaults

Jetzt wird das Skript nach jedem Start automatisch geladen und startet das Webradio Program im Hintergrund. Starten können wir es natürlich jetzt schon mit:

/etc/init.d/webradio start

Nun sollte sich das Radio mittels Taster starten lassen und die LED einschalten. Ob alles klappt, kann man sich mit

tail -f /var/log/syslog

ausgeben lassen. Falls es nicht funktioniert, muss man natürlich auf Fehlersuche gehen.

Das war es für heute. In der nächsten Folge löten wir uns die Uhrenplatine zusammen, die ich bereits habe fertigen lassen und machen uns Gedanken über die Alarmfunktionen.

Bis dahin wünsche ich allen Lesern fröhliches Basteln. 😉

Raspberry Pi Wecker mit Webradio (Teil 4)

In der letzten Folge hatten wir uns ja die Innereien des Uhrenmoduls angesehen. Heute wollen wir einen kurzen Blick darauf werfen, wie die Uhrzeit vom Raspberry auf das Uhrenmodul kommt und wie die Auswertung der Zeit erfolgt, die ja über die serielle Schnittstelle übertragen wird.

Zunächst müssen wir dem ATMega beibringen, Daten von der seriellen Schnittstelle zu lesen. Ich verwende hierzu die UART-Bibliothek von Peter Fleury.  Um diese zu verwenden, müssen wir die Bibliothek mittels des Befehls:

uart_init( UART_BAUD_SELECT(UART_BAUD_RATE,F_CPU) );

initialisieren. Die UART_BAUD_RATE beträgt in meinem Falle 9600 Baud, es geht natürlich auch schneller. Die Geschwindigkeit ist aber für die Übertragung der Zeit völlig ausreichend. Nun müssen wir in der Hauptschleife des Programm zyklisch prüfen, ob Daten an der seriellen Schnittstelle anliegen, dafür basteln wir uns eine Funktion:

void getUart() {
    /*
     * Get received character from ringbuffer
     * uart_getc() returns in the lower byte the received character and
     * in the higher byte (bitmask) the last receive error
     * UART_NO_DATA is returned when no data is available.
     *
     */
    char c = uart_getc();

    if ( c & UART_NO_DATA )
    {
        /*
         * no data available from UART
         */
    }
    else
    {
        /*
         * new data available from UART
         * check for Frame or Overrun error
         */
        if ( c & UART_FRAME_ERROR )
        {
            /* Framing Error detected, i.e no stop bit detected */
            uart_puts_P("UART Frame Error: ");
        }
        if ( c & UART_OVERRUN_ERROR )
        {
            /*
             * Overrun, a character already present in the UART UDR register was
             * not read by the interrupt handler before the next character arrived,
             * one or more received characters have been dropped
             */
            uart_puts_P("UART Overrun Error: ");
        }
        if ( c & UART_BUFFER_OVERFLOW )
        {
            /*
             * We are not reading the receive buffer fast enough,
             * one or more received character have been dropped
             */
            uart_puts_P("Buffer overflow error: ");
        }

        if (charNum < 16) {

            if ((unsigned char)c == 'r') {

            	parseCommand(buffer,charNum);

            	uart_puts("rn");
            	uart_puts("Ok.rn");

            	charNum = 0;
            }
            else {
            	if (isalnum(c) || isblank(c)) {
					uart_putc(c);
					buffer[charNum] = (unsigned char)c;
					charNum++;
            	}
            }

        }

    }

}

Da wir jeweils immer nur ein Zeichen einlesen können, müssen wir eine maximale Länge (hier 16) definieren und uns merken, an welcher Position wir uns befinden. Am Ende des Kommandos erfolgt ein CR LF, damit der Controller weiss, daß wir einen kompletten Befehl empfangen haben. Jetzt fehlt nur noch das Auswerten des empfangenen Kommandos und das geht so:

void parseCommand(char* cmd, uint8_t len) {

	if (strncmp(cmd,"time",4) == 0) {

		char* t = &buffer[5];

		uint16_t time   = atoi(t);

		int hour = time / 100;
		int minute = time - ((time / 100) * 100);

		tmp_date.hour = hour;
		tmp_date.minute = minute;
		tmp_date.month = 1;
		tmp_date.year = 0;
		tmp_date.day = 1;

		if (isValidTime(tmp_date)) {
			current_date.hour = hour;
			current_date.minute = minute;
		}

	}
	else if(strncmp(cmd,"gettime",7) == 0) {
		sprintf(buffer,"rn%02d:%02d",current_date.hour,current_date.minute);
		uart_puts(buffer);
	}

}

Nun können wir über die serielle Schnittstelle die Zeit empfangen und die aktuelle Zeit ausgeben. Wie aber senden wir nun die Daten vom Raspberry?

Hierzu benötigen wir zunächst ein einfaches Python Script, was die Zeit als Kommandozeilenargument übergeben bekommt und diese dann an die serielle Schnittstelle sendet:

import serial
import sys
ser = serial.Serial("/dev/ttyAMA0",9600);
ser.write("time "+sys.argv[1]+"rn")
ser.close()

Und das Shellscript, welches wir später periodisch über einen Cronjob anstossen, um die Zeit zu synchronisieren:

#!/bin/bash ntpdate -s 0.de.pool.ntp.org date +"%H%M" | xargs python /home/pi/webradio/time.py

Damit haben wir nun alles zusammen und können einen Cronjob anlegen, der einmal in der Stunde die Zeit aktualisiert, hierzu verwenden wir den Befehl crontab -e und fügen folgende Zeile hinzu:

0 */1 * * * /home/pi/webradio/time.sh

Das Script muss sich natürlich am entsprechenden Ort befinden.

Das war's dann für heute. In der nächsten Folge schauen wir uns an, wie das eigentliche Webradio auf dem Raspberry funktioniert und wie wir selbiges mit externen Tastern steuern können.