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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""CircuitPython displayio.Bitmap equivalent."""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Bitmap:
|
|
8
|
+
"""Bitmap for pixel data storage.
|
|
9
|
+
|
|
10
|
+
Stores pixel data as palette indices. Each pixel is an index
|
|
11
|
+
into a Palette object that defines the actual colors.
|
|
12
|
+
Compatible with CircuitPython's displayio.Bitmap API.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, width, height, value_count):
|
|
16
|
+
"""Initialize bitmap with specified dimensions.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
width: Bitmap width in pixels
|
|
20
|
+
height: Bitmap height in pixels
|
|
21
|
+
value_count: Number of different values each pixel can have
|
|
22
|
+
"""
|
|
23
|
+
self.width = width
|
|
24
|
+
self.height = height
|
|
25
|
+
self.value_count = value_count
|
|
26
|
+
|
|
27
|
+
# Determine data type based on value count
|
|
28
|
+
if value_count <= 2:
|
|
29
|
+
self._bits_per_value = 1
|
|
30
|
+
dtype = np.uint8
|
|
31
|
+
elif value_count <= 4:
|
|
32
|
+
self._bits_per_value = 2
|
|
33
|
+
dtype = np.uint8
|
|
34
|
+
elif value_count <= 16:
|
|
35
|
+
self._bits_per_value = 4
|
|
36
|
+
dtype = np.uint8
|
|
37
|
+
elif value_count <= 256:
|
|
38
|
+
self._bits_per_value = 8
|
|
39
|
+
dtype = np.uint8
|
|
40
|
+
else:
|
|
41
|
+
self._bits_per_value = 16
|
|
42
|
+
dtype = np.uint16
|
|
43
|
+
|
|
44
|
+
# Create buffer to store pixel values
|
|
45
|
+
self._buffer = np.zeros((height, width), dtype=dtype)
|
|
46
|
+
|
|
47
|
+
def __setitem__(self, index, value):
|
|
48
|
+
"""Set pixel value.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
index: (x, y) tuple or flat index
|
|
52
|
+
value: Palette index value
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(index, tuple):
|
|
55
|
+
x, y = index
|
|
56
|
+
if not (0 <= x < self.width and 0 <= y < self.height):
|
|
57
|
+
raise IndexError(f"Pixel index ({x}, {y}) out of bounds")
|
|
58
|
+
else:
|
|
59
|
+
# Convert flat index to x, y
|
|
60
|
+
y = index // self.width
|
|
61
|
+
x = index % self.width
|
|
62
|
+
if not (0 <= index < self.width * self.height):
|
|
63
|
+
raise IndexError(f"Pixel index {index} out of bounds")
|
|
64
|
+
|
|
65
|
+
if not 0 <= value < self.value_count:
|
|
66
|
+
raise ValueError(f"Pixel value {value} out of range")
|
|
67
|
+
|
|
68
|
+
self._buffer[y, x] = value
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, index):
|
|
71
|
+
"""Get pixel value.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
index: (x, y) tuple or flat index
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Palette index value
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(index, tuple):
|
|
80
|
+
x, y = index
|
|
81
|
+
if not (0 <= x < self.width and 0 <= y < self.height):
|
|
82
|
+
raise IndexError(f"Pixel index ({x}, {y}) out of bounds")
|
|
83
|
+
else:
|
|
84
|
+
# Convert flat index to x, y
|
|
85
|
+
y = index // self.width
|
|
86
|
+
x = index % self.width
|
|
87
|
+
if not (0 <= index < self.width * self.height):
|
|
88
|
+
raise IndexError(f"Pixel index {index} out of bounds")
|
|
89
|
+
|
|
90
|
+
return int(self._buffer[y, x])
|
|
91
|
+
|
|
92
|
+
def fill(self, value):
|
|
93
|
+
"""Fill entire bitmap with a single value.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Palette index value to fill with
|
|
97
|
+
"""
|
|
98
|
+
if not 0 <= value < self.value_count:
|
|
99
|
+
raise ValueError(f"Pixel value {value} out of range")
|
|
100
|
+
|
|
101
|
+
self._buffer.fill(value)
|
|
102
|
+
|
|
103
|
+
def blit(self, x, y, source_bitmap, *, x1=0, y1=0, x2=None, y2=None, skip_index=None):
|
|
104
|
+
"""Copy pixels from another bitmap.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
x: Destination X coordinate
|
|
108
|
+
y: Destination Y coordinate
|
|
109
|
+
source_bitmap: Source Bitmap to copy from
|
|
110
|
+
x1: Source left edge (default 0)
|
|
111
|
+
y1: Source top edge (default 0)
|
|
112
|
+
x2: Source right edge (default source width)
|
|
113
|
+
y2: Source bottom edge (default source height)
|
|
114
|
+
skip_index: Palette index to treat as transparent
|
|
115
|
+
"""
|
|
116
|
+
# Set default source bounds
|
|
117
|
+
if x2 is None:
|
|
118
|
+
x2 = source_bitmap.width
|
|
119
|
+
if y2 is None:
|
|
120
|
+
y2 = source_bitmap.height
|
|
121
|
+
|
|
122
|
+
# Calculate copy region
|
|
123
|
+
src_width = x2 - x1
|
|
124
|
+
src_height = y2 - y1
|
|
125
|
+
|
|
126
|
+
# Clip to destination bounds
|
|
127
|
+
copy_width = min(src_width, self.width - x)
|
|
128
|
+
copy_height = min(src_height, self.height - y)
|
|
129
|
+
|
|
130
|
+
if copy_width <= 0 or copy_height <= 0:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Clip source region
|
|
134
|
+
if x < 0:
|
|
135
|
+
x1 -= x
|
|
136
|
+
copy_width += x
|
|
137
|
+
x = 0
|
|
138
|
+
if y < 0:
|
|
139
|
+
y1 -= y
|
|
140
|
+
copy_height += y
|
|
141
|
+
y = 0
|
|
142
|
+
|
|
143
|
+
# Copy pixel data
|
|
144
|
+
src_data = source_bitmap._buffer[y1:y1 + copy_height, x1:x1 + copy_width]
|
|
145
|
+
|
|
146
|
+
if skip_index is not None:
|
|
147
|
+
# Copy only non-transparent pixels
|
|
148
|
+
mask = src_data != skip_index
|
|
149
|
+
dst_slice = self._buffer[y:y + copy_height, x:x + copy_width]
|
|
150
|
+
dst_slice[mask] = src_data[mask]
|
|
151
|
+
else:
|
|
152
|
+
# Copy all pixels
|
|
153
|
+
self._buffer[y:y + copy_height, x:x + copy_width] = src_data
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def bits_per_value(self):
|
|
157
|
+
"""Get the number of bits per pixel value."""
|
|
158
|
+
return self._bits_per_value
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""CircuitPython displayio.Display equivalent."""
|
|
3
|
+
|
|
4
|
+
from ..core.led_matrix import LEDMatrix
|
|
5
|
+
from ..core.color_utils import rgb565_to_rgb888
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Display:
|
|
9
|
+
"""Display manages the screen output.
|
|
10
|
+
|
|
11
|
+
This class provides the main interface for showing groups and
|
|
12
|
+
managing display properties like brightness and rotation.
|
|
13
|
+
Compatible with CircuitPython's displayio.Display API.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, display_bus, *, width, height, rotation=0,
|
|
17
|
+
color_depth=16, auto_refresh=True, backlight_pin=None,
|
|
18
|
+
brightness=1.0, auto_brightness=False, backlight_on_high=True,
|
|
19
|
+
native_frames_per_second=60):
|
|
20
|
+
"""Initialize Display.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
display_bus: Display bus interface (e.g., FourWire)
|
|
24
|
+
width: Display width in pixels
|
|
25
|
+
height: Display height in pixels
|
|
26
|
+
rotation: Display rotation in degrees (0, 90, 180, 270)
|
|
27
|
+
color_depth: Color depth in bits (default 16)
|
|
28
|
+
auto_refresh: Whether to automatically refresh display
|
|
29
|
+
backlight_pin: Pin for backlight control (ignored in simulator)
|
|
30
|
+
brightness: Initial brightness (0.0 to 1.0)
|
|
31
|
+
auto_brightness: Whether to automatically adjust brightness
|
|
32
|
+
backlight_on_high: Whether backlight is on when pin is high
|
|
33
|
+
native_frames_per_second: Native refresh rate
|
|
34
|
+
"""
|
|
35
|
+
self.display_bus = display_bus
|
|
36
|
+
self.width = width
|
|
37
|
+
self.height = height
|
|
38
|
+
self.rotation = rotation
|
|
39
|
+
self.color_depth = color_depth
|
|
40
|
+
self.auto_refresh = auto_refresh
|
|
41
|
+
self._brightness = brightness
|
|
42
|
+
self.auto_brightness = auto_brightness
|
|
43
|
+
self.native_frames_per_second = native_frames_per_second
|
|
44
|
+
|
|
45
|
+
# Current root group
|
|
46
|
+
self.root_group = None
|
|
47
|
+
|
|
48
|
+
# Create LED matrix for rendering
|
|
49
|
+
self._matrix = LEDMatrix(width, height)
|
|
50
|
+
self._matrix.set_brightness(brightness)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def brightness(self):
|
|
54
|
+
"""Get display brightness."""
|
|
55
|
+
return self._brightness
|
|
56
|
+
|
|
57
|
+
@brightness.setter
|
|
58
|
+
def brightness(self, value):
|
|
59
|
+
"""Set display brightness.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
value: Brightness from 0.0 to 1.0
|
|
63
|
+
"""
|
|
64
|
+
self._brightness = max(0.0, min(1.0, value))
|
|
65
|
+
self._matrix.set_brightness(self._brightness)
|
|
66
|
+
|
|
67
|
+
def show(self, group_or_tilegrid):
|
|
68
|
+
"""Show a Group or TileGrid on the display.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
group_or_tilegrid: Group or TileGrid to display
|
|
72
|
+
"""
|
|
73
|
+
self.root_group = group_or_tilegrid
|
|
74
|
+
if self.auto_refresh:
|
|
75
|
+
self.refresh()
|
|
76
|
+
|
|
77
|
+
def refresh(self, *, target_frames_per_second=None, minimum_frames_per_second=1):
|
|
78
|
+
"""Manually refresh the display.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
target_frames_per_second: Target refresh rate
|
|
82
|
+
minimum_frames_per_second: Minimum acceptable refresh rate
|
|
83
|
+
"""
|
|
84
|
+
if self.root_group is None:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Clear the matrix
|
|
88
|
+
self._matrix.clear()
|
|
89
|
+
|
|
90
|
+
# Render the root group
|
|
91
|
+
self._render_group(self.root_group, 0, 0, 1)
|
|
92
|
+
|
|
93
|
+
# Update the display
|
|
94
|
+
self._matrix.render()
|
|
95
|
+
|
|
96
|
+
def _render_group(self, group, offset_x, offset_y, scale):
|
|
97
|
+
"""Recursively render a group and its contents.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
group: Group to render
|
|
101
|
+
offset_x: X offset from parent
|
|
102
|
+
offset_y: Y offset from parent
|
|
103
|
+
scale: Cumulative scale factor
|
|
104
|
+
"""
|
|
105
|
+
if hasattr(group, 'hidden') and group.hidden:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Calculate group position
|
|
109
|
+
group_x = offset_x + (group.x if hasattr(group, 'x') else 0) * scale
|
|
110
|
+
group_y = offset_y + (group.y if hasattr(group, 'y') else 0) * scale
|
|
111
|
+
group_scale = scale * (group.scale if hasattr(group, 'scale') else 1)
|
|
112
|
+
|
|
113
|
+
# Check if this is a Group with items
|
|
114
|
+
if hasattr(group, '_items'):
|
|
115
|
+
# Render all items in the group
|
|
116
|
+
for item in group._items:
|
|
117
|
+
self._render_group(item, group_x, group_y, group_scale)
|
|
118
|
+
# Check if this is a TileGrid
|
|
119
|
+
elif hasattr(group, 'bitmap') and hasattr(group, 'pixel_shader'):
|
|
120
|
+
self._render_tilegrid(group, group_x, group_y, group_scale)
|
|
121
|
+
# Check if this is a Label (from adafruit_display_text)
|
|
122
|
+
elif hasattr(group, '_bitmap') and hasattr(group, 'color'):
|
|
123
|
+
self._render_label(group, group_x, group_y, group_scale)
|
|
124
|
+
|
|
125
|
+
def _render_tilegrid(self, tilegrid, x, y, scale):
|
|
126
|
+
"""Render a TileGrid to the display.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tilegrid: TileGrid to render
|
|
130
|
+
x: X position
|
|
131
|
+
y: Y position
|
|
132
|
+
scale: Scale factor
|
|
133
|
+
"""
|
|
134
|
+
if tilegrid.hidden:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Get bitmap and palette
|
|
138
|
+
bitmap = tilegrid.bitmap
|
|
139
|
+
palette = tilegrid.pixel_shader
|
|
140
|
+
|
|
141
|
+
# Render each tile
|
|
142
|
+
for tile_y in range(tilegrid.height):
|
|
143
|
+
for tile_x in range(tilegrid.width):
|
|
144
|
+
tile_index = tilegrid[tile_x, tile_y]
|
|
145
|
+
|
|
146
|
+
# Calculate tile source position
|
|
147
|
+
src_tile_x = (tile_index % tilegrid._tiles_per_row) * tilegrid.tile_width
|
|
148
|
+
src_tile_y = (tile_index // tilegrid._tiles_per_row) * tilegrid.tile_height
|
|
149
|
+
|
|
150
|
+
# Calculate destination position
|
|
151
|
+
dst_x = x + tile_x * tilegrid.tile_width * scale
|
|
152
|
+
dst_y = y + tile_y * tilegrid.tile_height * scale
|
|
153
|
+
|
|
154
|
+
# Render tile pixels
|
|
155
|
+
for py in range(tilegrid.tile_height):
|
|
156
|
+
for px in range(tilegrid.tile_width):
|
|
157
|
+
# Get pixel from bitmap
|
|
158
|
+
pixel_value = bitmap[src_tile_x + px, src_tile_y + py]
|
|
159
|
+
|
|
160
|
+
# Skip transparent pixels
|
|
161
|
+
if palette.is_transparent(pixel_value):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Get color from palette
|
|
165
|
+
color = palette.get_rgb888(pixel_value)
|
|
166
|
+
|
|
167
|
+
# Apply scaling
|
|
168
|
+
for sy in range(scale):
|
|
169
|
+
for sx in range(scale):
|
|
170
|
+
pixel_x = int(dst_x + px * scale + sx)
|
|
171
|
+
pixel_y = int(dst_y + py * scale + sy)
|
|
172
|
+
|
|
173
|
+
# Set pixel if within bounds
|
|
174
|
+
if 0 <= pixel_x < self.width and 0 <= pixel_y < self.height:
|
|
175
|
+
self._matrix.set_pixel(pixel_x, pixel_y, color)
|
|
176
|
+
|
|
177
|
+
def _render_label(self, label, x, y, scale):
|
|
178
|
+
"""Render a Label to the display.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
label: Label to render
|
|
182
|
+
x: X position
|
|
183
|
+
y: Y position
|
|
184
|
+
scale: Scale factor
|
|
185
|
+
"""
|
|
186
|
+
# Labels are Groups that contain a TileGrid, so render them as groups
|
|
187
|
+
self._render_group(label, x, y, scale)
|
|
188
|
+
|
|
189
|
+
def get_matrix(self):
|
|
190
|
+
"""Get the underlying LED matrix.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
LEDMatrix instance
|
|
194
|
+
"""
|
|
195
|
+
return self._matrix
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""CircuitPython displayio.FourWire equivalent."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FourWire:
|
|
6
|
+
"""FourWire interface for display communication.
|
|
7
|
+
|
|
8
|
+
This is a stub implementation for API compatibility.
|
|
9
|
+
The simulator doesn't need actual SPI communication.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, spi_bus, *, command=None, chip_select=None, reset=None,
|
|
13
|
+
baudrate=24000000, polarity=0, phase=0):
|
|
14
|
+
"""Initialize FourWire interface.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
spi_bus: SPI bus object (ignored in simulator)
|
|
18
|
+
command: Command pin (ignored in simulator)
|
|
19
|
+
chip_select: Chip select pin (ignored in simulator)
|
|
20
|
+
reset: Reset pin (ignored in simulator)
|
|
21
|
+
baudrate: SPI baudrate (ignored in simulator)
|
|
22
|
+
polarity: SPI polarity (ignored in simulator)
|
|
23
|
+
phase: SPI phase (ignored in simulator)
|
|
24
|
+
"""
|
|
25
|
+
self.spi_bus = spi_bus
|
|
26
|
+
self.command = command
|
|
27
|
+
self.chip_select = chip_select
|
|
28
|
+
self.reset = reset
|
|
29
|
+
self.baudrate = baudrate
|
|
30
|
+
self.polarity = polarity
|
|
31
|
+
self.phase = phase
|
|
32
|
+
self._locked = False
|
|
33
|
+
|
|
34
|
+
def reset(self):
|
|
35
|
+
"""Reset the display (no-op in simulator)."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def send(self, command, data):
|
|
39
|
+
"""Send command and data (no-op in simulator).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
command: Command byte
|
|
43
|
+
data: Data bytes
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def __enter__(self):
|
|
48
|
+
"""Context manager entry."""
|
|
49
|
+
self._locked = True
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
53
|
+
"""Context manager exit."""
|
|
54
|
+
self._locked = False
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""CircuitPython displayio.Group equivalent."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Group:
|
|
6
|
+
"""Group organizes display elements into a tree structure.
|
|
7
|
+
|
|
8
|
+
Groups can contain other groups, creating a display hierarchy.
|
|
9
|
+
Elements in a group can be positioned, scaled, and hidden.
|
|
10
|
+
Compatible with CircuitPython's displayio.Group API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, *, scale=1, x=0, y=0):
|
|
14
|
+
"""Initialize Group.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
scale: Integer scale factor (default 1)
|
|
18
|
+
x: X position of group (default 0)
|
|
19
|
+
y: Y position of group (default 0)
|
|
20
|
+
"""
|
|
21
|
+
self.scale = scale
|
|
22
|
+
self.x = x
|
|
23
|
+
self.y = y
|
|
24
|
+
self.hidden = False
|
|
25
|
+
self._items = []
|
|
26
|
+
|
|
27
|
+
def append(self, item):
|
|
28
|
+
"""Add an item to the group.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
item: Display object (Group, TileGrid, etc.) to add
|
|
32
|
+
"""
|
|
33
|
+
if item in self._items:
|
|
34
|
+
raise ValueError("Item already in group")
|
|
35
|
+
self._items.append(item)
|
|
36
|
+
|
|
37
|
+
def insert(self, index, item):
|
|
38
|
+
"""Insert an item at a specific position.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
index: Position to insert at
|
|
42
|
+
item: Display object to insert
|
|
43
|
+
"""
|
|
44
|
+
if item in self._items:
|
|
45
|
+
raise ValueError("Item already in group")
|
|
46
|
+
self._items.insert(index, item)
|
|
47
|
+
|
|
48
|
+
def remove(self, item):
|
|
49
|
+
"""Remove an item from the group.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
item: Display object to remove
|
|
53
|
+
"""
|
|
54
|
+
self._items.remove(item)
|
|
55
|
+
|
|
56
|
+
def pop(self, index=-1):
|
|
57
|
+
"""Remove and return an item.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
index: Index of item to remove (default -1 for last item)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Removed display object
|
|
64
|
+
"""
|
|
65
|
+
return self._items.pop(index)
|
|
66
|
+
|
|
67
|
+
def index(self, item):
|
|
68
|
+
"""Find the index of an item.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
item: Display object to find
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Index of item in group
|
|
75
|
+
"""
|
|
76
|
+
return self._items.index(item)
|
|
77
|
+
|
|
78
|
+
def __len__(self):
|
|
79
|
+
"""Get number of items in group."""
|
|
80
|
+
return len(self._items)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, index):
|
|
83
|
+
"""Get item at index.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
index: Index of item to get
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Display object at index
|
|
90
|
+
"""
|
|
91
|
+
return self._items[index]
|
|
92
|
+
|
|
93
|
+
def __setitem__(self, index, item):
|
|
94
|
+
"""Replace item at index.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
index: Index to replace at
|
|
98
|
+
item: New display object
|
|
99
|
+
"""
|
|
100
|
+
# Remove old item if it exists elsewhere in group
|
|
101
|
+
if item in self._items:
|
|
102
|
+
self._items.remove(item)
|
|
103
|
+
self._items[index] = item
|
|
104
|
+
|
|
105
|
+
def __iter__(self):
|
|
106
|
+
"""Iterate over items in group."""
|
|
107
|
+
return iter(self._items)
|
|
108
|
+
|
|
109
|
+
def sort(self, key=None):
|
|
110
|
+
"""Sort items in group.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
key: Optional key function for sorting
|
|
114
|
+
"""
|
|
115
|
+
self._items.sort(key=key)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def auto_write(self):
|
|
119
|
+
"""Get auto_write setting (always True for compatibility)."""
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
@auto_write.setter
|
|
123
|
+
def auto_write(self, value):
|
|
124
|
+
"""Set auto_write (no-op for compatibility)."""
|
|
125
|
+
pass # No-op for API compatibility
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""CircuitPython displayio.OnDiskBitmap equivalent."""
|
|
3
|
+
|
|
4
|
+
from PIL import Image
|
|
5
|
+
import numpy as np
|
|
6
|
+
from .bitmap import Bitmap
|
|
7
|
+
from .palette import Palette
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OnDiskBitmap:
|
|
11
|
+
"""Load bitmap images from disk.
|
|
12
|
+
|
|
13
|
+
Supports loading BMP and other image formats from disk.
|
|
14
|
+
Creates a bitmap and palette from the loaded image.
|
|
15
|
+
Compatible with CircuitPython's displayio.OnDiskBitmap API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, file_path):
|
|
19
|
+
"""Initialize OnDiskBitmap from file.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
file_path: Path to image file
|
|
23
|
+
"""
|
|
24
|
+
self.file_path = file_path
|
|
25
|
+
self._load_image()
|
|
26
|
+
|
|
27
|
+
def _load_image(self):
|
|
28
|
+
"""Load image from disk and create bitmap/palette."""
|
|
29
|
+
# Load image using PIL
|
|
30
|
+
img = Image.open(self.file_path)
|
|
31
|
+
|
|
32
|
+
# Convert to RGB if needed
|
|
33
|
+
if img.mode != 'RGB':
|
|
34
|
+
if img.mode == 'P':
|
|
35
|
+
# Palette mode - extract palette
|
|
36
|
+
img = img.convert('RGB')
|
|
37
|
+
elif img.mode == 'RGBA':
|
|
38
|
+
# Convert RGBA to RGB with white background
|
|
39
|
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
40
|
+
background.paste(img, mask=img.split()[3])
|
|
41
|
+
img = background
|
|
42
|
+
else:
|
|
43
|
+
img = img.convert('RGB')
|
|
44
|
+
|
|
45
|
+
# Get image dimensions
|
|
46
|
+
self.width = img.width
|
|
47
|
+
self.height = img.height
|
|
48
|
+
|
|
49
|
+
# Convert to numpy array
|
|
50
|
+
img_array = np.array(img)
|
|
51
|
+
|
|
52
|
+
# Create palette with unique colors (up to 256)
|
|
53
|
+
unique_colors = {}
|
|
54
|
+
color_index = 0
|
|
55
|
+
|
|
56
|
+
# Create bitmap
|
|
57
|
+
self.bitmap = Bitmap(self.width, self.height, 256)
|
|
58
|
+
|
|
59
|
+
# Process pixels and build palette
|
|
60
|
+
for y in range(self.height):
|
|
61
|
+
for x in range(self.width):
|
|
62
|
+
color = tuple(img_array[y, x])
|
|
63
|
+
|
|
64
|
+
if color not in unique_colors:
|
|
65
|
+
if color_index < 256:
|
|
66
|
+
unique_colors[color] = color_index
|
|
67
|
+
color_index += 1
|
|
68
|
+
else:
|
|
69
|
+
# Find closest existing color
|
|
70
|
+
closest_idx = self._find_closest_color(color, unique_colors)
|
|
71
|
+
unique_colors[color] = closest_idx
|
|
72
|
+
|
|
73
|
+
self.bitmap[x, y] = unique_colors[color]
|
|
74
|
+
|
|
75
|
+
# Create palette. Hand Palette the plain-int RGB888 tuple and let
|
|
76
|
+
# Palette.__setitem__ do the single, correct RGB565 conversion. (Passing
|
|
77
|
+
# rgb888_to_rgb565() the raw numpy uint8 channels overflowed the dtype —
|
|
78
|
+
# (r & 0xF8) << 8 wrapped to 0 — zeroing red/green so every image went
|
|
79
|
+
# blue; it also double-converted, since __setitem__ re-converts ints.)
|
|
80
|
+
self.palette = Palette(len(unique_colors))
|
|
81
|
+
for color, idx in unique_colors.items():
|
|
82
|
+
self.palette[idx] = (int(color[0]), int(color[1]), int(color[2]))
|
|
83
|
+
|
|
84
|
+
def _find_closest_color(self, target_color, color_dict):
|
|
85
|
+
"""Find closest color in palette.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
target_color: Target RGB color tuple
|
|
89
|
+
color_dict: Dictionary of existing colors
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Index of closest color
|
|
93
|
+
"""
|
|
94
|
+
min_distance = float('inf')
|
|
95
|
+
closest_idx = 0
|
|
96
|
+
|
|
97
|
+
for color, idx in color_dict.items():
|
|
98
|
+
# Calculate color distance (simple RGB distance)
|
|
99
|
+
distance = sum((a - b) ** 2 for a, b in zip(target_color, color))
|
|
100
|
+
if distance < min_distance:
|
|
101
|
+
min_distance = distance
|
|
102
|
+
closest_idx = idx
|
|
103
|
+
|
|
104
|
+
return closest_idx
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def pixel_shader(self):
|
|
108
|
+
"""Get the palette (alias for CircuitPython compatibility)."""
|
|
109
|
+
return self.palette
|