Multi-Stop Bus Tracker (and a tide display. and possibly weather.)

The render of the Bus Display

I live in SF, with some of the best public transit in the country. I would like to know when the next bus will be arriving at a stop near my house. Not just one stop, either: there are several bus lines that will all take me downtown, but they’re all served by different stops. I need a device that will ping the city’s API for all the bus stops near my house, and condense that into a list I can put on a small display somewhere. I don’t want to dedicate a whole computer to this task, so I need to build an embedded solution.

The standard MUNI bus display at some bus stops

This is somewhat different than the displays actually found at bus stops around the city (above). By their nature, the displays at one bus stop do not track the arrivals at other nearby bus stops. It can be done, I just need the data. This can be done through the 511.org API. Given any bus stop in the city, this API will give me predicted times of arrivals each bus scheduled at that stop, its line number/letter, and its destination. There is a rate limit on this API of 60 requests per hour, but that only means I’m sending a request every 65 seconds.

But there are a few problems:

Problem 1: HTTPS and Decompressing JSON

Getting data from the 511.org API presents two immediate challenges: the API requires HTTPS connections, and it only returns gzipped JSON data. This is an unusual requirement for embedded projects; most examples of accessing web APIs on microcontrollers use plain HTTP, and compressed data is rarely seen in embedded applications.

HTTPS was the easier of these problems to solve; the SSLClient library for Arduino handles the SSL/TLS handshake and encryption. The only tricky part was getting the correct SSL certificates in the right format. A web-based tool creates a trust_anchors.h file containing the certificates needed for SSL verification - you just input the domain name (api.511.org) and it generates the properly formatted certificate array that SSLClient needs. With the correct certificates in place, SSLClient manages the encrypted connection transparently.

Figuring out the payload was another matter entirely. At first, every response I was getting from the server was garbage, even when telling the server to send me uncompressed JSON. Looking at the hex dump of the data I was receiving told me another story.

The key insight that told me it was gzipped JSON

I can not tell you how many times looking at a hex dump has saved my ass.

The 0x1F 0x8B is the magic number for gzip encoding. Once I saw that, I knew I needed a library for decompressing gzip on an embedded platform. I found the miniz library, a portable library in C that handles the decompression. Even with this library, it’s tricky; I need to skip past the 10-byte header, extract the uncompressed size from the last 4 bytes, and decompress everything in between. I also need to manage the memory correctly with malloc() and free(), and no I haven’t learned Rust yet.

Problem 2: Organizing this data

The 511.org API gives me a lot of data for each bus stop - route numbers, destinations, arrival times, and current locations. But I need to track multiple stops at once, and merge all this information into something useful. This meant creating a data structure that could handle multiple stops and multiple arrivals at each stop.

The solution I came up with uses a simple array of stop data structures. Each stop structure contains its stop code (a unique identifier for each bus stop) and an array of upcoming arrivals. Each arrival contains the bus line number, destination, arrival time, and stop name. The size of these arrays is fixed - I can track up to 20 arrivals per stop, which is more than enough for my needs.

When new data comes in from the API, I clear out the old arrivals for that stop and fill in the new ones. There’s also a cleanup function that removes arrivals that have already passed. The most interesting part is how the display function handles all this data - it looks through all stops and all arrivals, groups buses going to the same destination, and creates a consolidated list of arrival times. For example, if the 48 bus is coming to the same stop in 5, 20, and 35 minutes, it shows up as one line: “48 Inbound in 5, 20, 35 minutes”. Pinging two stops means I can also get “28 to the Presidio in 2, 12, 23 minutes”.

Getting the bus displays four bus stops around Market and Van Ness

The above is the device pinging four bus stops ({“15696”, “15565”, “13220”, “15678”}) around Market and Van Ness. Each stop has several bus lines on it, and the code correctly displays the line, destination, and arrival time. This might seem like overkill for just checking bus times, but it means I can glance at the display and immediately see every bus that’s coming to any stop near my house, sorted by arrival time.

Problem 3: Let’s add a display!

This thing needs a display, something low power, too. I settled on an ePaper display, the Microtips MT-DEPG0750RWU790F30, a 7.5” display with a resolution of 480x800. There are a few things that brought me to this display: it has an on-chip framebuffer, which can vastly increase capabilities if you’re smart, and programmable waveforms for the eInk. This is a display meant for price labels in a grocery store, so the refresh rate isn’t great, but with partial updates it will be more than sufficient.

The breakout PCB for the e-paper panel

Schematic for the Epaper driver

I whipped up a board in KiCad that would support this board. The panel driver circuit is ripped straight from the datasheet and drives the panel with SPI. In addition to that, I added a few solderable standoffs and a power circuit built around TI’s TPS560200, giving me 3.3V from 4.5V to 17V. There is a vertical USB-C (power only) on the board, because i envision this being something like a picture frame. There is also a ~12V power input ORed with the USB-C power input in case I ever want to install this inside a wall.

Writing the driver/library for the e-paper display proceeded as it usually does when I write a display driver – tearing my hair out and somehow it magically works.

An enclosure

The enclosure is a 3-piece ordeal. The legs bolt onto the back with heat-set inserts installed in the back. The PCB/EPD assembly mounts to the back of the display with the solderable standoffs installed on the PCB. The front is a snap-fit assembly, with the geometry shown in the sectional diagram below.

These parts were 3D printed on a Prusa Mk4 in carbon fiber-filled PC. I’m only building one of these, after all.

Front and back of the enclosure

sectional analysis of the enclosure

back