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,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)