Advertisement
Guest User

PiPhi.py

a guest
Aug 21st, 2014
337
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.38 KB | None | 0 0
  1. #! /usr/bin/python
  2.  
  3. # UI wrapper for 'pianobar' client for Pandora, using Adafruit 16x2 LCD
  4. # Pi Plate for Raspberry Pi.
  5. # Written by Adafruit Industries. MIT license.
  6. #
  7. # Required hardware includes any internet-connected Raspberry Pi
  8. # system, any of the Adafruit 16x2 LCD w/Keypad Pi Plate varieties
  9. # and either headphones or amplified speakers.
  10. # Required software includes the Adafruit Raspberry Pi Python Code
  11. # repository, pexpect library and pianobar. A Pandora account is
  12. # also necessary.
  13. #
  14. # Resources:
  15. # http://www.adafruit.com/products/1109 RGB Positive 16x2 LCD + Keypad
  16. # http://www.adafruit.com/products/1110 RGB Negative 16x2 LCD + Keypad
  17. # http://www.adafruit.com/products/1115 Blue & White 16x2 LCD + Keypad
  18.  
  19. import atexit, pexpect, pickle, socket, time, subprocess
  20. from Adafruit_I2C import Adafruit_I2C
  21. from Adafruit_MCP230xx import Adafruit_MCP230XX
  22. from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
  23.  
  24.  
  25. # Constants:
  26. RGB_LCD = False # Set to 'True' if using color backlit LCD
  27. HALT_ON_EXIT = False # Set to 'True' to shut down system when exiting
  28. MAX_FPS = 6 if RGB_LCD else 4 # Limit screen refresh rate for legibility
  29. VOL_MIN = -30
  30. VOL_MAX = 5
  31. VOL_DEFAULT = 0
  32. HOLD_TIME = 3.0 # Time (seconds) to hold select button for shut down
  33. PICKLEFILE = '/home/pi/.config/pianobar/state.p'
  34.  
  35. # Global state:
  36. volCur = VOL_MIN # Current volume
  37. volNew = VOL_DEFAULT # 'Next' volume after interactions
  38. volSpeed = 1.0 # Speed of volume change (accelerates w/hold)
  39. volSet = False # True if currently setting volume
  40. paused = False # True if music is paused
  41. staSel = False # True if selecting station
  42. volTime = 0 # Time of last volume button interaction
  43. playMsgTime = 0 # Time of last 'Playing' message display
  44. staBtnTime = 0 # Time of last button press on station menu
  45. xTitle = 16 # X position of song title (scrolling)
  46. xInfo = 16 # X position of artist/album (scrolling)
  47. xStation = 0 # X position of station (scrolling)
  48. xTitleWrap = 0
  49. xInfoWrap = 0
  50. xStationWrap = 0
  51. songTitle = ''
  52. songInfo = ''
  53. stationNum = 0 # Station currently playing
  54. stationNew = 0 # Station currently highlighted in menu
  55. stationList = ['']
  56. stationIDs = ['']
  57.  
  58. # Char 7 gets reloaded for different modes. These are the bitmaps:
  59. charSevenBitmaps = [
  60. [0b10000, # Play (also selected station)
  61. 0b11000,
  62. 0b11100,
  63. 0b11110,
  64. 0b11100,
  65. 0b11000,
  66. 0b10000,
  67. 0b00000],
  68. [0b11011, # Pause
  69. 0b11011,
  70. 0b11011,
  71. 0b11011,
  72. 0b11011,
  73. 0b11011,
  74. 0b11011,
  75. 0b00000],
  76. [0b00000, # Next Track
  77. 0b10100,
  78. 0b11010,
  79. 0b11101,
  80. 0b11010,
  81. 0b10100,
  82. 0b00000,
  83. 0b00000]]
  84.  
  85.  
  86. # --------------------------------------------------------------------------
  87.  
  88.  
  89. # Exit handler tries to leave LCD in a nice state.
  90. def cleanExit():
  91. if lcd is not None:
  92. time.sleep(0.5)
  93. lcd.backlight(lcd.OFF)
  94. lcd.clear()
  95. lcd.stop()
  96. if pianobar is not None:
  97. pianobar.kill(0)
  98.  
  99.  
  100. def shutdown():
  101. lcd.clear()
  102. if HALT_ON_EXIT:
  103. if RGB_LCD: lcd.backlight(lcd.YELLOW)
  104. lcd.message('Wait 30 seconds\nto unplug...')
  105. # Ramp down volume over 5 seconds while 'wait' message shows
  106. steps = int((volCur - VOL_MIN) + 0.5) + 1
  107. pause = 5.0 / steps
  108. for i in range(steps):
  109. pianobar.send('(')
  110. time.sleep(pause)
  111. subprocess.call("sync")
  112. cleanExit()
  113. subprocess.call(["shutdown", "-h", "now"])
  114. else:
  115. exit(0)
  116.  
  117.  
  118. # Draws song title or artist/album marquee at given position.
  119. # Returns new position to avoid global uglies.
  120. def marquee(s, x, y, xWrap):
  121. lcd.setCursor(0, y)
  122. if x > 0: # Initially scrolls in from right edge
  123. lcd.message(' ' * x + s[0:16-x])
  124. else: # Then scrolls w/wrap indefinitely
  125. lcd.message(s[-x:16-x])
  126. if x < xWrap: return 0
  127. return x - 1
  128.  
  129.  
  130. def drawPlaying():
  131. lcd.createChar(7, charSevenBitmaps[0])
  132. lcd.setCursor(0, 1)
  133. lcd.message('\x07 Playing ')
  134. return time.time()
  135.  
  136.  
  137. def drawPaused():
  138. lcd.createChar(7, charSevenBitmaps[1])
  139. lcd.setCursor(0, 1)
  140. lcd.message('\x07 Paused ')
  141.  
  142.  
  143. def drawNextTrack():
  144. lcd.createChar(7, charSevenBitmaps[2])
  145. lcd.setCursor(0, 1)
  146. lcd.message('\x07 Next track... ')
  147.  
  148.  
  149. # Draw station menu (overwrites fulls screen to facilitate scrolling)
  150. def drawStations(stationNew, listTop, xStation, staBtnTime):
  151. last = len(stationList)
  152. if last > 2: last = 2 # Limit stations displayed
  153. ret = 0 # Default return value (for station scrolling)
  154. line = 0 # Line counter
  155. msg = '' # Clear output string to start
  156. for s in stationList[listTop:listTop+2]: # For each station...
  157. sLen = len(s) # Length of station name
  158. if (listTop + line) == stationNew: # Selected station?
  159. msg += chr(7) # Show selection cursor
  160. if sLen > 15: # Is station name longer than line?
  161. if (time.time() - staBtnTime) < 0.5:
  162. # Just show start of line for half a sec
  163. s2 = s[0:15]
  164. else:
  165. # After that, scrollinate
  166. s2 = s + ' ' + s[0:15]
  167. xStationWrap = -(sLen + 2)
  168. s2 = s2[-xStation:15-xStation]
  169. if xStation > xStationWrap:
  170. ret = xStation - 1
  171. else: # Short station name - pad w/spaces if needed
  172. s2 = s[0:15]
  173. if sLen < 15: s2 += ' ' * (15 - sLen)
  174. else: # Not currently-selected station
  175. msg += ' ' # No cursor
  176. s2 = s[0:15] # Clip or pad name to 15 chars
  177. if sLen < 15: s2 += ' ' * (15 - sLen)
  178. msg += s2 # Add station name to output message
  179. line += 1
  180. if line == last: break
  181. msg += '\n' # Not last line - add newline
  182. lcd.setCursor(0, 0)
  183. lcd.message(msg)
  184. return ret
  185.  
  186.  
  187. def getStations():
  188. lcd.clear()
  189. lcd.message('Retrieving\nstation list...')
  190. pianobar.expect('Select station: ', timeout=10)
  191. # 'before' is now string of stations I believe
  192. # break up into separate lines
  193. a = pianobar.before.splitlines()
  194. names = []
  195. ids = []
  196. # Parse each line
  197. for b in a[:-1]: # Skip last line (station select prompt)
  198. # Occasionally a queued up 'TIME: -XX:XX/XX:XX' string or
  199. # 'new playlist...' appears in the output. Station list
  200. # entries have a known format, so it's straightforward to
  201. # skip these bogus lines.
  202. # print '\"{}\"'.format(b)
  203. if (b.find('playlist...') >= 0): continue
  204. # if b[0:5].find(':') >= 0: continue
  205. # if (b.find(':') >= 0) or (len(b) < 13): continue
  206. # Alternate strategy: must contain either 'QuickMix' or 'Radio':
  207. # Somehow the 'playlist' case would get through this check. Buh?
  208. if b.find('Radio') or b.find('QuickMix'):
  209. id = b[5:7].strip()
  210. name = b[13:].strip()
  211. # If 'QuickMix' found, always put at head of list
  212. if name == 'QuickMix':
  213. ids.insert(0, id)
  214. names.insert(0, name)
  215. else:
  216. ids.append(id)
  217. names.append(name)
  218. return names, ids
  219.  
  220.  
  221. # --------------------------------------------------------------------------
  222. # Initialization
  223.  
  224. atexit.register(cleanExit)
  225.  
  226. lcd = Adafruit_CharLCDPlate()
  227. lcd.begin(16, 2)
  228. lcd.clear()
  229.  
  230. # Create volume bargraph custom characters (chars 0-5):
  231. for i in range(6):
  232. bitmap = []
  233. bits = (255 << (5 - i)) & 0x1f
  234. for j in range(8): bitmap.append(bits)
  235. lcd.createChar(i, bitmap)
  236.  
  237. # Create up/down icon (char 6)
  238. lcd.createChar(6,
  239. [0b00100,
  240. 0b01110,
  241. 0b11111,
  242. 0b00000,
  243. 0b00000,
  244. 0b11111,
  245. 0b01110,
  246. 0b00100])
  247.  
  248. # By default, char 7 is loaded in 'pause' state
  249. lcd.createChar(7, charSevenBitmaps[1])
  250.  
  251. # Get last-used volume and station name from pickle file
  252. try:
  253. f = open(PICKLEFILE, 'rb')
  254. v = pickle.load(f)
  255. f.close()
  256. volNew = v[0]
  257. defaultStation = v[1]
  258. except:
  259. defaultStation = None
  260.  
  261. # Show IP address (if network is available). System might be freshly
  262. # booted and not have an address yet, so keep trying for a couple minutes
  263. # before reporting failure.
  264. t = time.time()
  265. while True:
  266. if (time.time() - t) > 120:
  267. # No connection reached after 2 minutes
  268. if RGB_LCD: lcd.backlight(lcd.RED)
  269. lcd.message('Network is\nunreachable')
  270. time.sleep(30)
  271. exit(0)
  272. try:
  273. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  274. s.connect(('8.8.8.8', 0))
  275. if RGB_LCD: lcd.backlight(lcd.GREEN)
  276. else: lcd.backlight(lcd.ON)
  277. lcd.message('My IP address is\n' + s.getsockname()[0])
  278. time.sleep(5)
  279. break # Success -- let's hear some music!
  280. except:
  281. time.sleep(1) # Pause a moment, keep trying
  282.  
  283. # Launch pianobar as pi user (to use same config data, etc.) in background:
  284. print('Spawning pianobar...')
  285. pianobar = pexpect.spawn('sudo -u pi pianobar')
  286. print('Receiving station list...')
  287. pianobar.expect('Get stations... Ok.\r\n', timeout=30)
  288. stationList, stationIDs = getStations()
  289. try: # Use station name from last session
  290. stationNum = stationList.index(defaultStation)
  291. except: # Use first station in list
  292. stationNum = 0
  293. print 'Selecting station ' + stationIDs[stationNum]
  294. pianobar.sendline(stationIDs[stationNum])
  295.  
  296.  
  297. # --------------------------------------------------------------------------
  298. # Main loop. This is not quite a straight-up state machine; there's some
  299. # persnickety 'nesting' and canceling among mode states, so instead a few
  300. # global booleans take care of it rather than a mode variable.
  301.  
  302. if RGB_LCD: lcd.backlight(lcd.ON)
  303. lastTime = 0
  304.  
  305. pattern_list = pianobar.compile_pattern_list(['SONG: ', 'STATION: ', 'TIME: '])
  306.  
  307. while pianobar.isalive():
  308.  
  309. # Process all pending pianobar output
  310. while True:
  311. try:
  312. x = pianobar.expect(pattern_list, timeout=0)
  313. if x == 0:
  314. songTitle = ''
  315. songInfo = ''
  316. xTitle = 16
  317. xInfo = 16
  318. xTitleWrap = 0
  319. xInfoWrap = 0
  320. x = pianobar.expect(' \| ')
  321. if x == 0: # Title | Artist | Album
  322. print 'Song: "{}"'.format(pianobar.before)
  323. s = pianobar.before + ' '
  324. n = len(s)
  325. xTitleWrap = -n + 2
  326. # 1+ copies + up to 15 chars for repeating scroll
  327. songTitle = s * (1 + (16 / n)) + s[0:16]
  328. x = pianobar.expect(' \| ')
  329. if x == 0:
  330. print 'Artist: "{}"'.format(pianobar.before)
  331. artist = pianobar.before
  332. x = pianobar.expect('\r\n')
  333. if x == 0:
  334. print 'Album: "{}"'.format(pianobar.before)
  335. s = artist + ' < ' + pianobar.before + ' > '
  336. n = len(s)
  337. xInfoWrap = -n + 2
  338. # 1+ copies + up to 15 chars for repeating scroll
  339. songInfo = s * (2 + (16 / n)) + s[0:16]
  340. elif x == 1:
  341. x = pianobar.expect(' \| ')
  342. if x == 0:
  343. print 'Station: "{}"'.format(pianobar.before)
  344. elif x == 2:
  345. # Time doesn't include newline - prints over itself.
  346. x = pianobar.expect('\r', timeout=1)
  347. if x == 0:
  348. print 'Time: {}'.format(pianobar.before)
  349. # Periodically dump state (volume and station name)
  350. # to pickle file so it's remembered between each run.
  351. try:
  352. f = open(PICKLEFILE, 'wb')
  353. pickle.dump([volCur, stationList[stationNum]], f)
  354. f.close()
  355. except:
  356. pass
  357. except pexpect.EOF:
  358. break
  359. except pexpect.TIMEOUT:
  360. break
  361.  
  362.  
  363. # Poll all buttons once, avoids repeated I2C traffic for different cases
  364. b = lcd.buttons()
  365. btnUp = b & (1 << lcd.UP)
  366. btnDown = b & (1 <<lcd.DOWN)
  367. btnLeft = b & (1 <<lcd.LEFT)
  368. btnRight = b & (1 <<lcd.RIGHT)
  369. btnSel = b & (1 <<lcd.SELECT)
  370.  
  371. # Certain button actions occur regardless of current mode.
  372. # Holding the select button (for shutdown) is a big one.
  373. if btnSel:
  374.  
  375. t = time.time() # Start time of button press
  376. while lcd.buttonPressed(lcd.SELECT): # Wait for button release
  377. if (time.time() - t) >= HOLD_TIME: # Extended hold?
  378. shutdown() # We're outta here
  379. # If tapped, different things in different modes...
  380. if staSel: # In station select menu...
  381. pianobar.send('\n') # Cancel station select
  382. staSel = False # Cancel menu and return to
  383. if paused: drawPaused() # play or paused state
  384. else: # In play/pause state...
  385. volSet = False # Exit volume-setting mode (if there)
  386. paused = not paused # Toggle play/pause
  387. pianobar.send('p') # Toggle pianobar play/pause
  388. if paused: drawPaused() # Display play/pause change
  389. else: playMsgTime = drawPlaying()
  390.  
  391. # Right button advances to next track in all modes, even paused,
  392. # when setting volume, in station menu, etc.
  393. elif btnRight:
  394.  
  395. drawNextTrack()
  396. if staSel: # Cancel station select, if there
  397. pianobar.send('\n')
  398. staSel = False
  399. paused = False # Un-pause, if there
  400. volSet = False
  401. pianobar.send('n')
  402.  
  403. # Left button enters station menu (if currently in play/pause state),
  404. # or selects the new station and returns.
  405. elif btnLeft:
  406.  
  407. staSel = not staSel # Toggle station menu state
  408. if staSel:
  409. # Entering station selection menu. Don't return to volume
  410. # select, regardless of outcome, just return to normal play.
  411. pianobar.send('s')
  412. lcd.createChar(7, charSevenBitmaps[0])
  413. volSet = False
  414. cursorY = 0 # Cursor position on screen
  415. stationNew = 0 # Cursor position in list
  416. listTop = 0 # Top of list on screen
  417. xStation = 0 # X scrolling for long station names
  418. # Just keep the list we made at start-up
  419. # stationList, stationIDs = getStations()
  420. staBtnTime = time.time()
  421. drawStations(stationNew, listTop, 0, staBtnTime)
  422. else:
  423. # Just exited station menu with selection - go play.
  424. stationNum = stationNew # Make menu selection permanent
  425. print 'Selecting station: "{}"'.format(stationIDs[stationNum])
  426. pianobar.sendline(stationIDs[stationNum])
  427. paused = False
  428.  
  429. # Up/down buttons either set volume (in play/pause) or select station
  430. elif btnUp or btnDown:
  431.  
  432. if staSel:
  433. # Move up or down station menu
  434. if btnDown:
  435. if stationNew < (len(stationList) - 1):
  436. stationNew += 1 # Next station
  437. if cursorY < 1: cursorY += 1 # Move cursor
  438. else: listTop += 1 # Y-scroll
  439. xStation = 0 # Reset X-scroll
  440. elif stationNew > 0: # btnUp implied
  441. stationNew -= 1 # Prev station
  442. if cursorY > 0: cursorY -= 1 # Move cursor
  443. else: listTop -= 1 # Y-scroll
  444. xStation = 0 # Reset X-scroll
  445. staBtnTime = time.time() # Reset button time
  446. xStation = drawStations(stationNew, listTop, 0, staBtnTime)
  447. else:
  448. # Not in station menu
  449. if volSet is False:
  450. # Just entering volume-setting mode; init display
  451. lcd.setCursor(0, 1)
  452. volCurI = int((volCur - VOL_MIN) + 0.5)
  453. n = int(volCurI / 5)
  454. s = (chr(6) + ' Volume ' +
  455. chr(5) * n + # Solid brick(s)
  456. chr(volCurI % 5) + # Fractional brick
  457. chr(0) * (6 - n)) # Spaces
  458. lcd.message(s)
  459. volSet = True
  460. volSpeed = 1.0
  461. # Volume-setting mode now active (or was already there);
  462. # act on button press.
  463. if btnUp:
  464. volNew = volCur + volSpeed
  465. if volNew > VOL_MAX: volNew = VOL_MAX
  466. else:
  467. volNew = volCur - volSpeed
  468. if volNew < VOL_MIN: volNew = VOL_MIN
  469. volTime = time.time() # Time of last volume button press
  470. volSpeed *= 1.15 # Accelerate volume change
  471.  
  472. # Other logic specific to unpressed buttons:
  473. else:
  474. if staSel:
  475. # In station menu, X-scroll active station name if long
  476. if len(stationList[stationNew]) > 15:
  477. xStation = drawStations(stationNew, listTop, xStation,
  478. staBtnTime)
  479. elif volSet:
  480. volSpeed = 1.0 # Buttons released = reset volume speed
  481. # If no interaction in 4 seconds, return to prior state.
  482. # Volume bar will be erased by subsequent operations.
  483. if (time.time() - volTime) >= 4:
  484. volSet = False
  485. if paused: drawPaused()
  486.  
  487. # Various 'always on' logic independent of buttons
  488. if not staSel:
  489. # Play/pause/volume: draw upper line (song title)
  490. if songTitle is not None:
  491. xTitle = marquee(songTitle, xTitle, 0, xTitleWrap)
  492.  
  493. # Integerize current and new volume values
  494. volCurI = int((volCur - VOL_MIN) + 0.5)
  495. volNewI = int((volNew - VOL_MIN) + 0.5)
  496. volCur = volNew
  497. # Issue change to pianobar
  498. if volCurI != volNewI:
  499. d = volNewI - volCurI
  500. if d > 0: s = ')' * d
  501. else: s = '(' * -d
  502. pianobar.send(s)
  503.  
  504. # Draw lower line (volume or artist/album info):
  505. if volSet:
  506. if volNewI != volCurI: # Draw only changes
  507. if(volNewI > volCurI):
  508. x = int(volCurI / 5)
  509. n = int(volNewI / 5) - x
  510. s = chr(5) * n + chr(volNewI % 5)
  511. else:
  512. x = int(volNewI / 5)
  513. n = int(volCurI / 5) - x
  514. s = chr(volNewI % 5) + chr(0) * n
  515. lcd.setCursor(x + 9, 1)
  516. lcd.message(s)
  517. elif paused == False:
  518. if (time.time() - playMsgTime) >= 3:
  519. # Display artist/album (rather than 'Playing')
  520. xInfo = marquee(songInfo, xInfo, 1, xInfoWrap)
  521.  
  522. # Throttle frame rate, keeps screen legible
  523. while True:
  524. t = time.time()
  525. if (t - lastTime) > (1.0 / MAX_FPS): break
  526. lastTime = t
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement