It was a cold morning in Zurich. I walked to the tram stop as usual. Upon arriving, I saw that the next departure was in 9 minutes, which meant 9 minutes of waiting outside in the cold. It turned out that the tram schedule varies from day to day. No warning, no heads-up. Just me, freezing and staring at an empty track.
That was the moment I decided I needed a departure display at home, so next time I can wait warm at home. I want to know before I leave the house whether I need to hurry or whether I have time for another sip of coffee. What I need is a tiny display next to the front door that shows when the next tram leaves.
The Good News: Open DataSwitzerland publishes real-time public transport data through an open API: transport.opendata.ch. No API key, no registration, no rate limits. One GET request and you get live departures for any stop in the country. So at least some of my tax money is well spent, I can actually build something with it.
The BuildThe display layout is modeled after the actual departure boards you see at Zurich tram stops. Station name, line number, destination, minutes until departure. If you've ridden a tram in Zurich, you'll recognize it.
The one thing I added myself is the urgency icons. How much time you have changes what you see:
10+ minutes → just the number. You're fine.5–9 minutes → a little running man appears. Time to put on your shoes.Under 5 minutes → dash indicator. You should already be out the door.0 minutes → Bus leaving now, same as on the original departure screens.
The Offline FallbackHere's the thing: I turn off our WiFi router at night. That means every morning when the router boots back up, there's a window where the display has no connection. And of course, morning is exactly when I need departure times the most.
So I embedded the complete timetable directly into flash memory. Switzerland publishes GTFS data (General Transit Feed Specification), the same standardized schedule format used by transit agencies worldwide, through opentransportdata.swiss. I wrote a Python script that downloads the official GTFS feed, parses out the departures for my configured stop and lines, and compresses it as small as possible.
In practice it looks something like this:
const ScheduleTime tramSchedule[] = {
{ 4, 50, 0x1F, "Stettbach, Bahnhof"}, // Weekdays 04:50
{ 4, 50, 0x20, "Bahnhofstrasse/HB"}, // Saturday 04:50
{ 4, 57, 0x0F, "Bahnhofstrasse/HB"}, // Mon-Thu 04:57
// ... 3,746 tram entries + 2,224 bus entries
};Nearly 6, 000 schedule entries, and it all fits comfortably in the ESP32-C6's flash. The schedule only changes once a year when the transit authority updates the timetable, so I regenerate it annually with one command:
The Power Button (That Doesn't Break the Battery)You may already wondered why the power switch is not cutting off the battery but instead connects to GPIO 2 and GND. Because I need the internal RTC to keep counting.
With the internal pull-up resistor enabled, the pin reads HIGH when the switch is open (awake) and LOW when closed (asleep). Why not just cut the power?
When you flip the switch, the firmware saves the current Unix timestamp to RTC memory, powers off the display, disconnects WiFi, and enters deep sleep. The ESP32 draws microamps in this state, the battery lasts about 30 days with 3–5 minutes of display usage per day. When you flip the switch back, the chip wakes on the GPIO interrupt, restores the timestamp from RTC memory, and immediately knows what time it is.
Battery ManagementEven the Seeed Studio ESP32 has an integrated battery charger, but getting the current charging state of the battery is not included. Since it operates at 3.3V and the battery has a voltage of around 4.2V when fully charged, we need a voltage divider to protect the ADC. Two 200k ohm resistors soldered to the back of the espresso machine did the trick. The software then converts the 3.2V-4.2V to 0-100% and the corresponding icon.
The Enclosure3D printed in PLA, designed in Fusion 360. It went through a couple of iterations to get right, a window for the OLED, a slot for the toggle switch, USB-C access for charging. The display friction-fits into the window, and the whole thing snaps together without screws.
The goal was something that looks like a little gadget, not a prototype. It hangs next to our front door and honestly, guests think it came from a store.
Wrapping UpTramli hangs next to our front door now. On the way out, I glance at it. Running man? Better walk fast. Just a number? Take your time. It's become one of those things that's so embedded in the daily routine that you forget you built it.
The whole project took 2-3 weekends. If you have basic soldering skills and a 3D printer, you can build one too.







_t9PF3orMPd.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)





Comments