Werkstatt-Radio: Software, GPIO und Webradio

Titelbild zum Werkstatt-Radio mit Raspberry Pi

Lesezeit: 13 Minuten

Jetzt bekommt das Werkstatt Radio Leben eingehaucht. Im dritten Teil geht es um die Software und deren Entwicklung: Wie werden die GPIO beschaltet? Wie spielt man Webradio ab? Wie erstellt man eine Benutzeroberfläche?

Dafür gliedert sich der Blog-Artikel in 4 Kapitel. Im ersten Kapitel geht es um die Voraussetzungen für die Programmierung. Ich habe als Programmierumgebung Lazarus gewählt. Wie die GPIO-Pins des Raspberry Pi mit Lazarus ausgewertet und geschaltet werden, beschreibe ich im zweiten Kapitel. In Kapitel 3 spielen wir das erste Webradio ab. Vorher brauchen wir aber noch die passende URL. Wie alles unter einer Benutzeroberfläche zusammengefügt, zeige ich in Kapitel 4.

Das neue Smiley-Design blieb auch in meiner Familie nicht unbemerkt. Plötzlich interessierten sich auch mein Sohn und meine Tochter für die Software. Der Druck etwas Cooles zu machen, war darum umso größer. Was daraus geworden ist, sehen Sie jetzt.

Einen Link für den Download des Quelltextes und einer läuffähigen Version für den Raspberry Pi finden Sie am Ende des Artikels (oder hier).

1 Freepascal, Lazarus und Packages

Wir möchten mit Freepascal unter Lazarus entwickeln. Das bedeutet, auch Dinge direkt mit unserer Elektronik auszuprobieren. In diesem Fall ist es am besten, direkt auf dem Raspberry Pi zu arbeiten. Dafür muss Lazarus und Freepascal auf dem Pi installiert werden.

1.1 Freepascal und Lazarus

Und es gibt mehrere Möglichkeiten, wie Sie das machen können. Der einfache Weg geht über apt-get install. Allerdings bekommen Sie dann eine ziemlich alte Version von Lazarus.

sudo apt-get update
sudo apt-get install fpc
sudo apt-get install lazarus

Wie Sie die aktuelle Version bekommen, habe ich in diesem Blog-Artikel beschrieben. Das würde ich Ihnen auch empfehlen Aktuelle Lazarus-Version auf einem Raspberry Pi installieren.

1.2 Wiringpi für GPIO

Die Verbindung zu den GPIO-Pins realisieren wir über Gordon Hendersons wiringpi. Falls es noch nicht auf Ihrem Raspberry installiert ist, holen Sie das gleich nach.

sudo apt-get install wiringpi

Damit Freepascal die C-Routinen von wiringpi aufrufen kann, benötigen wir noch einen Wrapper. Ich habe nicht den empfohlenen aus Freepascal.org von Alex Schaller verwendet. Ich benutze den von bw38.de. Der ist einfach aktueller und bw38.de hat auch eine äußerst informative Webseite für Lazarus und Raspberry Pi erstellt.

1.3 BASS für Webradio

Um Webradio abzuspielen, brauchen wir auch eine Bibliothek. Das kann Lazarus nicht auf Haus aus. Hier gibt es eine Reihe zur Auswahl. Mir gefällt schon einige Zeit die Bibliothek BASS von un4seen.com sehr gut. Die Bibliothek bietet viele Möglichkeiten – auch zum Abspielen von MP3-Dateien – und ist einfach zu integrieren.

Die aktuelle Version gibt es unter http://www.un4seen.com/.

Damit wir die gleiche Basis für die Entwicklung haben, finden Sie beide Bibliotheken in meinem Quellcode am Ende des Blog-Artikels unter Downloads.

2 GPIO-Pins für Inputs und Outputs

Und jetzt geht es richtig los. Um unsere Elektronikkomponenten aus dem letzten Teil auch einsetzen zu können, müssen wir die GPIO’s entsprechend ansteuern oder auswerten.

2.1 Vorbereitung

Damit Lazarus auf die Routinen von Gordon Henderson zugreifen kann, müssen wir nur die Unit lazwiring in die uses-Anweisung einbinden.

