Go HTTP server that manages ESP32-powered LED display devices. Serves widget instructions to polling devices based on a playlist model with server-owned timing. Sister project to led-kurokku-esp (device firmware).
go build -o kurokku-esp-server ./cmd/server
./kurokku-esp-serverRequires Redis for all state (config persistence and ephemeral playlist state). Configure Redis with appendonly yes for durability.
| Var | Default | Description |
|---|---|---|
KUROKKU_LISTEN_ADDR |
:8080 |
HTTP listen address |
KUROKKU_REDIS_ADDR |
localhost:6379 |
Redis address |
KUROKKU_TM1637_ALERT_SCROLL_SPEED_MS |
150 |
Scroll speed (ms/col) for alert messages on tm1637 devices |
KUROKKU_TM1637_ALERT_REPEATS |
3 |
Number of times alert messages repeat on tm1637 devices |
Everything lives in Redis — device config, playlists, and ephemeral playlist state. No SQLite, no CGo dependency. Redis keys:
kurokku:device:{id}— device JSONkurokku:devices— set of device IDs (index)kurokku:playlist:{id}— playlist JSON (includes entries)kurokku:playlists— set of playlist IDs (index)device:{id}:playlist_state— ephemeral cursor (index, started_at, version)kurokku:alert:<id>— alert JSON (AlertConfig with message, priority, display_duration)kurokku:ota_pending:{id}— admin-queued OTA command (JSON{url, queued_at}), TTL 10 min, consumed via GETDEL on next device pollkurokku:firmware:{display_type}— default firmware URL for a display type (admin-editable)
Each device is assigned a playlist — an ordered list of widget entries, each with a duration. Every widget (including clock) has a finite duration. The server cycles through entries, advancing when the duration elapses.
The server controls when to advance the playlist. On poll:
- If playlist version changed → reset to entry 0, send instruction
- If current entry's duration elapsed → advance to next entry (wrapping), send instruction
- Otherwise → respond with brightness/poll_ms only (no instruction)
Alerts are stored as individual Redis keys at kurokku:alert:<id>, each containing a JSON AlertConfig (id, message, priority, display_duration, delete_after_display). The server detects changes via Redis keyspace notifications and resets all devices to their alert widget position. Multiple alerts are sorted by priority and concatenated. Low-priority alerts can be filtered by a cron schedule. If no alerts are active (after filtering), the alert entry is skipped and the resolver advances to the next playlist entry. The nalssi weather service can push temperature and alerts automatically.
OTA is an out-of-band one-shot command, not a playlist entry. Admin queues via the UI or JSON API; the device poll handler calls PopOTA (GETDEL) before the resolver. If pending, the server returns {type:"ota", url} and skips playlist resolution. The device downloads, flashes, and reboots; on next poll the pending key is gone and normal playlist resumes. If the device is offline, the TTL drops the command after ~10 minutes.
Devices report their running firmware version on each poll via ?firmware_version=..., captured into DeviceStatus.FirmwareVersion and shown in the admin UI. Note: the firmware sends + literally in the semver build-metadata separator, which Go's url.Query() decodes as space, so the handler normalizes spaces back to +.
GET /api/v1/devices/{device_id}/instruction?display_type=max7219&firmware_version=0.1.0+abc123
GET/PUT/DELETE /api/v1/admin/devices/{device_id}
GET /api/v1/admin/devices
GET/PUT/DELETE /api/v1/admin/playlists/{playlist_id}
GET /api/v1/admin/playlists
POST /api/v1/admin/devices/{device_id}/ota body {"url": "..."} — queue OTA (10 min TTL)
DELETE /api/v1/admin/devices/{device_id}/ota — cancel pending OTA
GET /api/v1/admin/firmware/{display_type} — get saved default URL
PUT /api/v1/admin/firmware/{display_type} body {"url": "..."} — set/clear default URL
cmd/server/— entrypoint, wiringinternal/api/— HTTP handlers for device polling and admin CRUDinternal/config/— environment-based configurationinternal/model/— data types (Device, Playlist, PlaylistEntry, Widget, ServerResponse)internal/playlist/— Resolver: evaluates playlist state per device on each pollinternal/store/— Redis-backed persistence for devices and playlistsinternal/alert/— Redis pub/sub listener for weather alerts
github.com/redis/go-redis/v9— Redis client- Standard library
net/http,log/slog