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,37 @@
1
+ from .base import LEDMatrixControllerError
2
+
3
+
4
+ class GridDefinitionError(LEDMatrixControllerError, ValueError):
5
+ """
6
+ Base class for all grid definition errors.
7
+
8
+ A "grid definition error" is an error that occurs while defining a grid for the LED matrix, either in translation
9
+ from data to the LED matrix or in the grid itself. This includes errors such as malformed grids, invalid grid
10
+ sizes, etc.
11
+
12
+ Provided Subclasses:
13
+ - MalformedGridError:
14
+ Raised when the grid structure does not match expectations. This can include issues such as incorrect
15
+ dimensions, missing elements, or other structural problems.
16
+ """
17
+ default_message = 'An error has occurred within the grid definition!'
18
+
19
+
20
+ class MalformedGridError(GridDefinitionError):
21
+ """
22
+ Raised when the grid is malformed.
23
+
24
+ Class Attributes:
25
+ default_message (str):
26
+ **Read-Only**. The default message for the error.
27
+ """
28
+ default_message = 'The grid is malformed!'
29
+
30
+ def __init__(self, more_info: str = None, **kwargs) -> None:
31
+ if more_info is not None:
32
+ self.default_message = f"{self.default_message}\n\n Additional information from caller:\n {more_info}"
33
+
34
+ super().__init__(message=self.default_message, **kwargs)
35
+
36
+
37
+ __all__ = ['GridDefinitionError', 'MalformedGridError']
@@ -0,0 +1,68 @@
1
+ from inspyre_toolbox.exceptional import CustomRootException
2
+ from .base import LEDMatrixControllerError
3
+
4
+
5
+ class MatrixError(LEDMatrixControllerError):
6
+ """
7
+ Base class for all LED matrix errors.
8
+
9
+ Subclasses of this exception are used to indicate specific errors that can occur while interacting with the LED
10
+ matrix hardware.
11
+
12
+ Provided Subclasses:
13
+ - MatrixConnectionError:
14
+ Raised when the LED matrix cannot be connected to.
15
+
16
+ - MatrixInUseError:
17
+ Raised when the LED matrix is already in use by another process.
18
+ """
19
+ default_message = 'An error occurred while interacting with the LED matrix!'
20
+
21
+ def __init__(self, message: str = None, device_name: str = None) -> None:
22
+ full_msg = self.default_message
23
+
24
+ if device_name is not None:
25
+ full_msg += f' (Device: {device_name})'
26
+
27
+ if message is not None:
28
+ full_msg = f'{full_msg}\n\n Additional Info:\n{" " * 4}{message}'
29
+
30
+ super().__init__(full_msg)
31
+
32
+
33
+ class MatrixConnectionError(MatrixError):
34
+ """
35
+ Raised when the LED matrix cannot be connected to.
36
+ """
37
+ default_message = 'Unable to connect to the LED matrix!'
38
+
39
+ def __init__(self, message: str = None) -> None:
40
+ if message is None:
41
+ message = self.default_message
42
+ else:
43
+ message = f'{self.default_message}\n\n Additional Info:\n{" " * 4}{message}'
44
+
45
+ super().__init__(message)
46
+
47
+
48
+ class MatrixInUseError(MatrixConnectionError):
49
+ """
50
+ Raised when the LED matrix is already in use by another process.
51
+ """
52
+ message = 'The LED matrix is already in use, and cannot be used by this program! Please close the other ' \
53
+ 'instance/process and try again!'
54
+
55
+ def __init__(self):
56
+ super().__init__(self.message)
57
+
58
+
59
+ class InvalidBrightnessError(MatrixError):
60
+ """
61
+ Raised when the brightness value provided is invalid.
62
+ """
63
+ default_message = 'The brightness value provided is invalid! Value must be an integer between 0 and 255!'
64
+
65
+ def __init__(self, brightness: int):
66
+ message = f'{self.default_message}\n\n Brightness: {brightness}'
67
+
68
+ super().__init__(message)
@@ -0,0 +1,36 @@
1
+ from .base import CustomRootException
2
+
3
+
4
+ class MiscError(CustomRootException):
5
+ pass
6
+
7
+
8
+ class ImplicitNameDerivationError(MiscError, ValueError):
9
+ """
10
+ Raised by `freeze_setter` when it's used to decorate a property setter
11
+ whose name begins with a single underscore (e.g., `_my_prop`) without an
12
+ explicit `attr_name` being provided.
13
+
14
+ This error occurs because the default behavior of `freeze_setter` is to
15
+ derive the underlying private attribute name by prepending an underscore
16
+ to the setter's name. If the setter is `_my_prop`, this would implicitly
17
+ attempt to create `__my_prop`. Attributes starting with double underscores
18
+ trigger Python's name mangling, which can lead to unexpected behavior and
19
+ make the underlying attribute name less predictable if not explicitly intended.
20
+
21
+ To prevent this implicit and potentially confusing name derivation,
22
+ `freeze_setter` requires that you explicitly specify the `attr_name`
23
+ when decorating setters that start with a single underscore.
24
+
25
+ TL;DR: When using `freeze_setter` on a property setter named `_my_prop`,
26
+ you must explicitly specify the `attr_name` as "__my_prop" to avoid confusion
27
+ about how the underlying attribute will be named, and to confirm your intent.
28
+ """
29
+ default_message = (
30
+ "Implicitly derived attribute names are not allowed for properties "
31
+ "whose names begin with a single underscore. Please provide an "
32
+ "`attr_name` argument."
33
+ )
34
+
35
+ def __init__(self, **kwargs):
36
+ super().__init__(message=self.default_message, **kwargs)
@@ -0,0 +1,20 @@
1
+ from typing import List
2
+ import json
3
+ import importlib.resources
4
+
5
+ from is_matrix_forge.assets.font_map import FontMap
6
+
7
+ # Load the default font map shipped with the package using importlib.resources
8
+ with importlib.resources.open_text("is_matrix_forge.assets", "char_map.json", encoding="utf-8") as f:
9
+ _FONT_DATA = json.load(f)
10
+ FONT_MAP = FontMap(font_map=_FONT_DATA)
11
+
12
+
13
+ def convert_font(ch: str) -> List[int]:
14
+ """Return the 5x6 glyph list for a single character."""
15
+ return FONT_MAP.lookup(ch, kind="character")
16
+
17
+
18
+ def convert_symbol(symbol: str) -> List[int]:
19
+ """Return the glyph list for a named symbol."""
20
+ return FONT_MAP.lookup(symbol, kind="symbol")
@@ -0,0 +1,172 @@
1
+ """
2
+ Hardware communication module for LED Matrix.
3
+
4
+ This module provides low-level functions for communicating with LED matrix hardware.
5
+ It includes functions for sending commands, controlling brightness, and other basic operations.
6
+ """
7
+
8
+ from is_matrix_forge.led_matrix.commands import send_command
9
+ from is_matrix_forge.led_matrix.commands.map import CommandVals
10
+ from is_matrix_forge.led_matrix.helpers.core.lazy_mod_load import LazyModuleLoader
11
+ from enum import IntEnum
12
+
13
+
14
+ # Import the necessary functions from the controller module
15
+
16
+
17
+ class PatternVals(IntEnum):
18
+ Percentage = 0x00
19
+ Gradient = 0x01
20
+ DoubleGradient = 0x02
21
+ DisplayLotus = 0x03
22
+ ZigZag = 0x04
23
+ FullBrightness = 0x05
24
+ DisplayPanic = 0x06
25
+ DisplayLotus2 = 0x07
26
+
27
+
28
+ class Game(IntEnum):
29
+ Snake = 0x00
30
+ Pong = 0x01
31
+ Tetris = 0x02
32
+ GameOfLife = 0x03
33
+
34
+
35
+ class GameOfLifeStartParam(IntEnum):
36
+ Currentmatrix = 0x00
37
+ Pattern1 = 0x01
38
+ Blinker = 0x02
39
+ Toad = 0x03
40
+ Beacon = 0x04
41
+ Glider = 0x05
42
+
43
+ def __str__(self):
44
+ return self.name.lower()
45
+
46
+ def __repr__(self):
47
+ return str(self)
48
+
49
+ @staticmethod
50
+ def argparse(s):
51
+ try:
52
+ return GameOfLifeStartParam[s.lower().capitalize()]
53
+ except KeyError:
54
+ return s
55
+
56
+
57
+ class GameControlVal(IntEnum):
58
+ Up = 0
59
+ Down = 1
60
+ Left = 2
61
+ Right = 3
62
+ Quit = 4
63
+
64
+
65
+ def disconnect_dev(dev):
66
+ """
67
+ Disconnect the device from the system.
68
+
69
+ Parameters:
70
+ dev (str):
71
+ The device to disconnect.
72
+ """
73
+ global DISCONNECTED_DEVS
74
+ if dev in DISCONNECTED_DEVS:
75
+ return
76
+ DISCONNECTED_DEVS.append(dev)
77
+
78
+
79
+ def bootloader_jump(dev):
80
+ """Reboot into the bootloader to flash new firmware"""
81
+ send_command(dev, CommandVals.BootloaderReset, [0x00])
82
+
83
+
84
+ def brightness(dev, b: int):
85
+ """Adjust the brightness scaling of the entire screen."""
86
+ send_command(dev, CommandVals.Brightness, [b])
87
+
88
+
89
+ def get_brightness(dev):
90
+ """Adjust the brightness scaling of the entire screen."""
91
+ res = send_command(dev, CommandVals.Brightness, with_response=True)
92
+ return int(res[0])
93
+
94
+
95
+ def get_version(dev):
96
+ """Get the device's firmware version"""
97
+ res = send_command(dev, CommandVals.Version, with_response=True)
98
+ if not res:
99
+ return 'Unknown'
100
+ major = res[0]
101
+ minor = (res[1] & 0xF0) >> 4
102
+ patch = res[1] & 0xF
103
+ pre_release = res[2]
104
+
105
+ version = f"{major}.{minor}.{patch}"
106
+ if pre_release:
107
+ version += " (Pre-release)"
108
+ return version
109
+
110
+
111
+ def send_serial(dev, s, command):
112
+ """Send serial command by using existing serial connection"""
113
+ try:
114
+ s.write(command)
115
+ except (IOError, OSError) as _ex:
116
+ disconnect_dev(dev.device)
117
+ # print("Error: ", ex)
118
+
119
+
120
+ def animate(dev, b: bool):
121
+ """Enable/disable animation"""
122
+ send_command(dev, CommandVals.Animate, [0x01 if b else 0x00])
123
+
124
+
125
+ def get_animate(dev):
126
+ """Check if animation is enabled"""
127
+ res = send_command(dev, CommandVals.Animate, with_response=True)
128
+ return bool(res[0])
129
+
130
+
131
+ def get_pwm_freq(dev):
132
+ """Adjust the brightness scaling of the entire screen."""
133
+ res = send_command(dev, CommandVals.PwmFreq, with_response=True)
134
+ freq = int(res[0])
135
+ if freq == 0:
136
+ return 29000
137
+ elif freq == 1:
138
+ return 3600
139
+ elif freq == 2:
140
+ return 1800
141
+ elif freq == 3:
142
+ return 900
143
+ else:
144
+ return None
145
+
146
+
147
+ def pwm_freq(dev, freq):
148
+ """Set the PWM frequency"""
149
+ if freq == "29kHz":
150
+ send_command(dev, CommandVals.PwmFreq, [0])
151
+ elif freq == "3.6kHz":
152
+ send_command(dev, CommandVals.PwmFreq, [1])
153
+ elif freq == "1.8kHz":
154
+ send_command(dev, CommandVals.PwmFreq, [2])
155
+ elif freq == "900Hz":
156
+ send_command(dev, CommandVals.PwmFreq, [3])
157
+
158
+
159
+ def percentage(dev, p):
160
+ """Fill a percentage of the screen. Bottom to top"""
161
+ send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p])
162
+
163
+
164
+ def define_controllers(threaded=False):
165
+ from is_matrix_forge.led_matrix.controller.helpers import get_controllers
166
+ return get_controllers(threaded)
167
+
168
+
169
+ # CONTROLLERS = define_controllers()
170
+
171
+
172
+
@@ -0,0 +1,165 @@
1
+ import serial
2
+ from inspyre_toolbox.chrono.sleep import NegateSwitch
3
+ from inspyre_toolbox.path_man import provision_path
4
+
5
+ from serial.tools.list_ports_common import ListPortInfo
6
+ from threading import Thread
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Union, Optional, List, Any, ByteString
11
+
12
+
13
+ def get_json_from_file(path: Union[str, Path]) -> Any:
14
+ """
15
+ Load and parse a JSON file.
16
+
17
+ Args:
18
+ path (Union[str, Path]): The path to the JSON file to load.
19
+
20
+ Returns:
21
+ Any: The parsed JSON data.
22
+
23
+ Raises:
24
+ FileNotFoundError:
25
+ If the file does not exist.
26
+
27
+ IsADirectoryError:
28
+ If the path points to a directory.
29
+
30
+ json.JSONDecodeError:
31
+ If the file contains invalid JSON.
32
+ """
33
+ path = provision_path(path)
34
+ if not path.exists():
35
+ raise FileNotFoundError(f'Preset file not found: {path}')
36
+
37
+ if not path.is_file():
38
+ raise IsADirectoryError(f'Preset file is a directory: {path}')
39
+
40
+ with open(path, 'r') as f:
41
+ return json.load(f)
42
+
43
+
44
+ def load_from_file(
45
+ path: Union[str, Path],
46
+ expected_width: Optional[int] = None,
47
+ expected_height: Optional[int] = None,
48
+ fallback_duration: Optional[Union[int, float]] = None
49
+ ) -> List['Frame']:
50
+ """
51
+ Load a list of frames from a file.
52
+
53
+ Args:
54
+ path (Union[str, Path]): The path to the file to load.
55
+ expected_width (Optional[int]): The expected width of the frames in the file. Defaults to 9.
56
+ expected_height (Optional[int]): The expected height of the frames in the file. Defaults to 34.
57
+ fallback_duration (Optional[Union[int, float]]): The duration of the frames in the file.
58
+ Defaults to 0.33 (1/3 of a second).
59
+
60
+ Returns:
61
+ List[Frame]: A list of frames loaded from the file.
62
+
63
+ Raises:
64
+ FileNotFoundError: If the file does not exist.
65
+ IsADirectoryError: If the path points to a directory.
66
+ ValueError: If the file does not contain a valid JSON array.
67
+ """
68
+ from is_matrix_forge.led_matrix.display.animations.frame.helpers import migrate_frame
69
+ from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
70
+
71
+ width = expected_width or 9
72
+ height = expected_height or 34
73
+
74
+ data = get_json_from_file(path)
75
+
76
+ if not isinstance(data, list):
77
+ raise ValueError(f"File {path} does not contain a valid JSON array")
78
+
79
+ frames = []
80
+ for entry in data:
81
+
82
+ if not isinstance(entry, dict) and isinstance(entry, list) and is_valid_grid(entry, width, height):
83
+ entry = migrate_frame(entry, fallback_duration)
84
+ entry['duration'] = fallback_duration or .33
85
+
86
+ frames.append(entry)
87
+
88
+ return data
89
+
90
+
91
+ def disconnect_dev(dev: str) -> None:
92
+ """
93
+ Disconnect the device from the system.
94
+
95
+ Args:
96
+ dev (str): The device to disconnect.
97
+
98
+ Note:
99
+ This function adds the device to a global list of disconnected devices
100
+ to prevent further attempts to communicate with it.
101
+ """
102
+ global DISCONNECTED_DEVS
103
+ if dev in DISCONNECTED_DEVS:
104
+ return
105
+
106
+ DISCONNECTED_DEVS.append(dev)
107
+
108
+
109
+ def send_serial(dev: ListPortInfo, s: 'serial.Serial', command: ByteString) -> None:
110
+ """
111
+ Send serial command by using an existing serial connection.
112
+
113
+ Args:
114
+ dev (ListPortInfo): The device to send the command to.
115
+ s (serial.Serial): The serial connection to use.
116
+ command (ByteString): The command to send.
117
+
118
+ Raises:
119
+ IOError, OSError: If there is an error communicating with the device.
120
+ """
121
+ try:
122
+ s.write(command)
123
+ except (IOError, OSError) as _ex:
124
+ disconnect_dev(dev.device)
125
+ # print("Error: ", ex)
126
+
127
+
128
+ def identify_devices(devices: Optional[List[ListPortInfo]] = None) -> None:
129
+ """
130
+ Identify LED matrix devices by flashing an identification message on each
131
+ connected matrix. If no specific devices are provided, all detected devices
132
+ are used.
133
+
134
+ Parameters:
135
+ devices (Optional[List[ListPortInfo]], optional):
136
+ List of devices to identify. Defaults to ``None`` which will use all
137
+ detected devices.
138
+ """
139
+ from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
140
+
141
+ if devices is None:
142
+ from is_matrix_forge.led_matrix.helpers.device import DEVICES
143
+ devices = DEVICES
144
+
145
+ import logging
146
+
147
+ controllers = [LEDMatrixController(dev, 100) for dev in devices]
148
+
149
+ def safe_identify(controller):
150
+ try:
151
+ controller.identify()
152
+ except Exception as e:
153
+ logging.exception(f"Exception in identify thread for controller {controller}: {e}")
154
+
155
+ threads = []
156
+ for controller in controllers:
157
+ t = Thread(target=safe_identify, args=(controller,))
158
+ threads.append(t)
159
+ t.start()
160
+
161
+ for t in threads:
162
+ t.join()
163
+
164
+
165
+ running = NegateSwitch(False)
@@ -0,0 +1,17 @@
1
+ """
2
+
3
+
4
+ Author:
5
+ Inspyre Softworks
6
+
7
+ Project:
8
+ led-matrix-battery
9
+
10
+ File:
11
+ is_matrix_forge/led_matrix/helpers/core/__init__.py
12
+
13
+
14
+ Description:
15
+
16
+
17
+ """
@@ -0,0 +1,84 @@
1
+ import sys
2
+ import types
3
+ import logging
4
+ import pkgutil
5
+ from importlib import import_module
6
+ from typing import Set, Optional, Union
7
+
8
+
9
+ class LazyModuleLoader:
10
+ """
11
+ A utility to lazily load submodules and avoid circular imports.
12
+
13
+ Parameters
14
+ ----------
15
+ parent_module : str
16
+ The fully qualified name of the module (e.g., 'is_matrix_forge.led_matrix').
17
+
18
+ submodules : Optional[Set[str]]
19
+ A set of submodules to lazily load. If not provided, it will be auto-discovered.
20
+
21
+ fallback_module : Optional[str]
22
+ Optional fallback module to delegate attribute lookups to.
23
+
24
+ enable_logging : bool
25
+ Enable logging for import errors.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ parent_module: str,
31
+ submodules: Optional[Set[str]] = None,
32
+ fallback_module: Optional[str] = None,
33
+ enable_logging: bool = True
34
+ ):
35
+ self.module_name = parent_module
36
+ self.submodules = submodules or self._discover_submodules()
37
+ self.fallback_module = fallback_module
38
+ self.enable_logging = enable_logging
39
+ self.logger = logging.getLogger(__name__)
40
+ self._install()
41
+
42
+ def _discover_submodules(self) -> Set[str]:
43
+ try:
44
+ module = import_module(self.module_name)
45
+ path = module.__path__
46
+ return {name for _, name, is_pkg in pkgutil.iter_modules(path)}
47
+ except Exception as e:
48
+ if self.enable_logging:
49
+ self.logger.error(f"Error discovering submodules for '{self.module_name}': {e}")
50
+ return set()
51
+
52
+ def _install(self):
53
+ mod = sys.modules[self.module_name]
54
+ mod.__getattr__ = self.__getattr__
55
+ mod.__dir__ = self.__dir__
56
+ mod.__all__ = sorted(self.submodules)
57
+
58
+ def __getattr__(self, name):
59
+ if name in self.submodules:
60
+ try:
61
+ full_name = f"{self.module_name}.{name}"
62
+ module = import_module(full_name)
63
+ sys.modules[full_name] = module
64
+ return module
65
+ except ImportError as e:
66
+ if self.enable_logging:
67
+ self.logger.error(f"Failed to import submodule '{name}': {e}")
68
+ raise
69
+ elif self.fallback_module:
70
+ base = import_module(self.fallback_module)
71
+ try:
72
+ return getattr(base, name)
73
+ except AttributeError as e:
74
+ if self.enable_logging:
75
+ self.logger.error(
76
+ f"Fallback '{self.fallback_module}' has no attribute '{name}': {e}"
77
+ )
78
+ raise
79
+ else:
80
+ raise AttributeError(f"Module '{self.module_name}' has no attribute '{name}'")
81
+
82
+ def __dir__(self):
83
+ return sorted(set(sys.modules[self.module_name].__dict__) | self.submodules)
84
+