Würden wir nur Output-Pins verwenden, wäre das schon alles. Wir brauchen aber auch Input-Pins, die über Threads Ereignisse melden sollen. Unter Unix ist es erforderlich, dafür in der Hauptprogramm-Unit (lpr-Datei) die Unit cthreads einzubinden.

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads, cmem,
  {$ENDIF}{$ENDIF}
  Interfaces, // this includes the LCL widgetset
  Forms, main, gpiosim, wiringPi;

Wie man im Quellcode sieht, wurde es in Lazarus schon vorbereitet. Es muss nur über eine Definition aktiviert werden. Und das funktioniert über die Projekteinstellungen. Dort legen Sie eine eigene Definition UseCThreads an und aktivieren sie.

Einstellungen für UseCThreads
Einstellungen für UseCThreads

Jetzt fehlt noch die Initialisierung von wiringpi mit der Funktion wiringPiSetup(). Diese wird am besten im FormCreate-Ereignis untergebracht.

procedure TMainForm.FormCreate(Sender: TObject);     
begin
  //…
  wiringPiSetup();
  //…weitere Definitionen, wie Pins…

2.2 GPIO Output-Pins

Eine Pin-Definition beginnt bei mir immer mit der Definition von Konstanten. So bin ich während des Entwickelns von Elektronik und Software viel freier Anpassungen – ohne große Fehlermöglichkeiten – vorzunehmen.

Ich habe mir angewöhnt, das in einer eigenen Unit unterzubringen. In diesem Fall heißt die Unit myobjects.

const
  //Pin-Definitionen

  //Input-Pins
  PIN_ROTARY_TURN1 = 2;
  PIN_ROTARY_TURN2 = 0;
  PIN_ROTARY_CLICK = 3;
  PIN_SENSOR_MOTION = 21;
  PIN_SENSOR_IR = 22;

  //Output-Pins
  PIN_LED_R = 23;
  PIN_LED_G = 24;
  PIN_LED_B = 25;    

Die Definition passiert unter wiringpi mit pinmode.

In unserem Fall werden nur die „Lautsprecher-Augen“ als Output-Pins definiert. Jede Grundfarbe der Multi-LED hat einen Pin bekommen.

  pinmode(PIN_LED_R,pmOUTPUT);
  pinmode(PIN_LED_G,pmOUTPUT);
  pinmode(PIN_LED_B,pmOUTPUT);   

2.3 GPIO Input-Pins

Die Input-Pins brauchen ein wenig mehr Aufwand. Sie laufen im Hintergrund in einem eigenen Thread (durch wiringpi aber außerhalb von Lazarus) und sollen, wenn ein neues Signal auftritt, ein Ereignis in Lazarus erzeugen.

Zuständig für die Initialisierung ist unter wiringpi die Funktion wiringPiISR. ISR steht für Interrup-Service-Routine. Als Parameter braucht die Funktion den Pin, die Aktivierungsart (also steigende oder fallende Flanke oder beide) und eine Callback-Funktion.

procedure TMainForm.FormCreate(Sender: TObject);     
begin
  //…
  PinSensorMotion.Event:=ChangedSensorMotion;

  pinmode(PIN_SENSOR_MOTION,pmINPUT);
  wiringPiISR(PIN_MOTION_SENSOR,intRising,@CallbackSensorMotion);
  //…
end;

In der Callback-Funktion rufen wir über QueueAsyncCall, die eigentliche Prozedur zum Auswerten der Ereignisse auf. Verbunden sind die beiden durch TInputPin mittels TISTEvent.

type
  TISREvent = procedure(x: longint) of object;

  TInputPin = record
    Event : TISREvent;
    Debounce : double;
    Pin : integer;
    Command : integer;
  end;

var
  PinSensorMotion : TInputPin;
procedure CallbackSensorMotion;
begin
  Application.QueueAsyncCall(PinSensorMotion.Event,0);
end; 

In der eigentlichen Ereignis-Auswertung, können wir dann z. B. prüfen, ob das Signal gültig ist oder eine Software-Entprellung durchführen.

Im Fall des Bewegungssensors ist die Entprelldauer 1000 ms.

procedure TMainForm.ChangedSensorMotion(x: longint);
begin
  if MilliSecondsBetween(PinSensorMotion.Debounce,Now)>1000 then
    begin
      GpioSimForm.SetTickerSignal(PIN_SENSOR_MOTION,LevelToBool(digitalRead(PIN_SENSOR_MOTION)));
      MainForm.DoCommand(COMMAND_SENSOR_MOTION); //Übergabe an Befehlsausführung
      PinSensorMotion.Debounce:=Now;
    end;
end;   

Wurde ein gültiger Befehl erkannt, wird er an eine weitere Prozedur, den Befehlsinterpreter, weitergeleitet. Das hat den Vorteil, dass wir so neben den Pins auch andere Benutzer-Interaktionen, wie Klicks auf das Touch-Display, so weiterleiten und analog verarbeiten können.

Ablaufdiagramm für Input-Ereignisse
Ablaufdiagramm für Input-Ereignisse

Auf die Ereignisse des Bewegungsmelders und des Dreh-Encoders können wir genauso reagieren. Der Bewegungsmelder hat ruf nur ein anderen Kommando im Befehlsinterpreter auf (COMMAND_SENSOR_MOTION).

Beim Ereignis des Dreh-Encoders müssen wir prüfen, ob auch ein Signal am zweiten Pin anliegt. Dadurch können wir eine Aktion für eine Links- oder Rechts-Drehung erzeugen. Das Abfragen eines Pins erfolgt bei wiringpi mit digitalRead. Das Klick-Event der Dreh-Encoder-Taste braucht wieder nur eine Software-Entprellung, hier mit 200 ms.

procedure TMainForm.ChangedRotaryTurn(x: longint);
begin
  if MilliSecondsBetween(PinRotaryTurn.Debounce,Now)>200 then
    begin
      if digitalRead(PIN_ROTARY_TURN1)=digitalRead(PIN_ROTARY_TURN2) then
        MainForm.DoCommand(COMMAND_ENCODER_LEFT)
      else MainForm.DoCommand(COMMAND_ENCODER_RIGHT);
      PinRotaryTurn.Debounce:=Now;
    end;
end;

procedure TMainForm.ChangedRotaryClick(x: longint);
begin
  if MilliSecondsBetween(PinRotaryClick.Debounce,Now)>200 then
    begin
      MainForm.DoCommand(COMMAND_ENCODER_KEY);
      PinRotaryClick.Debounce:=Now;
    end;
end;   

Gerade beim Testen von Programmfunktionen bin ich nicht immer in der Nähe des Raspberry’s. Am liebsten arbeite ich per Remotedesktop von meinem Notebook aus und kann die GPIO-Funktionen nicht bedienen. Dafür baue ich mir meistens die Funktionen in der Software ein. Mit diesem Trick wird die Entwicklung deutlich einfacher. Sie können das GPIO-Formular ebenfalls per Konfigdatei ein- und ausblenden. Setzten in der app.conf die GPIO simulation.

GPIO simulation=1
Eingeblendete Software-GPIO-Funktionen
Eingeblendete Software-GPIO-Funktionen

3 Webradios finden und abspielen

3.1 Radio URL’s finden

Damit wir einen Webradiosender abspielen können, brauchen wir seine Adresse, die URL. Eine gute Ausgangsbasis sind beispielsweise die Seiten von:

Suchen Sie dann nach der Streaming-URL. Sie können die URL gleich im Browser ausprobieren, ob sie noch stimmt. Es kommt gar nicht so selten vor, dass diese geändert werden.

Haben Sie eine funktionierende integrieren wie sie in unseren Player. Für jeden Radiosender muss ein eigenes Konfigurationsfile im Ordner config/radio mit dem Sendernamen und der Erweiterung .radio erstellt werden.

Für den Radiosender Bayern 3 lautet die Streaming URL http://streams.br.de/bayern3_1.m3u.

Gehen Sie in den Ordner config/radio und legen eine neue Textdatei an oder kopieren sie eine vorhandene. Sie erkennen sie an der Erweiterung .radio.

Die Datei hat das Format einer Inidatei von Windows.

Für den Sender Bayern 3 heißt die Datei „Bayern 3.radio“ und hat diesen Inhalt:

[Setup] 
Title=Bayern 3
URL=http://streams.br.de/bayern3_1.m3u
Info=Bayern 3 ist toll
  
[Icon]
Small=bayern3.bmp
Big=bayern3.jpg

Den Title können Sie frei wählen. Er wird in der Listenauswahl angezeigt. Die Info ist nur eine zusätzliche Zeile. Sie kann auch leer bleiben.

Unter URL steht natürlich die Streaming URL.

In der Sektion Icon können Sie noch zwei Bilddateien hinterlegen. Ein Icon Small, dass in Auswahllisten angezeigt wird und ein Icon Big das während des Abspielens angezeigt wird. Die Bilddateien müssen sich auch im Ordner config/radio befinden.

Sie finden einige Beispiel-Sender bereits als .radio-Datei gespeichert. Für meine Kids ist die Auswahl vermutlich etwas zu „Old-School“.

3.2 BASS vorbereiten

Jetzt da wir die richtige URL kennen, müssen wir sie nur noch BASS mitteilen. Um BASS zu verwenden, fügen wir sie unserem Lazarus-Projekt hinzu.

uses
LCLIntf, LCLType, LMessages, Messages, SysUtils, Classes, ...lazdynamic_bass;

Als nächstes müssen die Funktionen von BASS geladen werden. Das bringen wir am besten in der FormCreate-Prozedure unter.

Jedes Betriebssystem braucht dafür eine passende Datei. Für den Raspberry Pi mit Raspbian heißt diese Datei libbass.so. Sie befindet sich auch im Download-Ordner mit dem anderen Quellcode.

Ich kopiere die Datei am liebsten in das Projektverzeichnis. Der Befehl zum Laden sieht dann so aus:

Load_BASSDLL(ExtractFilePath(paramstr(0))+'libbass.so');

Dann wird BASS initialisiert und falls es nicht klappt eine Meldung ausgegeben:

if not BASS_Init(-1, 44100, 0, Handle, nil) then
  MessageDlg('Achtung','BassInitError',mtWarning,[mbOk],0);

Die Parameter bedeuten: -1 das Standard-Ausgabegerät, 44100 Hz, 0: Stereo, 16 bit

Dann kommen noch drei Config-Einstellungen:

  BASS_SetConfig(BASS_CONFIG_NET_PLAYLIST, 1); // enable playlist processing
  BASS_SetConfig(BASS_CONFIG_NET_PREBUF, 0); // minimize automatic pre-buffering, so we can do it (and display it) instead
  BASS_SetConfigPtr(BASS_CONFIG_NET_PROXY, nil);

3.3 Webradio abspielen

Das eigentliche Abspielen ist nun sehr einfach. Am besten lagern wir das in eine Prozedur aus. Unter BASS sind mehrere Kanäle gleichzeitig möglich. Wir nutzen aber logischer Weise nur einen. Zugreifen können wir über eine Variable channel. Sie wird global oder im Haupt-Formular definiert.

var
  channel : HSTREAM;    

Die Prozedur zum Abspielen benötigt nur die URL des Radiosenders.Mit BASS_StreamCreateURL wird initialisiert und mit BASS_ChannelPlay abgespielt.

procedure TMainForm.PlayRadio(Url : string);
var
  Len, Progress: DWORD;
begin

  //Falls bereits Radio abgespielt wird, dieses beenden
  BASS_StreamFree(channel);


  //Neue URL
  channel:=BASS_StreamCreateURL(PAnsiChar(Url),0,
    BASS_STREAM_BLOCK or BASS_STREAM_STATUS or BASS_STREAM_AUTOFREE,nil,nil);
  if (channel<>0) then
    begin
      progress:=0;
      repeat
        len:=BASS_StreamGetFilePosition(channel,BASS_FILEPOS_END);
        if (len=DW_Error) then exit; // irgendetwas ging schief! 
        progress:=BASS_StreamGetFilePosition(channel,BASS_FILEPOS_BUFFER)*100 div len;
      until
      (progress>75) or (BASS_StreamGetFilePosition(channel, BASS_FILEPOS_CONNECTED)=0);
      // bis Puffer zu 75% voll (oder Download fertig ist)

    //Radio abspielen
    BASS_ChannelPlay(channel,false);
  end;
end;

Wollen wir die Ausgabe anhalten (Pause) gibt es die Funktion ChannelStop und mit einem erneuten Aufruf von ChannelPlay geht es weiter.

  BASS_ChannelStop(channel); 

3.4 META-Tags im Webradio Stream

Nun wird also das Webradio abgespielt. Aber es wäre schon schön, wenn wir ein paar Informationen zum aktuellen Sender und dem Titel hätten!

