A local-first browser controller for Twinkly Squares. It keeps the stock Twinkly firmware and coordinate map, so there is nothing to flash, open, or solder.
- Paint, erase, fill, and preview the entire matrix in real time
- Import still images, animated GIFs, and video with fit, sampling, saturation, contrast, gamma, and playback controls
- Turn a browser microphone into Spectrum, Bass Halo, or 3-Band Field visuals with sensitivity, smoothing, and live frequency meters
- Mirror a screen, window, or browser tab through the same media sampling and color-correction pipeline
- Run sixteen procedural 2D and particle effects with speed, intensity, and optional effect-specific controls
- Select curated palettes, build two-to-eight-stop gradients, and save custom palettes to the controller library
- Compose two effect layers with opacity and five blend modes
- Target the whole wall, a panel, a row, a column, or a custom rectangle; pin up to three additional 2D segments with mirroring, transpose, grouping, spacing, and offset transforms
- Save server-persisted scenes and timed playlists with eight transitions, including cut, crossfade, push, dissolve, wipe, shift, radial, and pixelate
- Capture up to 32 hand-built pixel frames, set each frame's hold time down to 25 ms, preview the loop, and bake it into controller movie storage
- See real scene thumbnails, exact browser-output previews, honest controller-local playback status, active-scene state, and playlist progress
- Organize scenes with folders, tags, favorites, search, filters, duplication, inline metadata editing, and merge-or-replace JSON backups
- Display scrolling text, a clock, and locally loaded fonts
- Schedule sleep, wake, brightness, stock-mode, and off actions
- Choose safe startup and stale-browser-frame behavior; unchanged startup and hold-last-frame remain the non-disruptive defaults
- Rotate the complete display to 0°, 90°, 180°, or 270°
- Control live brightness while custom frames are streaming
- Inspect advertised, measured, target, and delivered frame cadence plus relay gap, repeat, and missed-deadline telemetry
- Bake a finite effect, video clip, or static look into unused controller movie storage for browser-free playback without overwriting existing movies
- Integrate local tools through a versioned JSON API, OpenAPI document, CLI, Server-Sent Events, and a Home Assistant example
- Return to the original Twinkly animation at any time
The layout is read from the Twinkly controller at startup. This project has been physically tested with a 4×3, 768-pixel Twinkly Squares wall: 32×24 at 0°/180° and 24×32 at 90°/270°.
Requires Python 3.11 or newer and Node 20 or newer. The frontend is strict
TypeScript compiled once by npm run build; the Python server then serves the
emitted JavaScript from disk with no Node process at runtime.
cp config.example.json config.jsonEdit config.json and set the private IPv4 address of your Twinkly controller:
{
"deviceIp": "192.168.1.100"
}Then build the frontend and start the app:
npm ci && npm startnpm start rebuilds the frontend and launches the server. After a build, the
server can also run on its own:
python3 server.pyWhile editing frontend code, npm run watch recompiles TypeScript on save;
npm run watch:html does the same for the markup partials (or run
npm run build once after markup edits).
On macOS, you can also double-click scripts/start.command.
Open http://127.0.0.1:4312. Press Control-C to stop;
a graceful shutdown returns the panel to its saved Twinkly animation.
Scenes, playlists, palettes, automations, and runtime policy are stored in
.squares/ beside the controller. That directory and config.json are
ignored by Git.
The versioned local API is documented in docs/INTEGRATIONS.md. A few examples:
./scripts/squaresctl status
./scripts/squaresctl brightness 25
./scripts/squaresctl rotate 270
./scripts/squaresctl off
./scripts/squaresctl stockThe live OpenAPI document is served at http://127.0.0.1:4312/openapi.json.
Environment variables override the defaults:
| Variable | Purpose | Default |
|---|---|---|
TWINKLY_IP |
Twinkly controller IPv4 address | config.json |
HOST |
Web server bind address | 127.0.0.1 |
PORT |
Web server port | 4312 |
SQUARES_CONFIG |
Alternate configuration file | ./config.json |
SQUARES_LIBRARY |
Alternate scene/playlist file | ./.squares/library.json |
SQUARES_AUTOMATIONS |
Alternate automation file | ./.squares/automations.json |
SQUARES_RUNTIME_POLICY |
Alternate startup/frame-loss policy file | ./.squares/runtime.json |
ALLOW_UNAUTHENTICATED_LAN |
Explicitly allow a non-loopback bind | unset |
From the project directory:
./scripts/install-macos-service.shThis installs a user LaunchAgent, starts the controller at login, and keeps it running after a crash. Remove it with:
./scripts/uninstall-macos-service.shThe server has no authentication and therefore binds only to 127.0.0.1 by
default. A non-loopback bind is refused unless you make the risk explicit:
HOST=0.0.0.0 ALLOW_UNAUTHENTICATED_LAN=1 npm startUse that only on a trusted, firewalled home network. Never port-forward port 4312 or expose it to the internet. The app also rejects public IP addresses as panel targets. See SECURITY.md.
npm testnpm test builds the frontend (the strict tsc compile is the frontend
type gate) and runs the Python and browser suites against the compiled
output; CI additionally runs strict mypy over the backend and a coverage
gate. The suite covers HTTP
routes, device protocol behavior, coordinate mapping, brightness, rotation, state
synchronization, persistence, scheduling, API validation, palettes, zones,
blending, transitions, effects, audio analysis, live-input rendering, media
controls, scene organization, playlists, pixel clips, runtime failure policy,
relay telemetry, and safe movie payloads.
The connected controller advertises 40 FPS but reports a measured 38.46 FPS clock. The browser produces frames just under the panel's sustainable rate (1.5 FPS of headroom), and the relay forwards each fresh frame to the panel the moment it arrives — one clock end to end, with idle keepalive repeats only to hold realtime mode open. The UI reports fresh-frame cadence, delivery gaps, repeats, and missed deadlines; those host measurements do not claim that every frame lit physically. Controller-local movies use the controller's integer 38 FPS playback path and remove browser, HTTP, Python scheduling, and Wi-Fi cadence from ongoing playback. See docs/PERFORMANCE.md for the measurements.
The browser talks only to the local Python server. The server authenticates directly with the Twinkly controller over HTTP and streams RGB frames over the controller's local realtime protocol on UDP port 7777. The runtime uses the Python standard library and native browser APIs; TypeScript is a build-time dev dependency only, and every source file (backend, frontend, and markup partials) stays under 500 lines.
WLED is an excellent community-built LED firmware project. Its established product concepts—including presets and playlists, segments, palettes, transitions, effects, scheduling, and a JSON API—helped shape the roadmap for Squares Controller.
Squares Controller is an independent implementation for stock Twinkly hardware. It does not include or modify WLED firmware, source code, web UI assets, or branding, and it is not affiliated with the WLED or Twinkly projects. WLED is licensed under EUPL-1.2; Squares Controller remains MIT licensed. See NOTICE.md for the durable attribution statement.
MIT