Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #! /usr/bin/python
- # UI wrapper for 'pianobar' client for Pandora, using Adafruit 16x2 LCD
- # Pi Plate for Raspberry Pi.
- # Written by Adafruit Industries. MIT license.
- #
- # Required hardware includes any internet-connected Raspberry Pi
- # system, any of the Adafruit 16x2 LCD w/Keypad Pi Plate varieties
- # and either headphones or amplified speakers.
- # Required software includes the Adafruit Raspberry Pi Python Code
- # repository, pexpect library and pianobar. A Pandora account is
- # also necessary.
- #
- # Resources:
- # http://www.adafruit.com/products/1109 RGB Positive 16x2 LCD + Keypad
- # http://www.adafruit.com/products/1110 RGB Negative 16x2 LCD + Keypad
- # http://www.adafruit.com/products/1115 Blue & White 16x2 LCD + Keypad
- import atexit, pexpect, pickle, socket, time, subprocess
- from Adafruit_I2C import Adafruit_I2C
- from Adafruit_MCP230xx import Adafruit_MCP230XX
- from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
- # Constants:
- RGB_LCD = False # Set to 'True' if using color backlit LCD
- HALT_ON_EXIT = False # Set to 'True' to shut down system when exiting
- MAX_FPS = 6 if RGB_LCD else 4 # Limit screen refresh rate for legibility
- VOL_MIN = -30
- VOL_MAX = 5
- VOL_DEFAULT = 0
- HOLD_TIME = 3.0 # Time (seconds) to hold select button for shut down
- PICKLEFILE = '/home/pi/.config/pianobar/state.p'
- # Global state:
- volCur = VOL_MIN # Current volume
- volNew = VOL_DEFAULT # 'Next' volume after interactions
- volSpeed = 1.0 # Speed of volume change (accelerates w/hold)
- volSet = False # True if currently setting volume
- paused = False # True if music is paused
- staSel = False # True if selecting station
- volTime = 0 # Time of last volume button interaction
- playMsgTime = 0 # Time of last 'Playing' message display
- staBtnTime = 0 # Time of last button press on station menu
- xTitle = 16 # X position of song title (scrolling)
- xInfo = 16 # X position of artist/album (scrolling)
- xStation = 0 # X position of station (scrolling)
- xTitleWrap = 0
- xInfoWrap = 0
- xStationWrap = 0
- songTitle = ''
- songInfo = ''
- stationNum = 0 # Station currently playing
- stationNew = 0 # Station currently highlighted in menu
- stationList = ['']
- stationIDs = ['']
- # Char 7 gets reloaded for different modes. These are the bitmaps:
- charSevenBitmaps = [
- [0b10000, # Play (also selected station)
- 0b11000,
- 0b11100,
- 0b11110,
- 0b11100,
- 0b11000,
- 0b10000,
- 0b00000],
- [0b11011, # Pause
- 0b11011,
- 0b11011,
- 0b11011,
- 0b11011,
- 0b11011,
- 0b11011,
- 0b00000],
- [0b00000, # Next Track
- 0b10100,
- 0b11010,
- 0b11101,
- 0b11010,
- 0b10100,
- 0b00000,
- 0b00000]]
- # --------------------------------------------------------------------------
- # Exit handler tries to leave LCD in a nice state.
- def cleanExit():
- if lcd is not None:
- time.sleep(0.5)
- lcd.backlight(lcd.OFF)
- lcd.clear()
- lcd.stop()
- if pianobar is not None:
- pianobar.kill(0)
- def shutdown():
- lcd.clear()
- if HALT_ON_EXIT:
- if RGB_LCD: lcd.backlight(lcd.YELLOW)
- lcd.message('Wait 30 seconds\nto unplug...')
- # Ramp down volume over 5 seconds while 'wait' message shows
- steps = int((volCur - VOL_MIN) + 0.5) + 1
- pause = 5.0 / steps
- for i in range(steps):
- pianobar.send('(')
- time.sleep(pause)
- subprocess.call("sync")
- cleanExit()
- subprocess.call(["shutdown", "-h", "now"])
- else:
- exit(0)
- # Draws song title or artist/album marquee at given position.
- # Returns new position to avoid global uglies.
- def marquee(s, x, y, xWrap):
- lcd.setCursor(0, y)
- if x > 0: # Initially scrolls in from right edge
- lcd.message(' ' * x + s[0:16-x])
- else: # Then scrolls w/wrap indefinitely
- lcd.message(s[-x:16-x])
- if x < xWrap: return 0
- return x - 1
- def drawPlaying():
- lcd.createChar(7, charSevenBitmaps[0])
- lcd.setCursor(0, 1)
- lcd.message('\x07 Playing ')
- return time.time()
- def drawPaused():
- lcd.createChar(7, charSevenBitmaps[1])
- lcd.setCursor(0, 1)
- lcd.message('\x07 Paused ')
- def drawNextTrack():
- lcd.createChar(7, charSevenBitmaps[2])
- lcd.setCursor(0, 1)
- lcd.message('\x07 Next track... ')
- # Draw station menu (overwrites fulls screen to facilitate scrolling)
- def drawStations(stationNew, listTop, xStation, staBtnTime):
- last = len(stationList)
- if last > 2: last = 2 # Limit stations displayed
- ret = 0 # Default return value (for station scrolling)
- line = 0 # Line counter
- msg = '' # Clear output string to start
- for s in stationList[listTop:listTop+2]: # For each station...
- sLen = len(s) # Length of station name
- if (listTop + line) == stationNew: # Selected station?
- msg += chr(7) # Show selection cursor
- if sLen > 15: # Is station name longer than line?
- if (time.time() - staBtnTime) < 0.5:
- # Just show start of line for half a sec
- s2 = s[0:15]
- else:
- # After that, scrollinate
- s2 = s + ' ' + s[0:15]
- xStationWrap = -(sLen + 2)
- s2 = s2[-xStation:15-xStation]
- if xStation > xStationWrap:
- ret = xStation - 1
- else: # Short station name - pad w/spaces if needed
- s2 = s[0:15]
- if sLen < 15: s2 += ' ' * (15 - sLen)
- else: # Not currently-selected station
- msg += ' ' # No cursor
- s2 = s[0:15] # Clip or pad name to 15 chars
- if sLen < 15: s2 += ' ' * (15 - sLen)
- msg += s2 # Add station name to output message
- line += 1
- if line == last: break
- msg += '\n' # Not last line - add newline
- lcd.setCursor(0, 0)
- lcd.message(msg)
- return ret
- def getStations():
- lcd.clear()
- lcd.message('Retrieving\nstation list...')
- pianobar.expect('Select station: ', timeout=10)
- # 'before' is now string of stations I believe
- # break up into separate lines
- a = pianobar.before.splitlines()
- names = []
- ids = []
- # Parse each line
- for b in a[:-1]: # Skip last line (station select prompt)
- # Occasionally a queued up 'TIME: -XX:XX/XX:XX' string or
- # 'new playlist...' appears in the output. Station list
- # entries have a known format, so it's straightforward to
- # skip these bogus lines.
- # print '\"{}\"'.format(b)
- if (b.find('playlist...') >= 0): continue
- # if b[0:5].find(':') >= 0: continue
- # if (b.find(':') >= 0) or (len(b) < 13): continue
- # Alternate strategy: must contain either 'QuickMix' or 'Radio':
- # Somehow the 'playlist' case would get through this check. Buh?
- if b.find('Radio') or b.find('QuickMix'):
- id = b[5:7].strip()
- name = b[13:].strip()
- # If 'QuickMix' found, always put at head of list
- if name == 'QuickMix':
- ids.insert(0, id)
- names.insert(0, name)
- else:
- ids.append(id)
- names.append(name)
- return names, ids
- # --------------------------------------------------------------------------
- # Initialization
- atexit.register(cleanExit)
- lcd = Adafruit_CharLCDPlate()
- lcd.begin(16, 2)
- lcd.clear()
- # Create volume bargraph custom characters (chars 0-5):
- for i in range(6):
- bitmap = []
- bits = (255 << (5 - i)) & 0x1f
- for j in range(8): bitmap.append(bits)
- lcd.createChar(i, bitmap)
- # Create up/down icon (char 6)
- lcd.createChar(6,
- [0b00100,
- 0b01110,
- 0b11111,
- 0b00000,
- 0b00000,
- 0b11111,
- 0b01110,
- 0b00100])
- # By default, char 7 is loaded in 'pause' state
- lcd.createChar(7, charSevenBitmaps[1])
- # Get last-used volume and station name from pickle file
- try:
- f = open(PICKLEFILE, 'rb')
- v = pickle.load(f)
- f.close()
- volNew = v[0]
- defaultStation = v[1]
- except:
- defaultStation = None
- # Show IP address (if network is available). System might be freshly
- # booted and not have an address yet, so keep trying for a couple minutes
- # before reporting failure.
- t = time.time()
- while True:
- if (time.time() - t) > 120:
- # No connection reached after 2 minutes
- if RGB_LCD: lcd.backlight(lcd.RED)
- lcd.message('Network is\nunreachable')
- time.sleep(30)
- exit(0)
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.connect(('8.8.8.8', 0))
- if RGB_LCD: lcd.backlight(lcd.GREEN)
- else: lcd.backlight(lcd.ON)
- lcd.message('My IP address is\n' + s.getsockname()[0])
- time.sleep(5)
- break # Success -- let's hear some music!
- except:
- time.sleep(1) # Pause a moment, keep trying
- # Launch pianobar as pi user (to use same config data, etc.) in background:
- print('Spawning pianobar...')
- pianobar = pexpect.spawn('sudo -u pi pianobar')
- print('Receiving station list...')
- pianobar.expect('Get stations... Ok.\r\n', timeout=30)
- stationList, stationIDs = getStations()
- try: # Use station name from last session
- stationNum = stationList.index(defaultStation)
- except: # Use first station in list
- stationNum = 0
- print 'Selecting station ' + stationIDs[stationNum]
- pianobar.sendline(stationIDs[stationNum])
- # --------------------------------------------------------------------------
- # Main loop. This is not quite a straight-up state machine; there's some
- # persnickety 'nesting' and canceling among mode states, so instead a few
- # global booleans take care of it rather than a mode variable.
- if RGB_LCD: lcd.backlight(lcd.ON)
- lastTime = 0
- pattern_list = pianobar.compile_pattern_list(['SONG: ', 'STATION: ', 'TIME: '])
- while pianobar.isalive():
- # Process all pending pianobar output
- while True:
- try:
- x = pianobar.expect(pattern_list, timeout=0)
- if x == 0:
- songTitle = ''
- songInfo = ''
- xTitle = 16
- xInfo = 16
- xTitleWrap = 0
- xInfoWrap = 0
- x = pianobar.expect(' \| ')
- if x == 0: # Title | Artist | Album
- print 'Song: "{}"'.format(pianobar.before)
- s = pianobar.before + ' '
- n = len(s)
- xTitleWrap = -n + 2
- # 1+ copies + up to 15 chars for repeating scroll
- songTitle = s * (1 + (16 / n)) + s[0:16]
- x = pianobar.expect(' \| ')
- if x == 0:
- print 'Artist: "{}"'.format(pianobar.before)
- artist = pianobar.before
- x = pianobar.expect('\r\n')
- if x == 0:
- print 'Album: "{}"'.format(pianobar.before)
- s = artist + ' < ' + pianobar.before + ' > '
- n = len(s)
- xInfoWrap = -n + 2
- # 1+ copies + up to 15 chars for repeating scroll
- songInfo = s * (2 + (16 / n)) + s[0:16]
- elif x == 1:
- x = pianobar.expect(' \| ')
- if x == 0:
- print 'Station: "{}"'.format(pianobar.before)
- elif x == 2:
- # Time doesn't include newline - prints over itself.
- x = pianobar.expect('\r', timeout=1)
- if x == 0:
- print 'Time: {}'.format(pianobar.before)
- # Periodically dump state (volume and station name)
- # to pickle file so it's remembered between each run.
- try:
- f = open(PICKLEFILE, 'wb')
- pickle.dump([volCur, stationList[stationNum]], f)
- f.close()
- except:
- pass
- except pexpect.EOF:
- break
- except pexpect.TIMEOUT:
- break
- # Poll all buttons once, avoids repeated I2C traffic for different cases
- b = lcd.buttons()
- btnUp = b & (1 << lcd.UP)
- btnDown = b & (1 <<lcd.DOWN)
- btnLeft = b & (1 <<lcd.LEFT)
- btnRight = b & (1 <<lcd.RIGHT)
- btnSel = b & (1 <<lcd.SELECT)
- # Certain button actions occur regardless of current mode.
- # Holding the select button (for shutdown) is a big one.
- if btnSel:
- t = time.time() # Start time of button press
- while lcd.buttonPressed(lcd.SELECT): # Wait for button release
- if (time.time() - t) >= HOLD_TIME: # Extended hold?
- shutdown() # We're outta here
- # If tapped, different things in different modes...
- if staSel: # In station select menu...
- pianobar.send('\n') # Cancel station select
- staSel = False # Cancel menu and return to
- if paused: drawPaused() # play or paused state
- else: # In play/pause state...
- volSet = False # Exit volume-setting mode (if there)
- paused = not paused # Toggle play/pause
- pianobar.send('p') # Toggle pianobar play/pause
- if paused: drawPaused() # Display play/pause change
- else: playMsgTime = drawPlaying()
- # Right button advances to next track in all modes, even paused,
- # when setting volume, in station menu, etc.
- elif btnRight:
- drawNextTrack()
- if staSel: # Cancel station select, if there
- pianobar.send('\n')
- staSel = False
- paused = False # Un-pause, if there
- volSet = False
- pianobar.send('n')
- # Left button enters station menu (if currently in play/pause state),
- # or selects the new station and returns.
- elif btnLeft:
- staSel = not staSel # Toggle station menu state
- if staSel:
- # Entering station selection menu. Don't return to volume
- # select, regardless of outcome, just return to normal play.
- pianobar.send('s')
- lcd.createChar(7, charSevenBitmaps[0])
- volSet = False
- cursorY = 0 # Cursor position on screen
- stationNew = 0 # Cursor position in list
- listTop = 0 # Top of list on screen
- xStation = 0 # X scrolling for long station names
- # Just keep the list we made at start-up
- # stationList, stationIDs = getStations()
- staBtnTime = time.time()
- drawStations(stationNew, listTop, 0, staBtnTime)
- else:
- # Just exited station menu with selection - go play.
- stationNum = stationNew # Make menu selection permanent
- print 'Selecting station: "{}"'.format(stationIDs[stationNum])
- pianobar.sendline(stationIDs[stationNum])
- paused = False
- # Up/down buttons either set volume (in play/pause) or select station
- elif btnUp or btnDown:
- if staSel:
- # Move up or down station menu
- if btnDown:
- if stationNew < (len(stationList) - 1):
- stationNew += 1 # Next station
- if cursorY < 1: cursorY += 1 # Move cursor
- else: listTop += 1 # Y-scroll
- xStation = 0 # Reset X-scroll
- elif stationNew > 0: # btnUp implied
- stationNew -= 1 # Prev station
- if cursorY > 0: cursorY -= 1 # Move cursor
- else: listTop -= 1 # Y-scroll
- xStation = 0 # Reset X-scroll
- staBtnTime = time.time() # Reset button time
- xStation = drawStations(stationNew, listTop, 0, staBtnTime)
- else:
- # Not in station menu
- if volSet is False:
- # Just entering volume-setting mode; init display
- lcd.setCursor(0, 1)
- volCurI = int((volCur - VOL_MIN) + 0.5)
- n = int(volCurI / 5)
- s = (chr(6) + ' Volume ' +
- chr(5) * n + # Solid brick(s)
- chr(volCurI % 5) + # Fractional brick
- chr(0) * (6 - n)) # Spaces
- lcd.message(s)
- volSet = True
- volSpeed = 1.0
- # Volume-setting mode now active (or was already there);
- # act on button press.
- if btnUp:
- volNew = volCur + volSpeed
- if volNew > VOL_MAX: volNew = VOL_MAX
- else:
- volNew = volCur - volSpeed
- if volNew < VOL_MIN: volNew = VOL_MIN
- volTime = time.time() # Time of last volume button press
- volSpeed *= 1.15 # Accelerate volume change
- # Other logic specific to unpressed buttons:
- else:
- if staSel:
- # In station menu, X-scroll active station name if long
- if len(stationList[stationNew]) > 15:
- xStation = drawStations(stationNew, listTop, xStation,
- staBtnTime)
- elif volSet:
- volSpeed = 1.0 # Buttons released = reset volume speed
- # If no interaction in 4 seconds, return to prior state.
- # Volume bar will be erased by subsequent operations.
- if (time.time() - volTime) >= 4:
- volSet = False
- if paused: drawPaused()
- # Various 'always on' logic independent of buttons
- if not staSel:
- # Play/pause/volume: draw upper line (song title)
- if songTitle is not None:
- xTitle = marquee(songTitle, xTitle, 0, xTitleWrap)
- # Integerize current and new volume values
- volCurI = int((volCur - VOL_MIN) + 0.5)
- volNewI = int((volNew - VOL_MIN) + 0.5)
- volCur = volNew
- # Issue change to pianobar
- if volCurI != volNewI:
- d = volNewI - volCurI
- if d > 0: s = ')' * d
- else: s = '(' * -d
- pianobar.send(s)
- # Draw lower line (volume or artist/album info):
- if volSet:
- if volNewI != volCurI: # Draw only changes
- if(volNewI > volCurI):
- x = int(volCurI / 5)
- n = int(volNewI / 5) - x
- s = chr(5) * n + chr(volNewI % 5)
- else:
- x = int(volNewI / 5)
- n = int(volCurI / 5) - x
- s = chr(volNewI % 5) + chr(0) * n
- lcd.setCursor(x + 9, 1)
- lcd.message(s)
- elif paused == False:
- if (time.time() - playMsgTime) >= 3:
- # Display artist/album (rather than 'Playing')
- xInfo = marquee(songInfo, xInfo, 1, xInfoWrap)
- # Throttle frame rate, keeps screen legible
- while True:
- t = time.time()
- if (t - lastTime) > (1.0 / MAX_FPS): break
- lastTime = t
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement