Skip to content

seb-caw/worldclock

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

worldclock

A multi-timezone display for broadcast control rooms. Runs offline on a Raspberry Pi 5 with a few hundred lines of Python.

worldclock running on the Pi 5 Touch Display

Why

Live broadcast rooms juggle multiple timezones at once — every live event references "local time" in a different city. The off-the-shelf options are either network-dependent (a non-starter where control hardware doesn't touch the internet) or $300+ commercial units that do less than a Pi 5 and a few hundred lines of Python could.

The worldclock is a simple and small physical display showing the timezones we actually use, mounted in line of sight, no network surface, nothing to maintain.

It sits on the desk where I work, in daily use.

How it works

[Pi 5 boots]
    ↓
systemd starts worldclock service
    ↓
Python reads local time + applies configured timezone offsets
    ↓
Renders the UTC hero clock + configured timezone tiles to the display
    ↓
Refresh loop redraws the screen
  • Time source: Pi's hardware clock — backed by a DS3231 RTC. No NTP, no WiFi.
  • Display: Raspberry Pi Touch Display 2 (reports as 1280x720 in fullscreen)
  • Service: runs under systemd, auto-starts on boot, auto-restarts on crash.
  • Timezone configuration: each tile can be edited on the touchscreen with choices from timezones.py, saved to config.json so it persists across reboots.

Hardware

Component Part
Single-board computer Raspberry Pi 5 (1GB)
Storage microSD 32GB
Display Raspberry Pi Touch Display 2
Cooling Official Raspberry Pi 5 Active Cooler
RTC backup DS3231 module (I²C, CR2032 coin cell)
Power Official Pi 5 27W USB-C PSU
Case Waveshare Protective Case → 3D Printed Case Soon

Note on the RTC: The DS3231 is a HAT-style module but I connected it with four short female-to-female jumper wires rather than seating it directly on the header — seating it directly was not possible with the small case, and the active cooler covers most of the board. Wiring only the four I²C pins keeps it loose inside the case. See RTC setup below.

Build & install

Tested on Raspberry Pi OS (64-bit), Python 3.13.

Confirm your OS release with cat /etc/os-release and your Python with python3 --version before reporting issues — the systemd unit and config paths below assume a /boot/firmware/ layout (Bookworm and newer).

git clone https://github.com/seb-caw/worldclock.git
cd worldclock

# Create venv and install dependencies
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Run once to verify
python -m src.clock

requirements.txt:

pygame
tzdata

tzdata ships the IANA timezone database so zone math and DST transitions are correct without relying on the system zoneinfo being present.

Enable I²C (required for the RTC)

sudo raspi-config
# Interface Options → I2C → Enable → Finish → reboot

RTC setup (DS3231)

Wire four pins from the DS3231 to the Pi's GPIO header:

DS3231 pin Pi physical pin Function
VCC Pin 1 3.3V
SDA Pin 3 I²C data
SCL Pin 5 I²C clock
GND Pin 6 (or 9) Ground

Confirm the Pi can see the chip — it should appear at address 0x68:

sudo i2cdetect -y 1

Register the RTC by adding the overlay to /boot/firmware/config.txt (under [all]):

dtoverlay=i2c-rtc,ds3231

Reboot, then write the current (correct) system time into the RTC and read it back:

sudo hwclock -w   # write system time → RTC
sudo hwclock -r   # read RTC back
timedatectl       # RTC time should match local time

With a coin cell installed the DS3231 holds time through power cycles with negligible drift, so the device keeps accurate time fully offline.

Install as a systemd service (auto-start on boot)

sudo cp systemd/worldclock.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now worldclock
sudo systemctl status worldclock

The unit waits briefly for the desktop session before launching fullscreen, restarts on failure, and starts automatically on every boot:

[Unit]
Description=World Clock
After=graphical.target

[Service]
User=pi
WorkingDirectory=/home/pi/worldclock
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/pi/.Xauthority
ExecStartPre=/bin/sleep 5
ExecStart=/home/pi/worldclock/venv/bin/python -m src.clock
Restart=on-failure
RestartSec=5

[Install]
WantedBy=graphical.target

To stop it temporarily (e.g. to access the desktop over SSH):

sudo systemctl stop worldclock

Design decisions

No WiFi, no NTP — by choice, not by limitation. The Pi 5 has WiFi built in. I'm deliberately not using it. The environment this lives in doesn't expose control hardware to the internet, and an offline-by-design appliance has a far gentler failure mode (a few seconds of drift per year from the DS3231) than a networked one ("WiFi died at 2am and the clock has now been stuck for hours"). When the clock does need its time corrected, it's a deliberate manual act: plug in ethernet, let NTP sync once, unplug.

Pi 5 over a microcontroller. Tempting to use an ESP32 or Pico for the "right tool for the job" reasons — lower power, faster boot, no OS to maintain. I went with the Pi because I wanted hands-on use with a Pi and for this to be my first real-world Python project.

A locked-down network surface. Even though it normally runs offline, I stripped the device back to the minimum: disabled the printing (cups), discovery (avahi), and rpcbind services so the only listening port is SSH, which itself is hardened (non-default port, root login disabled, limited auth attempts). The goal is that if it ever does touch a network, it presents almost nothing.

What I'd do differently

  • The touchscreen was the real fight, not the clock logic. The official display reports touches as mouse events, not pygame finger events, so my first scroll/drag implementation silently did nothing on the hardware while working fine on my laptop. Reworking selection around a tap-vs-drag distance threshold (and adding an explicit Confirm step so a mis-tap can't change a zone) took longer than writing the timezone rendering.
  • Physical constraints bit harder than software ones. The active cooler covers the GPIO header and the RTC didn't fit the case — the "just plug in an RTC" step turned into removing the cooler, sourcing short jumper wires, and wiring only four pins. If I rebuilt it I'd choose the case, cooler, and RTC together as one mechanical decision rather than three separate orders.
  • Dev environment friction. Moving files between a Windows PC, a Mac, and the Pi surfaced every scp/SSH quirk going (WSL hijacking scp, a macOS password-prompt bug). Settling on one machine and rsync over SSH early would have saved a lot of dead time.

License

MIT — see LICENSE

Credits

World map background: BlankMap-World-v2 (public domain / CC0).

About

Fully offline Multi-timezone clock built on a Raspberry Pi 5

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages