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.
Files changed (116) hide show
  1. scrollkit-0.8.3/LICENSE +31 -0
  2. scrollkit-0.8.3/PKG-INFO +248 -0
  3. scrollkit-0.8.3/README.md +204 -0
  4. scrollkit-0.8.3/pyproject.toml +56 -0
  5. scrollkit-0.8.3/setup.cfg +4 -0
  6. scrollkit-0.8.3/src/scrollkit/__init__.py +16 -0
  7. scrollkit-0.8.3/src/scrollkit/app/__init__.py +6 -0
  8. scrollkit-0.8.3/src/scrollkit/app/base.py +918 -0
  9. scrollkit-0.8.3/src/scrollkit/app/memory.py +70 -0
  10. scrollkit-0.8.3/src/scrollkit/config/__init__.py +1 -0
  11. scrollkit-0.8.3/src/scrollkit/config/settings_manager.py +215 -0
  12. scrollkit-0.8.3/src/scrollkit/config/transition_names.py +31 -0
  13. scrollkit-0.8.3/src/scrollkit/dev/__init__.py +41 -0
  14. scrollkit-0.8.3/src/scrollkit/dev/capabilities.py +374 -0
  15. scrollkit-0.8.3/src/scrollkit/dev/harness.py +383 -0
  16. scrollkit-0.8.3/src/scrollkit/dev/metrics.py +91 -0
  17. scrollkit-0.8.3/src/scrollkit/dev/performance.py +174 -0
  18. scrollkit-0.8.3/src/scrollkit/dev/validation.py +245 -0
  19. scrollkit-0.8.3/src/scrollkit/display/__init__.py +14 -0
  20. scrollkit-0.8.3/src/scrollkit/display/_graphics.py +299 -0
  21. scrollkit-0.8.3/src/scrollkit/display/_recording.py +165 -0
  22. scrollkit-0.8.3/src/scrollkit/display/_sim_backend.py +99 -0
  23. scrollkit-0.8.3/src/scrollkit/display/bitmap_text.py +408 -0
  24. scrollkit-0.8.3/src/scrollkit/display/boards.py +186 -0
  25. scrollkit-0.8.3/src/scrollkit/display/colors.py +176 -0
  26. scrollkit-0.8.3/src/scrollkit/display/content.py +604 -0
  27. scrollkit-0.8.3/src/scrollkit/display/gradient_text.py +167 -0
  28. scrollkit-0.8.3/src/scrollkit/display/interface.py +126 -0
  29. scrollkit-0.8.3/src/scrollkit/display/simulator.py +80 -0
  30. scrollkit-0.8.3/src/scrollkit/display/text_fill.py +59 -0
  31. scrollkit-0.8.3/src/scrollkit/display/text_pixels.py +250 -0
  32. scrollkit-0.8.3/src/scrollkit/display/unified.py +595 -0
  33. scrollkit-0.8.3/src/scrollkit/effects/__init__.py +28 -0
  34. scrollkit-0.8.3/src/scrollkit/effects/drip_splash.py +253 -0
  35. scrollkit-0.8.3/src/scrollkit/effects/easing.py +131 -0
  36. scrollkit-0.8.3/src/scrollkit/effects/overlay.py +92 -0
  37. scrollkit-0.8.3/src/scrollkit/effects/particles.py +355 -0
  38. scrollkit-0.8.3/src/scrollkit/effects/reveal_splash.py +132 -0
  39. scrollkit-0.8.3/src/scrollkit/effects/scrolling.py +363 -0
  40. scrollkit-0.8.3/src/scrollkit/effects/swarm_reveal.py +512 -0
  41. scrollkit-0.8.3/src/scrollkit/effects/text_render.py +25 -0
  42. scrollkit-0.8.3/src/scrollkit/effects/transitions.py +873 -0
  43. scrollkit-0.8.3/src/scrollkit/exceptions.py +55 -0
  44. scrollkit-0.8.3/src/scrollkit/network/__init__.py +1 -0
  45. scrollkit-0.8.3/src/scrollkit/network/http_client.py +505 -0
  46. scrollkit-0.8.3/src/scrollkit/network/mdns.py +44 -0
  47. scrollkit-0.8.3/src/scrollkit/network/wifi_manager.py +382 -0
  48. scrollkit-0.8.3/src/scrollkit/ota/__init__.py +13 -0
  49. scrollkit-0.8.3/src/scrollkit/ota/client.py +528 -0
  50. scrollkit-0.8.3/src/scrollkit/ota/display_progress.py +125 -0
  51. scrollkit-0.8.3/src/scrollkit/ota/manifest.py +206 -0
  52. scrollkit-0.8.3/src/scrollkit/ota/publish.py +379 -0
  53. scrollkit-0.8.3/src/scrollkit/simulator/ATTRIBUTION.md +20 -0
  54. scrollkit-0.8.3/src/scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
  55. scrollkit-0.8.3/src/scrollkit/simulator/LICENSE +176 -0
  56. scrollkit-0.8.3/src/scrollkit/simulator/README.md +87 -0
  57. scrollkit-0.8.3/src/scrollkit/simulator/__init__.py +11 -0
  58. scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
  59. scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
  60. scrollkit-0.8.3/src/scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
  61. scrollkit-0.8.3/src/scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
  62. scrollkit-0.8.3/src/scrollkit/simulator/adafruit_display_text/label.py +336 -0
  63. scrollkit-0.8.3/src/scrollkit/simulator/bitmaptools.py +50 -0
  64. scrollkit-0.8.3/src/scrollkit/simulator/core/__init__.py +8 -0
  65. scrollkit-0.8.3/src/scrollkit/simulator/core/color_utils.py +62 -0
  66. scrollkit-0.8.3/src/scrollkit/simulator/core/device_benchmarks.json +254 -0
  67. scrollkit-0.8.3/src/scrollkit/simulator/core/feasibility.py +160 -0
  68. scrollkit-0.8.3/src/scrollkit/simulator/core/hardware_profile.py +191 -0
  69. scrollkit-0.8.3/src/scrollkit/simulator/core/led_matrix.py +307 -0
  70. scrollkit-0.8.3/src/scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
  71. scrollkit-0.8.3/src/scrollkit/simulator/core/performance_manager.py +253 -0
  72. scrollkit-0.8.3/src/scrollkit/simulator/core/pixel_buffer.py +188 -0
  73. scrollkit-0.8.3/src/scrollkit/simulator/devices/__init__.py +7 -0
  74. scrollkit-0.8.3/src/scrollkit/simulator/devices/base_device.py +79 -0
  75. scrollkit-0.8.3/src/scrollkit/simulator/devices/matrixportal_s3.py +87 -0
  76. scrollkit-0.8.3/src/scrollkit/simulator/displayio/__init__.py +12 -0
  77. scrollkit-0.8.3/src/scrollkit/simulator/displayio/bitmap.py +158 -0
  78. scrollkit-0.8.3/src/scrollkit/simulator/displayio/display.py +195 -0
  79. scrollkit-0.8.3/src/scrollkit/simulator/displayio/fourwire.py +54 -0
  80. scrollkit-0.8.3/src/scrollkit/simulator/displayio/group.py +125 -0
  81. scrollkit-0.8.3/src/scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
  82. scrollkit-0.8.3/src/scrollkit/simulator/displayio/palette.py +115 -0
  83. scrollkit-0.8.3/src/scrollkit/simulator/displayio/tilegrid.py +155 -0
  84. scrollkit-0.8.3/src/scrollkit/simulator/fonts/3x5.bdf +2474 -0
  85. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
  86. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
  87. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
  88. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
  89. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
  90. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
  91. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
  92. scrollkit-0.8.3/src/scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
  93. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
  94. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
  95. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
  96. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
  97. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
  98. scrollkit-0.8.3/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
  99. scrollkit-0.8.3/src/scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
  100. scrollkit-0.8.3/src/scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
  101. scrollkit-0.8.3/src/scrollkit/simulator/fonts/viii.bdf +2659 -0
  102. scrollkit-0.8.3/src/scrollkit/simulator/terminalio/__init__.py +20 -0
  103. scrollkit-0.8.3/src/scrollkit/utils/__init__.py +1 -0
  104. scrollkit-0.8.3/src/scrollkit/utils/color_utils.py +54 -0
  105. scrollkit-0.8.3/src/scrollkit/utils/diagnostics.py +227 -0
  106. scrollkit-0.8.3/src/scrollkit/utils/error_handler.py +347 -0
  107. scrollkit-0.8.3/src/scrollkit/utils/system_utils.py +245 -0
  108. scrollkit-0.8.3/src/scrollkit/utils/url_utils.py +46 -0
  109. scrollkit-0.8.3/src/scrollkit/web/__init__.py +6 -0
  110. scrollkit-0.8.3/src/scrollkit/web/settings_server.py +328 -0
  111. scrollkit-0.8.3/src/scrollkit/web/wifi_setup.py +331 -0
  112. scrollkit-0.8.3/src/scrollkit.egg-info/PKG-INFO +248 -0
  113. scrollkit-0.8.3/src/scrollkit.egg-info/SOURCES.txt +114 -0
  114. scrollkit-0.8.3/src/scrollkit.egg-info/dependency_links.txt +1 -0
  115. scrollkit-0.8.3/src/scrollkit.egg-info/requires.txt +22 -0
  116. scrollkit-0.8.3/src/scrollkit.egg-info/top_level.txt +1 -0
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2024-2026 Michael Winslow Czeiszperger
2
+ """ScrollKit Application framework."""
3
+
4
+ from .base import SLDKApp, ScrollKitApp
5
+
6
+ __all__ = ['SLDKApp', 'ScrollKitApp']