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,431 @@
1
+
2
+ import time
3
+ from pathlib import Path
4
+ from threading import Thread
5
+ from time import sleep
6
+ from typing import Optional, Union
7
+
8
+ from inspy_logger import Loggable
9
+ from inspyre_toolbox.syntactic_sweets.classes import validate_type
10
+ from serial.tools.list_ports_common import ListPortInfo
11
+
12
+ from is_matrix_forge.common.helpers import percentage_to_value
13
+ from is_matrix_forge.led_matrix import pattern, get_animate, animate, percentage, LEDMatrixController
14
+ from is_matrix_forge.led_matrix.display.animations import goodbye_animation
15
+ from is_matrix_forge.led_matrix.helpers.device import check_device
16
+ from is_matrix_forge.monitor import DEFAULT_PLUGGED_SOUND, DEFAULT_UNPLUGGED_SOUND, MOD_LOGGER, get_plugged_status, \
17
+ PowerMonitorNotRunningError, ECH
18
+ from is_matrix_forge.notify.sounds import Sound
19
+ from is_matrix_forge.monitor.helpers import get_battery_percentage
20
+
21
+
22
+ class PowerMonitor(Loggable):
23
+
24
+ # Properties
25
+ #
26
+ _running = False
27
+ DEFAULT_CHECK_INTERVAL = 5
28
+ __cycles = 0
29
+
30
+ def __init__(
31
+ self,
32
+ device,
33
+ battery_check_interval: int = 5,
34
+ plugged_alert: Optional[Union[str, Path]] = DEFAULT_PLUGGED_SOUND,
35
+ unplugged_alert: Optional[Union[str, Path]] = DEFAULT_UNPLUGGED_SOUND,
36
+
37
+ ):
38
+ super().__init__(MOD_LOGGER)
39
+ self.__battery_check_interval = None
40
+ self.__dev = None
41
+ self._last_state = None
42
+ self.__plugged_alert = None
43
+ self.__start_time = None
44
+ self.__stop_time = None
45
+ self.__thread = None
46
+ self.__unplugged_alert = None
47
+ self.__controller = None
48
+
49
+ self.set_device(device)
50
+
51
+ # Set some settings
52
+ # First alerts, if provided
53
+ if plugged_alert:
54
+ self.plugged_alert = plugged_alert
55
+
56
+ if unplugged_alert:
57
+ self.unplugged_alert = unplugged_alert
58
+
59
+ self.dev.brightness = percentage_to_value(5)
60
+ self.__battery_check_interval = battery_check_interval or self.DEFAULT_CHECK_INTERVAL
61
+
62
+ @property
63
+ def battery_check_interval(self):
64
+ """
65
+
66
+ Returns:
67
+
68
+ """
69
+ return self.__battery_check_interval
70
+
71
+ @battery_check_interval.setter
72
+ @validate_type(int, str, float, preferred_type=float, conversion_funcs=[float])
73
+ def battery_check_interval(self, new):
74
+
75
+ self.__battery_check_interval = new
76
+
77
+ @property
78
+ def controller(self):
79
+ return self.__controller
80
+
81
+ @property
82
+ def cycles(self):
83
+ return self.__cycles
84
+
85
+ @property
86
+ def dev(self):
87
+ return self.controller.device
88
+
89
+ @property
90
+ def device(self):
91
+ return self.dev
92
+
93
+ @property
94
+ def last_state(self) -> Optional[bool]:
95
+ """
96
+ The state of the connection to the power supply on last-check.
97
+
98
+ Returns:
99
+ Optional[bool]:
100
+ True;
101
+ The device was plugged into power.
102
+ False;
103
+ The device was unplugged from power.
104
+
105
+ None;
106
+ The monitor hasn't completed a check yet.
107
+ """
108
+ return self._last_state
109
+
110
+ @property
111
+ def plugged_alert(self) -> Sound:
112
+ return self.__plugged_alert or DEFAULT_PLUGGED_SOUND
113
+
114
+ @validate_type(Sound)
115
+ @plugged_alert.setter
116
+ def plugged_alert(self, new):
117
+ if not isinstance(new, Sound):
118
+ raise TypeError(f'plugged_alert must be of type `Sound`, not {type(new)}')
119
+
120
+ self.__plugged_alert = new
121
+
122
+ @property
123
+ def plugged_in(self):
124
+ """
125
+ Whether the device is currently plugged into power.
126
+
127
+ Returns:
128
+ bool:
129
+ True;
130
+ The device is currently plugged into power. This is not an indicator (necessarily) that;
131
+ - The battery level is increasing
132
+ - The battery level is not decreasing
133
+ - The battery is gaining a net-positive charge level
134
+
135
+ This is an indicator that;
136
+ - A power supply is connected to the device
137
+
138
+ False;
139
+ The device is currently unplugged from power.
140
+ """
141
+ return get_plugged_status()
142
+
143
+ @property
144
+ def running(self):
145
+ """
146
+ Whether the power-monitor is running.
147
+
148
+ Returns:
149
+ bool:
150
+ True;
151
+ The monitor is running.
152
+
153
+ False;
154
+ The monitor is not running.
155
+ """
156
+ return self._running
157
+
158
+ @running.setter
159
+ def running(self, new):
160
+ """
161
+ The setter for the `running` property. You can only set `running` to `False`, this will stop the monitor.
162
+
163
+ Parameters:
164
+ new:
165
+ The new value for `running`.
166
+
167
+ Returns:
168
+ None
169
+
170
+ Raises:
171
+ RuntimeError:
172
+ If you try to set `running` to `True`
173
+
174
+ TypeError:
175
+ If `new` is not of type `bool`
176
+ """
177
+ if not self._running and new:
178
+ raise RuntimeError('Cannot start monitor by setting running to `True`. Use the `start` method instead.')
179
+
180
+ if not isinstance(new, bool):
181
+ raise TypeError(f'running must be of type `bool`, not {type(new)}')
182
+
183
+ self._running = new
184
+
185
+ @property
186
+ def run_time(self):
187
+ """
188
+ The time the monitor has been running. This is the time since the monitor was started, or the time between
189
+ starting and stopping the monitor if it has been stopped.
190
+
191
+ Returns:
192
+ float:
193
+ The run time in seconds.
194
+ """
195
+ if not hasattr(self, 'start_time'):
196
+ raise PowerMonitorNotRunningError('Monitor hasn\'t even been started yet!')
197
+
198
+ recent = self.stop_time if hasattr(self, 'stop_time') else time.time()
199
+ return recent - self.start_time
200
+
201
+ @property
202
+ def start_time(self) -> Optional[float]:
203
+ return self.__start_time
204
+
205
+ @property
206
+ def stop_time(self) -> Optional[float]:
207
+ """
208
+ (**Read-only property**)
209
+
210
+ The time the monitor was last stopped.
211
+
212
+ Returns:
213
+ Optional[float]:
214
+ The time the monitor was last stopped.
215
+
216
+ `None`;
217
+ The monitor hasn't been stopped yet
218
+
219
+ """
220
+ return self.__stop_time
221
+
222
+ @property
223
+ def thread(self) -> Optional[Thread]:
224
+ """
225
+ The thread object that is running the monitor loop. If the monitor is not running in a separate thread, this
226
+ property will return `None`.
227
+
228
+ Returns:
229
+ Optional[Thread]:
230
+ The thread that is running the monitor loop; if the monitor is running in a separate thread.
231
+ None otherwise
232
+ """
233
+ if self.__thread is None:
234
+ self.class_logger.error('Either monitor is not running or it is not running in a separate thread.')
235
+
236
+ return self.__thread
237
+
238
+ @property
239
+ def unplugged(self):
240
+ """
241
+ Whether the laptop is currently unplugged from AC power.
242
+
243
+ Returns:
244
+ bool:
245
+ True;
246
+ The device is currently unplugged from power, and the battery is discharging.
247
+
248
+ False;
249
+ The device is currently plugged into power. This is not an indicator (necessarily) that;
250
+ - The battery level is increasing
251
+ - The battery level is not decreasing
252
+ - The battery is gaining a net-positive charge level
253
+ """
254
+ return not self.plugged_in
255
+
256
+ @property
257
+ def unplugged_alert(self) -> 'Sound':
258
+ """
259
+ The sound to play when the device is unplugged from power.
260
+
261
+ Returns:
262
+ Sound:
263
+ The sound to play when the device is unplugged from power.
264
+ """
265
+ if not self.__unplugged_alert:
266
+ return DEFAULT_UNPLUGGED_SOUND
267
+
268
+ return self.__unplugged_alert
269
+
270
+ @validate_type()
271
+ @unplugged_alert.setter
272
+ def unplugged_alert(self, new):
273
+ """
274
+ The setter for the `unplugged_alert` property. This must be of type `Sound`.
275
+
276
+ Parameters:
277
+ new:
278
+
279
+
280
+ Returns:
281
+
282
+ """
283
+ if not isinstance(new, Sound):
284
+ raise TypeError(f'unplugged_alert must be of type `Sound`, not {type(new)}')
285
+
286
+ self.__unplugged_alert = new
287
+
288
+ def notify(self, which: str):
289
+ """
290
+ Notify the user of a power event (plugged, unplugged).
291
+
292
+ Parameters:
293
+ which (str):
294
+ The type of notification to send ('plugged' or 'unplugged').
295
+ """
296
+ log = self.method_logger
297
+ # Just return if the first check hasn't been completed yet.
298
+
299
+ if self.last_state is None:
300
+ log.debug('Status: Not yet checked')
301
+ return
302
+
303
+ if which.lower() == 'plugged':
304
+ log.debug('Status: Plugged in')
305
+ if not self.last_state:
306
+ log.debug(f'Using {self.plugged_alert} to notify user...')
307
+ self.plugged_alert.notify()
308
+ elif which.lower() == 'unplugged':
309
+ log.debug('Status: Unplugged')
310
+ if self.last_state:
311
+ log.debug(f'Using {self.unplugged_alert} to notify user...')
312
+ self.unplugged_alert.notify()
313
+
314
+ def run(self):
315
+ """
316
+ Run the power monitor. This is where the main loop of the monitor is located. It (roughly) does the following
317
+ while playing notification sounds when the power supply is plugged in or unplugged:
318
+ 1. Check the battery level.
319
+ 2. If the power supply is unplugged, display the battery level on the LED matrix.
320
+ 3. If the power supply is plugged in, animate the LED matrix.
321
+ 4. Repeat.
322
+
323
+ Note:
324
+ This method is called by the `start` method and should not be called directly.
325
+ """
326
+ from .events import handle_event
327
+ log = self.method_logger
328
+ if not self._running:
329
+ log.error('Monitor is not running')
330
+ raise RuntimeError('Monitor is not running. If you want to start it, use the `start` method.')
331
+
332
+ log.debug('Running monitor...')
333
+
334
+ while self.running:
335
+
336
+ state = 'plugged' if self.plugged_in else 'unplugged'
337
+ handle_event(state, self)
338
+
339
+ if not self.running:
340
+ self.controller.clear()
341
+ break
342
+
343
+ self.__cycles += 1
344
+
345
+ sleep(self.battery_check_interval)
346
+
347
+ def set_device(self, device):
348
+
349
+ if isinstance(device, LEDMatrixController):
350
+ self.__controller = device
351
+ device = self.controller.device
352
+ elif not isinstance(device, ListPortInfo):
353
+ raise TypeError(f'device must be of type `ListPortInfo`, not {type(device)}')
354
+
355
+ if not check_device(device):
356
+ raise ValueError(f'device {device} is not available')
357
+
358
+ self.__dev = device
359
+ self.__controller = LEDMatrixController(self.dev)
360
+
361
+ def start(self, threaded=False):
362
+ """
363
+ Start the power monitor.
364
+
365
+ Parameters:
366
+ threaded (bool):
367
+ If True, run the monitor in a separate thread.
368
+
369
+ Returns:
370
+ Optional[Thread]:
371
+ The thread that is running the monitor loop; if `threaded` is True.
372
+ None otherwise.
373
+
374
+ """
375
+ log = self.method_logger
376
+ log.debug('Starting monitor')
377
+
378
+ if self.running:
379
+ log.warning('Monitor is already running')
380
+ raise RuntimeError('Monitor is already running')
381
+
382
+ self._running = True
383
+ log.debug('Set running to True')
384
+ self.__start_time = time.time()
385
+
386
+ if threaded:
387
+ t = Thread(target=self.run, daemon=True)
388
+ t.start()
389
+ ECH.register_handler(self.stop, kwargs={'reason': 'Program exited.'})
390
+
391
+ try:
392
+ self.run()
393
+ except KeyboardInterrupt:
394
+ log.warning('KeyboardInterrupt received, stopping monitor...')
395
+ self.stop(without_salutation=True, reason='Keyboard interrupt.')
396
+
397
+ def stop(self, without_salutation=False, reason=None):
398
+ """
399
+ Stop the power monitor.
400
+
401
+ Parameters:
402
+ without_salutation (bool):
403
+ If True, skip the goodbye animation that plays by default on the LED matrix.
404
+ """
405
+
406
+ log = self.method_logger
407
+
408
+ if not self.running:
409
+ log.error('Monitor is not running')
410
+ raise PowerMonitorNotRunningError("Can't call stop() on a monitor that is not running")
411
+ else:
412
+ log.debug('Stopping monitor...')
413
+ self.__stop_time = time.time()
414
+ self.running = False
415
+ log.debug('"running" flag set to False...waiting for thread to finish')
416
+
417
+ if reason:
418
+ log.info(f'Stopping monitor due to: {reason}')
419
+ else:
420
+ log.info('Stopping monitor. With no reason.')
421
+
422
+ if not without_salutation:
423
+ goodbye_animation(self.dev)
424
+ else:
425
+ log.debug('Skipping goodbye salutation...')
426
+
427
+ if get_animate(self.dev):
428
+ animate(self.dev, False)
429
+
430
+ log.debug('Clearing LED matrix...')
431
+ self.controller.clear()
File without changes
@@ -0,0 +1,6 @@
1
+ from .base import Sound
2
+ from ...assets.audio import PLUG_ALERT_MAP
3
+
4
+
5
+ PLUGGED_NOTIFY = Sound(PLUG_ALERT_MAP['plugged'], 'plugged')
6
+ UNPLUGGED_NOTIFY = Sound(PLUG_ALERT_MAP['unplugged'], 'unplugged')
@@ -0,0 +1,106 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import Union, Optional
4
+
5
+ from chime import play_wav
6
+
7
+ from inspyre_toolbox.syntactic_sweets.classes.decorators.aliases import add_aliases, method_alias
8
+
9
+ from is_matrix_forge.log_engine import ROOT_LOGGER as PARENT_LOGGER
10
+ from is_matrix_forge.assets.audio import PLUG_ALERT_MAP
11
+
12
+
13
+ MOD_LOGGER = PARENT_LOGGER.get_child('notify.sounds.base')
14
+
15
+
16
+ @add_aliases
17
+ class Sound(ABC):
18
+ """
19
+ Abstract base class for audio notifications.
20
+ """
21
+ __allowed_types: set = {'plugged', 'unplugged'}
22
+
23
+ def __init__(
24
+ self,
25
+ wav_file: Union[str, Path],
26
+ notify_type: str,
27
+ ) -> None:
28
+ self.__initialized = False
29
+ self.__notify_type = None
30
+ self.__wav_file = None
31
+
32
+ self.wav_file_path = wav_file
33
+ self.notify_type = notify_type
34
+ # Initialize here
35
+
36
+ @property
37
+ def ALLOWED_TYPES(self):
38
+ """
39
+ The allowed values for the notify_type property. Read-only property.
40
+
41
+ Returns:
42
+ set[str]:
43
+ A set of allowed notify-types.
44
+ """
45
+ return self.__allowed_types
46
+
47
+ @property
48
+ def notify_type(self):
49
+ return self.__notify_type
50
+
51
+ @notify_type.setter
52
+ def notify_type(self, new: str) -> None:
53
+ if self.__initialized:
54
+ raise RuntimeError('Cannot change notify type after initialization@')
55
+
56
+ if not isinstance(new, str):
57
+ raise TypeError(f'new must be of type `str`, not {type(new)}')
58
+
59
+ if new not in self.ALLOWED_TYPES:
60
+ raise ValueError(f'new must be one of {self.ALLOWED_TYPES}, not {new}')
61
+
62
+ self.__notify_type = new
63
+
64
+ @property
65
+ def wav_file_path(self) -> Path:
66
+ """
67
+ Get the path to the .wav file that will be played.
68
+
69
+ Returns:
70
+ Path:
71
+ The path to the .wav file.
72
+ """
73
+ return self.__wav_file
74
+
75
+ @wav_file_path.setter
76
+ def wav_file_path(self, new: Union[str, Path]) -> None:
77
+ """
78
+ Set the path to the .wav file that will be played. Setting this should be done during initialization. After
79
+ initialization, this should not (and will not) be changed, as it will break the sound.
80
+ """
81
+ if self.__initialized:
82
+ raise RuntimeError('Cannot change wave file path after initialization.')
83
+
84
+ if isinstance(new, str):
85
+ new = Path(new)
86
+
87
+ if not isinstance(new, Path):
88
+ raise TypeError(f'new must be of type `str` or `Path`, not {type(new)}')
89
+
90
+ self.__wav_file = new
91
+
92
+ @method_alias('play')
93
+ def notify(self) -> None:
94
+ """
95
+ Play the audio notification.
96
+
97
+ Returns:
98
+ None
99
+ """
100
+ play_wav(self.wav_file_path)
101
+
102
+ def __repr__(self):
103
+ return f"<Sound notify_type={self.notify_type!r} wav_file_path={self.wav_file_path!r}>"
104
+
105
+
106
+
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ """LED matrix aware progress bars."""
4
+
5
+ from typing import Optional, Any, Iterable, List
6
+
7
+ from tqdm import tqdm as _tqdm
8
+
9
+ try:
10
+ from is_matrix_forge.led_matrix.helpers.device import DEVICES
11
+ from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
12
+ except Exception: # pragma: no cover - dependency issues
13
+ DEVICES = []
14
+ LEDMatrixController = None # type: ignore
15
+
16
+ # Global cache of controller instances so multiple bars can share controllers
17
+ if 'LEDMatrixController' in globals() and LEDMatrixController is not None:
18
+ _CONTROLLERS: List[Optional[LEDMatrixController]] = [None for _ in range(len(DEVICES))]
19
+ else: # pragma: no cover - when dependencies missing
20
+ _CONTROLLERS = [] # type: ignore[var-annotated]
21
+
22
+ # Track how many LEDTqdm bars are active to rotate matrices
23
+ _ACTIVE_BARS: List['LEDTqdm'] = []
24
+
25
+
26
+ class LEDTqdm(_tqdm):
27
+ """A ``tqdm`` subclass that also renders progress on an LED matrix."""
28
+
29
+ def __init__(self, *args: Any, use_led: bool = True, matrix: Optional[Any] = None,
30
+ **kwargs: Any) -> None:
31
+ super().__init__(*args, **kwargs)
32
+
33
+ self._matrix = None
34
+ if use_led:
35
+ index = min(len(_ACTIVE_BARS), max(len(DEVICES) - 1, 0))
36
+ self._matrix = self._init_matrix(matrix, index)
37
+ if self._matrix is not None:
38
+ try:
39
+ self._matrix.clear()
40
+ except Exception: # pragma: no cover - hardware errors
41
+ pass
42
+
43
+ self._last_percent = -1
44
+ _ACTIVE_BARS.append(self)
45
+
46
+ @staticmethod
47
+ def _get_controller(index: int):
48
+ if LEDMatrixController is None or index >= len(DEVICES): # pragma: no cover - hardware missing
49
+ return None
50
+ ctrl = _CONTROLLERS[index]
51
+ if ctrl is None:
52
+ try:
53
+ ctrl = LEDMatrixController(DEVICES[index], 100)
54
+ except Exception:
55
+ ctrl = None
56
+ _CONTROLLERS[index] = ctrl
57
+ return ctrl
58
+
59
+ @classmethod
60
+ def _init_matrix(cls, matrix: Optional[Any], index: int):
61
+ if matrix is not None:
62
+ if LEDMatrixController and isinstance(matrix, LEDMatrixController):
63
+ return matrix
64
+ try:
65
+ return LEDMatrixController(matrix, 100) # type: ignore
66
+ except Exception:
67
+ return None
68
+ return cls._get_controller(index)
69
+
70
+ def _render_led(self) -> None:
71
+ if not self._matrix or not self.total:
72
+ return
73
+ percent = int(self.n / self.total * 100)
74
+ if percent != self._last_percent:
75
+ try:
76
+ self._matrix.draw_percentage(percent)
77
+ except Exception:
78
+ # Ignore hardware errors in progress display
79
+ pass
80
+ self._last_percent = percent
81
+
82
+ def update(self, n: int = 1) -> None: # type: ignore[override]
83
+ super().update(n)
84
+ self._render_led()
85
+
86
+ def close(self) -> None:
87
+ super().close()
88
+ if self._matrix is not None:
89
+ try:
90
+ self._matrix.clear()
91
+ except Exception: # pragma: no cover - hardware errors
92
+ pass
93
+ if self in _ACTIVE_BARS:
94
+ _ACTIVE_BARS.remove(self)
95
+
96
+
97
+ def tqdm(iterable: Optional[Iterable[Any]] = None, *args: Any, **kwargs: Any):
98
+ """Return an :class:`LEDTqdm` instance like ``tqdm.tqdm``."""
99
+ return LEDTqdm(iterable, *args, **kwargs)
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2025 Inspyre-Softworks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software (led-matrix-battery) and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.