Skip to content
aSbiEL0 edited this page Mar 13, 2026 · 1 revision

zero2dash Project Wiki

The zero2dash project provides a self‑contained dashboard for a Raspberry Pi running Pi‑hole. It drives a 320×240 TFT display connected to /dev/fb1 directly (no X server or browser) and rotates through small modules that present useful information such as Pi‑hole statistics, a calendar, exchange rates, photos and local tram departures. All interaction is via a resistive touch‑screen.

This wiki explains how the project boots and runs, how modules are selected and executed, what each module does, and summarises planned improvements identified in the code review. It also documents the systemd services and timers that orchestrate the dashboard.

Overview of repository structure

  • boot/ – Contains boot_selector.py and assets used during boot. This script presents a boot‑time splash GIF and allows the user to choose day (dashboard) or night (blackout) mode.
  • modules/ – Each dashboard page lives in its own directory. The modules currently shipped are pihole, calendash, currency, photos, trams, and blackout (night mode). Most modules provide a long‑running Python script to draw content on the framebuffer.
  • display_rotator.py – Central controller for day‑time operation. It reads modules.txt, cycles through modules, spawns each as a child process and handles touch events for navigation and screen power management.
  • _config.py – Utility functions for reading environment variables with type checking and for reporting configuration validation errors.
  • systemd/ – Unit and timer files used to start the display, night mode and periodic update jobs. These deploy the dashboard as a set of services on the Pi.
  • docs/, tests/, cache/ – Additional documentation, tests and assets.

Boot process and run‑time services

Boot selection

After boot, the boot-selector.service starts boot/boot_selector.py. The script shows a startup GIF while Linux finishes initialising and then displays a split screen with a day and night icon. The user can tap either side to select the mode. Touch calibration and detection happen inside the script: it enumerates input devices and normalises raw touch coordinates to screen coordinates before checking which half was touched. On selection it calls systemctl start display.service (for day mode) or systemctl start night.service (for night mode) and exits. If no selection is made within a timeout the default is day mode. The script writes directly to /dev/fb1 through a local FramebufferWriter and converts images to 16‑bit RGB565, duplicating conversion logic also found in many modules.

Day mode

The display service (defined in systemd/display.service) launches display_rotator.py in the project root. The service runs continuously (restart policy always) and is marked as mutually exclusive with the night service. A separate timer (systemd/day.timer) starts the display service at 07:00 each day and stops the night service.

display_rotator.py is a monolithic controller responsible for:

  1. Discovery of page scripts. It reads modules.txt (one module per line) or looks for any display.py scripts in modules/*/ if no manifest is present. Modules can be disabled via environment variables prefixed with ROTATOR_DISABLE_ and durations customised with ROTATOR_TIMEOUT_{MODULE}.
  2. Launching modules. Each page script (e.g., modules/pihole/display.py) is executed as a child process via subprocess.Popen. The rotator waits for the child to exit, capturing the exit code. If the script exits successfully it advances to the next module; if it fails, the rotator applies an exponential back‑off and quarantine to avoid hammering misbehaving modules. The back‑off behaviour is controlled by ROTATOR_BACKOFF_EXP and ROTATOR_QUARANTINE_SECS【601947419472537†L343-L419】.
  3. User interaction. The rotator listens to touch input events to perform navigation: a quick tap on the right half of the screen advances to the next page, a tap on the left half goes back, and a long press toggles the screen on/off. It implements automatic screen blanking using ioctl calls (FBIOBLANK) or sysfs fallbacks. A ScreenPower helper toggles the display and tracks state【601947419472537†L345-L419】.
  4. Environment configuration. The script reads numerous environment variables (e.g., ROTATOR_INPUT_PATH, ROTATOR_TOUCH_RADIUS, ROTATOR_TIMEOUT_DEFAULT) via _config.get_env. Defaults include a 30 second page duration, 5 second exponential back‑off on failures and 30 minutes quarantine.
  5. Exit codes. On a normal exit (e.g., due to a long press power‑off) the script returns 0; on fatal configuration errors it returns a non‑zero exit code. During rotation it monitors child exit codes and logs them.

Night mode

The night service (systemd/night.service) runs modules/blackout/blackout.py, which displays a mostly black screen with a bouncing Raspberry Pi logo. A long press on the screen launches the boot selector to switch to day mode. The night timer (systemd/night.timer) starts the night service at 22:00 each evening and the day timer stops it at 07:00.

Update and housekeeping timers

Zero2dash uses systemd timers to refresh data for certain modules automatically:

Timer Purpose Schedule Starts service
currency-update.timer Refresh the GBP/PLN exchange rate image by running modules/currency/currency-rate.py Daily at 06:00【839634756100288†L0-L9】 currency-update.service【825775215004173†L0-L11】
tram.timer Update the Firswood Metrolink timetable by downloading a fresh GTFS feed via tram_gtfs_refresh.py Daily at 04:15【525183046801563†L0-L9】 tram.service【502093519153372†L0-L11】
tram-alerts.timer Download Bee Network tram alerts every 15 minutes and on boot On boot + every 15 minutes【430560504995956†L3-L9】 tram-alerts.service【806497119456996†L1-L10】
day.timer Switch the screen to day mode 07:00 every day【945619904535199†L1-L7】 display.service
night.timer Switch the screen to night mode 22:00 every day【459795352642673†L1-L7】 night.service

The timers use AccuracySec directives to coarsen the wake‑up time, reducing power consumption.

Module selection and lifecycle

Modules run in the foreground when invoked by the rotator. The order of modules is defined by the text file modules.txt (one module name per line), giving the default sequence pihole → calendash → currency → photos → trams【204985933709994†L0-L4】. The rotator searches modules/MODULE/display.py to find the entry script for each module. If modules.txt is absent, it discovers any subdirectories under modules that contain display.py and uses alphabetical order. Environment variables allow modules to be disabled (ROTATOR_DISABLE_PIHOLE=1), specify timeouts (ROTATOR_TIMEOUT_PHOTOS=45) or randomise selection (ROTATOR_SELECTION_RANDOM=1). When a module script terminates, the rotator moves to the next module; if the script exits quickly (e.g., due to an error) a back‑off delay is applied before retrying to avoid CPU churn.

Touch‑driven navigation

While a module is displaying, the rotator listens to /dev/input/event* for touch events. A short tap on the right half of the screen triggers a next event, causing the current module process to be terminated and the next module started. A short tap on the left half triggers a previous event (rotator maintains a history). A long press toggles the screen power by calling ScreenPower.toggle(); this is implemented using framebuffer ioctls or sysfs files to blank/unblank the display. When the screen is blanked, touch events immediately wake it.

Module reference

Blackout module (modules/blackout)

The blackout module provides the night mode page. Its script blackout.py clears the framebuffer to black, draws a Raspberry Pi logo that bounces around the screen and can optionally show the time when tapped. It uses PIL to draw the icon and time overlay. Long presses launch the boot selector by invoking systemctl start boot-selector.service. The module replicates a FramebufferWriter class and an rgb888_to_rgb565 conversion routine. Configuration variables allow custom fonts, colours, minimum/maximum bounce speeds and time display toggling. Use case: provide a low‑light, burn‑in friendly display overnight.

Pi‑hole module (modules/pihole)

  • pihole_api.py defines a PiHoleClient dataclass that abstracts the Pi‑hole v6 API and legacy JSON endpoints. It normalises the host URL, handles authentication (password or API token) and returns a summary of total DNS queries, blocked queries and blocked percentage【346205868842820†L31-L63】. The client caches sessions and falls back to the legacy API if authentication fails.

  • piholestats_manual.py is the module’s main page script. It periodically queries the Pi‑hole API or token, measures CPU temperature and draws a frame showing total queries, blocked queries and percentage blocked on a background image. It writes the image to the framebuffer in a loop with a configurable refresh interval. The script uses environment variables such as PIHOLE_HOST, PIHOLE_API_TOKEN, PIHOLE_PASSWORD, PIHOLE_VERIFY_TLS, PIHOLE_REFRESH_SECS and PIHOLE_BACKGROUND. The script includes its own rgb565 conversion and font‑fitting logic and has a --self-test mode. Use case: provide real‑time Pi‑hole stats to the user.

  • display.py simply calls piholestats_manual.py for backwards compatibility.

Calendash module (modules/calendash)

This module shows upcoming events from a Google Calendar on the dashboard.

  • calendash-api.py authenticates with Google using OAuth credentials (client ID, secret and refresh token) and fetches future events. It supports OAuth via local web server, console or device code and stores tokens in a JSON file. The script validates environment variables such as CALENDAR_ID, CALENDAR_MAX_RESULTS, CALENDAR_TIME_ZONE, CALENDAR_CLIENT_ID, CALENDAR_CLIENT_SECRET, CALENDAR_TOKEN_PATH and output path. It draws a 320×240 image listing events with start times and titles and writes the image to disk. If there are no upcoming events it displays a friendly message. The script implements HTTP retry logic and uses dataclasses for configuration and snapshots. It also supports --check-config and --auth-only modes and includes --self-test logic. Use case: show an agenda of upcoming events.

  • display_impl.py reads the generated PNG (default calendash.png) and writes it to the framebuffer. It listens for a touch to exit after showing the image or times out after a few seconds【769731672444811†L0-L36】.

  • display.py is a thin wrapper around display_impl.py for compatibility.

Currency module (modules/currency)

  • currency-rate.py downloads the current GBP→PLN exchange rate from the Polish National Bank API. Configuration variables include CURRENCY_RATE_API_BASE, CURRENCY_OUTPUT_PATH, CURRENCY_BACKGROUND, CURRENCY_STATE_PATH and timeouts. The script loads previous state to decide whether to refresh (e.g., only once a day). It fetches recent rates, picks the most recent value within 24 hours and writes a PNG containing the rate, currency suffix and date. It logs operations, supports --self-test and --check-config and uses dataclasses for config and snapshots. Use case: show the daily exchange rate.

  • display.py loads the generated currency.png and writes it to the framebuffer; it acts as a compatibility wrapper.

Photos module (modules/photos)

This module displays a random photograph on the dashboard and allows syncing photos from remote sources.

  • photos-shuffle.py selects a random image from multiple sources: a local photo directory, a Google Photos album or a Google Drive folder. It uses OAuth for Google Photos and a service account for Drive. The script validates configuration variables such as PHOTOS_LOCAL_DIR, PHOTOS_GOOGLE_ALBUM_ID, PHOTOS_DRIVE_FOLDER_ID, PHOTOS_SERVICE_ACCOUNT_PATH, PHOTOS_CACHE_DIR, overlay options and brightness adjustments. It downloads remote photos to a local cache, chooses a random file, crops and resizes to fit the display, optionally overlays a Google Photos logo, adjusts brightness and writes the frame to the framebuffer. It replicates rgb565 conversion and includes a --self-test.

  • drive-sync.py synchronises a shared Google Drive folder to the local photos directory. It lists remote files, downloads new or updated files, deletes files that were removed from the remote folder and maintains a JSON state file. It can optionally run photo-resize.py after syncing【996881898368972†L22-L42】.

  • photo-resize.py compresses and scales existing photos in the local directory to half size using PIL’s LANCZOS filter to save disk space【503550625646233†L1-L14】.

  • display.py simply calls photos-shuffle.py for backwards compatibility【961726698132410†L0-L17】.

Use cases: display random personal or curated photos; keep the local photo library synchronised and optimised.

Trams module (modules/trams)

This module displays live tram departures for the Firswood stop on Greater Manchester’s Metrolink.

  • tram_gtfs_refresh.py downloads the GTFS feed from TfGM (configurable via TRAM_GTFS_URL) and constructs a compact JSON cache with service calendars and departures. It uses environment variables to configure the stop name or ID (TRAM_STOP_NAME, TRAM_STOP_ID), timezone (TRAM_TIMEZONE), direction label (TRAM_DIRECTION_LABEL), target headsigns (destinations) and user agent. The script parses multiple GTFS files (routes, trips, stop_times, calendar, calendar_dates), filters trips to the Metrolink agency, matches the configured stop to candidate stops and collects departures. It writes the cache to tram_timetable.json and only rewrites the cache when the content changes. It includes a robust self‑test that constructs a dummy feed and asserts behaviour【79940624118947†L493-L533】. The script is run daily by tram.timer.

  • tram_alerts_refresh.py fetches tram alerts from the Bee Network website. It accepts environment variables for the alerts URL (TRAM_ALERTS_URL), cache path, timeout and user agent. The script fetches HTML or JSON, uses a custom HTML parser to extract sections and scripts, normalises text, filters alerts relevant to Firswood and surrounds (keywords such as “Firswood”, “Cornbrook”, etc.)【590098709926278†L228-L233】 and writes tram_alerts.json. A 15‑minute timer keeps the cache fresh.【430560504995956†L3-L9】

  • display.py combines the timetable and alerts. It loads the background image, reads caches (tram_timetable.json and tram_alerts.json), computes upcoming departures via compute_upcoming_departures which filters by service calendar and calculates minutes until departure, fits fonts to text width using _fit_font, renders the departures and a scrolling ticker for alerts and writes the image to the framebuffer. The ticker scrolls at a configurable speed; if there are no alerts it displays “No current tram alerts”. The module supports --frame-log to log frame rate and --self-test. It runs continuously until killed by the rotator【601947419472537†L258-L297】.

Use case: show live tram departures and service alerts for local commuters.

Planned improvements and key issues

A code review highlighted several areas where zero2dash could be improved:

  • Monolithic rotator. display_rotator.py is ~1 000 lines long and mixes page discovery, process management, touch handling, screen power control and failure back‑off. Breaking it into smaller modules or adopting a plugin system would improve maintainability. Each responsibility (page discovery, scheduler, input handling, power management) should be isolated so modules can be swapped or extended easily【27331247576427†L120-L278】.

  • Duplicated framebuffer utilities. Many modules copy‑and‑paste FramebufferWriter and rgb888_to_rgb565 conversion functions (e.g., in the tram display, blackout, pihole and rotator scripts). These should be centralised into a shared utility module to reduce code duplication and ensure consistent behaviour【27331247576427†L240-L282】. Centralising would also make it easier to add optimised implementations (e.g., using NumPy) and to support other pixel formats.

  • Inconsistent error handling and exit codes. Modules vary in how they report configuration errors or runtime failures. Some simply print messages and return 0; others raise exceptions or exit with 1. The review recommends standardising exit codes (e.g., 2 for invalid configuration, 1 for runtime error) and using proper logging instead of print【27331247576427†L240-L282】.

  • Security considerations. Several scripts run as root and access /dev/fb1 directly. Running modules as an unprivileged user and using udev rules to grant access to /dev/fb1 and /dev/input would improve security. Sensitive tokens (e.g., Google Calendar refresh tokens) are stored in plain JSON under modules/calendash; consider using environment variables or a secrets manager to avoid exposing credentials【27331247576427†L240-L282】.

  • Testing gaps. Only a few modules include inline self tests and there is a single PyTest file for trams. A unified test suite with unit tests for all modules and integration tests that mock the framebuffer would catch regressions. Continuous integration (CI) could run these tests on push【27331247576427†L240-L282】.

  • Documentation. While the README.md and docs/pi-update-guide.md describe installation and use, there is limited in‑code documentation. The review suggests adding a top‑level design overview, diagrams showing how modules interact (boot flow, rotator loop), listing all environment variables with descriptions and default values, and referencing the systemd services and timers.

  • Logging. Most scripts use bare prints or silent failures. Introducing Python’s logging module with consistent log levels (info/warning/error), timestamps and optional JSON logs would help debugging and monitoring【79940624118947†L457-L485】.

  • Code quality improvements. Use dataclasses for configuration where appropriate, unify command‑line arguments across modules (e.g., --self-test, --check-config), remove unused functions and variables, and add type annotations. Consider using argparse sub‑commands for modules to share common options. The repository could adopt black and flake8 to standardise formatting and linting.

  • Extensibility. Provide a clear API for adding new modules. For example, define a base class that modules can subclass to implement render(), with built‑in support for timeouts, caching, logging and configuration. Document how to add a module (directory structure, display.py, environment variables) and update modules.txt.

Conclusion

Zero2dash is a thoughtfully designed dashboard for Pi‑hole users that packs a lot of functionality into a small screen. Its modular architecture allows different pages to be written in isolation, and systemd services and timers keep data fresh without user intervention. However, the project would benefit from refactoring the rotator into smaller components, centralising duplicated code, standardising error handling and improving security. Expanding the test suite and documentation will make the system more maintainable and welcoming to contributors.