Radio Pi Plate

Adafruit LCD Pi Plate Rocks!

The RGB 16×2 LCD+Keypad Pi Plate from Adafruit Industries makes a great addition to an Internet radio project.  This little circuit board makes it easy to add an LCD display and numeric keypad to a Raspberry Pi.

One of the neat things about this board is that it uses only two pins on the R-Pi GPIO header to control: the 16×2 Character LCD; up to 3 backlight pins; and 5 keypad buttons.  It uses an MCP23017-E/SP I2C port expander chip to make this happen.

The best part is you don’t really lose those two pins either, since you can still add I2C based sensors, RTCs, etc and have them share the I2C bus.

In this post, we update our original Raspberry Pi Internet Radio project using this cool Pi Plate.  Our new radio shares many of the features of the original radio and adds: compact size; a color-changing LCD display; and more buttons for an improved user interface.

The Software

Once again, we’re using the Adafruit Occidentalis Linux distribution for the R-Pi.  Three python classes from Adafruit handle all of the heavy-lifting for the LCD display, I2C bus, and MCP23017 port expander.  We did have to make a few small changes to the Adafruit python classes to allow us to read all the buttons at once.  You’ll find the details below.

Programming this beauty turned out to be a little trickier than the previous radio.  The Pi Plate uses a serial interface to the R-Pi that’s quite a bit slower than a directly connected LCD.  Polling for button changes in the same loop that handles the LCD made the buttons feel somewhat unresponsive.  The solution was to move the LCD display to a separate worker thread and to use a Python queue to pass update messages.  You’ll see the details in the source code below.

Parts

To build this project, I used:

I like the Edimax WiFi dongle because it works well and requires low power which makes it perfect for use with the Raspberry Pi without a powered USB hub.

YouTube Video

Here’s a 14-minute YouTube video that describes the radio in detail and demonstrates many of its features, including:

  • the multi-color LCD display;
  • key pad controls;
  • volume control;
  • menu system; and
  • loadable playlist.

The Python Source Code

The source code for this project is in one file called radio.py, version 2.1.

#!/usr/bin/python

# radio.py, version 2.1 (RGB LCD Pi Plate version)
# February 17, 2013
# Written by Sheldon Hartling for Usual Panic
# BSD license, all text above must be included in any redistribution
#

#
# based on code from Kyle Prier (http://wwww.youtube.com/meistervision)
# and AdaFruit Industries (https://www.adafruit.com)
# Kyle Prier - https://www.dropbox.com/s/w2y8xx7t6gkq8yz/radio.py
# AdaFruit   - https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code.git, Adafruit_CharLCDPlate
#

#dependancies
from Adafruit_I2C          import Adafruit_I2C
from Adafruit_MCP230xx     import Adafruit_MCP230XX
from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
from datetime              import datetime
from subprocess            import *
from time                  import sleep, strftime
from Queue                 import Queue
from threading             import Thread

import smbus

# initialize the LCD plate
#   use busnum = 0 for raspi version 1 (256MB) 
#   and busnum = 1 for raspi version 2 (512MB)
LCD = Adafruit_CharLCDPlate(busnum = 1)

# Define a queue to communicate with worker thread
LCD_QUEUE = Queue()

# Globals
PLAYLIST_MSG   = []
STATION        = 1
NUM_STATIONS   = 0

# Buttons
NONE           = 0x00
SELECT         = 0x01
RIGHT          = 0x02
DOWN           = 0x04
UP             = 0x08
LEFT           = 0x10
UP_AND_DOWN    = 0x0C
LEFT_AND_RIGHT = 0x12



# ----------------------------
# WORKER THREAD
# ----------------------------

# Define a function to run in the worker thread
def update_lcd(q):
   
   while True:
      msg = q.get()
      # if we're falling behind, skip some LCD updates
      while not q.empty():
         q.task_done()
         msg = q.get()
      LCD.setCursor(0,0)
      LCD.message(msg)
      q.task_done()
   return



# ----------------------------
# MAIN LOOP
# ----------------------------

def main():
   global STATION, NUM_STATIONS, PLAYLIST_MSG

   # Stop music player
   output = run_cmd("mpc stop" )

   # Setup AdaFruit LCD Plate
   LCD.begin(16,2)
   LCD.clear()
   LCD.backlight(LCD.VIOLET)

   # Create the worker thread and make it a daemon
   worker = Thread(target=update_lcd, args=(LCD_QUEUE,))
   worker.setDaemon(True)
   worker.start()
   
   # Display startup banner
   LCD_QUEUE.put('Welcome to\nUsualPanic Radio', True)

   # Load our station playlist
   load_playlist()
   sleep(2)
   LCD.clear()



# ----------------------------
# START THE MUSIC!
# ----------------------------

   # Start music player
   LCD_QUEUE.put(PLAYLIST_MSG[STATION - 1], True)
   run_cmd("mpc volume +100")
   mpc_play(STATION)
   countdown_to_play = 0
      
   # Main loop
   while True:
      press = read_buttons()

      # LEFT button pressed
      if(press == LEFT):
         STATION -= 1
         if(STATION < 1):
            STATION = NUM_STATIONS
         LCD_QUEUE.put(PLAYLIST_MSG[STATION - 1], True)
         # start play in 300msec unless another key pressed
         countdown_to_play = 3

      # RIGHT button pressed
      if(press == RIGHT):
         STATION += 1
         if(STATION > NUM_STATIONS):
            STATION = 1
         LCD_QUEUE.put(PLAYLIST_MSG[STATION - 1], True)
         # start play in 300msec unless another key pressed
         countdown_to_play = 3

      # UP button pressed
      if(press == UP):
         output = run_cmd("mpc volume +2")

      # DOWN button pressed
      if(press == DOWN):
         output = run_cmd("mpc volume -2")

      # SELECT button pressed
      if(press == SELECT):
         menu_pressed()

      # If we haven't had a key press in 300 msec
      # go ahead and issue the MPC command
      if(countdown_to_play > 0):
         countdown_to_play -= 1
         if(countdown_to_play == 0):
            # Play requested station
            mpc_play(STATION)

      delay_milliseconds(99)
   update_lcd.join()


# ----------------------------
# READ SWITCHES
# ----------------------------

def read_buttons():

   buttons = LCD.readButtons()
   # Debounce push buttons
   if(buttons != 0):
      while(LCD.readButtons() != 0):
         delay_milliseconds(1)
   return buttons



def delay_milliseconds(milliseconds):
   seconds = milliseconds / float(1000)	# divide milliseconds by 1000 for seconds
   sleep(seconds)



# ----------------------------
# LOAD PLAYLIST OF STATIONS
# ----------------------------

def load_playlist():
   global STATION, NUM_STATIONS, PLAYLIST_MSG

   # Run shell script to add all stations
   # to the MPC/MPD music player playlist
   output = run_cmd("mpc clear")
   output = run_cmd("/home/pi/projects/radio/radio_playlist.sh")

   # Load PLAYLIST_MSG list
   PLAYLIST_MSG = []
   with open ("/home/pi/projects/radio/radio_playlist.sh", "r") as playlist:
      # Skip leading hash-bang line
      for line in playlist:
         if line[0:1] != '#!':  
               break
      # Remaining comment lines are loaded
      for line in playlist:
         if line[0] == "#" :
            PLAYLIST_MSG.append(line.replace(r'\n','\n')[1:-1] + "                ")
   playlist.close()
   NUM_STATIONS = len(PLAYLIST_MSG)


# ----------------------------
# RADIO SETUP MENU
# ----------------------------

def menu_pressed():
   global STATION

   MENU_LIST = [
      '1. Display Time \n   & IP Address ',
      '2. Output Audio \n   to HDMI      ',
      '3. Output Audio \n   to Headphones',
      '4. Auto Select  \n   Audio Output ',
      '5. Reload       \n   Playlist File',
      '6. System       \n   Shutdown!    ',
      '7. Exit         \n                ']

   item = 0
   LCD.clear()
   LCD.backlight(LCD.YELLOW)
   LCD_QUEUE.put(MENU_LIST[item], True)

   keep_looping = True
   while (keep_looping):

      # Wait for a key press
      press = read_buttons()

      # UP button
      if(press == UP):
         item -= 1
         if(item < 0):
            item = len(MENU_LIST) - 1
         LCD_QUEUE.put(MENU_LIST[item], True)

      # DOWN button
      elif(press == DOWN):
         item += 1
         if(item >= len(MENU_LIST)):
            item = 0
         LCD_QUEUE.put(MENU_LIST[item], True)

      # SELECT button = exit
      elif(press == SELECT):
         keep_looping = False

         # Take action
         if(  item == 0):
            # 1. display time and IP address
            display_ipaddr()
         elif(item == 1):
            # 2. audio output to HDMI
            output = run_cmd("amixer -q cset numid=3 2")
         elif(item == 2):
            # 3. audio output to headphone jack
            output = run_cmd("amixer -q cset numid=3 1")
         elif(item == 3):
            # 4. audio output auto-select
            output = run_cmd("amixer -q cset numid=3 0")
         elif(item == 4):
            # 5. reload our station playlist
            LCD_QUEUE.put("Reloading\nPlaylist File...", True)
            load_playlist()
            sleep(2)
            STATION = 1
            LCD_QUEUE.put(PLAYLIST_MSG[STATION - 1], True)
            mpc_play(STATION)
         elif(item == 5):
            # 6. shutdown the system
            LCD_QUEUE.put('Shutting down\nLinux now! ...  ', True)
            LCD_QUEUE.join()
            output = run_cmd("mpc clear")
            output = run_cmd("sudo shutdown now")
            LCD.clear()
            LCD.backlight(LCD.OFF)
            exit(0)
      else:
         delay_milliseconds(99)

   # Restore display
   LCD.backlight(LCD.VIOLET)
   LCD_QUEUE.put(PLAYLIST_MSG[STATION - 1], True)



# ----------------------------
# DISPLAY TIME AND IP ADDRESS
# ----------------------------

def display_ipaddr():
   show_wlan0 = "ip addr show wlan0 | cut -d/ -f1 | awk '/inet/ {printf \"w%15.15s\", $2}'"
   show_eth0  = "ip addr show eth0  | cut -d/ -f1 | awk '/inet/ {printf \"e%15.15s\", $2}'"
   ipaddr = run_cmd(show_eth0)
   if ipaddr == "":
      ipaddr = run_cmd(show_wlan0)

   LCD.backlight(LCD.VIOLET)
   i = 29
   muting = False
   keep_looping = True
   while (keep_looping):

      # Every 1/2 second, update the time display
      i += 1
      #if(i % 10 == 0):
      if(i % 5 == 0):
         LCD_QUEUE.put(datetime.now().strftime('%b %d  %H:%M:%S\n')+ ipaddr, True)

      # Every 3 seconds, update ethernet or wi-fi IP address
      if(i == 60):
         ipaddr = run_cmd(show_eth0)
         i = 0
      elif(i == 30):
         ipaddr = run_cmd(show_wlan0)

      # Every 100 milliseconds, read the switches
      press = read_buttons()
      # Take action on switch press
      
      # UP button pressed
      if(press == UP):
         output = run_cmd("mpc volume +2")

      # DOWN button pressed
      if(press == DOWN):
         output = run_cmd("mpc volume -2")

      # SELECT button = exit
      if(press == SELECT):
         keep_looping = False

      # LEFT or RIGHT toggles mute
      elif(press == LEFT or press == RIGHT ):
         if muting:
            #amixer command not working, can't use next line
            #output = run_cmd("amixer -q cset numid=2 1")
            mpc_play(STATION)
            # work around a problem.  Play always starts at full volume
            delay_milliseconds(400)
            output = run_cmd("mpc volume +2")
            output = run_cmd("mpc volume -2")
         else:
            #amixer command not working, can't use next line
            #output = run_cmd("amixer -q cset numid=2 0")
            output = run_cmd("mpc stop" )
         muting = not muting
         
      delay_milliseconds(99)



# ----------------------------

def run_cmd(cmd):
   p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT)
   output = p.communicate()[0]
   return output



#def run_cmd_nowait(cmd):
#   pid = Popen(cmd, shell=True, stdout=NONE, stderr=STDOUT).pid



def mpc_play(STATION):
   pid = Popen(["/usr/bin/mpc", "play", '%d' % ( STATION )]).pid



if __name__ == '__main__':
  main()

Modifying the Adafruit Python Classes

We made a few small changes to the Adafruit Python classes to allow us to read all the buttons at once.  To implement these changes, you’ll need to add the following code.

Add this new method to the Adafruit_CharLCDPlate.py class; and

    def readButtons(self):
        return self.mcp.input_all()

Add this new method to the Adafruit_MCP230xx.py class.

    def input_all(self):
        """ return all 5 switches, as a bitmap """
        assert self.num_gpios >= 16, "16bits required"
        return  0x1f ^ ( self.i2c.readU8(MCP23017_GPIOA) & 0x1F )

The Radio Play List

The radio play list / channel line-up is loaded dynamically from a file called radio_playlist.sh.  This file is processed twice by the radio.py program.  The first time it is executed as a shell script and the second time it is read as a text file to load the channel display names.

Don’t forget to change the Linux file permissions on this file to allow execute access.

> chmod 775 radio_playlist.sh

Here’s the sample file I used in my video …

#! /bin/sh

#1 WOLF Seattle  \ncountry
mpc add http://7279.live.streamtheworld.com:80/KKWFFMAAC_SC

#2 CHFX Halifax  \ncountry
mpc add mms://ysj00ms01s0.aliant.net/CHFX

#3 CJCB Sydney   \ncountry
mpc add mms://ysj00ms01s0.aliant.net/CJCB

#4 CFCY CharTown \ncountry
mpc add mms://ysj00ms01s0.aliant.net/CFCY

#5 SomaFM, Lush  \nfemale vocals
mpc add http://mp1.somafm.com:8800

#6 CHNS Halifax  \nclassic rock
mpc add mms://ysj00ms01s0.aliant.net/CHNS

