Raspberry Pi Zero E-Ink Bus Predictions

Adam Derewecki
3 min readAug 10, 2019

--

About a year ago, I used a Raspberry Pi 3+ and 7-inch LCD screen to create a bus schedule for my morning bus routes. It’s a huge time and stress saver to have always-on predictions instead of finding my phone and navigating to the bus prediction app.

However, the LCD screen is super bright and consumes a lot of power. For a recent hardware hackathon I attended, I decided to re-build this project using the lower-power Raspberry Pi Zero W and the much-much-lower-power PaPiRus e-ink display.

7-inch LCD power hog

Hardware Preparation

The PaPiRus comes with a HAT that connects to the Pi Zero W through a set of header pins. The pins need to be soldered to the Pi Zero and then the PaPiRus HAT snaps into the board.

Software Preparation

  1. Download Raspbian (torrent is usually fastest)
  2. Download Etcher (OS X)
  3. Write the image to the card
  4. Create /Volumes/boot/wpa_supplicant.conf:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
network={
ssid="YOUR_NETWORK_NAME"
psk="YOUR_PASSWORD"
key_mgmt=WPA-PSK
}

5. Enable ssh access by creating a file:

$ touch /Volumes/boot/ssh

Now you can eject the card, put it in the Pi Zero W and boot it up. If you have access to your router’s admin interface, you can find the IP address for the Pi in the DHCP client list. If you don’t have access to that, you can put on your cowboy hat and scan your network:

for i in `seq 2 254`; do
ssh -o ConnectTimeout=1 -vv pi@192.168.0.$i
done

By default, the username is pi and the password is raspberry. I recommend using screen or tmux to keep a session you can reconnect to.

Python Resources

https://github.com/PiSupply/PaPiRus — the official Python API for manipulating the PaPiRus screen from a Raspberry Pi.

https://github.com/billtubbs/text-bitmaps — Instead of doing a full-screen refresh, the PaPiRus allows you to do a partial update and only reset the affected pixels. This doesn’t work with the text writing API, but does work with the Bitmap image API. This text-bitmaps library helps convert text into Bitmap images to be loaded.

https://github.com/derwiki/nextbus-eink — all of the code used in this post, fully functional and ready to go out of the box.

Loading Predictions

This is a simple GET request for an XML payload. In the interest of jankiness, I’ve decided to parse the XML response with a simple regex instead of a full XML parser.

import urllib
import re
URLS = {
'10 Inbound Wisconsin & Madera': 'http://webservices.nextbus.com/service/publicXMLFeed?command=predictions&a=sf-muni&r=10&s=6966', # noqa E501
'48 Inbound Wisconsin & 25th': 'http://webservices.nextbus.com/service/publicXMLFeed?command=predictions&a=sf-muni&r=48&s=3513', # noqa E501
}
SECONDS_MINUTES_RE = re.compile('seconds="(\d+)" minutes="(\d+)"')def format_prediction(seconds, minutes):
return '{minutes}m{seconds}s'.format(
minutes=minutes,
seconds=int(seconds) % 60,
)
def request_predictions():
predictions = {}
for stop, url in URLS.items():
connection = urllib.urlopen(url)
xml = connection.read()
connection.close()
predictions[stop] = [
format_prediction(*seconds_minutes)
for seconds_minutes
in SECONDS_MINUTES_RE.findall(xml)
]
return predictions
print(request_predictions())

Sample output:

{'10 Inbound Wisconsin & Madera': ['10m0s', '36m0s', '51m0s', '83m0s'],
'48 Inbound Wisconsin & 25th': ['10m48s', '17m4s', '38m34s', '49m40s', '59m23s']}

Tying It All Together

All that’s left is an infinite loop that fetches predictions and displays them on the e-ink display. We use a display() helper function to handle converting text to bitmap and then displaying it on the screen. Beyond that, we have a while True that pauses for 5 seconds. The try/except is helpful because sometimes the Nextbus API doesn’t respond (often in the middle of the night).

from textbitmaps import bitmaps
from papirus import Papirus
from PIL import Image
import time
import datetime
import predictions


SCREEN = Papirus()
SCREEN.update()


def display(s, full=False):
img, _ = bitmaps.display_text_prop(
s,
display_size=(200, 96),
char_size=2,
)
img.save("tmp.bmp")
file = Image.open("tmp.bmp")
SCREEN.display(file)
if full:
SCREEN.update()
else:
SCREEN.partial_update()


i = 0
while True:
try:
route_lines = '\n'.join([
' %s: %s' % (line, ', '.join(times))
for line, times
in predictions.request_predictions()
])
i += 1
now = datetime.datetime.now().strftime('%-I:%M:%S %p')
buffer = '%s\n%s\n%s' % (
' %s' % now,
' NextBus',
route_lines,
)
display(buffer, full=i % 3 == 0)
except Exception as e:
print(str(e))

time.sleep(10)

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Adam Derewecki
Adam Derewecki

Written by Adam Derewecki

Hi! I’m Adam. I live in San Francisco, write code, take pictures, and practice yoga.

No responses yet

Write a response