Now that I'm taking Gambas command-line programming more seriously, I thought I might rewrite the software on my internet radios.
As mentioned in a recent post, Gambas can be used to write cli programs (command-line interface) as well as the more usual gui (graphical) applications.
So this post just describes the software aspects of this project, while the other design details can be found in earlier posts.
Three of my 16 Raspberry Pi boards are used as internet radios. Two of them (KitchenPi and the Insomniacs Bedside Radio) are used every single day.
All three radios seem to run very reliably, barring the occasional wifi glitch. They all use a similar Python program, on headless Pi systems (one A, one A+ and one B+).
the requirements
Each radio operates in a similar fashion:-
- a simple push-button is used to select the radio source
- the source may be stored music files, or one of several internet radio streams
- each time the button is pressed, the system steps through the range of sources
- the text-to-speech engine "eSpeak" is used to announce the name/description of the currently selected source
- the media player "mplayer" is used to play the selected source
- the power to the radio is simply switched off when no longer required (i.e. no shutdown procedure)
For control of the Pi gpio I have chosen to use wiringPi, although I could have used pigpio.
the Gambas code
First of all, add the declarations. For wiringPi:-
'declare gpio library & methods using wiringPi
Library "/usr/local/lib/libwiringPi"
Public Extern wiringPiSetup() As Integer 'Initialises wiringPi
Public Extern pinMode(pin As Integer, pud As Integer) 'sets Pin mode (in/output)
Public Extern digitalRead(pin As Integer) As Integer 'returns the input state (low=0, high=1)
Const SWITCH_INPUT As Integer = 7 'the Pi switch input pin
Program Source stuff:-
'Program Sources
Const JUKE_BOX As Integer = 0
Const RADIO_CAROLINE As Integer = 1
Const PLANET_ROCK As Integer = 2
Const RADIO_2 As Integer = 3
Const RADIO_4 As Integer = 4
Const WORLD_SERVICE As Integer = 5
Const HIGHEST_SOURCE As Integer = 5 'change if you change the number of sources
Const LAST_SOURCE As String = "/home/pi/LastSource" 'source # file store
Const PLAYER As String = "mplayer"
Const MUSIC As String = "/home/pi/Music/"
Const URL_CAROLINE As String = "http://sc6.radiocaroline.net:8040/listen.pls" '128k
Const URL_ROCK As String = "http://tx.sharp-stream.com/icecast.php?i=planetrock.mp3" '112k
Const URL_R2 As String = "http://www.listenlive.eu/bbcradio2.m3u" '128k
Const URL_R4 As String = "http://www.listenlive.eu/bbcradio4.m3u" '128k
Const URL_WORLD As String = "http://wsdownload.bbc.co.uk/worldservice/meta/live/shoutcast/mp3/eieuk.pls" '48k mp3
The event timers:-
Public hTimerPlayer As Timer 'timer to ckeck player is running
Public hTimerSwitch As Timer 'timer to check switch status
Now the Main routine, which runs when the program is started:-
Public Sub Main()
Dim hFile As File
'this timer checks whether media player is running
hTimerPlayer = New Timer As "tmrPlayerCheck"
hTimerPlayer.Delay = 10000
hTimerPlayer.start
'this timer checks if switch has been pressed
hTimerSwitch = New Timer As "tmrSwitchStatus"
hTimerSwitch.Delay = 300
hTimerSwitch.start
'config gpio
wiringPiSetup()
pinMode(SWITCH_INPUT, 0)
'set Pi volume
Exec ["amixer", "sset", "PCM,0", "100%"] '100% for best quality
If Not Exist(LAST_SOURCE) Then 'file with the last source ref
hfile = Open LAST_SOURCE For Create
Write #hFile, "0", Len("0") 'create file & use default source (Juke-box)
Close #hFile
Endif
'update playlist in case music files have been added/removed since player was last used
Shell "find " & MUSIC & " -type f -iname *.ogg -o -name *.wma -o -iname *.mp3 > " & MUSIC & "playlist"
'use music source from last time program was run
SelectSource(GetPreviousSource()) 'run the player with the previously selected source
End
The Gambas Shell and Exec commands are similar, but Exec cannot be used with redirection operators like ">" hence my use of Shell to generate a playlist in the code above.
Here are the routines called from the Main() routine;
In the SelectSource function, espeak is executed, then followed by Wait. This is to stop the announcement talking-over the start of the stream (although this only seems to be a problem with the faster Pi models like Pi2 & Pi3).
Public Function SelectSource(iSource As Integer) As Integer
'choose source based upon value of iSource, announce it, then load mplayer & play it
Select iSource
Case RADIO_CAROLINE
Exec ["espeak", "-ven+f4", "-s110", "-k5", "-a", "30", "Radio Caroline"] Wait
Exec ["mplayer", "-cache", "64", "-playlist", URL_CAROLINE]
Case PLANET_ROCK
Exec ["espeak", "-ven+m5", "-s100", "-k5", "-a", "30", "Planet Rock"] Wait
Exec ["mplayer", "-cache", "64", "-playlist", URL_ROCK]
Case RADIO_2
Exec ["espeak", "-ven+f1", "-s100", "-k5", "-a", "30", "BBC Radio two"] Wait
Exec ["mplayer", "-cache", "64", "-playlist", URL_R2]
Case RADIO_4
Exec ["espeak", "-ven+m1", "-s100", "-k5", "-a", "30", "BBC Radio four"] Wait
Exec ["mplayer", "-cache", "64", "-playlist", URL_R4]
Case WORLD_SERVICE
Exec ["espeak", "-ven+m3", "-s110", "-k5", "-a", "30", "The BBC world service"] Wait
Exec ["mplayer", "-cache", "64", "-playlist", URL_WORLD]
Case Else 'Juke-Box
Exec ["espeak", "-ven+f2", "-s90", "-k9", "-a", "30", "play that funky music, White boy"] Wait
'shuffle the music playlist each time the Juke-box is restarted
Exec ["mplayer", "-shuffle", "-playlist", MUSIC & "playlist"]
End Select
End
For more info on eSpeak see my earlier post.
File read and write routines;-
Public Function GetPreviousSource() As Integer
'open file and retrieve Program source number
Dim hFile As File
Dim strFile As String
If Access(LAST_SOURCE, gb.Read) Then
hFile = Open LAST_SOURCE For Read
Read #hFile, strfile, -512
Close #hFile
Return CInt(strfile)
Else
Return 0
Endif
End
Public Function SaveCurrentSource(iSource As Integer) As Boolean
Dim hFile As File
If Access(LAST_SOURCE, gb.Write) Then
hFile = Open LAST_SOURCE For Write
Write #hFile, CStr(iSource), Len(CStr(iSource))
Close #hFile
Return True
Endif
End
...and finally the event timers.
The switch timer checks the switch status about 3 times per second, which is plenty fast enough. However, if the switch has been pressed, the timer interval increases to 2s to stop the program from stepping through the program sources too quickly.
Public Sub tmrSwitchStatus_Timer()
Dim strProcIDs As String
Dim strPID As String
Dim intNext As Integer
If digitalRead(SWITCH_INPUT) = 0 Then 'someone is pushing the button!
hTimerSwitch.Delay = 2000 'debounce switch
Exec ["pgrep", PLAYER] To strProcIDs
If Len(strProcIDs) > 1 Then
Do
strPID = Mid(strProcIDs, 1, InStr(strProcIDs, Chr(10)) - 1)
Exec ["kill", strPID] Wait
strProcIDs = Mid(strProcIDs, InStr(strProcIDs, Chr(10)) + 1)
Loop Until strProcIDs = ""
intNext = GetPreviousSource() + 1
If intNext > HIGHEST_SOURCE Then
intNext = 0
Endif
SaveCurrentSource(intNext)
SelectSource(GetPreviousSource())
Endif
Else
hTimerSwitch.Delay = 300
Endif
Catch
'error handler (trap it, but don't do anything)
End
The player timer checks that the mplayer task is still running. If not it restarts it.
Public Sub tmrPlayerCheck_Timer()
'if player stops running, this should detect it and restart it
Dim sOutput As String
Exec ["pgrep", "-f", "-l", PLAYER] Wait To sOutput
If Split(Trim$(sOutput), gb.newLine).Count < 1 Then 'player is not running
SelectSource(GetPreviousSource())
Endif
End
Comparing this code with my original Python program, the Gambas version has about 35% more lines of text. There are 2 reasons for this. As Gambas is a BASIC dialect, it is naturally more wordy. But I have also included more white-space and declared more routines in this Gambas version to make the code more readable.
The time taken to type more text is often offset by the time wasted in the future, when you come back to it, and have to work out what it all means!
The switch operates much better on my Gambas version. I can hold the switch down and the system will step through each source. With my Python code, I had to press/release/press to step through the sources.
deployment
An executable can be created from within the Gambas IDE, which can then be run on a Pi once you have installed the Gambas runtime, eSpeak, mplayer and wiringPi.
For my lounge radio I used a fresh install of Raspbian Jessie Lite on a headless Pi, so my workflow looked like this:-
- image an SD card & use gParted to expand the file system
- edit the /etc/wpa_supplicant/wpa_supplicant.conf file by adding wifi details
- create new folder: /home/pi/Music and set permissions to "anyone"
- copy music files to Music folder
- copy the internet radio exe (e.g. InternetRadio.gambas) to /home/pi and set execute permissions to "anyone"
- download wiringPi, unzip and copy files/folder structure to /home/pi
- rename wiringPi-xxxxxx to wPi
- set execute permissions for /home/pi/wPi/build to "anyone"
- ensure wifi dongle is fitted, then boot
- find Pi IP address from router
- Access Pi from Linux laptop: ssh pi@{ipAddress}
- Run raspi-config and change: password, hostname, audio (to jack)
- after reboot, ssh again
- sudo apt update
- sudo apt upgrade
- sudo apt install espeak mplayer gambas3-runtime
- install wiringPi: cd wPi then run ./build
- test radio: /home/pi/InternetRadio.gambas
- autorun: sudo nano /etc/rc.local
- add on a new line just before the "exit" line: /home/pi/InternetRadio.gambas &
- now re-boot and final test
For KitchenPi I just did everything via SSH and kept the existing operating system. So this radio only needed the Gambas runtime, my Gambas exe, wiringPi and a quick edit to /etc/rc.local.
It would be nice to use a faster-booting OS like Arch Linux, but that's a rainy day job for the future.
No comments:
Post a Comment