Auch das geht mit BASS ziemlich komfortabel. Über die Funktion BASS_ChannelGetTags lassen sich Informationen aus dem Stream abrufen. Natürlich nur, wenn das Webradio diese auch sendet. Aber das tun heute fast alle Sender. Rückgegeben wird von BASS ein PAnsiChar.

BASS unterteilt in verschiedene META-Tag-Bereiche. Informationen zum Radiosender finden wir unter BASS_TAG_ICY. Im Rückgabewert taucht bei Erfolg der Textteil „icy-name:Radio Beispiel Supertoll“ auf. Da sich dieser nicht ändert, reicht es ihn beim Senderwechsel einzulesen.

Über BASS_TAG_META kann man Informationen zum aktuellen Stream bekommen. Bei den meisten Radios sind das Interpret und Titel zum aktuellen Song oder andere Infos. Das steht hinter dem Textteil „StreamTitle=“.

Ein Beispiel für den StreamTitle sieht so aus:

procedure GetMetaTags;
var
  title : string;
  p,t : dword;
  meta: PAnsiChar;
  i,po: Integer;  
begin
  meta:=BASS_ChannelGetTags(channel,BASS_TAG_META);
  if (meta<>nil) then
    begin
      po:=Pos('StreamTitle=',String(AnsiString(meta)));
      if (po>0) then
        begin
          po:=po+13;
          title:=(SysToUTF8(Copy(meta,po,Pos(';',String(meta))-po-1)));
        end;
    end;
end;  

4 Alles zu einer Bedienoberfläche zusammenfügen

Für die Bedienoberfläche kommt es vor allem auf Ihre Kreativität und Ihre Umsetzungsfähigkeiten an. Möglich ist fast alles, was Sie sich vorstellen können. Mag sein, dass der Aufwand dafür dann vielleicht etwas höher wird.

4.1 Grundaufbau des Webradio Menüs

Ich habe mich für einen ganz simplen Aufbau entschieden:
Senderauswahl, Player, Setup, Fertig.

Menüstruktur des Webradios
Menüstruktur des Webradios

Der Aufwand war aber trotzdem hoch. Ich wünsche Ihnen, dass Sie Ihren etwas reduzieren können, wenn Sie bei mir im Code spicken können.

Um die einzelnen Menüseiten mit Lazarus umzusetzen, gibt es auch eine Reihe von Möglichkeiten. Eine TPageControl-Komponete eignet sich gut, genauso können einfach Panels verwendet werden, die dann jeweils in den Vordergrund gebracht werden. Beide sind aber für die Entwicklung und Test aufwändig, da immer mehrere Klicks erforderlich sind, um die entsprechende Seite in den Vordergrund zu bringen. Deshalb verwende ich TNotebook-Komponenten. Seiten können mit einem Klick im Objektinspektor gewechselt werden.

4.2 TItemPanel und TItemPanelList

Für die Bedienung, das heißt die Buttons, die ein User anklicken oder auswählen kann, habe ich mir ein eigenes Element erstellt: ein TItemPanel.

TItemPanel und seine Ansichten
TItemPanel und seine Ansichten

Es wird für verschiedene anklickbare oder auswählbare Elemente benutzt. Das sind beispielsweise Radiosender, Symbole im Player-Menü oder die Icons im Hauptmenü.

Die Definition in Lazarus ist ebenfalls in der Unit myobjects.pas umgesetzt.

  TItemPanel = class(TPanel)
    private
      …
    public
      Image : TImage;
      constructor Create(AOwner : TComponent); override;
      destructor Destroy; override;
      procedure Assign(RadioItem : TItemPanel);
      procedure SaveToInifile;
      procedure SaveToInifile(Inifile : TInifile);
      procedure LoadFromInifile(Filename : string);
      procedure LoadFromInifile(Inifile : TInifile);
      procedure UpdateView(ReloadPicture : boolean);
      procedure Resize(w, h: integer);
      procedure SetMouseEvents(MouseUpEvent, MouseDownEvent: TMouseEvent;
        MouseMoveEvent: TMouseMoveEvent);
    published
      property Title : string read FTitle write FTitle;
      property Url : string read FUrl write FUrl;
      property Info : string read FInfo write FInfo;
      property TextColor : TColor read FTextColor write SetTextcolor;
      property Background : TColor read FBackground write SetBackground;
      property OnPanelClick : TNotifyEvent read FOnPanelClick write FOnPanelClick;
      property Filename : string read FFilename write FFilename;
      property FilenameLarge : string read FFilenameLarge write FFilenameLarge;
      property Group : TRadioItemGroup read FGroup write FGroup;
      property Selected : boolean read FSelected write SetSelected;
      property BackgroundSelected : TColor read FBackgroundSelected write SetBackgroundSelected;
      property Transparent : boolean read FTransparent write SetTransparent;
      property Command : integer read FCommand write FCommand;
  end;   

