Skip to main content

ScrollKit

Most LED-matrix libraries get you a scrolling "Hello, World" and stop. I built ScrollKit for what comes next: over-the-air updates to boards in the field, fault-tolerant data refresh, real transitions and effects, and a built-in web server users control from a browser. The hard part isn't any single feature. It's running all of them at once on a microcontroller without the display stuttering. It also runs on a desktop simulator I wrote that exports its own GIFs and videos, like the one below.

Built by Michael Czeiszperger

📖 Full documentation: scrollkit.dev

ScrollKit hero: a swarm assembles the ScrollKit logo, sheen sweeps over it, then it colorizes to electric-blue/magenta/gold, all rendered on a 64×32 LED panel

Installation

For a stable desktop install, use the version published on PyPI. A Git tag such as v0.8.4 is a released PyPI build; master is the development branch and can contain unreleased work. Pin an exact version in a project that needs reproducible builds.

# Stable desktop install with simulator support
pip install "scrollkit[simulator]"
# Or pin a tested release: pip install "scrollkit[simulator]==X.Y.Z"

# To contribute, test unreleased work, or run the bundled demos: clone master
# and install it editable.
git clone https://github.com/czei/scrollkit.git
cd scrollkit && pip install -e ".[simulator]"

# CircuitPython — deploy a tested copy (or matching .mpy build) of scrollkit/
# inside your app payload; a device should not track Git master directly.
# Physical setup and deployment: https://scrollkit.dev/guide/device-setup/

The release-* branches and the optional live OTA branch are deployment channels for application payloads, not alternate pip installation channels.

Quick Start

import asyncio
from scrollkit.app.base import ScrollKitApp
from scrollkit.display.content import ScrollingText
from scrollkit.display.simulator import SimulatorDisplay   # desktop-only import

class HelloWorldApp(ScrollKitApp):
    async def create_display(self):
        # Open the desktop simulator window. On CircuitPython hardware, delete
        # this override (and its import) — the default display drives the panel.
        return SimulatorDisplay(width=64, height=32)

    async def setup(self):
        self.content_queue.add(
            ScrollingText("Hello, LED Matrix!", y=12, color=0x00AAFF))

asyncio.run(HelloWorldApp().run())

The top-level scrollkit package deliberately performs no imports (every import costs RAM on CircuitPython), so you always import from submodules, e.g. from scrollkit.app.base import ScrollKitApp. See the getting-started guide for the full ScrollKitApp / UnifiedDisplay API.

The showcase reel

Sixty seconds of what the library actually ships — an endless self-scheduling teaser where every act names the effect it's playing: the boid swarm assembles SWARM, a velvet sheen sweeps VELVET, IRIS irises in and out. All 13 transitions, all 13 palette treatments, splashes, scrollers, bitmap-text effects, particles, and two animated characters, mixed by a weighted-age scheduler so it never repeats itself.

ScrollKit showcase reel: a flying owl air-drops the letters of SCROLLKIT, then named effects play act by act on a 64×32 LED panel

Run it yourself with python demos/hard/showcase_reel.py, or browse the Demo Gallery for the rest.

Architecture

ScrollKit runs unchanged on the MatrixPortal S3 (CircuitPython) and a desktop pygame simulator. Your app subclasses ScrollKitApp and talks to one display abstraction; the library picks a backend at import time and brokers every external system the sign touches:

flowchart TB
    app["Your app<br/>(subclasses ScrollKitApp)"] --> core["ScrollKitApp · UnifiedDisplay<br/>ContentQueue · effects · config"]
    core -->|CircuitPython| hw["MatrixPortal S3<br/>displayio → RGBMatrix panel"]
    core -->|desktop| sim["pygame simulator"]
    core <-->|HttpClient — synchronous| api(["HTTP data API"])
    core <-->|SettingsWebServer| browser(["Browser config UI"])
    core -->|raw.githubusercontent.com| gh(["GitHub OTA"])

Subsystem dependencies (dashed = lazy import; dev and simulator are desktop-only, raising ImportError on the device):

flowchart LR
    app["app"] --> display["display"]
    app --> config["config"]
    app -.->|lazy| utils["utils"]
    app -.->|lazy| effects["effects"]
    app -.->|lazy| web["web"]
    effects --> display
    display -.->|desktop| simulator["simulator"]
    config -.->|lazy| utils
    network["network"] --> config
    network --> utils
    ota["ota"] --> exceptions["exceptions"]
    dev["dev"] --> display
    dev --> effects
    dev --> simulator
    classDef desktop stroke-dasharray:6 4;
    class dev,simulator desktop;

See the Architecture guide for the full write-up, including the invariants this graph enforces.

Package Structure

