Skip to content

Latest commit

 

History

43 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

timebox

⚠️ This repository is an experiment in AI-assisted development. Nearly all code and documentation here was written by an AI agent in a single day, driven by live trial against the hardware. It works, and it was security-reviewed at the end (TS-26-002 records the five security issues that process created and how they were caught) — but use it with the appropriate skepticism: residual risks may remain. Read the code before running it, especially the parts that touch pairing, the system D-Bus, and your audio stack.

Notifications on a Divoom TimeBox Evo from Linux: icons or scrolling text on the 16x16 panel, with a sound from the box speaker. Control goes over BLE, sound over classic A2DP, both held simultaneously.

Why the architecture looks the way it does: TBX-26-001 — all memos in the register.

The TimeBox Evo running the live audio visualizer during development

Setup (one-time)

# 1. BlueZ experimental interfaces (for per-device bearer pinning)
sudo sed -i 's/^#Experimental = false/Experimental = true/' /etc/bluetooth/main.conf
sudo systemctl restart bluetooth
systemctl --user restart wireplumber   # wireplumber loses BlueZ on restart

# 2. Find your box's MAC (shows up as "TimeBox-Evo-light" /
#    "TimeBox-Evo-audio" while scanning) and export it:
bluetoothctl scan on
export TIMEBOX_ADDRESS=<box-mac>

# 3. Pair the box (classic bond) and trust it — trusted means its
#    audio link latches silently at every power-on
bluetoothctl pair $TIMEBOX_ADDRESS
bluetoothctl trust $TIMEBOX_ADDRESS

# If your box asks for a PIN, export it so the daemon/CLI can answer
# pairing prompts automatically (default: 0000):
export TIMEBOX_PIN=<your-pin>

# 4. Python reference environment
python -m venv python/.venv
python/.venv/bin/pip install bleak dbus_fast divoom_protocol

Daemon (recommended)

Connects once, holds both links, serves notifications from a FIFO with sub-second latency:

python/.venv/bin/python python/timebox_daemon.py &

Install as a systemd user service

# Config (address required; PIN only if your box asks for one;
# LATLON only if you want weather on the box's clock — see below)
mkdir -p ~/.config/timebox
printf 'TIMEBOX_ADDRESS=<box-mac>\nTIMEBOX_PIN=<pin>\nTIMEBOX_LATLON=<lat>,<lon>\n' > ~/.config/timebox/env
chmod 600 ~/.config/timebox/env

# Python reference + venv into ~/.local/share/timebox/python
mkdir -p ~/.local/share/timebox/python
cp python/timebox_daemon.py python/timebox_bridge.py python/timebox_notify.py ~/.local/share/timebox/python/
python -m venv ~/.local/share/timebox/python/.venv
~/.local/share/timebox/python/.venv/bin/pip install bleak dbus_fast divoom_protocol

# Unit into ~/.config/systemd/user, then enable and start
cp python/systemd/timebox-daemon.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now timebox-daemon

# Logs / status
journalctl --user -u timebox-daemon -f
systemctl --user status timebox-daemon

The Python units run the copy in ~/.local/share/timebox/python — adjust WorkingDirectory/ExecStart in the .service files if you install elsewhere. The daemon restarts automatically (15 s backoff) if the box is unreachable.

Then notifications are one shell line — JSON per line into the FIFO:

FIFO=$XDG_RUNTIME_DIR/timebox.fifo

# Envelope icon with unread count, default chime
echo '{"count": 3}' > $FIFO

# Scrolling text (A-Z, digits, punctuation; umlauts transliterated)
echo '{"text": "Build failed!"}' > $FIFO

# Green, faster scroll, no sound
echo '{"text": "Deploy OK", "icon_color": [0,255,80], "fps": 15, "silent": true}' > $FIFO

# Custom sound and colors
echo '{"count": 7, "icon_color": [40,200,255], "sound": "/usr/share/sounds/ocean/stereo/bell.oga"}' > $FIFO

# Live spectrum of whatever the system is playing
echo '{"visualizer": true}' > $FIFO            # endless, 16 rising bars
echo '{"visualizer": true, "mode": "wave"}' > $FIFO  # scrolling spectrogram
echo '{"visualizer": true, "mode": "tunnel"}' > $FIFO  # psychedelic tunnel
echo '{"visualizer": true, "palette": "fire"}' > $FIFO  # recolor: fire, ocean, bass, synth, mono…
echo '{"visualizer": false}' > $FIFO           # stop

