display-hata is a Python application for Raspberry Pi Zero 2W that drives an SH1106 OLED display (128x64, blue) to cycle through information screens. Uses luma.oled for hardware rendering and luma.emulator (pygame) for local development.
- Board: Raspberry Pi Zero 2W
- Display: SH1106 OLED, 128x64 resolution, blue
- Interface: SPI (SCLK→P11, MOSI→P10, DC→P24, CS→P8/CE0, RST→P25)
- Dev mode:
luma.emulatorwith pygame (no physical hardware needed)
The display uses the SPI interface. Enable it before installing the software.
Non-interactive (one-liner):
sudo raspi-config nonint do_spi 0Interactive: Run sudo raspi-config, navigate to Interface Options → SPI, select Yes, then reboot.
After rebooting, verify SPI is active:
ls /dev/spidev0.*
# Expected: /dev/spidev0.0Install required system packages:
sudo apt-get update
sudo apt-get install -y \
git \
python3-dev \
python3-pip \
python3-venv \
libopenjp2-7 \
libjpeg-dev \
libfreetype6-dev \
libssl-dev \
libffi-dev \
nmapspidev and RPi.GPIO are Python packages and will be installed automatically by pip in the next step.
- Device layer — factory that returns either a real
luma.oled.device.sh1106or aluma.emulator.device.pygamedevice based on environment. - Screen abstraction — each screen extends
Screen(inscreens/base.py) and implementsdraw(). Screens have three key properties:interval: float— many seconds this screen stays visible before the loop moves to the next one.live: bool = False— whenTrue, the screen redraws continuously every 0.5s for its interval (e.g. ticking clock). WhenFalse, it draws once and sleeps.prefetch()— optional hook called in a background thread while the previous screen is displayed, so slow I/O (HTTP requests) completes before the screen is drawn.
- Screen loop — main loop cycles through registered screens on a timer. Before each screen is shown, its
prefetch()has already run in a background thread during the previous screen's interval.
| Screen | Description |
|---|---|
date |
Current date and time with ticking seconds |
weather |
Temperature and condition via Open-Meteo API |
smart_bikes |
Bike availability at a configured Smart Bike station |
adsb |
Aircraft count within 50 km via adsb.lol / adsb.fi |
cpu |
CPU usage percentage |
strava |
Cycling distance and goal progress via Strava API |
bf6 |
Battlefield 6 K/D ratio and kill/death counts |
map |
ASCII art map with randomly blinking city dots |
lan |
Number of active devices on the local network |
satellites |
ISS overhead indicator + Galileo and Starlink counts |
Copy the example config and edit it:
cp config.example.json config.jsonconfig.json fields:
| Field | Type | Description |
|---|---|---|
screens |
string[] | Ordered list of screens to display. Only listed screens are shown. |
weather |
object | lat and lon for the weather screen. |
smart_bikes |
object | station — Tartu Smart Bike station name. |
adsb |
object | city (display label), lat, lon, and optional radius_km (default 50). |
strava |
object | goal_km (default 1000) and period (ytd, all, or recent). |
bf6 |
object | username — Battlefield 6 player name. platform (default "pc"). |
map |
object | No required fields. Accepts duration. |
lan |
object | No required fields. Accepts duration. Requires nmap installed on the Pi. |
satellites |
object | lat, lon, and optional min_elevation (degrees, default 30). Requires N2YO_API_KEY in .env. |
Every screen section accepts an optional duration (number) — seconds the screen stays visible before cycling to the next one. Defaults to 5.
Valid screen names: date, weather, smart_bikes, adsb, cpu, strava, bf6, map, lan, satellites.
Screens without config (date, cpu, map, lan) don't need a config section.
Example:
{
"screens": ["date", "weather", "smart_bikes", "adsb", "cpu", "strava"],
"date": {
"duration": 5
},
"weather": {
"lat": 58.38,
"lon": 26.72,
"duration": 5
},
"smart_bikes": {
"station": "Raatuse",
"duration": 5
},
"adsb": {
"city": "Tartu",
"lat": 58.38,
"lon": 26.72,
"radius_km": 50,
"duration": 5
},
"cpu": {
"duration": 5
},
"strava": {
"goal_km": 1000,
"period": "ytd",
"duration": 5
},
"bf6": {
"username": "YourBF6Username",
"platform": "pc",
"duration": 5
}
}Clone the repository:
git clone <repo-url>
cd display-hataCreate a virtual environment and install dependencies:
python3 -m venv .venv
source .venv/bin/activate
pip install --prefer-binary --extra-index-url https://www.piwheels.org/simple -r requirements.txtpiwheels provides pre-built ARM wheels so compiled packages like Pillow install in seconds instead of building from source.
Copy the example config and edit it with your location and settings:
cp config.example.json config.json
nano config.jsonIf you use the strava screen, also set up credentials (see Strava Setup below).
Uses uv for dependency management:
uv sync --extra dev # includes luma.emulator (pygame)
uv run python main.py --emulatorThe strava screen requires OAuth2 credentials. One-time setup:
- Create a Strava API application at https://www.strava.com/settings/api
- Set the Authorization Callback Domain to
localhost - Run the helper script and follow the prompts:
python strava_auth.py- Copy the output into a
.envfile in the project root (see.env.example)
The app automatically refreshes access tokens (they expire every 6 hours) and caches them in .strava_cache.json. Both .env and .strava_cache.json are git-ignored.
The satellites screen requires a free N2YO API key:
- Register at https://www.n2yo.com/api/ (free, no credit card)
- Copy your API key and add it to your
.envfile:
N2YO_API_KEY=your_key_here
The screen shows:
- ISS above! — only when the ISS is currently overhead (omitted otherwise)
- N Galileo — Galileo (EU navigation) satellites above the elevation threshold
- N Starlink — Starlink satellites above the elevation threshold
The min_elevation config field (default 30) controls the minimum elevation angle in degrees. Only satellites above this angle are counted — higher values reduce noise from satellites near the horizon.
min_elevation |
What is counted |
|---|---|
0 |
Entire visible sky (horizon to zenith) |
30 |
Satellites ≥ 30° above horizon (default) |
45 |
High in the sky only |
Data is cached for 15 minutes to stay well within the free tier limit (1,000 requests/hour).
python main.py --emulator # run with pygame emulator (dev)
python main.py --gif out.gif # record to GIF, Ctrl+C to save (dev)
python main.py # run on real SH1106 display (Pi)Use systemd to run display-hata automatically on boot and keep it running.
sudo nano /etc/systemd/system/display-hata.servicePaste the following, adjusting WorkingDirectory and ExecStart if you cloned the repo to a different path:
[Unit]
Description=display-hata OLED screen cycler
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/display-hata
ExecStart=/home/pi/display-hata/.venv/bin/python main.py
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable display-hata
sudo systemctl start display-hataThe service will now start automatically on every boot.
| Action | Command |
|---|---|
| Start | sudo systemctl start display-hata |
| Stop | sudo systemctl stop display-hata |
| Restart | sudo systemctl restart display-hata |
| Status | sudo systemctl status display-hata |
| View logs | journalctl -u display-hata -f |
| Disable | sudo systemctl disable display-hata |