Da meistens mehre Items zusammen dargestellt werden, gibt es eine Liste, die diese Elemente zusammenfasst und verwaltet. Sie heißt logischerweise TItemPanelList.

  TItemPanelList = class
    private
      …
    public
      constructor Create;
      destructor Destory;
      procedure Clear;
      procedure Add(RadioItem : TItemPanel);
      procedure LoadFromDirectory(Path : string);
      function Count : integer;
      procedure SetEvent(NotifyEvent : TNotifyEvent);
      procedure Resize(w, h: integer);
      procedure SetMouseEvents(MouseUpEvent, MouseDownEvent: TMouseEvent;
        MouseMoveEvent: TMouseMoveEvent);
      function GetSelectedTop : integer;
      function GetSelected: TItemPanel;
      function GetSelectedCommand: integer;
      function GetLast(RadioItem : TItemPanel) : TItemPanel;
      function GetNext(RadioItem : TItemPanel) : TItemPanel;
    published
      property Data : TList read FData write FData;
      property BaseControl : TWinControl read FControl write FControl;
      property Selected : integer read FSelected write SetSelected;
      property Visible : boolean read FVisible write SetVisible;
  end;
TItemPanel und TItemPanelList im Beispiel
TItemPanel und TItemPanelList in Aktion in der Senderliste

4.3 Effekte

Ein Radioplayer braucht natürlich auch ein paar Effekte. Zum einen sind das unsere Sensoren: der Bewegungsmelder und der Infrarot-Sensor. Zum anderen Visualisierungen im Player selbst.

4.3.1 Reagieren auf Sensor-Ereignisse

Wird der Intrarot-Sensor ausgelöst und ist der Player sichtbar, sollen Bedienelemente eingeblendet werden: Play/Pause, Letzter und Nächster Radio-Sender und eine Möglichkeit ins Hauptmenü zu gelangen.

Eingeblendete Player-Buttons
Eingeblendete Player-Buttons: letzter Sender, Play/Pause, nächster Sender und Hauptmenü

Und damit das nicht langweilig aussieht, sollen sie animiert ein- und ausgeblendet werden.

Wir setzen im Radio um, dass die ItemPanels beim Einblenden von links aus immer größer werden und sich nach rechts ausdehnen. Beim Ausblenden ziehen sie sich nach links immer kleiner zusammen.

Der Trigger zum Einblenden ist natürlich der Sensor. Er löst in der Ereignis-Routine den Befehlsinterpreter mit dem Kommando COMMAND_SENSOR_IR aus.

procedure TMainForm.ChangedSensorIR(x: longint);
begin
  if MilliSecondsBetween(PinSensorIR.Debounce,Now)>500 then
    begin
      if digitalRead(PIN_SENSOR_IR)=levHIGH then MainForm.DoCommand(COMMAND_SENSOR_IR);
      PinSensorIR.Debounce:=Now;
    end;
end;  

In der Ereignis-Routine rufen wir eine Prozedur zum Ein- bzw. Ausblenden der TItemPanelList auf. Gleichzeitig möchten wir, dass die Bedienelemente nach 10 sek wieder automatisch ausgeblendet werden. Um das zu erfüllen, brauchen wir zwei Timer-Komponenten. Eine kümmert sich um die Animation des Ein-/Ausblendens, die zweite um das Antriggert des Ausblenden-Ereignisses.

Das hört sich komplizierter an, als es dann wirklich im Code ist. Für das automatische Ausblenden wird eine Variable TickerIR definiert. Sie zählt nur die Sekunden mit.

procedure TMainForm.StartTimerIR;
begin
  TickerIR:=0;
  GpioSimForm.SetTickerIR(TickerIR);

  //Falls Bewegungserkennung nicht klappt, Bildschirm ein
  if FScreenOn=false then ScreenOn(true);


  if IplPlayer.Visible=false then ShowPlayerIcons(true);

  if TimerIR.Enabled=false then TimerIR.Enabled:=true;
end;

In der Ereignis-Prozedur des Timer (ein Interval von 1000 ms reicht aus), passt nicht besonders viel. Der Ticker wird hochgezählt und wenn er 10 sek erreicht, wird der zweite Timer zum Ausblenden der Player-Icons aktiviert. Das passiert mit der Prozedur ShowPlayerIcons(true / false).

procedure TMainForm.TimerIRTimer(Sender: TObject);
begin
  TickerIR:=TickerIR+1;
  GpioSimForm.SetTickerIR(TickerIR);
  if TickerIR>=TimeoutIR then
    begin
      TimerIR.Enabled:=false;
      ShowPlayerIcons(false);
    end;
end;

procedure TMainForm.ShowPlayerIcons(IconsVisible : boolean);
begin
  if IconsVisible=true then TimerPlayerIcons.Tag:=1
  else TimerPlayerIcons.Tag:=0;

  TickerPlayerIcons:=0;
  TimerPlayerIcons.Enabled:=true;
end;

Die Ereignis-Routine des zweiten Timers kümmert sich um die Animation. Die interne Variable Tag wird als Erkennung für einblenden (1) oder ausblenden (0) verwendet.

Der Rest ist eigentlich nur eine Größenanpassung der ItemPanel-Elemente. Beim Intervall des Timers, muss man ein wenig schauen, was der Raspberry alles leisten kann. Bei mir sind es bei einem Raspberry Pi 3B+ 25 ms.

procedure TMainForm.TimerPlayerIconsTimer(Sender: TObject);
const
  MAX_TICKER=15;
var
  i,j : integer;
  RadioItem : TItemPanel;
begin
  TickerPlayerIcons:=TickerPlayerIcons+1;
  if TickerPlayerIcons>=MAX_TICKER then
    begin
      TimerPlayerIcons.Enabled:=false;
      if TimerPlayerIcons.Tag=0 then IplPlayer.Visible:=false;
      exit;
    end;

  if TimerPlayerIcons.Tag=1 then
    begin
      //Einblenden
      j:=Round(PanelImage.Width/7*(TickerPlayerIcons/MAX_TICKER));
    end
  else
    begin
      //Ausblenden
      j:=Round(PanelImage.Width/7*((MAX_TICKER-TickerPlayerIcons)/MAX_TICKER));
    end;

    for i:=0 to IplPlayer.Count-1 do
    begin
      RadioItem:=TItemPanel(IplPlayer.Data[i]);
      if i=IplPlayer.Count-1 then RadioItem.Left:=(i+2)*j
      else RadioItem.Left:=(i+1)*j;

      //RadioItem.Top:=Round(PanelImage.Height*0.3);
      RadioItem.Resize(Round(j*0.9),Round(PanelImage.Height*0.4));
    end;

  if TickerPlayerIcons=1 then
    begin
      if TimerPlayerIcons.Tag=1 then IplPlayer.Visible:=true;
    end;
end;

4.3.2 Weitere Visualisierungen

Auch hier ist die Grenze Ihre Kreativität und Lust. Ich habe den Player noch etwas aufgepeppt. Anstelle nur einen statischen Text mit dem Radiosender-Namen und dem Stream-Titel anzuzeigen, läuft bei mir ein großer Scrolltext von rechts nach links. Die Umsetzung davon steht in der separaten Unit scrollpanel.

TScrollPanel = class(TPanel)
    private
    …
    public
      constructor Create(TheOwner: TComponent); override;
      destructor Destroy; override;
      procedure UpdateView;

    protected
      procedure DoOnChangeBounds; override;

    published
      property Text : string read FText write SetText;
      property Interval : integer read FInterval write SetInterval;
      property Step : integer read FStep write SetStep;
      property Active : boolean read FActive write SetActive;
  end;   

Als Parameter benötigt die Komponente nur eine befüllte Eigenschaft Text. Mit Interval und Step können Sie die Geschwindigkeit des Scrollvorgangs einstellen. Ich arbeite mit Interval=30 und Step=10.

Was mir noch nicht gefällt ist die Umsetzung der Sender-Liste über eine TScrollbox. Auf meinem Notebook mit Touch-Funktion klappt alles noch gut. Ich habe versucht, ein wenig Android-Feeling beim Scrollen einzubauen. Scrollt der Benutzer die Liste nach oben oder unten, sorgt ein Timer, dass das Scrolling ein wenig nachläuft. Mit dem Raspberry Pi 7″ Display läuft es nicht rund. Hier wackelt bei mir das Scrollbild, so als ob das Ziehen mit dem FInger nicht sauber mit dem Display umzusetzen ist. Hier muss ich mir noch eine bessere Lösung überlegen.

4.4 Wie sieht alles zusammen aus?

Wie sieht nun alles zusammen aus? Dafür habe ich für Sie eine kleine Galerie zusammengestellt.

Es gibt noch viel mehr zu zeigen: wie die Lautsprecher-Augen leuchten oder animiert werden oder wie der Befehlsinterperter verwendet wird? Auf der andere Seite gibt es auch noch viel zu tun: Das Lichtband ist noch nicht richtig in Lazarus integriert. Momentan wird noch ein Python-Programm gestartet (über die app.conf) und das soll sich schon noch ändern.

Downloaden Sie am besten den Quellcode und schauen selbst.

Hier sind noch ein paar Eindrücke vom gesamten Projekt.

Innen sieht es auch interessant aus:

Ab natürlich muss ein Radio-Player als Smiley auch einen Smiley-Look haben:

Und als Video:

5 Wie geht’s weiter?

Ja das ist eine spannende Frage. Ideen habe ich jede Menge während des Projekts gesammelt und hätte schon Lust sie umzusetzen.

Mittlerweile steht aber mein ursprüngliches Werkstatt-Radio nicht in der Werkstatt, sondern der Smiley-Player im Zimmer meiner Tochter!

Das heißt wohl, dass ich bei Erweiterungen eher auf die Wünsche meiner neuen Kundin eingehen darf. Hier stehen eher Smartphone-Anbindung oder Youtube-Nutzung auf der Anforderungsliste.

Schauen Sie einfach wieder vorbei oder melden sich im Blog an, wenn Sie wissen möchten, wie es weitergeht.  Sie werden dann auch informiert, wenn es eine neue Software-Version des Smiley-Players gibt.

Ich wünsche Ihnen viel Freude bei der Umsetzung Ihres Werkstatt-Radios!

Download

Links

Lazarus auf Raspberry Pi installieren: https://techpluscode.de/aktuelle-lazarus-version-auf-einem-raspberry-pi-installieren/

Freepascal und Raspberry Pi: https://wiki.freepascal.org/Lazarus_on_Raspberry_Pi/de#3._wiringPi-Prozeduren_und_Funktionen

Gordon Hendersons wiringpi: http://wiringpi.com/download-and-install/

bw38.de https://www.bw38.de/lazarusbasics

BASS-Bibliothek zum Abspielen von Webradio http://www.un4seen.com/

Werkstatt-Radio, Teil 1: https://techpluscode.de/werkstatt-radio-mit-raspberry-pi/

Werkstatt-Radio, Teil 2: https://techpluscode.de/raspberry-pi-werkstatt-radio-elektronik-und-gehaeuse/

Dieser Beitrag hat 3 Kommentare

  1. Thomas Angielsky

    Wenn Sie die aktuelle Version von BASS verwenden, brauchen Sie die richtige Version der Library „libbass.so“. Die finden Sie im Ordner „hardfp“. Kopieren das File in den Entwicklungsordner Ihres Lazarus-Projektes. Dann klappt es auch mit der aktuellen BASS Version.

    Den Download finden Sie unter:

    http://www.un4seen.com/stuff/bass24-linux-arm.zip

    Das gilt auch, wenn es Probleme mit der aktuellen Raspbian Version (Kernel 4.19) gibt und das Lazarus-Smiley-Projekt nicht startet. „libbass.so“ austauschen – fertig!

  2. Thomas Angielsky

    Mit Raspotify lässt sich der Smiley-Player sehr einfach, um eine Spotify Ausgabe erweitern. Auf Github unter https://github.com/dtcooper/raspotify findet sich eine ausführliche Anleitung zur Installation auf dem Raspberry Pi.

    Es reicht aber eine Befehlszeile aus:
    curl -sL https://dtcooper.github.io/raspotify/install.sh | sh

    Jetzt nur noch die Soundausgabe auf dem Smiley-Player pausieren und am besten die Smiley-Ansicht einstellen. Dann das Streaming auf dem Smartphone starten. Perfekt!

Kommentar verfassen