IS-Matrix-Forge 1.0.0.dev1__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.
- is_matrix_forge/__init__.py +8 -0
- is_matrix_forge/assets/__init__.py +8 -0
- is_matrix_forge/assets/audio/__init__.py +36 -0
- is_matrix_forge/assets/audio/files/__init__.py +53 -0
- is_matrix_forge/assets/audio/files/plugged.wav +0 -0
- is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
- is_matrix_forge/assets/char_map.json +2150 -0
- is_matrix_forge/assets/font_map.py +119 -0
- is_matrix_forge/common/__init__.py +0 -0
- is_matrix_forge/common/decorators/__init__.py +152 -0
- is_matrix_forge/common/dirs.py +20 -0
- is_matrix_forge/common/helpers/__init__.py +132 -0
- is_matrix_forge/common/helpers/github_api.py +67 -0
- is_matrix_forge/designer_gui/__init__.py +8 -0
- is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
- is_matrix_forge/designer_gui/main_window/layout.py +117 -0
- is_matrix_forge/dev_tools/__init__.py +8 -0
- is_matrix_forge/dev_tools/debug.py +82 -0
- is_matrix_forge/dev_tools/presets.py +47 -0
- is_matrix_forge/inputmodule/__init__.py +164 -0
- is_matrix_forge/inputmodule/b1display.py +160 -0
- is_matrix_forge/inputmodule/c1minimal.py +35 -0
- is_matrix_forge/inputmodule/font.py +2166 -0
- is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
- is_matrix_forge/inputmodule/ledmatrix.py +516 -0
- is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
- is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
- is_matrix_forge/led_matrix/__init__.py +7 -0
- is_matrix_forge/led_matrix/commands/__init__.py +84 -0
- is_matrix_forge/led_matrix/commands/map.py +46 -0
- is_matrix_forge/led_matrix/constants.py +82 -0
- is_matrix_forge/led_matrix/controller/__init__.py +26 -0
- is_matrix_forge/led_matrix/controller/controller.py +635 -0
- is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
- is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
- is_matrix_forge/led_matrix/controller/multiton.py +32 -0
- is_matrix_forge/led_matrix/controller/threaded.py +72 -0
- is_matrix_forge/led_matrix/display/__init__.py +13 -0
- is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
- is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
- is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
- is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
- is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
- is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
- is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
- is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
- is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
- is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
- is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
- is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
- is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
- is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
- is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
- is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
- is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
- is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
- is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
- is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
- is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
- is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
- is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
- is_matrix_forge/led_matrix/display/media.py +199 -0
- is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
- is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
- is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
- is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
- is_matrix_forge/led_matrix/errors/__init__.py +4 -0
- is_matrix_forge/led_matrix/errors/base.py +14 -0
- is_matrix_forge/led_matrix/errors/grid.py +37 -0
- is_matrix_forge/led_matrix/errors/matrix.py +68 -0
- is_matrix_forge/led_matrix/errors/misc.py +36 -0
- is_matrix_forge/led_matrix/fonts.py +20 -0
- is_matrix_forge/led_matrix/hardware.py +172 -0
- is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
- is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
- is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
- is_matrix_forge/led_matrix/helpers/device.py +118 -0
- is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
- is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
- is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
- is_matrix_forge/led_matrix/led_matrix.py +25 -0
- is_matrix_forge/led_matrix/status.py +8 -0
- is_matrix_forge/led_matrix/tools/__init__.py +8 -0
- is_matrix_forge/led_matrix/tools/identify.py +27 -0
- is_matrix_forge/led_matrix/utils/__init__.py +41 -0
- is_matrix_forge/log_engine.py +25 -0
- is_matrix_forge/monitor/__init__.py +68 -0
- is_matrix_forge/monitor/errors.py +35 -0
- is_matrix_forge/monitor/events.py +78 -0
- is_matrix_forge/monitor/gui/__init__.py +0 -0
- is_matrix_forge/monitor/gui/errors.py +35 -0
- is_matrix_forge/monitor/gui/event/__init__.py +8 -0
- is_matrix_forge/monitor/gui/event/collection.py +153 -0
- is_matrix_forge/monitor/gui/event/errors.py +58 -0
- is_matrix_forge/monitor/gui/event/event.py +226 -0
- is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
- is_matrix_forge/monitor/gui/layout/base.py +147 -0
- is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
- is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
- is_matrix_forge/monitor/gui/windows/base.py +325 -0
- is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
- is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
- is_matrix_forge/monitor/helpers.py +55 -0
- is_matrix_forge/monitor/monitor.py +431 -0
- is_matrix_forge/notify/__init__.py +0 -0
- is_matrix_forge/notify/sounds/__init__.py +6 -0
- is_matrix_forge/notify/sounds/base.py +106 -0
- is_matrix_forge/progress.py +99 -0
- is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
- is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
- is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
- is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
- is_matrix_forge-1.0.0.dev1.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from is_matrix_forge.common.decorators import freeze_setter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Breather:
|
|
7
|
+
"""
|
|
8
|
+
Controls a “breathing” (fade in/out) effect on a controller’s brightness.
|
|
9
|
+
|
|
10
|
+
Parameters:
|
|
11
|
+
controller:
|
|
12
|
+
Any object with a mutable `brightness` attribute (0–100).
|
|
13
|
+
|
|
14
|
+
min_brightness (int):
|
|
15
|
+
Lowest brightness in the cycle. Defaults to 5.
|
|
16
|
+
|
|
17
|
+
max_brightness (int):
|
|
18
|
+
Highest brightness in the cycle. Defaults to 90.
|
|
19
|
+
|
|
20
|
+
step (int):
|
|
21
|
+
Amount to change brightness each tick. Defaults to 5.
|
|
22
|
+
|
|
23
|
+
breathe_fps (float):
|
|
24
|
+
How many brightness updates per second. Defaults to 30.
|
|
25
|
+
|
|
26
|
+
Methods:
|
|
27
|
+
|
|
28
|
+
start():
|
|
29
|
+
Launch the breathing loop in a background thread.
|
|
30
|
+
|
|
31
|
+
stop():
|
|
32
|
+
Halt the breathing loop and join the thread.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
controller,
|
|
37
|
+
min_brightness: int = 5,
|
|
38
|
+
max_brightness: int = 90,
|
|
39
|
+
step: int = 5,
|
|
40
|
+
breathe_fps: float = 30.0
|
|
41
|
+
):
|
|
42
|
+
self._controller = None
|
|
43
|
+
self._min_brightness = None
|
|
44
|
+
self._max_brightness = None
|
|
45
|
+
self._step = None
|
|
46
|
+
self._fps = None
|
|
47
|
+
|
|
48
|
+
self.controller = controller
|
|
49
|
+
|
|
50
|
+
self.__initial_brightness = self.controller.brightness
|
|
51
|
+
|
|
52
|
+
self.min_brightness = min_brightness
|
|
53
|
+
self.max_brightness = max_brightness
|
|
54
|
+
self.step = step
|
|
55
|
+
self.fps = breathe_fps
|
|
56
|
+
|
|
57
|
+
self._breathing = False
|
|
58
|
+
self._thread = None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def breathing(self) -> bool:
|
|
62
|
+
return self._breathing
|
|
63
|
+
|
|
64
|
+
@breathing.setter
|
|
65
|
+
def breathing(self, new: bool) -> None:
|
|
66
|
+
if not isinstance(new, bool):
|
|
67
|
+
raise TypeError(f'"breathing" must be of type `bool`, not {type(new)}')
|
|
68
|
+
|
|
69
|
+
if self.breathing and not new:
|
|
70
|
+
self.stop()
|
|
71
|
+
elif not self.breathing and new:
|
|
72
|
+
self.start()
|
|
73
|
+
|
|
74
|
+
self._breathing = new
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def controller(self) -> 'LEDMatrixController':
|
|
79
|
+
"""
|
|
80
|
+
The controller for the LED matrix device.
|
|
81
|
+
"""
|
|
82
|
+
return self._controller
|
|
83
|
+
|
|
84
|
+
@controller.setter
|
|
85
|
+
@freeze_setter()
|
|
86
|
+
def controller(self, new):
|
|
87
|
+
from is_matrix_forge.led_matrix import LEDMatrixController
|
|
88
|
+
if not isinstance(new, LEDMatrixController):
|
|
89
|
+
raise TypeError(f'controller must be LEDMatrixController, not {type(new)}')
|
|
90
|
+
self._controller = new
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def initial_brightness(self) -> int:
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
.. note::
|
|
97
|
+
This is a read-only property.
|
|
98
|
+
"""
|
|
99
|
+
return self.__initial_brightness
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def min_brightness(self) -> int:
|
|
103
|
+
"""
|
|
104
|
+
The minimum LED brightness in the cycle.
|
|
105
|
+
"""
|
|
106
|
+
return self._min_brightness
|
|
107
|
+
|
|
108
|
+
@min_brightness.setter
|
|
109
|
+
def min_brightness(self, new):
|
|
110
|
+
if not isinstance(new, int):
|
|
111
|
+
raise TypeError('"min_brightness" must be int')
|
|
112
|
+
if not 0 <= new <= 100:
|
|
113
|
+
raise ValueError('"min_brightness" must be between 0 and 100')
|
|
114
|
+
if self._max_brightness is not None and new > self._max_brightness:
|
|
115
|
+
raise ValueError('"min_brightness" must be <= max_brightness')
|
|
116
|
+
self._min_brightness = new
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def max_brightness(self) -> int:
|
|
120
|
+
"""
|
|
121
|
+
The maximum LED brightness in the cycle.
|
|
122
|
+
"""
|
|
123
|
+
return self._max_brightness
|
|
124
|
+
|
|
125
|
+
@max_brightness.setter
|
|
126
|
+
def max_brightness(self, new):
|
|
127
|
+
if not isinstance(new, int):
|
|
128
|
+
raise TypeError('"max_brightness" must be int')
|
|
129
|
+
if not 0 <= new <= 100:
|
|
130
|
+
raise ValueError('"max_brightness" must be between 0 and 100')
|
|
131
|
+
if self._min_brightness is not None and new < self._min_brightness:
|
|
132
|
+
raise ValueError('"max_brightness" must be >= min_brightness')
|
|
133
|
+
self._max_brightness = new
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def step(self) -> int:
|
|
137
|
+
"""
|
|
138
|
+
Amount to change brightness each tick.
|
|
139
|
+
"""
|
|
140
|
+
return self._step
|
|
141
|
+
|
|
142
|
+
@step.setter
|
|
143
|
+
def step(self, new):
|
|
144
|
+
if not isinstance(new, int):
|
|
145
|
+
raise TypeError('"step" must be int')
|
|
146
|
+
if new <= 0 or new > 100:
|
|
147
|
+
raise ValueError('"step" must be > 0 and <= 100')
|
|
148
|
+
self._step = new
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def fps(self) -> float:
|
|
152
|
+
"""
|
|
153
|
+
Frames per second for the breathing effect.
|
|
154
|
+
"""
|
|
155
|
+
return self._fps
|
|
156
|
+
|
|
157
|
+
@fps.setter
|
|
158
|
+
def fps(self, new):
|
|
159
|
+
if not isinstance(new, (int, float)):
|
|
160
|
+
raise TypeError('"fps" must be a number')
|
|
161
|
+
if new <= 0:
|
|
162
|
+
raise ValueError('"fps" must be > 0')
|
|
163
|
+
self._fps = float(new)
|
|
164
|
+
|
|
165
|
+
def _breath_loop(self):
|
|
166
|
+
interval = 1.0 / self._fps
|
|
167
|
+
# start at current brightness, clamped
|
|
168
|
+
current = max(self._min_brightness,
|
|
169
|
+
min(self._controller.brightness, self._max_brightness))
|
|
170
|
+
going_up = True
|
|
171
|
+
|
|
172
|
+
while self._breathing:
|
|
173
|
+
if going_up:
|
|
174
|
+
current += self._step
|
|
175
|
+
if current >= self._max_brightness:
|
|
176
|
+
current = self._max_brightness
|
|
177
|
+
going_up = False
|
|
178
|
+
else:
|
|
179
|
+
current -= self._step
|
|
180
|
+
if current <= self._min_brightness:
|
|
181
|
+
current = self._min_brightness
|
|
182
|
+
going_up = True
|
|
183
|
+
|
|
184
|
+
self._controller.brightness = current
|
|
185
|
+
time.sleep(interval)
|
|
186
|
+
|
|
187
|
+
def start(self):
|
|
188
|
+
"""
|
|
189
|
+
Begin the breathing effect in a background daemon thread.
|
|
190
|
+
No effect if already running.
|
|
191
|
+
"""
|
|
192
|
+
if not self._breathing:
|
|
193
|
+
self._breathing = True
|
|
194
|
+
self._thread = threading.Thread(target=self._breath_loop, daemon=True)
|
|
195
|
+
self._thread.start()
|
|
196
|
+
|
|
197
|
+
def stop(self):
|
|
198
|
+
"""
|
|
199
|
+
Stop the breathing effect and wait for the thread to finish.
|
|
200
|
+
"""
|
|
201
|
+
self._breathing = False
|
|
202
|
+
if self._thread:
|
|
203
|
+
self._thread.join()
|
|
204
|
+
self._thread = None
|
|
205
|
+
|
|
206
|
+
self.controller.brightness = self.initial_brightness
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from is_matrix_forge.common.decorators import freeze_setter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HardwareScroller:
|
|
11
|
+
"""
|
|
12
|
+
This class just manages the native 'animation' for the LED matrix.
|
|
13
|
+
|
|
14
|
+
This simply wraps commands to the LED matrix that can;
|
|
15
|
+
- Query the current state of the 'animating' flag
|
|
16
|
+
- Set the 'animating' flag
|
|
17
|
+
"""
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
controller,
|
|
21
|
+
do_not_keep_lit: bool = False
|
|
22
|
+
):
|
|
23
|
+
self._controller = None
|
|
24
|
+
self.__keep_lit = None
|
|
25
|
+
|
|
26
|
+
self.controller = controller
|
|
27
|
+
self.keep_lit = not do_not_keep_lit
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def controller(self):
|
|
31
|
+
"""
|
|
32
|
+
The controller (therefore the hardware) this scroller is managing.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The controller object.
|
|
36
|
+
"""
|
|
37
|
+
return self._controller
|
|
38
|
+
|
|
39
|
+
@controller.setter
|
|
40
|
+
@freeze_setter(allow_reassignment_to_same_value=True)
|
|
41
|
+
def controller(self, new: LEDMatrixController):
|
|
42
|
+
"""
|
|
43
|
+
The controller (therefore the hardware) this scroller is managing.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
new (LEDMatrixController):
|
|
47
|
+
The controller object.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
AssertionError:
|
|
54
|
+
If `new` isn't an instance of :class:`LEDMatrixController`.
|
|
55
|
+
|
|
56
|
+
See Also:
|
|
57
|
+
- :func:`~is_matrix_forge.common.decorators.freeze_setter`
|
|
58
|
+
- :meth:`controller`
|
|
59
|
+
- :attr:`controller`
|
|
60
|
+
- :class:`LEDMatrixController`
|
|
61
|
+
"""
|
|
62
|
+
from is_matrix_forge.led_matrix import LEDMatrixController
|
|
63
|
+
assert isinstance(new, LEDMatrixController), f"Expected {LEDMatrixController}, got {type(new)}"
|
|
64
|
+
|
|
65
|
+
self._controller = new
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_scrolling(self) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Checks if the LED is currently animating (meaning scrolling).
|
|
71
|
+
|
|
72
|
+
.. note::
|
|
73
|
+
This is a read-only property.
|
|
74
|
+
"""
|
|
75
|
+
return self.controller.animating
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def keep_lit(self) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Should the LED matrix be kept awake while scrolling?
|
|
82
|
+
"""
|
|
83
|
+
return self.__keep_lit
|
|
84
|
+
|
|
85
|
+
@keep_lit.setter
|
|
86
|
+
def keep_lit(self, new):
|
|
87
|
+
if not isinstance(new, bool):
|
|
88
|
+
raise TypeError('"keep_lit" must be of type "bool"')
|
|
89
|
+
self.__keep_lit = new
|
|
90
|
+
|
|
91
|
+
def start_scrolling(self):
|
|
92
|
+
pass
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
4
|
+
from is_matrix_forge.common.decorators import freeze_setter
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from is_matrix_forge.led_matrix import LEDMatrixController
|
|
8
|
+
|
|
9
|
+
# Make our base class generic so you can subclass it for other scrollers, too
|
|
10
|
+
C = TypeVar('C', bound='LEDMatrixController')
|
|
11
|
+
|
|
12
|
+
class Scroller(ABC, Generic[C]):
|
|
13
|
+
"""
|
|
14
|
+
Base functionality for scrolling-like animations on an LEDMatrixController.
|
|
15
|
+
|
|
16
|
+
Subclasses only need to implement start_scrolling (and stop_scrolling, if desired).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
controller: C,
|
|
22
|
+
*,
|
|
23
|
+
keep_lit: bool = True
|
|
24
|
+
):
|
|
25
|
+
self._controller: C | None = None
|
|
26
|
+
self._keep_lit: bool | None = None
|
|
27
|
+
|
|
28
|
+
# these setters enforce type checks/freeze behavior
|
|
29
|
+
self.controller = controller
|
|
30
|
+
self.keep_lit = keep_lit
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def controller(self) -> C:
|
|
34
|
+
"""The LEDMatrixController instance used for scrolling."""
|
|
35
|
+
return self._controller # type: ignore
|
|
36
|
+
|
|
37
|
+
@controller.setter
|
|
38
|
+
@freeze_setter(allow_reassignment_to_same_value=True)
|
|
39
|
+
def controller(self, new: C) -> None:
|
|
40
|
+
from is_matrix_forge.led_matrix.controller import LEDMatrixController
|
|
41
|
+
assert isinstance(new, LEDMatrixController), (
|
|
42
|
+
f"Expected LEDMatrixController, got {type(new).__name__}"
|
|
43
|
+
)
|
|
44
|
+
self._controller = new
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_scrolling(self) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Whether a scroll animation is currently in progress.
|
|
50
|
+
"""
|
|
51
|
+
return bool(self.controller.animating)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def keep_lit(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
If True, keep the LED matrix powered while scrolling.
|
|
57
|
+
"""
|
|
58
|
+
return self._keep_lit # type: ignore
|
|
59
|
+
|
|
60
|
+
@keep_lit.setter
|
|
61
|
+
def keep_lit(self, new: bool) -> None:
|
|
62
|
+
if not isinstance(new, bool):
|
|
63
|
+
raise TypeError('"keep_lit" must be a bool')
|
|
64
|
+
self._keep_lit = new
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def start_scrolling(self) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Begin the scroll animation. Must be implemented by subclasses.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def stop_scrolling(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Stop the scroll animation. Optional override by subclasses.
|
|
75
|
+
"""
|
|
76
|
+
self.controller.animating = False
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from easy_exit_calls import ExitCallHandler
|
|
2
|
+
from is_matrix_forge.common.decorators import freeze_setter, validate_type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ECH = ExitCallHandler()
|
|
6
|
+
DEFAULT_TIME_BETWEEN_JIGGLES = 50
|
|
7
|
+
MAX_TIME_BETWEEN_JIGGLES = 59 # Matrix goes to sleep after 60 seconds of command inactivity, so we need to jiggle
|
|
8
|
+
# at least once every minute
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MatrixJiggler:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
controller = None,
|
|
15
|
+
power_monitor = None,
|
|
16
|
+
interval: int = None,
|
|
17
|
+
do_not_clear_on_stop: bool = False,
|
|
18
|
+
|
|
19
|
+
):
|
|
20
|
+
if controller and power_monitor:
|
|
21
|
+
raise ValueError("Cannot specify both `controller` and `power_monitor`")
|
|
22
|
+
|
|
23
|
+
self.controller = controller
|
|
24
|
+
self.power_monitor = power_monitor
|
|
25
|
+
self.clear_on_stop = not do_not_clear_on_stop
|
|
26
|
+
self._controller = None
|
|
27
|
+
self.__interval = None
|
|
28
|
+
self.__last_jiggle_time = None
|
|
29
|
+
self._power_monitor = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def controller(self):
|
|
33
|
+
return self._controller
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def interval(self) -> float:
|
|
37
|
+
return self.__interval
|
|
38
|
+
|
|
39
|
+
@interval.setter
|
|
40
|
+
@validate_type([float, int], float)
|
|
41
|
+
def interval(self, new):
|
|
42
|
+
self.__interval = new
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def power_monitor(self):
|
|
46
|
+
return self._power_monitor
|
|
47
|
+
|
|
48
|
+
@power_monitor.setter
|
|
49
|
+
@freeze_setter()
|
|
50
|
+
def power_monitor(self, new):
|
|
51
|
+
from is_matrix_forge.monitor.monitor import PowerMonitor
|
|
52
|
+
|
|
53
|
+
if not isinstance(new, PowerMonitor):
|
|
54
|
+
raise TypeError(f"Expected {PowerMonitor}, got {type(new)}")
|
|
55
|
+
|
|
56
|
+
self._controller = new.controller
|
|
57
|
+
|
|
58
|
+
self._power_monitor = new
|
|
59
|
+
|
|
60
|
+
def cleanup(self):
|
|
61
|
+
if self.clear_on_stop:
|
|
62
|
+
self.controller.clear()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grid module for LED Matrix display.
|
|
3
|
+
|
|
4
|
+
This module provides the Grid class for representing and manipulating
|
|
5
|
+
the LED matrix display grid. It includes functionality for creating,
|
|
6
|
+
loading, and modifying grid patterns.
|
|
7
|
+
"""
|
|
8
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import generate_blank_grid
|
|
9
|
+
from is_matrix_forge.led_matrix.display.grid.grid import Grid
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# is_matrix_forge.led_matrix.display.grid.grid
|
|
2
|
+
"""
|
|
3
|
+
Grid module for LED Matrix display.
|
|
4
|
+
|
|
5
|
+
Author:
|
|
6
|
+
Inspyre Softworks
|
|
7
|
+
|
|
8
|
+
Project:
|
|
9
|
+
led-matrix-battery
|
|
10
|
+
|
|
11
|
+
File:
|
|
12
|
+
is_matrix_forge/led_matrix/display/grid/grid.py
|
|
13
|
+
|
|
14
|
+
Description:
|
|
15
|
+
This module provides the Grid class which represents a 2D grid of pixels
|
|
16
|
+
for the LED matrix display. It handles grid creation, manipulation, and
|
|
17
|
+
loading from files.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import itertools
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional, Union, ClassVar, Type, Any, Dict # Added Any, Dict
|
|
23
|
+
from ...constants import WIDTH as __WIDTH, HEIGHT as __HEIGHT, PRESETS_DIR
|
|
24
|
+
from .helpers import is_valid_grid, generate_blank_grid
|
|
25
|
+
from is_matrix_forge.common.helpers import coerce_to_int
|
|
26
|
+
|
|
27
|
+
MATRIX_HEIGHT = __HEIGHT
|
|
28
|
+
"""int: Height of the LED matrix grid in pixels.
|
|
29
|
+
This constant defines the default height for new Grid instances and for loading operations.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
MATRIX_WIDTH = __WIDTH
|
|
33
|
+
"""int: Width of the LED matrix grid in pixels.
|
|
34
|
+
This constant defines the default width for new Grid instances and for loading operations.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Grid:
|
|
39
|
+
"""
|
|
40
|
+
Represents a 2D column-major grid for the LED display (grid[x][y], 9×34).
|
|
41
|
+
Provides methods for grid creation, manipulation, and loading from files.
|
|
42
|
+
|
|
43
|
+
The Grid class allows for the management of pixel data in a column-major format,
|
|
44
|
+
supporting operations such as shifting, drawing, and loading from specifications or files.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
width: int = MATRIX_WIDTH,
|
|
50
|
+
height: int = MATRIX_HEIGHT,
|
|
51
|
+
fill_value: int = 0,
|
|
52
|
+
init_grid: List[List[int]] = None
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Initialize a Grid. If `init_grid` is provided, it must be column-major
|
|
56
|
+
with shape (width × height). Otherwise, create a blank grid.
|
|
57
|
+
"""
|
|
58
|
+
if init_grid is not None:
|
|
59
|
+
if not is_valid_grid(init_grid, width, height):
|
|
60
|
+
raise ValueError(f"init_grid must be {width}×{height} column-major 0/1 list")
|
|
61
|
+
self._grid = [col[:] for col in init_grid]
|
|
62
|
+
else:
|
|
63
|
+
if fill_value not in (0, 1):
|
|
64
|
+
raise ValueError("fill_value must be 0 or 1")
|
|
65
|
+
self._grid = generate_blank_grid(width=width, height=height, fill_value=fill_value)
|
|
66
|
+
|
|
67
|
+
self._width = width
|
|
68
|
+
self._height = height
|
|
69
|
+
self._fill_value = fill_value
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def grid(self) -> List[List[int]]:
|
|
73
|
+
"""Defensive copy of the column-major grid data."""
|
|
74
|
+
return [col[:] for col in self._grid]
|
|
75
|
+
|
|
76
|
+
@grid.setter
|
|
77
|
+
def grid(self, value: List[List[int]]) -> None:
|
|
78
|
+
"""Replace the internal grid; must be column-major and correct shape."""
|
|
79
|
+
if not is_valid_grid(value, self._width, self._height):
|
|
80
|
+
raise ValueError(f"grid must be {self._width}×{self._height} column-major 0/1 list")
|
|
81
|
+
self._grid = [col[:] for col in value]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def width(self) -> int:
|
|
85
|
+
"""Number of columns."""
|
|
86
|
+
return self._width
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def height(self) -> int:
|
|
90
|
+
"""Number of rows."""
|
|
91
|
+
return self._height
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def fill_value(self) -> int:
|
|
95
|
+
"""Default pixel fill (0 or 1)."""
|
|
96
|
+
return self._fill_value
|
|
97
|
+
|
|
98
|
+
@fill_value.setter
|
|
99
|
+
def fill_value(self, v: int) -> None:
|
|
100
|
+
"""Set default pixel fill; must be 0 or 1."""
|
|
101
|
+
if not isinstance(v, int) or v not in (0, 1):
|
|
102
|
+
raise ValueError("fill_value must be 0 or 1")
|
|
103
|
+
self._fill_value = v
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def cols(self) -> int:
|
|
107
|
+
"""Alias for width."""
|
|
108
|
+
return self._width
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def rows(self) -> int:
|
|
112
|
+
"""Alias for height."""
|
|
113
|
+
return self._height
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def load_blank_grid(
|
|
117
|
+
cls,
|
|
118
|
+
width: int = MATRIX_WIDTH,
|
|
119
|
+
height: int = MATRIX_HEIGHT,
|
|
120
|
+
fill_value: int = 0
|
|
121
|
+
) -> List[List[int]]:
|
|
122
|
+
"""Return a new blank column-major grid."""
|
|
123
|
+
return generate_blank_grid(width=width, height=height, fill_value=fill_value)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_spec(
|
|
127
|
+
cls,
|
|
128
|
+
spec: List[List[int]]
|
|
129
|
+
) -> 'Grid':
|
|
130
|
+
"""Instantiate directly from a column-major spec list."""
|
|
131
|
+
return cls(init_grid=spec)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_file(
|
|
135
|
+
cls,
|
|
136
|
+
filename: Union[str, Path],
|
|
137
|
+
frame_number: int = 0,
|
|
138
|
+
height: int = MATRIX_HEIGHT,
|
|
139
|
+
width: int = MATRIX_WIDTH
|
|
140
|
+
) -> 'Grid':
|
|
141
|
+
"""
|
|
142
|
+
Load a column-major grid from file (single grid or frames of grids).
|
|
143
|
+
"""
|
|
144
|
+
from ...helpers import load_from_file
|
|
145
|
+
raw = load_from_file(str(filename))
|
|
146
|
+
# Single-grid JSON: list of lists
|
|
147
|
+
if isinstance(raw, list) and raw and isinstance(raw[0], list):
|
|
148
|
+
grid_data = raw
|
|
149
|
+
# Frame-list JSON: list of dicts
|
|
150
|
+
elif isinstance(raw, list) and all(isinstance(f, dict) for f in raw):
|
|
151
|
+
frame = raw[frame_number]
|
|
152
|
+
grid_data = frame.get('grid')
|
|
153
|
+
if not isinstance(grid_data, list):
|
|
154
|
+
raise ValueError(f"Frame {frame_number} missing 'grid' list")
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError("Unsupported file structure for grid data.")
|
|
157
|
+
|
|
158
|
+
if not is_valid_grid(grid_data, width, height):
|
|
159
|
+
raise ValueError(f"Loaded grid is not {width}×{height} column-major 0/1 list")
|
|
160
|
+
|
|
161
|
+
return cls(width=width, height=height, init_grid=grid_data)
|
|
162
|
+
|
|
163
|
+
def draw(self, device: Any) -> None:
|
|
164
|
+
"""Draw this grid via device.draw_grid(grid)."""
|
|
165
|
+
if not hasattr(device, 'draw_grid') or not callable(device.draw_grid):
|
|
166
|
+
raise AttributeError("device.draw_grid(grid) not available")
|
|
167
|
+
device.draw_grid(self)
|
|
168
|
+
|
|
169
|
+
def get_pixel_value(self, x: int, y: int) -> int:
|
|
170
|
+
"""Return the value at column x, row y."""
|
|
171
|
+
if x < 0 or x >= self._width or y < 0 or y >= self._height:
|
|
172
|
+
raise IndexError(f"({x},{y}) out of bounds {self._width}×{self._height}")
|
|
173
|
+
return self._grid[x][y]
|
|
174
|
+
|
|
175
|
+
def get_shifted(
|
|
176
|
+
self,
|
|
177
|
+
dx: int = 0,
|
|
178
|
+
dy: int = 0,
|
|
179
|
+
wrap: bool = False
|
|
180
|
+
) -> 'Grid':
|
|
181
|
+
"""
|
|
182
|
+
Return a new Grid shifted by (dx, dy):
|
|
183
|
+
- dx > 0 moves right, dx < 0 moves left
|
|
184
|
+
- dy > 0 moves down, dy < 0 moves up
|
|
185
|
+
If wrap=True, shifts wrap around edges; otherwise, out-of-bounds fill with fill_value.
|
|
186
|
+
"""
|
|
187
|
+
new = generate_blank_grid(self._width, self._height, self._fill_value)
|
|
188
|
+
|
|
189
|
+
for c, r in itertools.product(range(self._width), range(self._height)):
|
|
190
|
+
src_c = (c + dx) % self._width if wrap else c + dx
|
|
191
|
+
src_r = (r + dy) % self._height if wrap else r + dy
|
|
192
|
+
|
|
193
|
+
if 0 <= src_c < self._width and 0 <= src_r < self._height:
|
|
194
|
+
new[c][r] = self._grid[src_c][src_r]
|
|
195
|
+
|
|
196
|
+
return Grid(width=self._width, height=self._height, fill_value=self._fill_value, init_grid=new)
|
|
197
|
+
|
|
198
|
+
def __getitem__(self, index: int) -> list[int]:
|
|
199
|
+
"""Allow column-major indexing: grid[col] → list of pixel values down that column."""
|
|
200
|
+
return self._grid[index]
|
|
201
|
+
|
|
202
|
+
def __len__(self) -> int:
|
|
203
|
+
"""Number of columns (i.e. grid width)."""
|
|
204
|
+
return len(self._grid)
|
|
205
|
+
|
|
206
|
+
def __iter__(self):
|
|
207
|
+
"""Iterate over columns."""
|
|
208
|
+
return iter(self._grid)
|