Skip to content

clebert/ticking-away

Repository files navigation

Ticking Away

...the moments that make up a dull day.

Ticking Away

A watchface inspired by Pink Floyd's "Dark Side of the Moon" album cover, featuring a prism that refracts light into a rainbow.

Targets

Concept

The minute hand acts as a light source firing a white ray toward the watch center. The ray enters a prism and disperses into a rainbow that targets the hour hand position. This creates a clock where time is displayed through the direction of light rays rather than traditional hands.

PNG Export

Build and run the PNG export binary to render the watchface to a PNG file:

zig build png -Doptimize=ReleaseFast
zig-out/bin/png <size> <hour> <minute> <output.png> [--grain | --dither-pebble | --dither-trmnl] [--supersample]
  • size: image size in pixels (square, diameter of the unit circle)
  • hour: hour (0-23)
  • minute: minute (0-59)
  • output.png: output file path
  • --grain: add film grain to the full-colour output
  • --dither-pebble: quantize the output to the Pebble 64-colour cube (Floyd–Steinberg)
  • --dither-trmnl: quantize the output to the TRMNL e-ink four greyscale levels (Floyd–Steinberg)
  • --supersample: render 2×2 and box-average down to antialias edges (off by default)

The texture flags are mutually exclusive; without any, no texture is applied.

zig build png -Doptimize=ReleaseFast && \
zig-out/bin/png 1024 7 14 logo.png --grain --supersample
zig build png -Doptimize=ReleaseFast && \
zig-out/bin/png 260 7 14 pebble.png --dither-pebble --supersample
zig build png -Doptimize=ReleaseFast && \
zig-out/bin/png 1964 7 14 wallpaper-14-inch.png --grain --supersample
zig build png -Doptimize=ReleaseFast && \
zig-out/bin/png 2234 7 14 wallpaper-16-inch.png --grain --supersample

Pebble Watchface

The watchface also runs on the Pebble Round 2 (gabbro, 260×260). The Zig render core in lib/ is cross-compiled to a freestanding Thumb static library and linked into a thin C app shell under bin/pebble.

Install the Pebble SDK once — it provides the pebble CLI, the arm-none-eabi toolchain, and the QEMU emulator:

uv tool install pebble-tool --python 3.13
pebble sdk install latest

Then cross-compile the render core, link the .pbw, and run it in the emulator:

zig build pebble-lib              # cross-compile bin/pebble/libwatchface.a
cd bin/pebble
pebble build                      # link the .pbw
pebble install --emulator gabbro  # boot QEMU and install; re-run if the first
                                  # call times out while the firmware boots
pebble screenshot watchface.png   # capture the rendered frame

TRMNL Watchface

The watchface also runs on the TRMNL OG — a 7.5″ 800×480 UC8179 e-ink panel driven by an ESP32-C3. The Zig render core in lib/ is cross-compiled to a bare-metal RV32IMC firmware under bin/trmnl that bit-bangs the panel directly: no ESP-IDF, no second-stage bootloader. The prism and rainbow are dithered to the panel's four greyscale levels with the same Floyd–Steinberg core the Pebble target uses.

The image is RAM-resident — a custom linker script lays it entirely into SRAM, and the ESP32-C3 ROM first-stage loader copies it in and jumps to the entry point (Espressif "Simple Boot"), so the flash holds nothing but the raw image at offset 0x0.

Flashing uses espflash:

brew install espflash

Enter download mode

The TRMNL has two controls: a power slide switch and a circular boot button below it. The ESP32-C3 has no auto-reset wired to USB, so download mode is entered by hand:

  1. Plug a data USB-C cable into the TRMNL — it enumerates as /dev/cu.usbmodem*, the ESP32-C3's native USB-Serial-JTAG (no driver, no external adapter).
  2. Slide the power switch off.
  3. Hold the boot button while sliding the switch on, then release.

Confirm it is listening with espflash list-ports (look for an "Espressif USB JTAG/serial debug unit"). Re-run this sequence before each flash: once the firmware's render loop takes over the chip leaves download mode.

Build and flash

The image is bare-metal (not an ESP-IDF app) and is reached without a reset, so every device-facing espflash call needs --before no-reset (talk to the manually-entered ROM loader, don't toggle DTR/RTS) and --ignore-app-descriptor (skip the ESP-IDF app-header check):

zig build trmnl                                                   # build zig-out/bin/trmnl

# Run once from RAM — volatile, reverts to the stock firmware on reset (best for iterating):
espflash flash --chip esp32c3 --ram --no-stub --before no-reset --after no-reset \
  --ignore-app-descriptor zig-out/bin/trmnl

# Or install persistently to flash offset 0x0 (no bootloader, no partition table), then power-cycle:
espflash save-image --chip esp32c3 --ignore-app-descriptor zig-out/bin/trmnl zig-out/bin/trmnl.bin
espflash write-bin --chip esp32c3 --before no-reset 0x0 zig-out/bin/trmnl.bin

A full e-ink refresh takes a few seconds; the watchface then holds on screen with no power. Writing to flash overwrites the stock firmware — restore it any time via trmnl.com/flash.

About

Ticking away the moments that make up a dull day.

Resources

Stars

Watchers

Forks

Contributors