Many more recipes — stereo splits, live knob tuning, waterfalls, comets, clock pages, shell and cron integration: EXAMPLES.md.

The visualizer records the default sink's monitor (what the speakers play — never the microphone), which KDE reports with the mic-in-use tray icon, labeled "TimeBox visualizer" — but only while a real microphone device is present; with none attached KDE shows nothing at all. In endless mode 10 s of silence hands the panel back to the clock; the capture — and with it the icon — pauses once the sink actually goes idle (a silent-but-open stream, like a paused AirPlay sender, keeps both alive). Everything resumes when audio plays again.

All keys (each optional):

Key Meaning Default
text scroll this text instead of the icon
fps scroll speed in frames/s (1 px per frame) 10
count badge number 0–99 on the icon 1
icon badge icon: "envelope", or "github" — an octocat head, for GitHub notifiers like gitify "envelope"
icon_color icon / text color [r,g,b] [255,60,40]
number_color badge color [r,g,b] [255,255,255]
background background color [r,g,b] [0,0,0]
brightness panel brightness 0–100 unchanged
sound audio file to play through the box message chime
silent true = no sound false
visualizer true starts the live spectrum, false stops it
seconds visualizer duration; omit for endless endless
mode visualizer look: "bars" (16 bands), "tunnel" — spectrum rings flowing inward, fading with age, one band per border pixel (60) — or "wave" — scrolling 16-band spectrogram, the newest spectrum entering at one edge and drifting across; switchable while running "bars"
spin tunnel rotation in border px per frame, −5…5; negative reverses, 0 stops; switchable while running 0.5
fade brightness decay with age — tunnel: per ring toward the center, wave: per column/row of history; 0…1; switchable while running 0.92
bands tunnel analyzer width, 2…60; switchable while running 60
stereo analyze L/R separately: bars mirror bottom (L) / top (R) at half height, tunnel splits into semicircles with the same frequency diametrically opposite, wave enters at the middle and ages outward (L left/down, R right/up); switchable while running false
dir wave scroll axis: "h" — newest column at the right, history drifting left, low bands at the bottom — or "v" — newest row on top, falling waterfall, low bands at the left; switchable while running "h"
palette visualizer colors, every mode. "auto" is each mode's native look (VU bars, rainbow tunnel/wave). Frequency palettes color by band: "rainbow" (HSV wheel), "bass" (bass→treble, red→violet), "synth" (magenta→cyan). Loudness palettes color by level, so peaks hit the hot end: "heat" (green→yellow→red VU), "fire" (blackbody dark→red→white), "ocean" (blue→cyan→white). "mono" paints one solid color. Switchable while running "auto"
color base color [r,g,b] for the mono palette [255,255,255]
clock pin the sub-views the box's clock cycles through: list from "time", "weather", "temp", "date", e.g. {"clock": ["time","weather"]}; each is a full-screen page (weather: an animated scene) shown ~15 s in turn; replayed whenever the daemon restores the clock TIMEBOX_CLOCK env, or time,weather
clock_color clock color [r,g,b] (only with clock) white
clock_face clock style: "digital", "analog" (round), "analog-square", "rainbow", "boxed", "digital-inverted" TIMEBOX_CLOCK_FACE env, or digital
clock_flash seconds the clock interrupts a running visualizer per flash; 0 = never 30
clock_every seconds between clock flashes (also the weather re-push cadence); applies from the next cycle 600

Weather on the clock

The box's built-in clock/weather display expects the Divoom phone app to feed it — without one it shows stale data forever. With TIMEBOX_LATLON=<lat>,<lon> set, the daemon takes over: it fetches the current temperature and conditions every 30 minutes from Bright Sky (a free, keyless JSON API for DWD open data) and re-pushes them to the box every clock_every seconds. Leave the variable unset to skip the weather fetch.

