Adafruit-NextBus/nextbus-matrix.py
2015-02-27 12:34:16 -08:00

180 lines
6.8 KiB
Python

# NextBus scrolling marquee display for Adafruit RGB LED matrix (64x32).
# Requires rgbmatrix.so library: github.com/adafruit/rpi-rgb-led-matrix
import atexit
import Image
import ImageDraw
import ImageFont
import math
import os
import time
from predict import predict
from rgbmatrix import Adafruit_RGBmatrix
# Configurable stuff ---------------------------------------------------------
# List of bus lines/stops to predict. Use routefinder.py to look up
# lines/stops for your location, copy & paste results here. The 4th
# string on each line can then be edited for brevity if desired.
stops = [
( 'actransit', '210', '0702640', 'Ohlone College' ),
( 'actransit', '232', '0704440', 'Fremont BART' ),
( 'actransit', '210', '0702630', 'Union Landing' ),
( 'actransit', '232', '0704430', 'NewPark Mall' ) ]
maxPredictions = 3 # NextBus shows up to 5; limit to 3 for simpler display
minTime = 0 # Drop predictions below this threshold (minutes)
shortTime = 5 # Times less than this are displayed in red
midTime = 10 # Times less than this are displayed yellow
width = 64 # Matrix size (pixels) -- change for different matrix
height = 32 # types (incl. tiling). Other code may need tweaks.
matrix = Adafruit_RGBmatrix(32, 2) # rows, chain length
fps = 20 # Scrolling speed (ish)
routeColor = (255, 255, 255) # Color for route labels (usu. numbers)
descColor = (110, 110, 110) # " for route direction/description
longTimeColor = ( 0, 255, 0) # Ample arrival time = green
midTimeColor = (255, 255, 0) # Medium arrival time = yellow
shortTimeColor = (255, 0, 0) # Short arrival time = red
minsColor = (110, 110, 110) # Commans and 'minutes' labels
noTimesColor = ( 0, 0, 255) # No predictions = blue
# TrueType fonts are a bit too much for the Pi to handle -- slow updates and
# it's hard to get them looking good at small sizes. A small bitmap version
# of Helvetica Regular taken from X11R6 standard distribution works well:
font = ImageFont.load(os.path.dirname(os.path.realpath(__file__))
+ '/helvR08.pil')
fontYoffset = -2 # Scoot up a couple lines so descenders aren't cropped
# Main application -----------------------------------------------------------
# Drawing takes place in offscreen buffer to prevent flicker
image = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(image)
currentTime = 0.0
prevTime = 0.0
# Clear matrix on exit. Otherwise it's annoying if you need to break and
# fiddle with some code while LEDs are blinding you.
def clearOnExit():
matrix.Clear()
atexit.register(clearOnExit)
# Populate a list of predict objects (from predict.py) from stops[].
# While at it, also determine the widest tile width -- the labels
# accompanying each prediction. The way this is written, they're all the
# same width, whatever the maximum is we figure here.
tileWidth = font.getsize(
'88' * maxPredictions + # 2 digits for minutes
', ' * (maxPredictions-1) + # comma+space between times
' minutes')[0] # 1 space + 'minutes' at end
w = font.getsize('No Predictions')[0] # Label when no times are available
if w > tileWidth: # If that's wider than the route
tileWidth = w # description, use as tile width.
predictList = [] # Clear list
for s in stops: # For each item in stops[] list...
predictList.append(predict(s)) # Create object, add to predictList[]
w = font.getsize(s[1] + ' ' + s[3])[0] # Route label
if(w > tileWidth): # If widest yet,
tileWidth = w # keep it
tileWidth += 6 # Allow extra space between tiles
class tile:
def __init__(self, x, y, p):
self.x = x
self.y = y
self.p = p # Corresponding predictList[] object
def draw(self):
x = self.x
label = self.p.data[1] + ' ' # Route number or code
draw.text((x, self.y + fontYoffset), label, font=font,
fill=routeColor)
x += font.getsize(label)[0]
label = self.p.data[3] # Route direction/desc
draw.text((x, self.y + fontYoffset), label, font=font,
fill=descColor)
x = self.x
if self.p.predictions == []: # No predictions to display
draw.text((x, self.y + fontYoffset + 8),
'No Predictions', font=font, fill=noTimesColor)
else:
isFirstShown = True
count = 0
for p in self.p.predictions:
t = p - (currentTime - self.p.lastQueryTime)
m = int(t / 60)
if m <= minTime: continue
elif m <= shortTime: fill=shortTimeColor
elif m <= midTime: fill=midTimeColor
else: fill=longTimeColor
if isFirstShown:
isFirstShown = False
else:
label = ', '
# The comma between times needs to
# be drawn in a goofball position
# so it's not cropped off bottom.
draw.text((x + 1,
self.y + fontYoffset + 8 - 2),
label, font=font, fill=minsColor)
x += font.getsize(label)[0]
label = str(m)
draw.text((x, self.y + fontYoffset + 8),
label, font=font, fill=fill)
x += font.getsize(label)[0]
count += 1
if count >= maxPredictions:
break
if count > 0:
draw.text((x, self.y + fontYoffset + 8),
' minutes', font=font, fill=minsColor)
# Allocate list of tile objects, enough to cover screen while scrolling
tileList = []
if tileWidth >= width: tilesAcross = 2
else: tilesAcross = int(math.ceil(width / tileWidth)) + 1
nextPrediction = 0 # Index of predictList item to attach to tile
for x in xrange(tilesAcross):
for y in xrange(0, 2):
tileList.append(tile(x * tileWidth + y * tileWidth / 2,
y * 17, predictList[nextPrediction]))
nextPrediction += 1
if nextPrediction >= len(predictList):
nextPrediction = 0
# Initialization done; loop forever ------------------------------------------
while True:
# Clear background
draw.rectangle((0, 0, width, height), fill=(0, 0, 0))
for t in tileList:
if t.x < width: # Draw tile if onscreen
t.draw()
t.x -= 1 # Move left 1 pixel
if(t.x <= -tileWidth): # Off left edge?
t.x += tileWidth * tilesAcross # Move off right &
t.p = predictList[nextPrediction] # assign prediction
nextPrediction += 1 # Cycle predictions
if nextPrediction >= len(predictList):
nextPrediction = 0
# Try to keep timing uniform-ish; rather than sleeping a fixed time,
# interval since last frame is calculated, the gap time between this
# and desired frames/sec determines sleep time...occasionally if busy
# (e.g. polling server) there'll be no sleep at all.
currentTime = time.time()
timeDelta = (1.0 / fps) - (currentTime - prevTime)
if(timeDelta > 0.0):
time.sleep(timeDelta)
prevTime = currentTime
# Offscreen buffer is copied to screen
matrix.SetImage(image.im.id, 0, 0)