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,635 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Author:
|
|
5
|
+
Inspyre Softworks
|
|
6
|
+
|
|
7
|
+
Project:
|
|
8
|
+
led-matrix-battery
|
|
9
|
+
|
|
10
|
+
File:
|
|
11
|
+
${DIR_PATH}/${FILE_NAME}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Description:
|
|
15
|
+
$DESCRIPTION
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
from time import sleep
|
|
22
|
+
from typing import Optional, Dict, Any, Union
|
|
23
|
+
|
|
24
|
+
from inspyre_toolbox.syntactic_sweets.classes import validate_type
|
|
25
|
+
from inspyre_toolbox.syntactic_sweets.classes.decorators.aliases import add_aliases, method_alias
|
|
26
|
+
from serial.tools.list_ports_common import ListPortInfo
|
|
27
|
+
|
|
28
|
+
from is_matrix_forge.common.helpers import coerce_to_int
|
|
29
|
+
from is_matrix_forge.led_matrix.constants import SLOT_MAP
|
|
30
|
+
from is_matrix_forge.led_matrix.display.text import show_string as _show_string_raw
|
|
31
|
+
from is_matrix_forge.led_matrix.controller import MultitonMeta
|
|
32
|
+
from is_matrix_forge.led_matrix.controller.helpers.threading import synchronized
|
|
33
|
+
from is_matrix_forge.led_matrix.commands.map import CommandVals
|
|
34
|
+
from is_matrix_forge.led_matrix.commands import send_command
|
|
35
|
+
from is_matrix_forge.led_matrix.display.effects.breather import Breather
|
|
36
|
+
from is_matrix_forge.led_matrix.display.text import show_string
|
|
37
|
+
|
|
38
|
+
COMMANDS = CommandVals
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@add_aliases
|
|
42
|
+
class LEDMatrixController(metaclass=MultitonMeta):
|
|
43
|
+
"""
|
|
44
|
+
Controller class for LED Matrix devices.
|
|
45
|
+
|
|
46
|
+
This class provides a high-level interface for interacting with LED matrix
|
|
47
|
+
hardware devices. It handles device communication, animation control,
|
|
48
|
+
pattern display, and other operations.
|
|
49
|
+
|
|
50
|
+
Properties:
|
|
51
|
+
device (ListPortInfo):
|
|
52
|
+
The connected LED matrix device.
|
|
53
|
+
|
|
54
|
+
animating (bool):
|
|
55
|
+
Whether the device is currently animating.
|
|
56
|
+
|
|
57
|
+
location (dict):
|
|
58
|
+
Physical location information for the device.
|
|
59
|
+
|
|
60
|
+
location_abbrev (str):
|
|
61
|
+
Abbreviated location identifier.
|
|
62
|
+
|
|
63
|
+
side_of_keyboard (str):
|
|
64
|
+
Which side of the keyboard the device is on.
|
|
65
|
+
|
|
66
|
+
slot (int):
|
|
67
|
+
The slot number of the device.
|
|
68
|
+
"""
|
|
69
|
+
FACTORY_DEFAULT_BRIGHTNESS = 75
|
|
70
|
+
"""
|
|
71
|
+
Default brightness level as a percentage of maximum
|
|
72
|
+
brightness (0-100).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
COMMANDS = COMMANDS
|
|
76
|
+
""" Command map for the LED matrix hardware."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
device,
|
|
81
|
+
default_brightness: Optional[int] = None,
|
|
82
|
+
skip_init_brightness_set: Optional[bool] = False,
|
|
83
|
+
skip_init_clear: Optional[bool] = False,
|
|
84
|
+
init_grid: Optional[Grid] = None,
|
|
85
|
+
do_not_show_grid_on_init: Optional[bool] = False,
|
|
86
|
+
thread_safe: Optional[bool] = False,
|
|
87
|
+
do_not_warn_on_thread_misuse: Optional[bool] = False
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Initialize a new LED Matrix Controller.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
device (ListPortInfo):
|
|
94
|
+
The LED matrix device to control.
|
|
95
|
+
|
|
96
|
+
default_brightness (Union[int, float, str]):
|
|
97
|
+
Default brightness level (0-100).
|
|
98
|
+
"""
|
|
99
|
+
self.__current_animation = None
|
|
100
|
+
if default_brightness is None:
|
|
101
|
+
default_brightness = self.FACTORY_DEFAULT_BRIGHTNESS
|
|
102
|
+
self.__default_brightness = self.__normalize_percent(default_brightness)
|
|
103
|
+
self.__breather = None
|
|
104
|
+
self.__brightness = None
|
|
105
|
+
self.__built_in_patterns = None
|
|
106
|
+
self.__device = None
|
|
107
|
+
self.__grid = None
|
|
108
|
+
self.__init_clear = None
|
|
109
|
+
self.__keep_image = False
|
|
110
|
+
self.__name = None
|
|
111
|
+
self.__set_brightness_on_init = None
|
|
112
|
+
self.__show_grid_on_init = None
|
|
113
|
+
self._thread_safe = None
|
|
114
|
+
self._cmd_lock = None
|
|
115
|
+
|
|
116
|
+
if skip_init_brightness_set is not None:
|
|
117
|
+
if not isinstance(skip_init_brightness_set, bool):
|
|
118
|
+
raise TypeError('skip_init_brightness_set must be a boolean.')
|
|
119
|
+
self.__set_brightness_on_init = not skip_init_brightness_set
|
|
120
|
+
|
|
121
|
+
if skip_init_clear is not None:
|
|
122
|
+
if not isinstance(skip_init_clear, bool):
|
|
123
|
+
raise TypeError('skip_init_clear must be a boolean.')
|
|
124
|
+
self.__init_clear = not skip_init_clear
|
|
125
|
+
|
|
126
|
+
if init_grid is not None:
|
|
127
|
+
if not isinstance(init_grid, Grid):
|
|
128
|
+
raise TypeError(f'init_grid must be of type `Grid`, not {type(init_grid)}')
|
|
129
|
+
|
|
130
|
+
self.__grid = init_grid
|
|
131
|
+
|
|
132
|
+
if do_not_show_grid_on_init is not None:
|
|
133
|
+
if not isinstance(do_not_show_grid_on_init, bool):
|
|
134
|
+
raise TypeError('do_not_show_grid_on_init must be a boolean.')
|
|
135
|
+
self.__show_grid_on_init = not do_not_show_grid_on_init
|
|
136
|
+
|
|
137
|
+
self.device = device
|
|
138
|
+
|
|
139
|
+
self.__set_up_thread_safety__(thread_safe)
|
|
140
|
+
|
|
141
|
+
def __post_init__(self):
|
|
142
|
+
from is_matrix_forge.led_matrix.display.effects.breather import Breather
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
self.__breather = Breather(self)
|
|
146
|
+
|
|
147
|
+
if self.clear_on_init:
|
|
148
|
+
self.clear()
|
|
149
|
+
|
|
150
|
+
if self.set_brightness_on_init:
|
|
151
|
+
self.set_brightness(self.__default_brightness)
|
|
152
|
+
|
|
153
|
+
if self.grid and self.show_grid_on_init:
|
|
154
|
+
self.draw_grid()
|
|
155
|
+
|
|
156
|
+
if not self.__built_in_patterns:
|
|
157
|
+
from is_matrix_forge.led_matrix.display.patterns.built_in import BuiltInPatterns
|
|
158
|
+
self.__built_in_patterns = BuiltInPatterns(self)
|
|
159
|
+
|
|
160
|
+
if self.breather is None:
|
|
161
|
+
self.__breather = Breather(self)
|
|
162
|
+
|
|
163
|
+
def __set_up_thread_safety__(self, thread_safe_opt: bool):
|
|
164
|
+
if thread_safe_opt is None or not thread_safe_opt:
|
|
165
|
+
self._thread_safe = False
|
|
166
|
+
return
|
|
167
|
+
else:
|
|
168
|
+
self._thread_safe = True
|
|
169
|
+
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def animating(self) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check if the device is currently animating.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
bool: True if the device is animating, False otherwise.
|
|
179
|
+
"""
|
|
180
|
+
return bool(send_command(self.device, COMMANDS.Animate, with_response=True)[0])
|
|
181
|
+
# return animate(self.device)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def breather(self) -> Breather:
|
|
185
|
+
"""
|
|
186
|
+
Get the Breather instance associated with the LED matrix controller.
|
|
187
|
+
|
|
188
|
+
.. note::
|
|
189
|
+
This is a read-only property.
|
|
190
|
+
"""
|
|
191
|
+
return self.__breather
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def breathing(self) -> bool:
|
|
195
|
+
return self.breather.breathing
|
|
196
|
+
|
|
197
|
+
@breathing.setter
|
|
198
|
+
def breathing(self, new: bool):
|
|
199
|
+
self.breather.breathing = new
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def brightness(self):
|
|
203
|
+
"""
|
|
204
|
+
Get the current brightness level of the LED matrix.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
int or float: The current brightness value, or the default if not set.
|
|
208
|
+
"""
|
|
209
|
+
return self.__brightness or self.__default_brightness
|
|
210
|
+
|
|
211
|
+
@brightness.setter
|
|
212
|
+
def brightness(self, new):
|
|
213
|
+
self.set_brightness(new, True)
|
|
214
|
+
self.__brightness = new
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def clear_on_init(self):
|
|
218
|
+
return self.__init_clear
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def cmd_lock(self):
|
|
222
|
+
if self._cmd_lock is None and self.thread_safe is True:
|
|
223
|
+
self._cmd_lock = threading.RLock()
|
|
224
|
+
|
|
225
|
+
return self._cmd_lock
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def current_animation(self):
|
|
229
|
+
return self.__current_animation
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def device(self) -> ListPortInfo:
|
|
233
|
+
"""
|
|
234
|
+
Get the current LED matrix device.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
ListPortInfo: The current device.
|
|
238
|
+
"""
|
|
239
|
+
return self.__device
|
|
240
|
+
|
|
241
|
+
@validate_type(ListPortInfo)
|
|
242
|
+
@device.setter
|
|
243
|
+
def device(self, device: ListPortInfo):
|
|
244
|
+
"""
|
|
245
|
+
Set the LED matrix device.
|
|
246
|
+
|
|
247
|
+
Parameters:
|
|
248
|
+
device (ListPortInfo):
|
|
249
|
+
The device to control.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
ValueError:
|
|
253
|
+
If `device` is `None` or empty.
|
|
254
|
+
"""
|
|
255
|
+
if not device:
|
|
256
|
+
raise ValueError('Device cannot be None or empty.')
|
|
257
|
+
|
|
258
|
+
self.__device = device
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def grid(self):
|
|
262
|
+
"""
|
|
263
|
+
The grid currently displayed on the device.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Grid:
|
|
267
|
+
The grid currently displayed on the device.
|
|
268
|
+
"""
|
|
269
|
+
return self.__grid
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def keep_image(self):
|
|
273
|
+
"""
|
|
274
|
+
Whether the controller has been instructed to keep the current grid showing.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
bool:
|
|
278
|
+
- True; if the controller has been instructed to keep the current grid showing.
|
|
279
|
+
- False; if the controller has not been instructed to keep the current grid showing.
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
return self.__keep_image
|
|
283
|
+
|
|
284
|
+
@validate_type(bool)
|
|
285
|
+
@keep_image.setter
|
|
286
|
+
def keep_image(self, new):
|
|
287
|
+
self.__keep_image = new
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def location(self) -> Dict[str, Any]:
|
|
291
|
+
"""
|
|
292
|
+
Get the physical location information for the device.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dict[str, Any]:
|
|
296
|
+
A dictionary containing location information such as abbreviation, side, and slot.
|
|
297
|
+
"""
|
|
298
|
+
return SLOT_MAP.get(self.device.location)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def location_abbrev(self) -> str:
|
|
302
|
+
"""
|
|
303
|
+
Get the abbreviated location identifier for the device.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
str:
|
|
307
|
+
The abbreviated location identifier (e.g., 'R1', 'L2').
|
|
308
|
+
"""
|
|
309
|
+
return self.location['abbrev']
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def name(self) -> str:
|
|
313
|
+
return self.__name if self.__name is not None else self.device.name
|
|
314
|
+
|
|
315
|
+
def play_animation(self, animation):
|
|
316
|
+
from is_matrix_forge.led_matrix.display.animations.animation import Animation
|
|
317
|
+
|
|
318
|
+
if not isinstance(animation, Animation):
|
|
319
|
+
raise TypeError(f'Expected `Animation`; got `{type(animation)}`!')
|
|
320
|
+
|
|
321
|
+
self.__current_animation = animation
|
|
322
|
+
|
|
323
|
+
return animation.play(devices=[self])
|
|
324
|
+
|
|
325
|
+
def scroll_text(self, text: str, skip_end_space: bool = False, loop: bool = False):
|
|
326
|
+
from is_matrix_forge.led_matrix.display.text.scroller import TextScroller
|
|
327
|
+
from is_matrix_forge.led_matrix.display.animations.animation import Animation
|
|
328
|
+
from is_matrix_forge.led_matrix import fonts as font
|
|
329
|
+
|
|
330
|
+
text_animation = Animation(TextScroller(font).scroll(text, skip_end_space))
|
|
331
|
+
text_animation.set_all_frame_durations(.03)
|
|
332
|
+
if loop:
|
|
333
|
+
text_animation.loop = True
|
|
334
|
+
return self.play_animation(text_animation)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def set_brightness_on_init(self):
|
|
338
|
+
"""
|
|
339
|
+
Whether the controller was instructed to set the brightness on init.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
bool:
|
|
343
|
+
- True; if the controller was instructed to set the brightness on init.
|
|
344
|
+
- False; if the controller was instructed to forego setting the brightness on init.
|
|
345
|
+
"""
|
|
346
|
+
return self.__set_brightness_on_init
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def show_grid_on_init(self) -> bool:
|
|
350
|
+
return self.__show_grid_on_init
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def side_of_keyboard(self) -> str:
|
|
354
|
+
"""
|
|
355
|
+
Get which side of the keyboard the device is on.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
str:
|
|
359
|
+
The side of the keyboard ('left' or 'right').
|
|
360
|
+
"""
|
|
361
|
+
return self.location['side']
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def slot(self) -> int:
|
|
365
|
+
"""
|
|
366
|
+
Get the slot number of the device.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
int:
|
|
370
|
+
The slot number (1 or 2).
|
|
371
|
+
"""
|
|
372
|
+
return self.location['slot']
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def thread_safe(self):
|
|
376
|
+
return self._thread_safe
|
|
377
|
+
|
|
378
|
+
@synchronized
|
|
379
|
+
def animate(self, enable: bool = True) -> None:
|
|
380
|
+
"""
|
|
381
|
+
Control animation on the LED matrix.
|
|
382
|
+
|
|
383
|
+
Parameters:
|
|
384
|
+
enable (bool, optional):
|
|
385
|
+
Whether to enable or disable animation. Defaults to True.
|
|
386
|
+
"""
|
|
387
|
+
from is_matrix_forge.led_matrix.hardware import animate
|
|
388
|
+
|
|
389
|
+
# Call the low-level animate function to control animation on the device
|
|
390
|
+
# The animate function also sets the status to 'animate' when enabled
|
|
391
|
+
animate(self.device, enable)
|
|
392
|
+
|
|
393
|
+
def draw(self, grid: 'Grid' = None) -> None:
|
|
394
|
+
return self.draw_grid(grid)
|
|
395
|
+
|
|
396
|
+
@synchronized
|
|
397
|
+
def draw_grid(self, grid: 'Grid' = None) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Draw a grid on the LED matrix.
|
|
400
|
+
|
|
401
|
+
Parameters:
|
|
402
|
+
grid (Grid):
|
|
403
|
+
The grid to display on the LED matrix.
|
|
404
|
+
"""
|
|
405
|
+
from is_matrix_forge.led_matrix.display.grid import Grid
|
|
406
|
+
from is_matrix_forge.led_matrix.display.helpers import render_matrix
|
|
407
|
+
|
|
408
|
+
grid = grid or self.grid
|
|
409
|
+
if not isinstance(grid, Grid):
|
|
410
|
+
grid = Grid(init_grid=grid)
|
|
411
|
+
render_matrix(self.device, grid.grid)
|
|
412
|
+
|
|
413
|
+
@synchronized
|
|
414
|
+
@method_alias('pattern')
|
|
415
|
+
def draw_pattern(self, pattern: str) -> None:
|
|
416
|
+
"""
|
|
417
|
+
Draw a pattern on the LED matrix.
|
|
418
|
+
|
|
419
|
+
Parameters:
|
|
420
|
+
pattern (str):
|
|
421
|
+
The pattern string to display.
|
|
422
|
+
|
|
423
|
+
Note:
|
|
424
|
+
This method is aliased as 'pattern'.
|
|
425
|
+
"""
|
|
426
|
+
from is_matrix_forge.led_matrix.display.patterns.built_in import BuiltInPatterns
|
|
427
|
+
pattern_pen = BuiltInPatterns(self)
|
|
428
|
+
pattern_pen.render(pattern)
|
|
429
|
+
|
|
430
|
+
@synchronized
|
|
431
|
+
def draw_percentage(self, percentage: int, clear_first: bool = False):
|
|
432
|
+
from is_matrix_forge.led_matrix.hardware import percentage as _show_percentage_raw
|
|
433
|
+
if not isinstance(percentage, int):
|
|
434
|
+
if isinstance(percentage, (float, str)):
|
|
435
|
+
percentage = coerce_to_int(percentage)
|
|
436
|
+
else:
|
|
437
|
+
raise TypeError(f'Percentage must be of type `int`. Not {type(percentage)}!')
|
|
438
|
+
|
|
439
|
+
if not 0 <= percentage <= 100:
|
|
440
|
+
raise ValueError('Percentage must be between 0 and 100')
|
|
441
|
+
|
|
442
|
+
if clear_first:
|
|
443
|
+
self.clear()
|
|
444
|
+
|
|
445
|
+
_show_percentage_raw(self.device, percentage)
|
|
446
|
+
|
|
447
|
+
@synchronized
|
|
448
|
+
def clear(self) -> None:
|
|
449
|
+
"""
|
|
450
|
+
Clear the LED matrix display.
|
|
451
|
+
|
|
452
|
+
Generates a blank grid and displays it on the LED matrix.
|
|
453
|
+
"""
|
|
454
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import generate_blank_grid
|
|
455
|
+
from is_matrix_forge.led_matrix.display.grid import Grid
|
|
456
|
+
|
|
457
|
+
data = generate_blank_grid()
|
|
458
|
+
self.__keep_image = False
|
|
459
|
+
grid = Grid(init_grid=data)
|
|
460
|
+
self.draw_grid(grid)
|
|
461
|
+
|
|
462
|
+
@synchronized
|
|
463
|
+
def display_location(self):
|
|
464
|
+
"""
|
|
465
|
+
Show the location abbreviation on the LED matrix.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
None
|
|
469
|
+
"""
|
|
470
|
+
from is_matrix_forge.led_matrix.display.text import show_string as _show_string_raw
|
|
471
|
+
_show_string_raw(self.device, self.location_abbrev)
|
|
472
|
+
|
|
473
|
+
@synchronized
|
|
474
|
+
def display_name(self):
|
|
475
|
+
"""
|
|
476
|
+
Shoe the device name (com port, unless named explicitly) on the LED matrix.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
None
|
|
480
|
+
"""
|
|
481
|
+
_show_string_raw(self.device, self.name)
|
|
482
|
+
|
|
483
|
+
@synchronized
|
|
484
|
+
def halt_animation(self) -> None:
|
|
485
|
+
"""
|
|
486
|
+
Stop any ongoing animation on the LED matrix.
|
|
487
|
+
|
|
488
|
+
This method checks if animation is currently running and stops it if needed.
|
|
489
|
+
"""
|
|
490
|
+
# Check if animation is currently running
|
|
491
|
+
if self.animating:
|
|
492
|
+
# Call animate with False to stop the animation
|
|
493
|
+
self.animate(False)
|
|
494
|
+
|
|
495
|
+
@synchronized
|
|
496
|
+
def identify(
|
|
497
|
+
self,
|
|
498
|
+
skip_clear: bool = False,
|
|
499
|
+
duration: float = 20.0,
|
|
500
|
+
cycles: int = 3
|
|
501
|
+
) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Display identification information on the LED matrix.
|
|
504
|
+
|
|
505
|
+
Shows the location abbreviation and device name for the specified number
|
|
506
|
+
of cycles, distributing the total duration evenly across all messages.
|
|
507
|
+
|
|
508
|
+
Parameters:
|
|
509
|
+
skip_clear (bool, optional):
|
|
510
|
+
If True, doesn't clear the display after identification (optional, defaults to `False`).
|
|
511
|
+
|
|
512
|
+
duration (float, optional):
|
|
513
|
+
The duration of the identification animation in seconds (optional, defaults to 20.0).
|
|
514
|
+
|
|
515
|
+
cycles (int, optional):
|
|
516
|
+
The number of times to repeat each identification message/animation (optional, defaults to 3).
|
|
517
|
+
"""
|
|
518
|
+
if not skip_clear:
|
|
519
|
+
self.clear()
|
|
520
|
+
|
|
521
|
+
messages = (self.location_abbrev, self.device.name)
|
|
522
|
+
if duration is not None and not isinstance(duration, (float, int)):
|
|
523
|
+
if isinstance(duration, str):
|
|
524
|
+
duration = float(duration)
|
|
525
|
+
else:
|
|
526
|
+
raise TypeError("Duration must be a float or integer.")
|
|
527
|
+
interval = duration / (cycles * len(messages))
|
|
528
|
+
|
|
529
|
+
for _ in range(cycles):
|
|
530
|
+
for msg in messages:
|
|
531
|
+
_show_string_raw(self.device, msg)
|
|
532
|
+
sleep(interval)
|
|
533
|
+
|
|
534
|
+
if not skip_clear:
|
|
535
|
+
self.clear()
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def is_animating(self):
|
|
539
|
+
return self.animating
|
|
540
|
+
|
|
541
|
+
@synchronized
|
|
542
|
+
def set_brightness(self, brightness: Union[int, float], __from_setter=False) -> None:
|
|
543
|
+
"""
|
|
544
|
+
Set the brightness level of the LED matrix.
|
|
545
|
+
|
|
546
|
+
Parameters:
|
|
547
|
+
brightness (Union[int, float]):
|
|
548
|
+
Brightness level as a percentage (0-100).
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
InvalidBrightnessError:
|
|
552
|
+
If the brightness value is invalid.
|
|
553
|
+
"""
|
|
554
|
+
from is_matrix_forge.led_matrix.hardware import brightness as _set_brightness_raw
|
|
555
|
+
from is_matrix_forge.led_matrix.errors import InvalidBrightnessError
|
|
556
|
+
from is_matrix_forge.common.helpers import \
|
|
557
|
+
percentage_to_value # Converts percentage values to raw hardware values
|
|
558
|
+
|
|
559
|
+
# Convert the percentage (0-100) to a hardware-specific value
|
|
560
|
+
# The hardware expects a different range than the user-friendly percentage
|
|
561
|
+
brightness_value = percentage_to_value(max_value=255, percent=brightness)
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
# Call the low-level function to set the brightness on the device
|
|
565
|
+
_set_brightness_raw(self.device, brightness_value)
|
|
566
|
+
except ValueError as e:
|
|
567
|
+
# If the low-level function raises a ValueError, wrap it in our custom
|
|
568
|
+
# InvalidBrightnessError to provide more context about the error
|
|
569
|
+
raise InvalidBrightnessError(brightness_value) from e
|
|
570
|
+
|
|
571
|
+
if not __from_setter:
|
|
572
|
+
self.__brightness = brightness
|
|
573
|
+
|
|
574
|
+
@staticmethod
|
|
575
|
+
# Internal methods
|
|
576
|
+
def __normalize_percent(val: Union[int, float, str]) -> int:
|
|
577
|
+
"""
|
|
578
|
+
Normalize a percentage value to an integer between 0 and 100.
|
|
579
|
+
|
|
580
|
+
Parameters:
|
|
581
|
+
val (Union[int, float, str]):
|
|
582
|
+
The value to normalize.
|
|
583
|
+
This value can be an integer, float, or string (with or without '%').
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
int:
|
|
587
|
+
Normalized percentage value as an integer between 0 and 100.
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
ValueError:
|
|
591
|
+
If the percentage is not between 0 and 100.
|
|
592
|
+
"""
|
|
593
|
+
# Handle string input (e.g., "75%") by stripping the '%' character and converting to float
|
|
594
|
+
if isinstance(val, str):
|
|
595
|
+
val = float(val.strip("%"))
|
|
596
|
+
|
|
597
|
+
# Convert to float first to handle any decimal values, then round and convert to int
|
|
598
|
+
# This ensures consistent handling of values like 75.6 (becomes 76)
|
|
599
|
+
percent = int(round(float(val)))
|
|
600
|
+
|
|
601
|
+
# Validate that the percentage is within the valid range (0-100)
|
|
602
|
+
if not (0 <= percent <= 100):
|
|
603
|
+
raise ValueError("Percentage must be between 0 and 100")
|
|
604
|
+
|
|
605
|
+
return percent
|
|
606
|
+
|
|
607
|
+
def __repr__(self) -> str:
|
|
608
|
+
"""
|
|
609
|
+
Return a string representation of the LEDMatrixController.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
str:
|
|
613
|
+
A string representation including the device, location, and brightness.
|
|
614
|
+
"""
|
|
615
|
+
dev = self.__dict__.get('_LEDMatrixController__device', None)
|
|
616
|
+
|
|
617
|
+
if hasattr(dev, 'device'):
|
|
618
|
+
dev_key = dev.device
|
|
619
|
+
elif hasattr(dev, 'name'):
|
|
620
|
+
dev_key = dev.name
|
|
621
|
+
else:
|
|
622
|
+
dev_key = repr(dev)
|
|
623
|
+
|
|
624
|
+
loc_key = (self.location or {}).get('abbrev', repr(self.location))
|
|
625
|
+
|
|
626
|
+
return (f"<{self.__class__.__name__} "
|
|
627
|
+
f"device={dev_key!r} "
|
|
628
|
+
f"location={loc_key!r} "
|
|
629
|
+
f"brightness={self.__default_brightness}%>")
|
|
630
|
+
|
|
631
|
+
def __setattr__(self, name, *args, **kwargs):
|
|
632
|
+
if name == 'FACTORY_DEFAULT_BRIGHTNESS':
|
|
633
|
+
raise AttributeError("FACTORY_DEFAULT_BRIGHTNESS is a read-only attribute")
|
|
634
|
+
|
|
635
|
+
super().__setattr__(name, *args, **kwargs)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
def get_controllers(threaded: bool = False) -> list['LEDMatrixController']:
|
|
4
|
+
from is_matrix_forge.led_matrix.constants import DEVICES
|
|
5
|
+
from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
|
|
6
|
+
|
|
7
|
+
return [LEDMatrixController(device, 100, thread_safe=True) for device in DEVICES]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from is_matrix_forge.log_engine import ROOT_LOGGER as PARENT_LOGGER
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def synchronized(method):
|
|
8
|
+
"""
|
|
9
|
+
Lock & warn if you’re calling off-thread with thread_safe=False.
|
|
10
|
+
|
|
11
|
+
Parameters:
|
|
12
|
+
method (callable): The method to wrap.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Callable: The wrapped method that will log/lock appropriately.
|
|
16
|
+
"""
|
|
17
|
+
@functools.wraps(method)
|
|
18
|
+
def wrapper(self, *args, **kwargs):
|
|
19
|
+
cur_thread_id = threading.get_ident()
|
|
20
|
+
|
|
21
|
+
# ⚠️ misuse detection: warn if another thread calls while thread_safe is False
|
|
22
|
+
if (
|
|
23
|
+
not getattr(self, "_thread_safe", False)
|
|
24
|
+
and getattr(self, "_warn_on_thread_misuse", True)
|
|
25
|
+
and cur_thread_id != getattr(self, "_owner_thread_id", None)
|
|
26
|
+
):
|
|
27
|
+
PARENT_LOGGER.warning(
|
|
28
|
+
"%r called from thread %r but thread_safe=False",
|
|
29
|
+
self,
|
|
30
|
+
threading.current_thread().name
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# 🔐 actual lock if thread_safe=True
|
|
34
|
+
if getattr(self, "_thread_safe", False) and getattr(self, "_cmd_lock", None):
|
|
35
|
+
with self._cmd_lock:
|
|
36
|
+
return method(self, *args, **kwargs)
|
|
37
|
+
|
|
38
|
+
# fallback: call method directly
|
|
39
|
+
return method(self, *args, **kwargs)
|
|
40
|
+
|
|
41
|
+
return wrapper
|