-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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.
-
boot/– Containsboot_selector.pyand 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 arepihole,calendash,currency,photos,trams, andblackout(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 readsmodules.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.
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.
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:
-
Discovery of page scripts. It reads
modules.txt(one module per line) or looks for anydisplay.pyscripts inmodules/*/if no manifest is present. Modules can be disabled via environment variables prefixed withROTATOR_DISABLE_and durations customised withROTATOR_TIMEOUT_{MODULE}. -
Launching modules. Each page script (e.g.,
modules/pihole/display.py) is executed as a child process viasubprocess.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 byROTATOR_BACKOFF_EXPandROTATOR_QUARANTINE_SECS【601947419472537†L343-L419】. -
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. AScreenPowerhelper toggles the display and tracks state【601947419472537†L345-L419】. -
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. - 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.
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.
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.
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.
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.
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.
-
pihole_api.pydefines aPiHoleClientdataclass 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.pyis 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 asPIHOLE_HOST,PIHOLE_API_TOKEN,PIHOLE_PASSWORD,PIHOLE_VERIFY_TLS,PIHOLE_REFRESH_SECSandPIHOLE_BACKGROUND. The script includes its own rgb565 conversion and font‑fitting logic and has a--self-testmode. Use case: provide real‑time Pi‑hole stats to the user. -
display.pysimply callspiholestats_manual.pyfor backwards compatibility.
This module shows upcoming events from a Google Calendar on the dashboard.
-
calendash-api.pyauthenticates 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 asCALENDAR_ID,CALENDAR_MAX_RESULTS,CALENDAR_TIME_ZONE,CALENDAR_CLIENT_ID,CALENDAR_CLIENT_SECRET,CALENDAR_TOKEN_PATHand 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-configand--auth-onlymodes and includes--self-testlogic. Use case: show an agenda of upcoming events. -
display_impl.pyreads the generated PNG (defaultcalendash.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.pyis a thin wrapper arounddisplay_impl.pyfor compatibility.
-
currency-rate.pydownloads the current GBP→PLN exchange rate from the Polish National Bank API. Configuration variables includeCURRENCY_RATE_API_BASE,CURRENCY_OUTPUT_PATH,CURRENCY_BACKGROUND,CURRENCY_STATE_PATHand 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-testand--check-configand uses dataclasses for config and snapshots. Use case: show the daily exchange rate. -
display.pyloads the generatedcurrency.pngand writes it to the framebuffer; it acts as a compatibility wrapper.
This module displays a random photograph on the dashboard and allows syncing photos from remote sources.
-
photos-shuffle.pyselects 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 asPHOTOS_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.pysynchronises 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 runphoto-resize.pyafter syncing【996881898368972†L22-L42】. -
photo-resize.pycompresses and scales existing photos in the local directory to half size using PIL’s LANCZOS filter to save disk space【503550625646233†L1-L14】. -
display.pysimply callsphotos-shuffle.pyfor backwards compatibility【961726698132410†L0-L17】.
Use cases: display random personal or curated photos; keep the local photo library synchronised and optimised.
This module displays live tram departures for the Firswood stop on Greater Manchester’s Metrolink.
-
tram_gtfs_refresh.pydownloads the GTFS feed from TfGM (configurable viaTRAM_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 totram_timetable.jsonand 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 bytram.timer. -
tram_alerts_refresh.pyfetches 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 writestram_alerts.json. A 15‑minute timer keeps the cache fresh.【430560504995956†L3-L9】 -
display.pycombines the timetable and alerts. It loads the background image, reads caches (tram_timetable.jsonandtram_alerts.json), computes upcoming departures viacompute_upcoming_departureswhich 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-logto 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.
A code review highlighted several areas where zero2dash could be improved:
-
Monolithic rotator.
display_rotator.pyis ~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
FramebufferWriterandrgb888_to_rgb565conversion 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/fb1directly. Running modules as an unprivileged user and usingudevrules to grant access to/dev/fb1and/dev/inputwould improve security. Sensitive tokens (e.g., Google Calendar refresh tokens) are stored in plain JSON undermodules/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.mdanddocs/pi-update-guide.mddescribe 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
loggingmodule 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 usingargparsesub‑commands for modules to share common options. The repository could adoptblackandflake8to 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 updatemodules.txt.
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.