This document details the technologies, libraries, protocols, and tools that make up the zeClock project.
- Python 3.9+: Main language chosen for its flexibility, rich image processing ecosystem, and ease of integration.
- Leverages modern async improvements (
asyncio) and advanced typing structures.
- Leverages modern async improvements (
- Pillow (PIL Fork) >= 9.0: Handles all graphical operations for the clock.
- Canvas creation: Monochrome grayscale images (8-bit
Lmode) and colorized true-color images (24-bitRGBmode). - Composition and layers: Cropping (
crop), pasting (paste), and merging animation frames with clock text. - Pixel manipulation: Direct access via
img.load()for parsing 4-bit binary formats.
- Canvas creation: Monochrome grayscale images (8-bit
- Pure Python + Pillow: All pixel operations are implemented without NumPy for maximum portability (runs on Raspberry Pi and low-power ARM boards without AVX support).
- Grayscale colorization: Per-pixel intensity Γ RGB color tuple via
bytearrayoperations. - RGB565 conversion (DMDServerBackend only): Per-pixel bit packing using
struct.pack_intofor big-endian TCP output. - RGB888 passthrough (ZeDMDBackend): Raw PIL Image bytes sent directly to libzedmd β no Python-level pixel conversion needed.
- Mask processing: Bit manipulation via Python
bytearrayand bitwise operators. - Image compositing: Byte-level iteration over
Image.tobytes()data for DotBlt blending. - Pixel-art upscaling (
overlay.py): Public 2Γ upscaling API consolidated in the overlay module.upscale_2x(img, mode)is the single entry point; it dispatches toepx_upscale_2x()(EPX/Scale2x algorithm, default),hq2x()(High Quality 2Γ β smoother curves via interpolation, may introduce intermediate gray values, best for pre-computed content), ornearest_upscale_2x()(pixel doubling). Used byBitmapFont.render_text()when rendering SD fonts on HD displays (256Γ64). EPX smooths diagonal edges and corners without introducing new colors or blurring, preserving pixel-art style. A hole-prevention rule ensures filled pixels are never replaced by empty ones (avoids gaps at inner corners). All upscalers also upscale any attachedmask_databitfield via_upscale_mask_2x()so DotBlt compositing remains correct after scaling. - Performance: At 128Γ32 (4096 pixels), pure Python loops are fast enough for 25 FPS rendering.
- Grayscale colorization: Per-pixel intensity Γ RGB color tuple via
- asyncio: Python's native framework for async code (
async/await).- Stable display frequency: The main loop dynamically calculates each frame's execution time to adapt
asyncio.sleepand guarantee a regular framerate. - Background pre-computation: Loading and rendering
.scnanimations runs viaasyncio.create_task. The main display loop stays smooth and responsive. - Cooperative yielding:
await asyncio.sleep(0)in intensive loading loops to yield back to the event loop.
- Stable display frequency: The main loop dynamically calculates each frame's execution time to adapt
- threading (standard module): Used in
ZeDMDBackendfor thread-safe access to the stream error flag. The libzedmd log callback executes on libzedmd's internal Run thread, so athreading.Lockprotects the shared error flag accessed by both the callback thread and the Python asyncio thread.
-
Direct Hardware (libzedmd via ctypes): Default communication path using the
ZeDMDBackend.- Loads
libzedmd.so/.dylib/.dllfrom~/.zeclock/lib/viactypes.CDLL. - C API calls:
ZeDMD_GetInstance,ZeDMD_OpenWiFi/ZeDMD_Open,ZeDMD_SetFrameSize,ZeDMD_EnableUpscaling,ZeDMD_SetBrightness,ZeDMD_RenderRgb888,ZeDMD_Close,ZeDMD_SetLogCallback,ZeDMD_FormatLogMessage,ZeDMD_GetPanelWidth,ZeDMD_GetPanelHeight. - Log callback: A ctypes
CFUNCTYPEcallback is registered viaZeDMD_SetLogCallbackto intercept libzedmd internal log messages.ZeDMD_FormatLogMessageresolvesva_listarguments into readable strings. Stream error patterns (e.g., "StreamBytes failed", "libserialport error", "TCP stream error", "UDP stream error") immediately set a thread-safe error flag to signal connection loss. - RGB888 passthrough: Raw RGB bytes from PIL Image are sent directly to libzedmd without any Python-level pixel conversion (3 bytes per pixel: Red, Green, Blue).
- Connection modes: WiFi (IP address) or USB serial (device path or auto-detection).
- Panel resolution detection: On connect, queries
ZeDMD_GetPanelWidth/ZeDMD_GetPanelHeightto auto-detect the physical panel dimensions and adapts the frame size accordingly. - Upscaling: Calls
ZeDMD_EnableUpscalingso libzedmd handles centering/scaling when the configured frame size differs from the physical panel size. - Connection error handling: Simple strategy β on any stream error detected via the log callback,
send_frame()immediately closes the instance and returnsFalse. The caller (main loop) handles the wait and reconnection with exponential backoff (initial 2s, max 30s, Γ1.5 multiplier). No periodic health checks or failure thresholds.
- Loads
-
Sunrise/Sunset API (
api.sunrise-sunset.org): Free REST API used byBrightnessSchedulerto fetch daily sunrise/sunset times based on geographic coordinates. No API key required. Results are cached for 30 minutes. Used only when[location]is configured with latitude/longitude. -
MQTT (via
remote/mqtt_remote.py): Primary remote control protocol. Bidirectional pub/sub communication for controlling the clock (on/off, force plugin, display text, brightness) and publishing state. Supports Home Assistant MQTT Discovery for automatic entity creation. Broker address configured inzeclock.ini. -
REST API (via
remote/rest_remote.py): HTTP-based remote control as a complement for simple integrations and Recalbox Web Manager. Provides the same command set as MQTT via HTTP endpoints, plus:- Web UI served as static files from
zeclock/remote/web/at/ui/(root/redirects to it). Provides a browser-based control panel. - Plugin list (
GET /api/plugins): Returns all registered plugins with state, frequency, source, active/forced status, and optionalweb_controlsmetadata exposed by plugins viaget_web_controls(). - Brightness (
/api/brightness):GETreturns current brightness state (override, SW dimming, time-only flag, auto/manual mode).POST {"brightness": 0-15}sets manual brightness override.POST /api/brightness/autoclears the override and resumes automatic scheduling. - Speaker Timer sub-API (
/api/speaker-timer/): Dedicated REST endpoints for the conference timer plugin βGET status,POST start(also forces the speaker-timer plugin active),POST pause,POST reset(also resumes normal plugin rotation),POST set(accepts{"seconds": N}or{"minutes": N}). - Alert (
POST /api/alert): Displays a rich alert with blinking border, auto-wrapped text, and optional icon. Body:{"text": "...", "duration": 15, "icon": "person", "color": [255,0,0]}. Duration clamped to 1β300s. Available icons:beacon,person,vehicle,animal,motion,warning,camera,info.
- Web UI served as static files from
-
TCP Sockets (standard
socketmodule): Alternative communication viaDMDServerBackendfor development.- DMDStream network header (big-endian):
- Magic word:
DMDStream\x00(10 bytes) - Version: 1 (uint8)
- Mode: 3 = RGB565 (uint32 big-endian)
- Dimensions: width, height (uint16 big-endian)
- Flags: buffered, disconnectOthers (uint8 each)
- Data size: payload length (uint32 big-endian)
- Magic word:
- Payload: RGB565 big-endian data (2 bytes per pixel, width Γ height Γ 2 bytes total).
- Persistent connection: Socket stays open between frames for continuous streaming.
- DMDStream network header (big-endian):
- argparse (standard module): CLI argument parsing.
--color: Clock color (orange, blue, red, purple, green, yellow, cyan, pink, auto).--animation-color: Animation color (independent from clock).--backend: Backend selection (auto, zedmd, dmdserver). Default: auto.--wifi-addr: ZeDMD WiFi IP address (overrides config file).--device: ZeDMD USB serial device path (overrides config file).--brightness: Display brightness 0-15 (overrides config file).--hd: Use ZeDMD HD resolution (256Γ64) instead of standard (128Γ32).--width/--height: Override display dimensions in pixels.--upscale: Upscaling algorithm for HD mode (epx,hq2x, ornearest).epx(default) uses EPX/Scale2x for smooth pixel-art scaling without new colors;hq2xuses High Quality 2Γ for smoother curves via interpolation (may introduce intermediate values, best for pre-computed content);nearestdoes fast pixel doubling.--bootstrap: Non-interactive automatic resource installation.--no-prompt: Disables interactive prompts.
INI-format config file parsed by backend_config.py. CLI arguments always take precedence over file values.
| Section | Key | Values | Default | Description |
|---|---|---|---|---|
[zedmd] |
wifi_addr |
IP address string | β | ZeDMD WiFi address |
[zedmd] |
device |
device path string | β | ZeDMD USB serial device |
[zedmd] |
brightness |
0β15 | 10 | Display brightness |
[dmdserver] |
host |
hostname/IP | localhost |
dmdserver host |
[dmdserver] |
port |
integer | 6789 | dmdserver TCP port |
[display] |
width |
integer | 128 | Frame width in pixels |
[display] |
height |
integer | 32 | Frame height in pixels |
[display] |
upscale |
epx | nearest |
epx |
Upscale algorithm used by libzedmd when the frame size differs from the physical panel size |
[brightness_schedule] |
default |
schedule line | β | Default brightness schedule (all days) |
[brightness_schedule] |
monday..sunday |
schedule line | β | Day-specific brightness schedule |
[brightness_schedule] |
max_brightness |
1β15 | 7 | Maximum HW brightness for 100% |
[brightness_schedule] |
time_only |
HH:MM-HH:MM |
β | Time-only mode range (no plugins) |
| [location] | latitude | float | β | Geographic latitude for sunrise/sunset |
| [location] | longitude | float | β | Geographic longitude for sunrise/sunset |
| [location] | sunrise_brightness | 0β100 | β | Brightness % during daytime |
| [location] | sunset_brightness | 0β100 | β | Brightness % during nighttime |
| [mqtt] | enabled | true | false | false | Enable MQTT remote control |
| [mqtt] | host | hostname/IP | localhost | MQTT broker address |
| [mqtt] | port | integer | 1883 | MQTT broker port |
| [mqtt] | username | string | β | MQTT authentication username |
| [mqtt] | password | string | β | MQTT authentication password |
| [mqtt] | device_id | string | zeclock | Unique device identifier (used in topics) |
| [mqtt] | topic_prefix | string | zeclock | MQTT topic prefix (e.g. zeclock/command/...) |
| [mqtt] | ha_discovery | true | false | true | Enable Home Assistant MQTT Discovery |
| [mqtt] | ha_discovery_prefix | string | homeassistant | HA Discovery topic prefix |
| [mqtt] | state_interval | float (β₯5) | 30.0 | Seconds between state publish messages |
| [rest_api] | enabled | true | false | false | Enable REST API remote control |
| [rest_api] | host | hostname/IP | 0.0.0.0 | REST API listen address |
| [rest_api] | port | integer | 8080 | REST API listen port |
Schedule line format: HH:MM-HH:MM brightness%, HH:MM-HH:MM brightness%, ... (supports overnight ranges and multiple entries per day).
- libzedmd (C shared library from
PPUC/libzedmd): Provides direct communication with ZeDMD hardware via anextern "C"API. Called through Python ctypes. Supports WiFi and USB connections. - ZeDMD: Open-source firmware for ESP32 / Teensy boards designed to drive RGB LED panels (128x32 or 256x64 pixels). Receives data from libzedmd via USB Serial or WiFi.
- dmdserver (optional, built from
libdmdutil): C++ daemon for development use. Listens for TCP connections (port 6789 by default), decodes RGB565 frames, and forwards them to hardware or a simulator. Used with--backend dmdserver.
Declared in pyproject.toml:
| Package | Version | Role |
|---|---|---|
pillow |
>= 9.0 | Image processing, canvas, composition |
pyyaml |
>= 6.0 | YAML configuration parsing (plugin system) |
aiohttp |
>= 3.8 | Async HTTP client (weather/stock API calls) |
colorama |
>= 0.4.6 | ANSI terminal colors (bootstrap messages) |
| Group | Packages | Role |
|---|---|---|
zedmd |
pyserial |
Direct USB communication with ZeDMD (without dmdserver) |
dev |
pytest>=7.0, pytest-asyncio>=0.21, hypothesis>=6.0, black>=22.0, flake8>=4.0, mypy>=0.950, pyyaml>=6.0 |
Code quality & testing |
The project exclusively uses the modern PEP 621 standard with the setuptools backend:
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project.scripts]
zeclock = "zeclock.clock:main"
[tool.setuptools.package-data]
zeclock = ["resources/fonts/*.fnt"]The package-data directive ensures bitmap font files (.fnt) in zeclock/resources/fonts/ are included in built wheels and sdists. This allows plugins and the rendering engine to locate fonts via importlib.resources or __file__-relative paths without relying solely on the runtime bootstrap to ~/.zeclock/resources/.
| Tool | Command | Description |
|---|---|---|
| pipx | pipx install zeclock |
Permanent isolated installation |
| uvx | uvx --from ... zeclock |
Instant execution without installation |
| pip | pip install -e ".[dev]" |
Editable development mode |
Rather than post-install hooks (incompatible with Wheels and sandboxed environments), zeClock uses a first-launch bootstrap:
- On startup,
installer.pychecks for~/.zeclock/lib/libzedmd.soand~/.zeclock/resources/animations/. - Fonts are bundled in the package (
zeclock/resources/fonts/) β no download needed. - If animations or libzedmd are missing, an interactive wizard offers automatic download.
- Non-interactive alternative:
zeclock --bootstrap.
βββββββββββββββββββββββββββββββββββββββββββββββ
β Header β
β - version (uint16 LE) β
β - font_name_len (uint8) β
β - font_name (ASCII, font_name_len bytes) β
β - cnt_font_info (uint16 LE) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Character Info (Γ cnt_font_info) β
β - ascii_char (uint8) β
β - width (uint16 LE) β
β - kerning (uint16 LE) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Dotmap (global bitmap) β
β - dots_width (uint16 LE) β
β - dots_height (uint16 LE) β
β - dots_bpp (uint16 LE) β
β - has_mask (uint16 LE) β
β - dots_data (4-bit/pixel, 2 pixels/byte) β
β - mask_data (1-bit/pixel, if has_mask) β
βββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Scene Header β
β - version (uint16 LE) β
β - cnt_item_dotmap (uint16 LE) β
β - cnt_item_storyboard (uint16 LE) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Storyboard (Γ cnt_item_storyboard) β
β - first_frame_delay (uint16 LE, ms) β
β - first_frame_layer (uint16 LE) β
β - first_blank (uint16 LE) β
β - frame_delay_ms (uint16 LE) β
β - frame_layer (uint16 LE) β
β - last_frame_delay (uint16 LE, ms) β
β - last_frame_layer (uint16 LE) β
β - last_blank (uint16 LE) β
β - clock_style (uint8: 0=std, 1=custom) β
β - custom_x (uint8) β
β - custom_y (uint8) β
β - reserved (17 bytes) β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Dotmap Frames (Γ cnt_item_dotmap) β
β - dots_width (uint16 LE) β
β - dots_height (uint16 LE) β
β - dots_bpp (uint16 LE) β
β - has_mask (uint16 LE) β
β - dots_data (4-bit/pixel) β
β - mask_data (1-bit/pixel, if has_mask) β
βββββββββββββββββββββββββββββββββββββββββββββββ
| Metric | Value |
|---|---|
| Framerate | 25 FPS (40ms/frame) or per-animation |
| Resolution | 128Γ32 or 256Γ64 pixels |
| ZeDMD format | RGB888 (3 bytes/pixel, sent directly) |
| DMDServer format | RGB565 big-endian (2 bytes/pixel, over TCP) |
| End-to-end latency | < 50ms |
| RAM (pre-computation) | ~50-200 MB depending on frame count |
| Clock colon blink | 500ms on / 500ms off |