scrollkit 0.8.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scrollkit-0.8.3/LICENSE +31 -0
- scrollkit-0.8.3/PKG-INFO +248 -0
- scrollkit-0.8.3/README.md +204 -0
- scrollkit-0.8.3/pyproject.toml +56 -0
- scrollkit-0.8.3/setup.cfg +4 -0
- scrollkit-0.8.3/src/scrollkit/__init__.py +16 -0
- scrollkit-0.8.3/src/scrollkit/app/__init__.py +6 -0
- scrollkit-0.8.3/src/scrollkit/app/base.py +918 -0
- scrollkit-0.8.3/src/scrollkit/app/memory.py +70 -0
- scrollkit-0.8.3/src/scrollkit/config/__init__.py +1 -0
- scrollkit-0.8.3/src/scrollkit/config/settings_manager.py +215 -0
- scrollkit-0.8.3/src/scrollkit/config/transition_names.py +31 -0
- scrollkit-0.8.3/src/scrollkit/dev/__init__.py +41 -0
- scrollkit-0.8.3/src/scrollkit/dev/capabilities.py +374 -0
- scrollkit-0.8.3/src/scrollkit/dev/harness.py +383 -0
- scrollkit-0.8.3/src/scrollkit/dev/metrics.py +91 -0
- scrollkit-0.8.3/src/scrollkit/dev/performance.py +174 -0
- scrollkit-0.8.3/src/scrollkit/dev/validation.py +245 -0
- scrollkit-0.8.3/src/scrollkit/display/__init__.py +14 -0
- scrollkit-0.8.3/src/scrollkit/display/_graphics.py +299 -0
- scrollkit-0.8.3/src/scrollkit/display/_recording.py +165 -0
- scrollkit-0.8.3/src/scrollkit/display/_sim_backend.py +99 -0
- scrollkit-0.8.3/src/scrollkit/display/bitmap_text.py +408 -0
- scrollkit-0.8.3/src/scrollkit/display/boards.py +186 -0
- scrollkit-0.8.3/src/scrollkit/display/colors.py +176 -0
- scrollkit-0.8.3/src/scrollkit/display/content.py +604 -0
- scrollkit-0.8.3/src/scrollkit/display/gradient_text.py +167 -0
- scrollkit-0.8.3/src/scrollkit/display/interface.py +126 -0
- scrollkit-0.8.3/src/scrollkit/display/simulator.py +80 -0
- scrollkit-0.8.3/src/scrollkit/display/text_fill.py +59 -0
- scrollkit-0.8.3/src/scrollkit/display/text_pixels.py +250 -0
- scrollkit-0.8.3/src/scrollkit/display/unified.py +595 -0
- scrollkit-0.8.3/src/scrollkit/effects/__init__.py +28 -0
- scrollkit-0.8.3/src/scrollkit/effects/drip_splash.py +253 -0
- scrollkit-0.8.3/src/scrollkit/effects/easing.py +131 -0
- scrollkit-0.8.3/src/scrollkit/effects/overlay.py +92 -0
- scrollkit-0.8.3/src/scrollkit/effects/particles.py +355 -0
- scrollkit-0.8.3/src/scrollkit/effects/reveal_splash.py +132 -0
- scrollkit-0.8.3/src/scrollkit/effects/scrolling.py +363 -0
- scrollkit-0.8.3/src/scrollkit/effects/swarm_reveal.py +512 -0
- scrollkit-0.8.3/src/scrollkit/effects/text_render.py +25 -0
- scrollkit-0.8.3/src/scrollkit/effects/transitions.py +873 -0
- scrollkit-0.8.3/src/scrollkit/exceptions.py +55 -0
- scrollkit-0.8.3/src/scrollkit/network/__init__.py +1 -0
- scrollkit-0.8.3/src/scrollkit/network/http_client.py +505 -0
- scrollkit-0.8.3/src/scrollkit/network/mdns.py +44 -0
- scrollkit-0.8.3/src/scrollkit/network/wifi_manager.py +382 -0
- scrollkit-0.8.3/src/scrollkit/ota/__init__.py +13 -0
- scrollkit-0.8.3/src/scrollkit/ota/client.py +528 -0
- scrollkit-0.8.3/src/scrollkit/ota/display_progress.py +125 -0
- scrollkit-0.8.3/src/scrollkit/ota/manifest.py +206 -0
- scrollkit-0.8.3/src/scrollkit/ota/publish.py +379 -0
- scrollkit-0.8.3/src/scrollkit/simulator/ATTRIBUTION.md +20 -0
- scrollkit-0.8.3/src/scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
- scrollkit-0.8.3/src/scrollkit/simulator/LICENSE +176 -0
- scrollkit-0.8.3/src/scrollkit/simulator/README.md +87 -0
- scrollkit-0.8.3/src/scrollkit/simulator/__init__.py +11 -0
- scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
- scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
- scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
- scrollkit-0.8.3/src/scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
- scrollkit-0.8.3/src/scrollkit/simulator/adafruit_display_text/label.py +336 -0
- scrollkit-0.8.3/src/scrollkit/simulator/bitmaptools.py +50 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/__init__.py +8 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/color_utils.py +62 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/device_benchmarks.json +254 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/feasibility.py +160 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/hardware_profile.py +191 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/led_matrix.py +307 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/performance_manager.py +253 -0
- scrollkit-0.8.3/src/scrollkit/simulator/core/pixel_buffer.py +188 -0
- scrollkit-0.8.3/src/scrollkit/simulator/devices/__init__.py +7 -0
- scrollkit-0.8.3/src/scrollkit/simulator/devices/base_device.py +79 -0
- scrollkit-0.8.3/src/scrollkit/simulator/devices/matrixportal_s3.py +87 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/__init__.py +12 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/bitmap.py +158 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/display.py +195 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/fourwire.py +54 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/group.py +125 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/palette.py +115 -0
- scrollkit-0.8.3/src/scrollkit/simulator/displayio/tilegrid.py +155 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/3x5.bdf +2474 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
- scrollkit-0.8.3/src/scrollkit/simulator/fonts/viii.bdf +2659 -0
- scrollkit-0.8.3/src/scrollkit/simulator/terminalio/__init__.py +20 -0
- scrollkit-0.8.3/src/scrollkit/utils/__init__.py +1 -0
- scrollkit-0.8.3/src/scrollkit/utils/color_utils.py +54 -0
- scrollkit-0.8.3/src/scrollkit/utils/diagnostics.py +227 -0
- scrollkit-0.8.3/src/scrollkit/utils/error_handler.py +347 -0
- scrollkit-0.8.3/src/scrollkit/utils/system_utils.py +245 -0
- scrollkit-0.8.3/src/scrollkit/utils/url_utils.py +46 -0
- scrollkit-0.8.3/src/scrollkit/web/__init__.py +6 -0
- scrollkit-0.8.3/src/scrollkit/web/settings_server.py +328 -0
- scrollkit-0.8.3/src/scrollkit/web/wifi_setup.py +331 -0
- scrollkit-0.8.3/src/scrollkit.egg-info/PKG-INFO +248 -0
- scrollkit-0.8.3/src/scrollkit.egg-info/SOURCES.txt +114 -0
- scrollkit-0.8.3/src/scrollkit.egg-info/dependency_links.txt +1 -0
- scrollkit-0.8.3/src/scrollkit.egg-info/requires.txt +22 -0
- scrollkit-0.8.3/src/scrollkit.egg-info/top_level.txt +1 -0
scrollkit-0.8.3/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This repository vendors third-party components that retain their own licenses
|
|
26
|
+
and copyright notices; this MIT license does not supersede them:
|
|
27
|
+
|
|
28
|
+
- src/scrollkit/simulator/adafruit_display_text/ and adafruit_bitmap_font/ —
|
|
29
|
+
Adafruit Industries (MIT). See the SPDX headers in those files.
|
|
30
|
+
- Bundled fonts under src/scrollkit/simulator/ — various foundries under the
|
|
31
|
+
SIL Open Font License (OFL). See src/scrollkit/simulator/ATTRIBUTION.md.
|
scrollkit-0.8.3/PKG-INFO
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scrollkit
|
|
3
|
+
Version: 0.8.3
|
|
4
|
+
Summary: LED Matrix Display Framework for CircuitPython and Desktop
|
|
5
|
+
Author-email: Michael Winslow Czeiszperger <michael@czei.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/czei/scrollkit
|
|
8
|
+
Project-URL: Repository, https://github.com/czei/scrollkit
|
|
9
|
+
Keywords: circuitpython,led-matrix,display,matrixportal,simulator
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: simulator
|
|
26
|
+
Requires-Dist: pygame>=2.0; extra == "simulator"
|
|
27
|
+
Requires-Dist: Pillow>=9.0; extra == "simulator"
|
|
28
|
+
Requires-Dist: numpy>=1.21; extra == "simulator"
|
|
29
|
+
Provides-Extra: web
|
|
30
|
+
Requires-Dist: aiohttp>=3.8; extra == "web"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: scrollkit[simulator]; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-timeout>=2.1; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
38
|
+
Requires-Dist: requests>=2.28; extra == "dev"
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
41
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
42
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# ScrollKit
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
*Built by [Michael Czeiszperger](http://czei.org)*
|
|
50
|
+
|
|
51
|
+
📖 **Full documentation: [scrollkit.dev](https://scrollkit.dev)**
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<!-- Absolute URL so the image also renders on the PyPI project page -->
|
|
55
|
+
<img src="https://raw.githubusercontent.com/czei/scrollkit/master/docs/assets/video/scrollkit-hero.gif" alt="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" width="640">
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Desktop development with simulator
|
|
62
|
+
pip install "scrollkit[simulator]"
|
|
63
|
+
|
|
64
|
+
# To modify ScrollKit itself (or run the demos): clone and install editable
|
|
65
|
+
git clone https://github.com/czei/scrollkit.git
|
|
66
|
+
cd scrollkit && pip install -e ".[simulator]"
|
|
67
|
+
|
|
68
|
+
# CircuitPython — copy scrollkit/ to your device's lib/ alongside your source
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
from scrollkit.app.base import ScrollKitApp
|
|
76
|
+
from scrollkit.display.content import ScrollingText
|
|
77
|
+
|
|
78
|
+
class HelloWorldApp(ScrollKitApp):
|
|
79
|
+
async def setup(self):
|
|
80
|
+
self.content_queue.add(
|
|
81
|
+
ScrollingText("Hello, LED Matrix!", y=12, color=0x00AAFF))
|
|
82
|
+
|
|
83
|
+
asyncio.run(HelloWorldApp().run()) # auto-detects MatrixPortal hardware vs desktop simulator
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> The top-level `scrollkit` package deliberately performs **no** imports (every
|
|
87
|
+
> import costs RAM on CircuitPython), so you always import from submodules, e.g.
|
|
88
|
+
> `from scrollkit.app.base import ScrollKitApp`. See the
|
|
89
|
+
> [getting-started guide](https://scrollkit.dev/getting-started/)
|
|
90
|
+
> for the full `ScrollKitApp` / `UnifiedDisplay` API.
|
|
91
|
+
|
|
92
|
+
## Architecture
|
|
93
|
+
|
|
94
|
+
ScrollKit runs unchanged on the MatrixPortal S3 (CircuitPython) and a desktop
|
|
95
|
+
pygame simulator. Your app subclasses `ScrollKitApp` and talks to one display
|
|
96
|
+
abstraction; the library picks a backend at import time and brokers every external
|
|
97
|
+
system the sign touches:
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
flowchart TB
|
|
101
|
+
app["Your app<br/>(subclasses ScrollKitApp)"] --> core["ScrollKitApp · UnifiedDisplay<br/>ContentQueue · effects · config"]
|
|
102
|
+
core -->|CircuitPython| hw["MatrixPortal S3<br/>displayio → RGBMatrix panel"]
|
|
103
|
+
core -->|desktop| sim["pygame simulator"]
|
|
104
|
+
core <-->|HttpClient — synchronous| api(["HTTP data API"])
|
|
105
|
+
core <-->|SettingsWebServer| browser(["Browser config UI"])
|
|
106
|
+
core -->|raw.githubusercontent.com| gh(["GitHub OTA"])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Subsystem dependencies (dashed = lazy import; `dev` and `simulator` are
|
|
110
|
+
desktop-only, raising `ImportError` on the device):
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
flowchart LR
|
|
114
|
+
app["app"] --> display["display"]
|
|
115
|
+
app --> config["config"]
|
|
116
|
+
app -.->|lazy| utils["utils"]
|
|
117
|
+
app -.->|lazy| effects["effects"]
|
|
118
|
+
app -.->|lazy| web["web"]
|
|
119
|
+
effects --> display
|
|
120
|
+
display -.->|desktop| simulator["simulator"]
|
|
121
|
+
config -.->|lazy| utils
|
|
122
|
+
network["network"] --> config
|
|
123
|
+
network --> utils
|
|
124
|
+
ota["ota"] --> exceptions["exceptions"]
|
|
125
|
+
dev["dev"] --> display
|
|
126
|
+
dev --> effects
|
|
127
|
+
dev --> simulator
|
|
128
|
+
classDef desktop stroke-dasharray:6 4;
|
|
129
|
+
class dev,simulator desktop;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
See the [Architecture guide](docs/guide/architecture.md) for the full write-up,
|
|
133
|
+
including the invariants this graph enforces.
|
|
134
|
+
|
|
135
|
+
## Package Structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
scrollkit/
|
|
139
|
+
├── app/ # ScrollKitApp base class, async run loop, memory helpers
|
|
140
|
+
├── display/ # UnifiedDisplay (auto-detects hardware vs simulator), content
|
|
141
|
+
│ ├── unified.py # Production display (device + desktop)
|
|
142
|
+
│ ├── content.py # DisplayContent / StaticText / ScrollingText / ContentQueue / Priority
|
|
143
|
+
│ ├── bitmap_text.py # Animated bitmap-font text + palette effects
|
|
144
|
+
│ ├── gradient_text.py # Gradient/multi-color text fill (GradientTextLayer)
|
|
145
|
+
│ └── colors.py # Continuous 24-bit color generators
|
|
146
|
+
├── effects/ # Transition contract (transitions.py) + standalone splash/particle helpers
|
|
147
|
+
├── network/ # Networking utilities
|
|
148
|
+
│ ├── http_client.py # Dual-implementation HTTP client (raises NetworkError)
|
|
149
|
+
│ ├── wifi_manager.py # WiFi connection lifecycle
|
|
150
|
+
│ └── mdns.py # <hostname>.local advertising (CircuitPython; no-op on desktop)
|
|
151
|
+
├── config/ # Configuration management
|
|
152
|
+
│ └── settings_manager.py # JSON-based persistent settings
|
|
153
|
+
├── ota/ # Over-the-air updates
|
|
154
|
+
│ ├── client.py # GitHub-release-based OTA client
|
|
155
|
+
│ ├── manifest.py # Update manifest model
|
|
156
|
+
│ ├── display_progress.py # Display-progress adapter over OTAClient
|
|
157
|
+
│ └── publish.py # Host-side release publishing (desktop/CI only)
|
|
158
|
+
└── utils/ # Utilities
|
|
159
|
+
├── error_handler.py # Logging and error handling
|
|
160
|
+
├── diagnostics.py # NVM boot/crash record + reboot-loop safe-mode breaker
|
|
161
|
+
├── color_utils.py # Named colors + settings-UI hex-string color table (no conversion helpers; int-based conversions live in display/colors.py)
|
|
162
|
+
├── system_utils.py # NTP / HTTP-Date system clock sync
|
|
163
|
+
└── url_utils.py # URL decoding and credential loading
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Core API
|
|
167
|
+
|
|
168
|
+
### Display
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from scrollkit.display.unified import UnifiedDisplay
|
|
172
|
+
from scrollkit.display.content import ContentQueue, ScrollingText
|
|
173
|
+
|
|
174
|
+
# Create display (auto-detects CircuitPython vs desktop)
|
|
175
|
+
display = UnifiedDisplay(width=64, height=32)
|
|
176
|
+
display.initialize()
|
|
177
|
+
|
|
178
|
+
# ScrollKitApp drives this queue's render loop for you (see Quick Start above);
|
|
179
|
+
# add() is all a subclass's setup() typically needs to call.
|
|
180
|
+
queue = ContentQueue()
|
|
181
|
+
queue.add(ScrollingText("Scrolling text", y=12, color=0x00AAFF))
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### HTTP Client
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from scrollkit.network.http_client import HttpClient
|
|
188
|
+
from scrollkit.exceptions import NetworkError
|
|
189
|
+
|
|
190
|
+
client = HttpClient()
|
|
191
|
+
try:
|
|
192
|
+
response = await client.get("https://api.example.com/data")
|
|
193
|
+
data = response.json()
|
|
194
|
+
except NetworkError as e:
|
|
195
|
+
print("fetch failed:", e)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Settings
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from scrollkit.config.settings_manager import SettingsManager
|
|
202
|
+
|
|
203
|
+
settings = SettingsManager("app_settings.json",
|
|
204
|
+
defaults={"hostname": "mydevice", "brightness": "0.5"},
|
|
205
|
+
bool_keys=["dark_mode"])
|
|
206
|
+
settings.set("hostname", "new-name")
|
|
207
|
+
settings.save_settings()
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Utilities
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from scrollkit.utils.error_handler import ErrorHandler
|
|
214
|
+
from scrollkit.display.colors import scale
|
|
215
|
+
from scrollkit.network.wifi_manager import is_dev_mode
|
|
216
|
+
|
|
217
|
+
logger = ErrorHandler("app.log")
|
|
218
|
+
logger.info("Application started")
|
|
219
|
+
|
|
220
|
+
color = scale(0xff0000, 0.5) # Dim red to 50%
|
|
221
|
+
|
|
222
|
+
if is_dev_mode():
|
|
223
|
+
print("running on desktop, not CircuitPython")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Platform Support
|
|
227
|
+
|
|
228
|
+
| Platform | Backend | Status |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| Adafruit MatrixPortal S3 | CircuitPython + displayio | ✅ Calibrated from device |
|
|
231
|
+
| Pimoroni Interstate 75 W (RP2350) | CircuitPython + rgbmatrix | ✅ Supported (perf profile uncalibrated) |
|
|
232
|
+
| Desktop (macOS/Linux/Windows) | SLDK Simulator | ✅ |
|
|
233
|
+
| Custom CircuitPython boards | displayio / rgbmatrix | 🔌 Extensible (see [Adding New Hardware](https://scrollkit.dev/guide/hardware/)) |
|
|
234
|
+
|
|
235
|
+
## How this was built
|
|
236
|
+
|
|
237
|
+
I wrote the first two shipping versions by hand in 2024, when all of this was
|
|
238
|
+
still one application. Splitting it into a library and a separate app layer, then
|
|
239
|
+
documenting the result, is the kind of project that dies quietly in a spare-time
|
|
240
|
+
backlog. So I used Claude Code and spec-driven development to handle the
|
|
241
|
+
refactoring and the first drafts, then went back through all of it in my own
|
|
242
|
+
voice, with my own screenshots. Yes, AI has touched a lot of this code. It was
|
|
243
|
+
also directed by an engineer who has shipped production software for a living,
|
|
244
|
+
including time on one of Sun Microsystems' API teams. Both are true.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# ScrollKit
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
*Built by [Michael Czeiszperger](http://czei.org)*
|
|
6
|
+
|
|
7
|
+
📖 **Full documentation: [scrollkit.dev](https://scrollkit.dev)**
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<!-- Absolute URL so the image also renders on the PyPI project page -->
|
|
11
|
+
<img src="https://raw.githubusercontent.com/czei/scrollkit/master/docs/assets/video/scrollkit-hero.gif" alt="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" width="640">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Desktop development with simulator
|
|
18
|
+
pip install "scrollkit[simulator]"
|
|
19
|
+
|
|
20
|
+
# To modify ScrollKit itself (or run the demos): clone and install editable
|
|
21
|
+
git clone https://github.com/czei/scrollkit.git
|
|
22
|
+
cd scrollkit && pip install -e ".[simulator]"
|
|
23
|
+
|
|
24
|
+
# CircuitPython — copy scrollkit/ to your device's lib/ alongside your source
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
from scrollkit.app.base import ScrollKitApp
|
|
32
|
+
from scrollkit.display.content import ScrollingText
|
|
33
|
+
|
|
34
|
+
class HelloWorldApp(ScrollKitApp):
|
|
35
|
+
async def setup(self):
|
|
36
|
+
self.content_queue.add(
|
|
37
|
+
ScrollingText("Hello, LED Matrix!", y=12, color=0x00AAFF))
|
|
38
|
+
|
|
39
|
+
asyncio.run(HelloWorldApp().run()) # auto-detects MatrixPortal hardware vs desktop simulator
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> The top-level `scrollkit` package deliberately performs **no** imports (every
|
|
43
|
+
> import costs RAM on CircuitPython), so you always import from submodules, e.g.
|
|
44
|
+
> `from scrollkit.app.base import ScrollKitApp`. See the
|
|
45
|
+
> [getting-started guide](https://scrollkit.dev/getting-started/)
|
|
46
|
+
> for the full `ScrollKitApp` / `UnifiedDisplay` API.
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
ScrollKit runs unchanged on the MatrixPortal S3 (CircuitPython) and a desktop
|
|
51
|
+
pygame simulator. Your app subclasses `ScrollKitApp` and talks to one display
|
|
52
|
+
abstraction; the library picks a backend at import time and brokers every external
|
|
53
|
+
system the sign touches:
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
flowchart TB
|
|
57
|
+
app["Your app<br/>(subclasses ScrollKitApp)"] --> core["ScrollKitApp · UnifiedDisplay<br/>ContentQueue · effects · config"]
|
|
58
|
+
core -->|CircuitPython| hw["MatrixPortal S3<br/>displayio → RGBMatrix panel"]
|
|
59
|
+
core -->|desktop| sim["pygame simulator"]
|
|
60
|
+
core <-->|HttpClient — synchronous| api(["HTTP data API"])
|
|
61
|
+
core <-->|SettingsWebServer| browser(["Browser config UI"])
|
|
62
|
+
core -->|raw.githubusercontent.com| gh(["GitHub OTA"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Subsystem dependencies (dashed = lazy import; `dev` and `simulator` are
|
|
66
|
+
desktop-only, raising `ImportError` on the device):
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
flowchart LR
|
|
70
|
+
app["app"] --> display["display"]
|
|
71
|
+
app --> config["config"]
|
|
72
|
+
app -.->|lazy| utils["utils"]
|
|
73
|
+
app -.->|lazy| effects["effects"]
|
|
74
|
+
app -.->|lazy| web["web"]
|
|
75
|
+
effects --> display
|
|
76
|
+
display -.->|desktop| simulator["simulator"]
|
|
77
|
+
config -.->|lazy| utils
|
|
78
|
+
network["network"] --> config
|
|
79
|
+
network --> utils
|
|
80
|
+
ota["ota"] --> exceptions["exceptions"]
|
|
81
|
+
dev["dev"] --> display
|
|
82
|
+
dev --> effects
|
|
83
|
+
dev --> simulator
|
|
84
|
+
classDef desktop stroke-dasharray:6 4;
|
|
85
|
+
class dev,simulator desktop;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
See the [Architecture guide](docs/guide/architecture.md) for the full write-up,
|
|
89
|
+
including the invariants this graph enforces.
|
|
90
|
+
|
|
91
|
+
## Package Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
scrollkit/
|
|
95
|
+
├── app/ # ScrollKitApp base class, async run loop, memory helpers
|
|
96
|
+
├── display/ # UnifiedDisplay (auto-detects hardware vs simulator), content
|
|
97
|
+
│ ├── unified.py # Production display (device + desktop)
|
|
98
|
+
│ ├── content.py # DisplayContent / StaticText / ScrollingText / ContentQueue / Priority
|
|
99
|
+
│ ├── bitmap_text.py # Animated bitmap-font text + palette effects
|
|
100
|
+
│ ├── gradient_text.py # Gradient/multi-color text fill (GradientTextLayer)
|
|
101
|
+
│ └── colors.py # Continuous 24-bit color generators
|
|
102
|
+
├── effects/ # Transition contract (transitions.py) + standalone splash/particle helpers
|
|
103
|
+
├── network/ # Networking utilities
|
|
104
|
+
│ ├── http_client.py # Dual-implementation HTTP client (raises NetworkError)
|
|
105
|
+
│ ├── wifi_manager.py # WiFi connection lifecycle
|
|
106
|
+
│ └── mdns.py # <hostname>.local advertising (CircuitPython; no-op on desktop)
|
|
107
|
+
├── config/ # Configuration management
|
|
108
|
+
│ └── settings_manager.py # JSON-based persistent settings
|
|
109
|
+
├── ota/ # Over-the-air updates
|
|
110
|
+
│ ├── client.py # GitHub-release-based OTA client
|
|
111
|
+
│ ├── manifest.py # Update manifest model
|
|
112
|
+
│ ├── display_progress.py # Display-progress adapter over OTAClient
|
|
113
|
+
│ └── publish.py # Host-side release publishing (desktop/CI only)
|
|
114
|
+
└── utils/ # Utilities
|
|
115
|
+
├── error_handler.py # Logging and error handling
|
|
116
|
+
├── diagnostics.py # NVM boot/crash record + reboot-loop safe-mode breaker
|
|
117
|
+
├── color_utils.py # Named colors + settings-UI hex-string color table (no conversion helpers; int-based conversions live in display/colors.py)
|
|
118
|
+
├── system_utils.py # NTP / HTTP-Date system clock sync
|
|
119
|
+
└── url_utils.py # URL decoding and credential loading
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Core API
|
|
123
|
+
|
|
124
|
+
### Display
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from scrollkit.display.unified import UnifiedDisplay
|
|
128
|
+
from scrollkit.display.content import ContentQueue, ScrollingText
|
|
129
|
+
|
|
130
|
+
# Create display (auto-detects CircuitPython vs desktop)
|
|
131
|
+
display = UnifiedDisplay(width=64, height=32)
|
|
132
|
+
display.initialize()
|
|
133
|
+
|
|
134
|
+
# ScrollKitApp drives this queue's render loop for you (see Quick Start above);
|
|
135
|
+
# add() is all a subclass's setup() typically needs to call.
|
|
136
|
+
queue = ContentQueue()
|
|
137
|
+
queue.add(ScrollingText("Scrolling text", y=12, color=0x00AAFF))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### HTTP Client
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from scrollkit.network.http_client import HttpClient
|
|
144
|
+
from scrollkit.exceptions import NetworkError
|
|
145
|
+
|
|
146
|
+
client = HttpClient()
|
|
147
|
+
try:
|
|
148
|
+
response = await client.get("https://api.example.com/data")
|
|
149
|
+
data = response.json()
|
|
150
|
+
except NetworkError as e:
|
|
151
|
+
print("fetch failed:", e)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Settings
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from scrollkit.config.settings_manager import SettingsManager
|
|
158
|
+
|
|
159
|
+
settings = SettingsManager("app_settings.json",
|
|
160
|
+
defaults={"hostname": "mydevice", "brightness": "0.5"},
|
|
161
|
+
bool_keys=["dark_mode"])
|
|
162
|
+
settings.set("hostname", "new-name")
|
|
163
|
+
settings.save_settings()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Utilities
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from scrollkit.utils.error_handler import ErrorHandler
|
|
170
|
+
from scrollkit.display.colors import scale
|
|
171
|
+
from scrollkit.network.wifi_manager import is_dev_mode
|
|
172
|
+
|
|
173
|
+
logger = ErrorHandler("app.log")
|
|
174
|
+
logger.info("Application started")
|
|
175
|
+
|
|
176
|
+
color = scale(0xff0000, 0.5) # Dim red to 50%
|
|
177
|
+
|
|
178
|
+
if is_dev_mode():
|
|
179
|
+
print("running on desktop, not CircuitPython")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Platform Support
|
|
183
|
+
|
|
184
|
+
| Platform | Backend | Status |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Adafruit MatrixPortal S3 | CircuitPython + displayio | ✅ Calibrated from device |
|
|
187
|
+
| Pimoroni Interstate 75 W (RP2350) | CircuitPython + rgbmatrix | ✅ Supported (perf profile uncalibrated) |
|
|
188
|
+
| Desktop (macOS/Linux/Windows) | SLDK Simulator | ✅ |
|
|
189
|
+
| Custom CircuitPython boards | displayio / rgbmatrix | 🔌 Extensible (see [Adding New Hardware](https://scrollkit.dev/guide/hardware/)) |
|
|
190
|
+
|
|
191
|
+
## How this was built
|
|
192
|
+
|
|
193
|
+
I wrote the first two shipping versions by hand in 2024, when all of this was
|
|
194
|
+
still one application. Splitting it into a library and a separate app layer, then
|
|
195
|
+
documenting the result, is the kind of project that dies quietly in a spare-time
|
|
196
|
+
backlog. So I used Claude Code and spec-driven development to handle the
|
|
197
|
+
refactoring and the first drafts, then went back through all of it in my own
|
|
198
|
+
voice, with my own screenshots. Yes, AI has touched a lot of this code. It was
|
|
199
|
+
also directed by an engineer who has shipped production software for a living,
|
|
200
|
+
including time on one of Sun Microsystems' API teams. Both are true.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scrollkit"
|
|
7
|
+
version = "0.8.3"
|
|
8
|
+
description = "LED Matrix Display Framework for CircuitPython and Desktop"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Michael Winslow Czeiszperger", email = "michael@czei.org"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["circuitpython", "led-matrix", "display", "matrixportal", "simulator"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
"Topic :: System :: Hardware",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# The desktop simulator needs numpy (pixel buffer / LED matrix) + pygame + Pillow.
|
|
33
|
+
# These are simulator-only — the base package and the on-device (CircuitPython)
|
|
34
|
+
# path must stay dependency-free.
|
|
35
|
+
simulator = ["pygame>=2.0", "Pillow>=9.0", "numpy>=1.21"]
|
|
36
|
+
web = ["aiohttp>=3.8"]
|
|
37
|
+
# dev pulls in the simulator so `pip install -e .[dev]` is the single command that
|
|
38
|
+
# gets everything the test suite needs (it runs against the simulator).
|
|
39
|
+
dev = ["scrollkit[simulator]", "pytest>=7.0", "pytest-asyncio>=0.21", "pytest-cov>=4.0", "pytest-timeout>=2.1", "ruff>=0.1", "requests>=2.28"]
|
|
40
|
+
docs = ["mkdocs>=1.5", "mkdocs-material>=9.0", "mkdocstrings[python]>=0.24"]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/czei/scrollkit"
|
|
44
|
+
Repository = "https://github.com/czei/scrollkit"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
include = ["scrollkit*"]
|
|
49
|
+
|
|
50
|
+
# Non-.py files the wheel/sdist must carry. Without this, pip installs would
|
|
51
|
+
# silently drop the simulator's BDF fonts and the hardware-calibration JSONs
|
|
52
|
+
# (CI's editable install can't catch that — it always sees the source tree).
|
|
53
|
+
[tool.setuptools.package-data]
|
|
54
|
+
"scrollkit.simulator" = ["LICENSE", "*.md"]
|
|
55
|
+
"scrollkit.simulator.core" = ["*.json"]
|
|
56
|
+
"scrollkit.simulator.fonts" = ["*.bdf", "*.bdf.license", "LICENSE*"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""ScrollKit - LED Matrix Display Framework for CircuitPython and Desktop.
|
|
3
|
+
|
|
4
|
+
A framework for building scrolling LED matrix applications that run unchanged on
|
|
5
|
+
CircuitPython hardware (Adafruit MatrixPortal S3) and a desktop pygame simulator.
|
|
6
|
+
|
|
7
|
+
This top-level package is intentionally lightweight: it exposes only version
|
|
8
|
+
metadata and performs NO eager submodule imports. On CircuitPython every imported
|
|
9
|
+
module costs RAM (a globals dict + bytecode), so callers import exactly what they
|
|
10
|
+
need from submodules, e.g.::
|
|
11
|
+
|
|
12
|
+
from scrollkit.app.base import ScrollKitApp
|
|
13
|
+
from scrollkit.display.unified import UnifiedDisplay
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.8.3"
|