scrollkit 0.8.3__py3-none-any.whl
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/__init__.py +16 -0
- scrollkit/app/__init__.py +6 -0
- scrollkit/app/base.py +918 -0
- scrollkit/app/memory.py +70 -0
- scrollkit/config/__init__.py +1 -0
- scrollkit/config/settings_manager.py +215 -0
- scrollkit/config/transition_names.py +31 -0
- scrollkit/dev/__init__.py +41 -0
- scrollkit/dev/capabilities.py +374 -0
- scrollkit/dev/harness.py +383 -0
- scrollkit/dev/metrics.py +91 -0
- scrollkit/dev/performance.py +174 -0
- scrollkit/dev/validation.py +245 -0
- scrollkit/display/__init__.py +14 -0
- scrollkit/display/_graphics.py +299 -0
- scrollkit/display/_recording.py +165 -0
- scrollkit/display/_sim_backend.py +99 -0
- scrollkit/display/bitmap_text.py +408 -0
- scrollkit/display/boards.py +186 -0
- scrollkit/display/colors.py +176 -0
- scrollkit/display/content.py +604 -0
- scrollkit/display/gradient_text.py +167 -0
- scrollkit/display/interface.py +126 -0
- scrollkit/display/simulator.py +80 -0
- scrollkit/display/text_fill.py +59 -0
- scrollkit/display/text_pixels.py +250 -0
- scrollkit/display/unified.py +595 -0
- scrollkit/effects/__init__.py +28 -0
- scrollkit/effects/drip_splash.py +253 -0
- scrollkit/effects/easing.py +131 -0
- scrollkit/effects/overlay.py +92 -0
- scrollkit/effects/particles.py +355 -0
- scrollkit/effects/reveal_splash.py +132 -0
- scrollkit/effects/scrolling.py +363 -0
- scrollkit/effects/swarm_reveal.py +512 -0
- scrollkit/effects/text_render.py +25 -0
- scrollkit/effects/transitions.py +873 -0
- scrollkit/exceptions.py +55 -0
- scrollkit/network/__init__.py +1 -0
- scrollkit/network/http_client.py +505 -0
- scrollkit/network/mdns.py +44 -0
- scrollkit/network/wifi_manager.py +382 -0
- scrollkit/ota/__init__.py +13 -0
- scrollkit/ota/client.py +528 -0
- scrollkit/ota/display_progress.py +125 -0
- scrollkit/ota/manifest.py +206 -0
- scrollkit/ota/publish.py +379 -0
- scrollkit/simulator/ATTRIBUTION.md +20 -0
- scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
- scrollkit/simulator/LICENSE +176 -0
- scrollkit/simulator/README.md +87 -0
- scrollkit/simulator/__init__.py +11 -0
- scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
- scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
- scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
- scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
- scrollkit/simulator/adafruit_display_text/label.py +336 -0
- scrollkit/simulator/bitmaptools.py +50 -0
- scrollkit/simulator/core/__init__.py +8 -0
- scrollkit/simulator/core/color_utils.py +62 -0
- scrollkit/simulator/core/device_benchmarks.json +254 -0
- scrollkit/simulator/core/feasibility.py +160 -0
- scrollkit/simulator/core/hardware_profile.py +191 -0
- scrollkit/simulator/core/led_matrix.py +307 -0
- scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
- scrollkit/simulator/core/performance_manager.py +253 -0
- scrollkit/simulator/core/pixel_buffer.py +188 -0
- scrollkit/simulator/devices/__init__.py +7 -0
- scrollkit/simulator/devices/base_device.py +79 -0
- scrollkit/simulator/devices/matrixportal_s3.py +87 -0
- scrollkit/simulator/displayio/__init__.py +12 -0
- scrollkit/simulator/displayio/bitmap.py +158 -0
- scrollkit/simulator/displayio/display.py +195 -0
- scrollkit/simulator/displayio/fourwire.py +54 -0
- scrollkit/simulator/displayio/group.py +125 -0
- scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
- scrollkit/simulator/displayio/palette.py +115 -0
- scrollkit/simulator/displayio/tilegrid.py +155 -0
- scrollkit/simulator/fonts/3x5.bdf +2474 -0
- scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
- scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
- scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
- scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
- scrollkit/simulator/fonts/viii.bdf +2659 -0
- scrollkit/simulator/terminalio/__init__.py +20 -0
- scrollkit/utils/__init__.py +1 -0
- scrollkit/utils/color_utils.py +54 -0
- scrollkit/utils/diagnostics.py +227 -0
- scrollkit/utils/error_handler.py +347 -0
- scrollkit/utils/system_utils.py +245 -0
- scrollkit/utils/url_utils.py +46 -0
- scrollkit/web/__init__.py +6 -0
- scrollkit/web/settings_server.py +328 -0
- scrollkit/web/wifi_setup.py +331 -0
- scrollkit-0.8.3.dist-info/METADATA +248 -0
- scrollkit-0.8.3.dist-info/RECORD +111 -0
- scrollkit-0.8.3.dist-info/WHEEL +5 -0
- scrollkit-0.8.3.dist-info/licenses/LICENSE +31 -0
- scrollkit-0.8.3.dist-info/top_level.txt +1 -0
scrollkit/app/memory.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Realistic free-RAM estimate shared by the run loop and the dev harness.
|
|
3
|
+
|
|
4
|
+
The run loop gates optional processes on free RAM (data updates need ~20-30 KB,
|
|
5
|
+
the web server ~50 KB). On real CircuitPython that comes from ``gc.mem_free()``.
|
|
6
|
+
On the desktop simulator there is no such limit, so historically these sites just
|
|
7
|
+
pretended ~100 KB was free — which means an AI agent verifying an app on the
|
|
8
|
+
simulator never sees the low-memory code paths a real MatrixPortal S3 would hit.
|
|
9
|
+
|
|
10
|
+
``free_memory()`` keeps that generous desktop default UNLESS the hardware-realism
|
|
11
|
+
simulation is active (``SimulatorDisplay(hardware_timing=True)`` /
|
|
12
|
+
``SCROLLKIT_HW_SIM=1``), in which case it reports the modeled *device* budget
|
|
13
|
+
minus the modeled live allocation. So the existing 20/30/50 KB ladder actually
|
|
14
|
+
gates against the device's tiny RAM during a simulated run, and the feasibility
|
|
15
|
+
report's "won't fit" warning lines up with what the loop decided.
|
|
16
|
+
|
|
17
|
+
Default behavior is unchanged: on hardware it's the real number; on desktop with
|
|
18
|
+
the sim OFF it's the same large constant as before.
|
|
19
|
+
|
|
20
|
+
Device-safe: ``gc`` is the only hard import; the simulator lookup is wrapped so it
|
|
21
|
+
never fails on CircuitPython (where the simulator package isn't present).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import gc
|
|
25
|
+
|
|
26
|
+
# Free RAM reported on desktop when the hardware sim is OFF. Matches the literal
|
|
27
|
+
# previously hard-coded across app/base.py, so default behavior is unchanged.
|
|
28
|
+
DESKTOP_FREE_BYTES = 100_000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ['free_memory', 'DESKTOP_FREE_BYTES']
|
|
32
|
+
|
|
33
|
+
def free_memory():
|
|
34
|
+
"""Best estimate of free RAM in bytes.
|
|
35
|
+
|
|
36
|
+
- CircuitPython hardware: the real ``gc.mem_free()``.
|
|
37
|
+
- Desktop + hardware sim ON: modeled device budget minus modeled usage.
|
|
38
|
+
- Desktop + hardware sim OFF: ``DESKTOP_FREE_BYTES`` (no behavior change).
|
|
39
|
+
"""
|
|
40
|
+
if hasattr(gc, "mem_free"):
|
|
41
|
+
# Real hardware (or any build exposing mem_free) — trust the device.
|
|
42
|
+
try:
|
|
43
|
+
return gc.mem_free()
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
modeled = _modeled_free_memory()
|
|
47
|
+
if modeled is not None:
|
|
48
|
+
return modeled
|
|
49
|
+
return DESKTOP_FREE_BYTES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _modeled_free_memory():
|
|
53
|
+
"""Free RAM implied by the active hardware PerformanceManager, or None.
|
|
54
|
+
|
|
55
|
+
The import lives entirely inside the (desktop-only) simulator package; any
|
|
56
|
+
failure simply means "no hardware model active" and we fall back.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
from ..simulator.core.performance_manager import get_active
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
manager = get_active()
|
|
63
|
+
if manager is None:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
budget = manager.profile.usable_ram_bytes
|
|
67
|
+
used = manager.estimated_peak_ram_bytes()
|
|
68
|
+
return max(0, budget - used)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings manager for handling user configuration.
|
|
3
|
+
Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from scrollkit.config.transition_names import TRANSITION_NAMES
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ['SettingsManager']
|
|
11
|
+
|
|
12
|
+
def _logger():
|
|
13
|
+
# Lazy: constructing ErrorHandler does a filesystem write-test, so it must
|
|
14
|
+
# not run merely from importing this module. Its own __new__ singleton
|
|
15
|
+
# guard makes repeat calls cheap.
|
|
16
|
+
from scrollkit.utils.error_handler import ErrorHandler
|
|
17
|
+
return ErrorHandler("error_log")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettingsManager:
|
|
21
|
+
"""
|
|
22
|
+
Manages application settings with persistence to a JSON file.
|
|
23
|
+
|
|
24
|
+
Applications can register their own defaults by calling
|
|
25
|
+
set_defaults({'key': value, ...}) after construction.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, filename, defaults=None, bool_keys=None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the settings manager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
filename: The name of the settings file
|
|
34
|
+
defaults: Optional dict of default settings
|
|
35
|
+
bool_keys: Optional list of keys that should be treated as booleans
|
|
36
|
+
(needed because CircuitPython's JSON parser may store them as strings)
|
|
37
|
+
"""
|
|
38
|
+
self.filename = filename
|
|
39
|
+
self.settings = self.load_settings()
|
|
40
|
+
self.scroll_speed = {"Slow": 0.06, "Medium": 0.04, "Fast": 0.02}
|
|
41
|
+
self._bool_keys = bool_keys or []
|
|
42
|
+
self._schema = []
|
|
43
|
+
|
|
44
|
+
# Built-in library settings — always defined so the default web UI shows them.
|
|
45
|
+
self.define("brightness_scale", 0.5, label="Brightness", min=0.0, max=1.0, step=0.05)
|
|
46
|
+
self.define("scroll_speed", "Medium", label="Scroll Speed",
|
|
47
|
+
options=["None", "Slow", "Medium", "Fast"])
|
|
48
|
+
self.define("default_color", 0xFFFFFF, label="Default Color", type="color")
|
|
49
|
+
# Choices derive from the single source of truth (config.transition_names),
|
|
50
|
+
# which the dispatch map in effects.transitions is tested to match — so a
|
|
51
|
+
# selectable name can never silently fail to dispatch. Plain "None" + list
|
|
52
|
+
# concatenation (no *-unpacking) for CircuitPython-parser safety.
|
|
53
|
+
self.define("transition_style", "None", label="Transition Style",
|
|
54
|
+
options=["None"] + list(TRANSITION_NAMES))
|
|
55
|
+
|
|
56
|
+
# Apply application-provided defaults
|
|
57
|
+
if defaults:
|
|
58
|
+
self.set_defaults(defaults)
|
|
59
|
+
|
|
60
|
+
def define(self, key, default, label=None, type=None, options=None,
|
|
61
|
+
min=None, max=None, step=None):
|
|
62
|
+
"""Declare a setting with display metadata for the auto-generating web UI.
|
|
63
|
+
|
|
64
|
+
The UI renders a form field for every defined setting in the order they
|
|
65
|
+
were declared. Type is inferred from the default value when not given:
|
|
66
|
+
bool -> checkbox, options list -> select, min/max -> range,
|
|
67
|
+
int/float -> number, else text. Use type="color" explicitly for
|
|
68
|
+
colour pickers (stored as int 0xRRGGBB).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
key: Settings key (used as the form field name)
|
|
72
|
+
default: Default value (only applied when no saved value exists)
|
|
73
|
+
label: Human-readable label; defaults to title-cased key name
|
|
74
|
+
type: Field type override ("text","number","range","color","select","checkbox")
|
|
75
|
+
options: List of string choices (implies type="select")
|
|
76
|
+
min: Numeric lower bound for range/number inputs
|
|
77
|
+
max: Numeric upper bound for range/number inputs
|
|
78
|
+
step: Numeric step for range/number inputs
|
|
79
|
+
"""
|
|
80
|
+
if type is not None:
|
|
81
|
+
resolved_type = type
|
|
82
|
+
elif options:
|
|
83
|
+
resolved_type = "select"
|
|
84
|
+
elif min is not None or max is not None:
|
|
85
|
+
resolved_type = "range"
|
|
86
|
+
elif isinstance(default, bool): # must check before int (bool subclasses int)
|
|
87
|
+
resolved_type = "checkbox"
|
|
88
|
+
elif isinstance(default, (int, float)):
|
|
89
|
+
resolved_type = "number"
|
|
90
|
+
else:
|
|
91
|
+
resolved_type = "text"
|
|
92
|
+
|
|
93
|
+
resolved_label = label if label is not None else SettingsManager.get_pretty_name(key)
|
|
94
|
+
|
|
95
|
+
self._schema.append({
|
|
96
|
+
"key": key,
|
|
97
|
+
"label": resolved_label,
|
|
98
|
+
"type": resolved_type,
|
|
99
|
+
"default": default,
|
|
100
|
+
"options": options,
|
|
101
|
+
"min": min,
|
|
102
|
+
"max": max,
|
|
103
|
+
"step": step,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if resolved_type == "checkbox":
|
|
107
|
+
self.add_bool_keys(key)
|
|
108
|
+
|
|
109
|
+
self.set_defaults({key: default})
|
|
110
|
+
|
|
111
|
+
def set_defaults(self, defaults):
|
|
112
|
+
"""
|
|
113
|
+
Register application-specific defaults. Only sets values that
|
|
114
|
+
are not already present in settings.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
defaults: Dict of {key: default_value}
|
|
118
|
+
"""
|
|
119
|
+
for key, value in defaults.items():
|
|
120
|
+
if key not in self.settings:
|
|
121
|
+
self.settings[key] = value
|
|
122
|
+
|
|
123
|
+
def add_bool_keys(self, *keys):
|
|
124
|
+
"""Register additional keys that should be treated as booleans."""
|
|
125
|
+
for key in keys:
|
|
126
|
+
if key not in self._bool_keys:
|
|
127
|
+
self._bool_keys.append(key)
|
|
128
|
+
|
|
129
|
+
def get_scroll_speed(self):
|
|
130
|
+
"""Return scroll speed in seconds per pixel (for timing loops)."""
|
|
131
|
+
return self.scroll_speed.get(
|
|
132
|
+
self.settings.get("scroll_speed", "Medium"), 0.04
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def get_scroll_speed_px(self):
|
|
136
|
+
"""Return scroll speed in pixels per second (for ScrollingText speed= arg).
|
|
137
|
+
|
|
138
|
+
Returns 0 when scroll_speed is "None" — ScrollingText treats 0 as
|
|
139
|
+
static-display mode: text is shown centred for a fixed duration rather
|
|
140
|
+
than scrolling.
|
|
141
|
+
"""
|
|
142
|
+
if self.settings.get("scroll_speed", "Medium") == "None":
|
|
143
|
+
return 0
|
|
144
|
+
secs = self.get_scroll_speed()
|
|
145
|
+
return int(round(1.0 / secs)) if secs > 0 else 25
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def get_pretty_name(settings_name):
|
|
149
|
+
"""
|
|
150
|
+
Convert a settings key to a display-friendly name.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
settings_name: The settings key
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A display-friendly name
|
|
157
|
+
"""
|
|
158
|
+
new_name = settings_name.replace("_", " ")
|
|
159
|
+
return " ".join(word[0].upper() + word[1:] for word in new_name.split(' '))
|
|
160
|
+
|
|
161
|
+
def load_settings(self):
|
|
162
|
+
"""
|
|
163
|
+
Load settings from the settings file.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A dictionary of settings
|
|
167
|
+
"""
|
|
168
|
+
_logger().info(f"Loading settings {self.filename}")
|
|
169
|
+
try:
|
|
170
|
+
with open(self.filename, 'r') as f:
|
|
171
|
+
return json.load(f)
|
|
172
|
+
except (OSError, ValueError):
|
|
173
|
+
# OSError: missing file. ValueError: corrupt JSON — CircuitPython's
|
|
174
|
+
# json.load raises ValueError (there is no JSONDecodeError), and a
|
|
175
|
+
# truncated flash write must not brick boot.
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
def save_settings(self):
|
|
179
|
+
"""Save settings to the settings file"""
|
|
180
|
+
_logger().info(f"Saving settings {self.filename}")
|
|
181
|
+
try:
|
|
182
|
+
with open(self.filename, 'w') as f:
|
|
183
|
+
json.dump(self.settings, f)
|
|
184
|
+
except OSError as e:
|
|
185
|
+
_logger().error(e, f"Error saving settings to {self.filename}")
|
|
186
|
+
|
|
187
|
+
def get(self, key, default=None):
|
|
188
|
+
"""
|
|
189
|
+
Get a setting by key with a default value.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
key: The settings key
|
|
193
|
+
default: The default value if the key is not found
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The setting value, or the default if not found
|
|
197
|
+
"""
|
|
198
|
+
value = self.settings.get(key, default)
|
|
199
|
+
|
|
200
|
+
# Special handling for boolean settings that might be stored as strings
|
|
201
|
+
# This can happen with CircuitPython's JSON parser
|
|
202
|
+
if key in self._bool_keys and isinstance(value, str):
|
|
203
|
+
return value.lower() == "true"
|
|
204
|
+
|
|
205
|
+
return value
|
|
206
|
+
|
|
207
|
+
def set(self, key, value):
|
|
208
|
+
"""
|
|
209
|
+
Set a setting by key.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
key: The settings key
|
|
213
|
+
value: The value to set
|
|
214
|
+
"""
|
|
215
|
+
self.settings[key] = value
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Canonical list of built-in transition names — the single source of truth.
|
|
3
|
+
|
|
4
|
+
This module is **literal-only**: it imports nothing, so it is safe to import on
|
|
5
|
+
the device boot path (e.g. from ``settings_manager``) without dragging in the
|
|
6
|
+
heavy ``scrollkit.effects`` package. The name -> class dispatch lives in
|
|
7
|
+
``scrollkit.effects.transitions`` (``_TRANSITION_MAP`` / ``transition_factory``);
|
|
8
|
+
``test_transition_registry`` asserts the two stay in lockstep, in this order.
|
|
9
|
+
|
|
10
|
+
Order is UI-facing: the settings dropdown / web form render the choices in this
|
|
11
|
+
order (with ``"None"`` prepended), so do not reorder casually.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
TRANSITION_NAMES = (
|
|
15
|
+
"Drop from Sky",
|
|
16
|
+
"Pixel Dissolve",
|
|
17
|
+
"Column Rain",
|
|
18
|
+
"Gradual Reveal",
|
|
19
|
+
"Scan Fold",
|
|
20
|
+
"Horizontal Wipe",
|
|
21
|
+
"Glitch Bars",
|
|
22
|
+
"Diagonal Wipe",
|
|
23
|
+
"Iris Snap",
|
|
24
|
+
"Venetian Shutters",
|
|
25
|
+
"Mosaic Resolve",
|
|
26
|
+
"CRT Collapse",
|
|
27
|
+
"Light Slit",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ['TRANSITION_NAMES']
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Desktop-only developer / AI-agent affordances for ScrollKit.
|
|
3
|
+
|
|
4
|
+
Everything here exists to help a human or an AI agent build and *verify* a
|
|
5
|
+
ScrollKit app on the desktop simulator before flashing it to real hardware:
|
|
6
|
+
a headless run harness, pixel metrics, and (via the simulator package) an honest
|
|
7
|
+
estimate of how the app would perform on the slow, RAM-constrained MatrixPortal
|
|
8
|
+
S3.
|
|
9
|
+
|
|
10
|
+
This subpackage is **never** meant to run on the device. It pulls in numpy,
|
|
11
|
+
pygame, and other desktop-only machinery that would waste scarce RAM (or simply
|
|
12
|
+
not import) on CircuitPython. Importing it there raises ImportError immediately,
|
|
13
|
+
on purpose, so device code can't accidentally depend on it. Nothing in
|
|
14
|
+
``scrollkit`` core (``app/``, ``display/``, the top-level ``__init__``) imports
|
|
15
|
+
this module.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# Hard stop on CircuitPython — these tools are desktop-only by design.
|
|
21
|
+
if getattr(sys, "implementation", None) is not None \
|
|
22
|
+
and getattr(sys.implementation, "name", None) == "circuitpython":
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"scrollkit.dev is a desktop-only development/verification toolkit and "
|
|
25
|
+
"cannot run on CircuitPython. Use it from the desktop simulator to "
|
|
26
|
+
"build and check apps before deploying to the device."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .harness import ( # noqa: E402
|
|
30
|
+
RunResult, run_headless, run_headless_async, record_gif, record_video)
|
|
31
|
+
from .capabilities import capabilities, as_text # noqa: E402
|
|
32
|
+
from .validation import validate, ValidationReport, Issue # noqa: E402
|
|
33
|
+
from .performance import performance_guide, performance_text # noqa: E402
|
|
34
|
+
from . import metrics # noqa: E402,F401
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"RunResult", "run_headless", "run_headless_async", "record_gif",
|
|
38
|
+
"record_video", "metrics",
|
|
39
|
+
"capabilities", "as_text", "validate", "ValidationReport", "Issue",
|
|
40
|
+
"performance_guide", "performance_text",
|
|
41
|
+
]
|