The idle panel shows the box's clock channel. Its enabled sub-views are full-screen pages the box cycles through (~15 s each): plain time, an animated weather scene, temperature, date — the firmware cannot merge them onto one page. Pick pages with the clock key, e.g. echo '{"clock": ["time","weather"]}' > $FIFO, or set the boot default with TIMEBOX_CLOCK=time,weather in the env file (a plain TIMEBOX_CLOCK=time means a steady, never-cycling clock). While the visualizer runs, the clock still gets air time: clock_flash seconds every clock_every seconds (default 30 s every 10 min, 0 disables), after which the spectrum takes the panel back.

The daemon also keeps the box from wandering: it sets the box's startup channel to the clock (persistent, so a power-cycled box boots into the clock instead of its demo carousel) and re-asserts the clock on an idle panel every clock_every seconds — a channel switched via the box's hardware button finds its way back within one cycle.

KDE notifications on the box

python/timebox_bridge.py mirrors KDE's notification bell: allow-listed apps bump an unread count badge on the panel (envelope + number). It listens on the session bus, so KDE's own notifications keep working untouched — and only the count is ever sent to the box, never the message text.

# Which apps reach the box (nothing is forwarded if unset).
# The name is the app's D-Bus app_name — see it with:
#   dbus-monitor --session "interface='org.freedesktop.Notifications'"
echo 'TIMEBOX_ONLY_APPS=Thunderbird,Nextcloud' >> ~/.config/timebox/env

cp python/systemd/timebox-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now timebox-bridge
journalctl --user -u timebox-bridge -f

The badge counts notifications that are still unread: dismissing one (or clicking it) decrements it, while one that merely times out on screen stays counted — same as the bell icon. While the visualizer runs, an arriving badge shows on top of the bars for a few seconds instead of persisting.

The badge icon names the source: an octocat head while gitify is the only app with unread notifications, the envelope otherwise (any unread mail wins — deterministic, no flip-flopping while both are pending).

Gitify batches new GitHub notifications into a single popup ("You have N notifications"); the badge shows that N — parsed from the popup, keyed off its locale-invariant "Gitify" title — instead of counting the popup as one. Unread batches add up until dismissed, like any other notification.

How it works and why it eavesdrops rather than replaces KDE's notification daemon: TBX-26-003.

One-shot CLI

Same options as flags. If the daemon is running, the CLI hands the notification to it and returns instantly; otherwise it connects itself (~10–20 s, audio may retry for up to a few minutes):

python/.venv/bin/python python/timebox_notify.py --count 5
python/.venv/bin/python python/timebox_notify.py --text "Kaffee ist fertig!" --icon-color 255,180,0
python/.venv/bin/python python/timebox_notify.py --count 12 --silent --brightness 60

Security notes

  • The panel is a public display. The box's BLE side is unencrypted and unauthenticated by design (its firmware never pairs the LE link): everything you display — including notification text — is readable by a BLE sniffer in radio range, and anyone in range can connect and draw on the panel. Don't send confidential content. Audio (A2DP) is link-encrypted via the classic bond.
  • The FIFO is your trust boundary. It is created 0600 inside $XDG_RUNTIME_DIR (the daemon refuses to start without it); whoever can write it can display content, start the visualizer, and play any file readable by your user (sound key).
  • The daemon registers a Bluetooth agent that auto-answers pairing and authorization only for the configured box address; requests from any other device are rejected.
  • The daemon's journal log records notification keys, not content.
  • The KDE bridge eavesdrops the session bus, so it sees every notification in-process — but it reads only the app name (plus, for gitify's batch popups, the integer inside), forwards only a count, and logs neither. No notification text ever reaches the box (or the air).

Troubleshooting

  • First notification after box power-cycle is slow (~15 s) — expected; the daemon re-establishes BLE, everything after is instant.
  • No sound, br-connection-busy — the box only reliably brings audio up itself, at power-on. Power-cycle the box; trusted, it latches within seconds. A stuck connect attempt clears with bluetoothctl block <mac> + unblock.
  • Sink missing although connectedsystemctl --user restart wireplumber (happens after bluetoothd restarts).
  • A PIN dialog appears — the daemon answers it itself while running, using $TIMEBOX_PIN (default 0000). Set the variable in the daemon's environment if your box uses a different PIN.

About

An experiment in AI development - a live notifier and sound visualizer using an Divoom Timebox Evo

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages