Display wiring In this post, I want to showcase a project I did over a recent weekend: a display for your home, that shows the Munich public transport departures in real time. I mounted it next to the door in my apartment. This way I always know whether I’ll be able to catch my train or bus before leaving the house. Naturally, it is aware of delays and cancellations. The data is fetched from the API directly available from MVG, which is also used by the MVGO app as well as the website mvg.de. Internet connection is achieved over WI-FI.

Hardware Link to heading

The project is using a d1 mini microcontroller (ESP8266) and a 2.9" epaper display from WeAct Studio, both of which can be obtained on AliExpress for less than 15€ in total.

The epaper display is available in a black and white as well as a 3 colour (black, white and red) variant. I strongly recommend the black and white version, as the 3 colour version does not support partial refresh and has an overly long refresh time of 19s, during which the display flashes continuously.

The d1 mini mounted to the back of the epaper display Image 2: The d1 mini mounted to the back of the epaper display

The d1 mini is held in place by double-sided adhesive on the back of the display, and the included cables are directly soldered to the microcontroller. The wiring is as follows:

Display pin wire color d1 mini pin
1 (BUSY) purple 16
2 (RES) orange 5 (SCL)
3 (D/C) white 4 (SDA)
4 (CS) blue 15 (SS)
5 (SCL) green 14 (SCK)
6 (SDA) yellow 13 (MOSI)
7 (GND) black GND
8 (VCC) red 3V3

Display wiring Image 3: Display wiring

The device can then be powered using a regular USB phone charger. The mounting holes can be used to mount it to the wall.

Software Link to heading

The firmware I wrote for the display is open sourced under the GPL and available on GitLab: https://gitlab.com/aahuber/esp-mvg-display It is build using platformio and the Arduino framework.

In order to build and flash it to the microcontroller, at first the config file src/config.h should be changed. The following parameters are available:

Parameter Description
WIFI_SSID The SSID of the wifi network, to be connected to
WIFI_PASS The wifi password
STATION_NAME The name of the station (displayed in the title line on the display)
GLOBAL_ID The global ID of the station for which departures should be displayed. In order to obtain this for your station refer to the MVG API section
SHOW_TITLE If this is defined, a title line with the station name will be show
OFFSET_MINUTES Don’t show departures sooner than configured value

Then, the firmware can be built and flashed to the ESP using:

pio run -t upload

If everything went correctly, the microcontroller should connect to the configured WI-FI network on boot and display the current departures on the screen.

MVG API Link to heading

To obtain current departure times the JSON API provided by MVG is used. While it is not publicly documented it is used by the MVG website and the MVG app, by using the browser’s development tools on the MVG website it is fairly easy to figure out how it works on a superficial level.

The API is provided under the endpoint https://www.mvg.de/api/fib/v2.

To get the next 10 departures from Marienplatz, one can query the following URL: https://www.mvg.de/api/fib/v2/departure?globalId=de:09162:2&limit=10&transportTypes=UBAHN,TRAM,BUS,REGIONAL_BUS,SBAHN,SCHIFF. (Here de:09162:2 corresponds to the global ID of the Marienplatz station) The response will be a list of json objects looking something like this:

{
    "plannedDepartureTime":1726232520000,
    "realtime":true,
    "delayInMinutes":6,
    "realtimeDepartureTime":1726232928000,
    "transportType":"UBAHN",
    "label":"U6",
    "divaId":"010U6",
    "network":"swm",
    "trainType":"",
    "destination":"Klinikum Großhadern",
    "cancelled":false,
    "sev":false,
    "platform":2,
    "platformChanged":false,
    "messages":[],
    "bannerHash":"",
    "occupancy":"MEDIUM",
    "stopPointGlobalId":"de:09162:2:52:52"
}

The global ID of a station can be obtained via the API as well. For example, a query to https://www.mvg.de/api/fib/v2/location?query=Marienplatz&locationTypes=STATION will return a list of stations containing Marienplatz in their name. (Note that on Marienplatz in Munich there are actually multiple stations and also other towns within the MVV area have a Marienplatz, hence multiple results). Each station object in the response contains its global ID and looks something like this:

{
    "type":"STATION",
    "latitude":48.137245,
    "longitude":11.575421,
    "place":"München",
    "name":"Marienplatz",
    "globalId":"de:09162:2",
    "divaId":2,
    "hasZoomData":true,
    "transportTypes":["UBAHN","BUS","SBAHN"],
    "surroundingPlanLink":"MP",
    "aliases":"Rathaus Bf. Bahnhof München Muenchen Munchen Alter Peter Peterskirche Frauenkirche Dom Liebfrauendom Meisterfeier Viktualienmarkt Kaufingerstrasse MP",
    "tariffZones":"m"
}

Future Work Link to heading

There is a couple of things I’d like to add soon: - Error handling for failed requests to the API - Proper support of daylight saving time - 3D printed case and wall-mount