#7 CFQM Moncton  \nadult contempory
mpc add mms://ysj00ms01s0.aliant.net/CFQM

#8 CBD  St John  \npublic radio
mpc add mms://ysj00ms01s0.aliant.net/CBD

#9 CJYC St John  \nclassic rock
mpc add mms://ysj00ms01s0.aliant.net/CJYC

#10 CHLQ ChrlTown \nhot AC
mpc add mms://ysj00ms01s0.aliant.net/CHLQ

#11 CHTN ChrlTown\nclassic hits
mpc add mms://ysj00ms01s0.aliant.net/CHTN

#12 CJRW ChrlTown\nclassic rock
mpc add mms://ysj00ms01s0.aliant.net/CJRW

#13 CBC Halifax  \nradio 2
mpc add http://3143.live.streamtheworld.com:80/CBC_R2_HFX_H_SC

#14 BBC London UK\nradio 1
mpc add "mms://wmlive-nonacl.bbc.net.uk/wms/bbc_ami/radio1/radio1_bb_live_int_ep1_sl0?BBC-UID=349e068f3a2633271418974ca119245ef80d149440d04184c40fc9b78246568b&SSO2-UID="

#15 CBC Halifax  \nradio 1
mpc add http://3383.live.streamtheworld.com:80/CBC_R1_HFX_H_SC

125 thoughts on “Radio Pi Plate

  1. Tangoman

    This is perfect.

    I had been struggling with my own program to do exactly this with no python or programming skills – managed to get mine working but no were as functional or nice as yours. (i did use Mplayer not MPC, as I could not get bbc live radio to work with MPC)

    But I am having one problem with your script.
    LCD.readButtons – I get an error, that doesn’t exist at all in my Adafruit_CharLCDPlate .py typed to change it to LCD.buttonPressed but doesnt seem that simple.

    Are you using an old GIT version of CharLCDPlate or is there something else i am missing?

    Hope you can help – your program would get me a home run with the wife!

    Cheers

    Reply
    1. sheldon Post author

      Hi “Tangoman”

      Great catch! My apologies, I wrote this code months ago and forgot that I had to modify the Adafruit classes. I’ve added the details to the post. Good luck with your project and the “home run”!

      Reply
      1. Gary Payne

        Could you please send me the updates to the adafruit class files.

        The radio iscurrently playing on the first channel but the buttons are not working. The LCD test does indicate they are functional.

        I saw someone else whose similar problem was solved with the 3 new files.

        Thanks, Gary

        Reply
      2. Lionel Rees

        Hi Sheldon,
        Thanks for a great project.

        I have tried to duplicate your Pi Plate radio.I have been able to get
        to the point where the radio is functioning with the first station loaded
        and displayed on the LCD. My remaining problem relates to the push buttons; I can’t get them to work. Do you have a working radio.py and
        the associated Adafruit files? If you can send me a set of working files
        for the Pi Plate radio I would be grateful. I am new to python and trying
        to learn.

        Regards,
        Lionel

        Reply
  2. Tangoman

    All working perfectly – it’s great!

    I am just looking around now to see what case i can put it all into – but this saved me heaps of time on the programming side of things.

    All credit and kudos to usualpanic.

    Reply
    1. Lionel Rees

      Hi ,
      I am having problems getting my push buttons to work. Could I get
      a copy of you script? I have see other posts relating to this issue by
      still unable to solve the problem.

      Thanks

      Reply
  3. Mike

    The Halifax stations caught my eye because I work in halifax, Nova Scotia. Most of the posts are from the US. Cool.

    Reply
  4. Barry

    A great little radio project using the raspberry pi and Adafruit’s nice RGB Character lcd plate!
    A big thanks to Sheldon for his help in obtaining the original class files for this project.
    Works great! Nice Canadian Halifax Radio Stations to listen too!
    Regards Barry

    Reply
  5. Olaf

    Hello,
    I’m so sorry this nice application for my raspi v2 with the rgb-lcd-plate is not running here.

    This are the error messages:
    Traceback (most recent call last):
    File “/home/pi/radio/radio.py”, line 356, in
    main()
    File “/home/pi/radio/radio.py”, line 109, in main
    press = read_buttons()
    File “/home/pi/radio/radio.py”, line 159, in read_buttons
    buttons = LCD.readButtons()
    File “/home/pi/radio/Adafruit_CharLCDPlate.py”, line 82, in readButtons
    return self.mcp.input_all()
    AttributeError: Adafruit_CharLCDPlate instance has no attribute ‘mcp’

    Changes to the latest Adafruit Python classes are made
    Music is playing and station info is displayed on lcd but the buttons are not functioning.
    Hoping if there is any help possible
    Olaf

    Reply
    1. Gerald