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.
Files changed (123) hide show
  1. is_matrix_forge/__init__.py +8 -0
  2. is_matrix_forge/assets/__init__.py +8 -0
  3. is_matrix_forge/assets/audio/__init__.py +36 -0
  4. is_matrix_forge/assets/audio/files/__init__.py +53 -0
  5. is_matrix_forge/assets/audio/files/plugged.wav +0 -0
  6. is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
  7. is_matrix_forge/assets/char_map.json +2150 -0
  8. is_matrix_forge/assets/font_map.py +119 -0
  9. is_matrix_forge/common/__init__.py +0 -0
  10. is_matrix_forge/common/decorators/__init__.py +152 -0
  11. is_matrix_forge/common/dirs.py +20 -0
  12. is_matrix_forge/common/helpers/__init__.py +132 -0
  13. is_matrix_forge/common/helpers/github_api.py +67 -0
  14. is_matrix_forge/designer_gui/__init__.py +8 -0
  15. is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
  16. is_matrix_forge/designer_gui/main_window/layout.py +117 -0
  17. is_matrix_forge/dev_tools/__init__.py +8 -0
  18. is_matrix_forge/dev_tools/debug.py +82 -0
  19. is_matrix_forge/dev_tools/presets.py +47 -0
  20. is_matrix_forge/inputmodule/__init__.py +164 -0
  21. is_matrix_forge/inputmodule/b1display.py +160 -0
  22. is_matrix_forge/inputmodule/c1minimal.py +35 -0
  23. is_matrix_forge/inputmodule/font.py +2166 -0
  24. is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
  25. is_matrix_forge/inputmodule/ledmatrix.py +516 -0
  26. is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
  27. is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
  28. is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
  29. is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
  30. is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
  31. is_matrix_forge/led_matrix/__init__.py +7 -0
  32. is_matrix_forge/led_matrix/commands/__init__.py +84 -0
  33. is_matrix_forge/led_matrix/commands/map.py +46 -0
  34. is_matrix_forge/led_matrix/constants.py +82 -0
  35. is_matrix_forge/led_matrix/controller/__init__.py +26 -0
  36. is_matrix_forge/led_matrix/controller/controller.py +635 -0
  37. is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
  38. is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
  39. is_matrix_forge/led_matrix/controller/multiton.py +32 -0
  40. is_matrix_forge/led_matrix/controller/threaded.py +72 -0
  41. is_matrix_forge/led_matrix/display/__init__.py +13 -0
  42. is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
  43. is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
  44. is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
  45. is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
  46. is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
  47. is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
  48. is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
  49. is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
  50. is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
  51. is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
  52. is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
  53. is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
  54. is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
  55. is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
  56. is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
  57. is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
  58. is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
  59. is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
  60. is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
  61. is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
  62. is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
  63. is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
  64. is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
  65. is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
  66. is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
  67. is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
  68. is_matrix_forge/led_matrix/display/media.py +199 -0
  69. is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
  70. is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
  71. is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
  72. is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
  73. is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
  74. is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
  75. is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
  76. is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
  77. is_matrix_forge/led_matrix/errors/__init__.py +4 -0
  78. is_matrix_forge/led_matrix/errors/base.py +14 -0
  79. is_matrix_forge/led_matrix/errors/grid.py +37 -0
  80. is_matrix_forge/led_matrix/errors/matrix.py +68 -0
  81. is_matrix_forge/led_matrix/errors/misc.py +36 -0
  82. is_matrix_forge/led_matrix/fonts.py +20 -0
  83. is_matrix_forge/led_matrix/hardware.py +172 -0
  84. is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
  85. is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
  86. is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
  87. is_matrix_forge/led_matrix/helpers/device.py +118 -0
  88. is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
  89. is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
  90. is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
  91. is_matrix_forge/led_matrix/led_matrix.py +25 -0
  92. is_matrix_forge/led_matrix/status.py +8 -0
  93. is_matrix_forge/led_matrix/tools/__init__.py +8 -0
  94. is_matrix_forge/led_matrix/tools/identify.py +27 -0
  95. is_matrix_forge/led_matrix/utils/__init__.py +41 -0
  96. is_matrix_forge/log_engine.py +25 -0
  97. is_matrix_forge/monitor/__init__.py +68 -0
  98. is_matrix_forge/monitor/errors.py +35 -0
  99. is_matrix_forge/monitor/events.py +78 -0
  100. is_matrix_forge/monitor/gui/__init__.py +0 -0
  101. is_matrix_forge/monitor/gui/errors.py +35 -0
  102. is_matrix_forge/monitor/gui/event/__init__.py +8 -0
  103. is_matrix_forge/monitor/gui/event/collection.py +153 -0
  104. is_matrix_forge/monitor/gui/event/errors.py +58 -0
  105. is_matrix_forge/monitor/gui/event/event.py +226 -0
  106. is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
  107. is_matrix_forge/monitor/gui/layout/base.py +147 -0
  108. is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
  109. is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
  110. is_matrix_forge/monitor/gui/windows/base.py +325 -0
  111. is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
  112. is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
  113. is_matrix_forge/monitor/helpers.py +55 -0
  114. is_matrix_forge/monitor/monitor.py +431 -0
  115. is_matrix_forge/notify/__init__.py +0 -0
  116. is_matrix_forge/notify/sounds/__init__.py +6 -0
  117. is_matrix_forge/notify/sounds/base.py +106 -0
  118. is_matrix_forge/progress.py +99 -0
  119. is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
  120. is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
  121. is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
  122. is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
  123. 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