Arrivals is an open source project for real-time public transit info. It’s also an exploration of weird Kotlin Multiplatform targets: a macOS menu bar app, a CLI, even a web widget on the landing page.

A small screen showing arrivals at Shoreditch High Street

I added a simple desktop UI with Compose Multiplatform and used it to build a physical transit times display. Let’s walk through the build.

Hardware

The two key components are a Raspberry Pi and a super-wide DSI display from Waveshare. I picked the Raspberry Pi 4 as a middle ground between the Pi 5 (which is way overpowered for this scenario) and the Pi Zero 2 (which unfortunately doesn’t have a DSI display output). The full bill of materials is:

Raspberry Pi 4 mounted to the back of the display

The 8.8" panel has a native resolution of 480x1920 – an aspect ratio which feels custom-made for this project. There’s also a 7.9" variant which can be easier to source and works for this build with a few config tweaks, but the panel quality is noticeably lower.

Both display options come with DSI cables and screws to attach the Raspberry Pi. Assembly is pretty self-explanatory, but the ribbon cable covers the SD card slot so you’ll want to flash the OS before connecting that up.

Case

There are no off-the-shelf enclosures for these screens, so I designed a 3D-printed display bezel and Raspberry Pi case. The model is published on MakerWorld. It’s two printed parts fastened with 4 additional M3 screws.

3D-printed case parts fresh off the printer

Raspberry Pi setup

We’re going to configure the Raspberry Pi so it boots directly into the full screen Arrivals app.

Flashing the OS

Open the Raspberry Pi Imager and select Raspberry Pi OS (64-bit). Configure Wi-Fi credentials, username, password, and hostname. Enabling SSH for remote access is the easiest way to follow the rest of the steps, but a USB keyboard works too.

Don’t choose the Lite OS! We need all the X11 window manager dependencies installed.

Display configuration

The display requires some extra configuration. Mount the SD card on your computer before transferring it to the Raspberry Pi and edit config.txt to include:

dtoverlay=vc4-kms-v3d
dtoverlay=vc4-kms-dsi-waveshare-panel,8_8_inch

The panel’s native orientation is portrait, so we need to rotate it. Open /boot/firmware/cmdline.txt and append the following to the end of the line, separated by a space:

video=DSI-1:480x1920M,rotate=270

Use rotate=90 or rotate=270 depending on your preferred orientation (270 if you’re printing the case). This handles the TTY (console) mode. We’ll set the window manager orientation after we boot.

First boot

The desktop environment will be in portrait mode initially. You can follow the Waveshare wiki guide to correct it via Preferences > Screen Configuration. We’re not actually going to use this desktop environment though. The goal is to run the Arrivals app without any window manager chrome.

Configure the system to boot to the command line via Preferences > Raspberry Pi Configuration > Boot: To CLI (or sudo raspi-config in the terminal).

Before installing anything else, update the system:

sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y

Kiosk mode

We want the app to launch automatically in a bare X session with no desktop, taskbar, or window chrome. This is sometimes called “kiosk mode”.

  1. Install unclutter to hide the mouse cursor:

    sudo apt install unclutter
    
  2. Create an empty kiosk script. We’ll add our app run command later:

    touch ~/kiosk && chmod +x ~/kiosk
    
  3. Create ~/.xinitrc to configure the X session. This handles screen rotation, disables power management, hides the cursor, and runs the kiosk script:

    # Rotate display (use 'left' instead of 'right' for the opposite orientation)
    xrandr --output DSI-1 --rotate right
    # Rotate touch input too (`xinput list` to find the right device ID)
    xinput --set-prop 6 --type=float "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1
    
    xset s off         # Don't activate the screensaver
    xset -dpms         # Disable DPMS (Energy Star) features
    xset s noblank     # Don't blank the video device
    unclutter &        # Hide the mouse cursor
    ~/kiosk            # Start our app!
    
  4. Create ~/.bash_profile to start the X session automatically on login. This checks that we’re on the physical terminal (tty1) so it won’t trigger over SSH. exec replaces the shell process with X, so if X exits you get a clean login prompt:

    if [[ "$(tty)" == "/dev/tty1" ]]; then
        exec startx
    fi
    

Software

Arrivals has release builds for macOS, but the Compose Desktop target needs to be built locally on the Raspberry Pi. If you’re using an authenticated transit system like TfL, you’ll also need to configure an API key. See the README for prerequisites.

Building on device

  1. Install a JDK, since Compose Multiplatform is a JVM-based rather than native target:

    sudo apt install openjdk-17-jdk
    
  2. Clone the repository and build the distributable:

    git clone https://github.com/jdamcd/arrivals-kmp.git
    cd arrivals-kmp
    ./gradlew :desktop:createDistributable
    

This can take a lonnnng time on the first build.

Configuration

The app reads station config from a YAML file at ~/.arrivals.yml. A config for trains at Shoreditch High Street, platform 2, via the TfL API looks like:

mode: tfl
stop: 910GSHRDHST
platform: 2

The stop field is a station ID. You can use the CLI in the repo to search for IDs: arrivals search tfl "shoreditch high street". Public transit systems in NYC, Berlin, the SF Bay Area, and beyond are supported with different config options.

Running the app

Add the launch command to ~/kiosk. The display resolution parameters force the app to run fullscreen with no chrome:

#!/bin/bash
~/arrivals-kmp/desktop/build/compose/binaries/main/app/ArrivalsDesktop/bin/ArrivalsDesktop -pi 1920 480

Now reboot the device. It’ll launch straight into the arrivals display and refresh every 60 seconds.

Conclusion

It was fun pushing Kotlin Multiplatform into unusual territory. Now the arrival times for my local train station are sitting on a shelf at home. A smart plug turns it on every weekday morning, and I can check for delays on my way out the door.