scrollkit/
├── app/               # ScrollKitApp base class, async run loop, memory helpers
├── display/           # UnifiedDisplay (auto-detects hardware vs simulator), content
│   ├── unified.py                # Production display (device + desktop)
│   ├── content.py                # DisplayContent / StaticText / ScrollingText / ContentQueue / Priority
│   ├── bitmap_text.py            # Animated bitmap-font text + palette effects
│   ├── gradient_text.py          # Gradient/multi-color text fill (GradientTextLayer)
│   └── colors.py                 # Continuous 24-bit color generators
├── effects/           # Transition contract (transitions.py) + standalone splash/particle helpers
├── network/           # Networking utilities
│   ├── http_client.py            # Dual-implementation HTTP client (raises NetworkError)
│   ├── wifi_manager.py           # WiFi connection lifecycle
│   └── mdns.py                   # <hostname>.local advertising (CircuitPython; no-op on desktop)
├── config/            # Configuration management
│   └── settings_manager.py       # JSON-based persistent settings
├── ota/               # Over-the-air updates
│   ├── client.py                 # GitHub-release-based OTA client
│   ├── manifest.py               # Update manifest model
│   ├── display_progress.py       # Display-progress adapter over OTAClient
│   └── publish.py                # Host-side release publishing (desktop/CI only)
└── utils/             # Utilities
    ├── error_handler.py          # Logging and error handling
    ├── diagnostics.py            # NVM boot/crash record + reboot-loop safe-mode breaker
    ├── color_utils.py            # Named colors + settings-UI hex-string color table (no conversion helpers; int-based conversions live in display/colors.py)
    ├── system_utils.py           # NTP / HTTP-Date system clock sync
    └── url_utils.py              # URL decoding and credential loading

Core API

Display

from scrollkit.display.unified import UnifiedDisplay
from scrollkit.display.content import ContentQueue, ScrollingText

# Create display (auto-detects CircuitPython vs desktop)
display = UnifiedDisplay(width=64, height=32)
display.initialize()

# ScrollKitApp drives this queue's render loop for you (see Quick Start above);
# add() is all a subclass's setup() typically needs to call.
queue = ContentQueue()
queue.add(ScrollingText("Scrolling text", y=12, color=0x00AAFF))

HTTP Client

from scrollkit.network.http_client import HttpClient
from scrollkit.exceptions import NetworkError

client = HttpClient()
try:
    response = await client.get("https://api.example.com/data")
    data = response.json()
except NetworkError as e:
    print("fetch failed:", e)

Settings

from scrollkit.config.settings_manager import SettingsManager

settings = SettingsManager("app_settings.json",
    defaults={"hostname": "mydevice", "brightness": "0.5"},
    bool_keys=["dark_mode"])
settings.set("hostname", "new-name")
settings.save_settings()

Utilities

from scrollkit.utils.error_handler import ErrorHandler
from scrollkit.display.colors import scale
from scrollkit.network.wifi_manager import is_dev_mode

logger = ErrorHandler("app.log")
logger.info("Application started")

color = scale(0xff0000, 0.5)  # Dim red to 50%

if is_dev_mode():
    print("running on desktop, not CircuitPython")

Platform Support

Platform Backend Status
Adafruit MatrixPortal S3 CircuitPython + displayio ✅ Calibrated from device
Desktop (macOS/Linux/Windows) SLDK Simulator ✅
Custom CircuitPython boards displayio / rgbmatrix 🔌 Extensible (see Adding New Hardware)

How this was built

I wrote the first two shipping versions by hand in 2024, when all of this was still one application. Splitting it into a library and a separate app layer, then documenting the result, is the kind of project that dies quietly in a spare-time backlog. So I used Claude Code and spec-driven development to handle the refactoring and the first drafts, then went back through all of it in my own voice, with my own screenshots. Yes, AI has touched a lot of this code. It was also directed by an engineer who has shipped production software for a living, including time on one of Sun Microsystems' API teams. Both are true.

Acknowledgements

ScrollKit's over-the-air update feature was inspired by Ronald Dehuysser's micropython-ota-updater. The current implementation is ScrollKit's own manifest-based design and uses none of his code — but the idea came from his project, and it deserves the shout-out.

License

MIT

Metadata

Release files for scrollkit 0.11.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for scrollkit 0.11.1
File Size Uploaded
scrollkit-0.11.1.tar.gz 483.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for scrollkit 0.11.1
File Interpreter ABI Platform
scrollkit-0.11.1-py3-none-any.whl Python 3 none any Details

Total release size: 1.0 MB

Release files / scrollkit-0.11.1.tar.gz

Download URL scrollkit-0.11.1.tar.gz
Size 483.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e545560f09757d07109057bfd7cc1d10d66dcad71484ef697adad5959e4613ad
BLAKE2b-256 checksum
How to use checksums
15c053337535c737db137502f555a2a4f83ee7d1f0f12109ccb8418a5819e621
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release files / scrollkit-0.11.1-py3-none-any.whl

Download URL scrollkit-0.11.1-py3-none-any.whl
Size 531.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4fa70ceda9dee0d83a26c57de0af0597f4049ad2d1e5c050f7ba4298fa9d8b4c
BLAKE2b-256 checksum
How to use checksums
54f721083dc094799dc41d4ec3cc1f58dffda1efae2af50c37a3382541de7792
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.11.1 This release

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.5

2 release files

0.8.4

2 release files

0.8.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page