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,16 @@
1
+ from serial import SerialException, Serial
2
+ from serial.tools import list_ports
3
+
4
+ from ...led_matrix.helpers.device import DEVICES
5
+ from ...log_engine import ROOT_LOGGER as PARENT_LOGGER
6
+
7
+ MOD_LOGGER = PARENT_LOGGER.get_child('inputmodule.helpers')
8
+
9
+
10
+ MOD_LOGGER.debug(f'Found {len(DEVICES)} devices.')
11
+
12
+ if len(DEVICES) == 1:
13
+ DEVICE = DEVICES[0]
14
+ else:
15
+ MOD_LOGGER.warning(f'Found {len(DEVICES)} devices. Device choice must be explicit.')
16
+
@@ -0,0 +1,516 @@
1
+ import time
2
+
3
+ import serial
4
+
5
+ from . import font
6
+ from . import send_command, CommandVals, PatternVals, FWK_MAGIC, send_serial, brightness
7
+
8
+ from is_matrix_forge.led_matrix.helpers.status_handler import get_status, set_status
9
+
10
+ WIDTH = 9
11
+ HEIGHT = 34
12
+ PATTERNS = [
13
+ "All LEDs on",
14
+ '"LOTUS" sideways',
15
+ "Gradient (0-13% Brightness)",
16
+ "Double Gradient (0-7-0% Brightness)",
17
+ "Zigzag",
18
+ '"PANIC"',
19
+ '"LOTUS" Top Down',
20
+ "All brightness levels (1 LED each)",
21
+ "Every Second Row",
22
+ "Every Third Row",
23
+ "Every Fourth Row",
24
+ "Every Fifth Row",
25
+ "Every Sixth Row",
26
+ "Every Second Col",
27
+ "Every Third Col",
28
+ "Every Fourth Col",
29
+ "Every Fifth Col",
30
+ "Checkerboard",
31
+ "Double Checkerboard",
32
+ "Triple Checkerboard",
33
+ "Quad Checkerboard",
34
+ ]
35
+ PWM_FREQUENCIES = [
36
+ "29kHz",
37
+ "3.6kHz",
38
+ "1.8kHz",
39
+ "900Hz",
40
+ ]
41
+
42
+
43
+ def get_pwm_freq(dev):
44
+ """Adjust the brightness scaling of the entire screen."""
45
+ res = send_command(dev, CommandVals.PwmFreq, with_response=True)
46
+ freq = int(res[0])
47
+ if freq == 0:
48
+ return 29000
49
+ elif freq == 1:
50
+ return 3600
51
+ elif freq == 2:
52
+ return 1800
53
+ elif freq == 3:
54
+ return 900
55
+ else:
56
+ return None
57
+
58
+
59
+ def percentage(dev, p):
60
+ """Fill a percentage of the screen. Bottom to top"""
61
+ send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p])
62
+
63
+
64
+ def animate(dev, b: bool):
65
+ """Tell the firmware to start/stop animation.
66
+ Scrolls the currently saved grid vertically down."""
67
+ if b:
68
+ set_status('animate')
69
+ send_command(dev, CommandVals.Animate, [b])
70
+
71
+
72
+ def get_animate(dev):
73
+ """Tell the firmware to start/stop animation.
74
+ Scrolls the currently saved grid vertically down."""
75
+ res = send_command(dev, CommandVals.Animate, with_response=True)
76
+ return bool(res[0])
77
+
78
+
79
+ def image_bl(dev, image_file):
80
+ """Display an image in black and white
81
+ Confirmed working with PNG and GIF.
82
+ Must be 9x34 in size.
83
+ Sends everything in a single command
84
+ """
85
+ # Initialize a list of 39 bytes (39*8 = 312 bits, enough for 9x34 = 306 pixels)
86
+ vals = [0 for _ in range(39)]
87
+
88
+ from PIL import Image
89
+
90
+ # Open the image and convert to RGB format
91
+ im = Image.open(image_file).convert("RGB")
92
+ width, height = im.size
93
+
94
+ # Verify image dimensions match the LED matrix size
95
+ assert width == 9
96
+ assert height == 34
97
+
98
+ # Get all pixel values as a flat list
99
+ pixel_values = list(im.getdata())
100
+
101
+ # Process each pixel
102
+ for i, pixel in enumerate(pixel_values):
103
+ # Calculate average brightness of RGB components
104
+ brightness = sum(pixel) / 3
105
+
106
+ # If brightness is above half of max (127.5), consider it "on"
107
+ # This creates a binary black/white effect
108
+ if brightness > 0xFF / 2:
109
+ # Calculate which byte in vals array this pixel belongs to (i/8)
110
+ # Then set the appropriate bit within that byte (i%8)
111
+ # This packs 8 pixels into each byte of the vals array
112
+ vals[int(i / 8)] |= 1 << i % 8
113
+
114
+ # Send the packed binary data to the device
115
+ send_command(dev, CommandVals.Draw, vals)
116
+
117
+
118
+ def camera(dev):
119
+ """Play a live view from the webcam, for fun"""
120
+ set_status('camera')
121
+ with serial.Serial(dev.device, 115200) as s:
122
+ import cv2
123
+
124
+ capture = cv2.VideoCapture(1)
125
+ ret, frame = capture.read()
126
+
127
+ scale_y = HEIGHT / frame.shape[0]
128
+
129
+ # Scale the video to 34 pixels height
130
+ dim = (HEIGHT, int(round(frame.shape[1] * scale_y)))
131
+ # Find the starting position to crop the width to be centered
132
+ # For very narrow videos, make sure to stay in bounds
133
+ start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2)))
134
+ end_x = min(dim[1], start_x + WIDTH)
135
+
136
+ # Pre-process the video into resized, cropped, grayscale frames
137
+ while get_status() == 'camera':
138
+ ret, frame = capture.read()
139
+ if not ret:
140
+ print("Failed to capture video frames")
141
+ break
142
+
143
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
144
+
145
+ resized = cv2.resize(gray, (dim[1], dim[0]))
146
+ cropped = resized[0:HEIGHT, start_x:end_x]
147
+
148
+ for x in range(0, cropped.shape[1]):
149
+ vals = [0 for _ in range(HEIGHT)]
150
+
151
+ for y in range(0, HEIGHT):
152
+ vals[y] = cropped[y, x]
153
+
154
+ send_col(dev, s, x, vals)
155
+ commit_cols(dev, s)
156
+
157
+
158
+ def video(dev, video_file):
159
+ set_status('video')
160
+ """Resize and play back a video"""
161
+ with serial.Serial(dev.device, 115200) as s:
162
+ import cv2
163
+
164
+ capture = cv2.VideoCapture(video_file)
165
+ ret, frame = capture.read()
166
+
167
+ scale_y = HEIGHT / frame.shape[0]
168
+
169
+ # Scale the video to 34 pixels height
170
+ dim = (HEIGHT, int(round(frame.shape[1] * scale_y)))
171
+ # Find the starting position to crop the width to be centered
172
+ # For very narrow videos, make sure to stay in bounds
173
+ start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2)))
174
+ end_x = min(dim[1], start_x + WIDTH)
175
+
176
+ processed = []
177
+
178
+ # Pre-process the video into resized, cropped, grayscale frames
179
+ while get_status() == 'video':
180
+ ret, frame = capture.read()
181
+ if not ret:
182
+ print("Failed to read video frames")
183
+ break
184
+
185
+ gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
186
+
187
+ resized = cv2.resize(gray, (dim[1], dim[0]))
188
+ cropped = resized[0:HEIGHT, start_x:end_x]
189
+
190
+ processed.append(cropped)
191
+
192
+ # Determine frame delay based on the video's FPS. Default to 30 FPS if
193
+ # the information isn't available.
194
+ fps = capture.get(cv2.CAP_PROP_FPS)
195
+ try:
196
+ fps = float(fps)
197
+ if fps <= 0 or fps != fps:
198
+ raise ValueError
199
+ except Exception:
200
+ fps = 30.0
201
+ frame_delay = 1.0 / fps
202
+
203
+ # Write it out to the module one frame at a time while respecting the
204
+ # frame rate.
205
+ for frame in processed:
206
+ start = time.time()
207
+ for x in range(0, cropped.shape[1]):
208
+ vals = [0 for _ in range(HEIGHT)]
209
+
210
+ for y in range(0, HEIGHT):
211
+ vals[y] = frame[y, x]
212
+
213
+ send_col(dev, s, x, vals)
214
+ commit_cols(dev, s)
215
+
216
+ elapsed = time.time() - start
217
+ if frame_delay > elapsed:
218
+ time.sleep(frame_delay - elapsed)
219
+
220
+
221
+ def pixel_to_brightness(pixel):
222
+ """Calculate pixel brightness from an RGB triple"""
223
+ assert len(pixel) == 3
224
+ brightness = sum(pixel) / len(pixel)
225
+
226
+ # Poor man's scaling to make the greyscale pop better.
227
+ # Should find a good function.
228
+ if brightness > 200:
229
+ brightness = brightness
230
+ elif brightness > 150:
231
+ brightness = brightness * 0.8
232
+ elif brightness > 100:
233
+ brightness = brightness * 0.5
234
+ elif brightness > 50:
235
+ brightness = brightness
236
+ else:
237
+ brightness = brightness * 2
238
+
239
+ return int(brightness)
240
+
241
+
242
+ def image_greyscale(dev, image_file):
243
+ """Display an image in greyscale
244
+ Sends each 1x34 column and then commits => 10 commands
245
+ """
246
+ with serial.Serial(dev.device, 115200) as s:
247
+ from PIL import Image
248
+
249
+ im = Image.open(image_file).convert("RGB")
250
+ width, height = im.size
251
+ assert width == 9
252
+ assert height == 34
253
+ pixel_values = list(im.getdata())
254
+ for x in range(0, WIDTH):
255
+ vals = [0 for _ in range(HEIGHT)]
256
+
257
+ for y in range(HEIGHT):
258
+ vals[y] = pixel_to_brightness(pixel_values[x + y * WIDTH])
259
+
260
+ send_col(dev, s, x, vals)
261
+ commit_cols(dev, s)
262
+
263
+
264
+ def send_col(dev, s, x, vals):
265
+ """Stage greyscale values for a single column. Must be committed with commit_cols()"""
266
+ command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals
267
+ send_serial(dev, s, command)
268
+
269
+
270
+ def commit_cols(dev, s):
271
+ """Commit the changes from sending individual cols with send_col(), displaying the matrix.
272
+ This makes sure that the matrix isn't partially updated."""
273
+ command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00]
274
+ send_serial(dev, s, command)
275
+
276
+
277
+ def checkerboard(dev, n):
278
+ with serial.Serial(dev.device, 115200) as s:
279
+ for x in range(0, WIDTH):
280
+ vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT / 2)
281
+ if x % (n * 2) < n:
282
+ # Rotate once
283
+ vals = vals[n:] + vals[:n]
284
+
285
+ send_col(dev, s, x, vals)
286
+ commit_cols(dev, s)
287
+
288
+
289
+ def every_nth_col(dev, n):
290
+ with serial.Serial(dev.device, 115200) as s:
291
+ for x in range(0, WIDTH):
292
+ vals = [(0xFF if x % n == 0 else 0) for _ in range(HEIGHT)]
293
+
294
+ send_col(dev, s, x, vals)
295
+ commit_cols(dev, s)
296
+
297
+
298
+ def every_nth_row(dev, n):
299
+ with serial.Serial(dev.device, 115200) as s:
300
+ for x in range(0, WIDTH):
301
+ vals = [(0xFF if y % n == 0 else 0) for y in range(HEIGHT)]
302
+
303
+ send_col(dev, s, x, vals)
304
+ commit_cols(dev, s)
305
+
306
+
307
+ def breathing(dev):
308
+ """Animate breathing brightness.
309
+ Keeps currently displayed grid"""
310
+ set_status('breathing')
311
+ # Bright ranges appear similar, so we have to go through those faster
312
+ while get_status() == 'breathing':
313
+ # Go quickly from 250 to 50
314
+ for i in range(10):
315
+ time.sleep(0.03)
316
+ brightness(dev, 250 - i * 20)
317
+
318
+ # Go slowly from 50 to 0
319
+ for i in range(10):
320
+ time.sleep(0.06)
321
+ brightness(dev, 50 - i * 5)
322
+
323
+ # Go slowly from 0 to 50
324
+ for i in range(10):
325
+ time.sleep(0.06)
326
+ brightness(dev, i * 5)
327
+
328
+ # Go quickly from 50 to 250
329
+ for i in range(10):
330
+ time.sleep(0.03)
331
+ brightness(dev, 50 + i * 20)
332
+
333
+
334
+ def eq(dev, vals):
335
+ """Display 9 values in equalizer diagram starting from the middle, going up and down"""
336
+ matrix = [[0 for _ in range(34)] for _ in range(9)]
337
+
338
+ for col, val in enumerate(vals[:9]):
339
+ row = int(34 / 2)
340
+ above = int(val / 2)
341
+ below = val - above
342
+
343
+ for i in range(above):
344
+ matrix[col][row + i] = 0xFF
345
+ for i in range(below):
346
+ matrix[col][row - 1 - i] = 0xFF
347
+
348
+ render_matrix(dev, matrix)
349
+
350
+
351
+ def render_matrix(dev, matrix):
352
+ """Show a black/white matrix
353
+ Send everything in a single command"""
354
+ # Initialize a byte array to hold the binary representation of the matrix
355
+ # 39 bytes = 312 bits, which is enough for 9x34 = 306 pixels
356
+ vals = [0x00 for _ in range(39)]
357
+
358
+ # Iterate through each position in the 9x34 matrix
359
+ for x in range(9):
360
+ for y in range(34):
361
+ # Convert 2D coordinates to a linear index
362
+ # The matrix is stored in column-major order (y changes faster than x)
363
+ i = x + 9 * y
364
+
365
+ # If the pixel at this position is "on" (non-zero)
366
+ if matrix[x][y]:
367
+ # Calculate which byte in the vals array this pixel belongs to
368
+ byte_index = int(i / 8)
369
+
370
+ # Calculate which bit within that byte represents this pixel
371
+ bit_position = i % 8
372
+
373
+ # Set the corresponding bit in the appropriate byte
374
+ # This efficiently packs 8 pixels into each byte
375
+ vals[byte_index] = vals[byte_index] | (1 << bit_position)
376
+
377
+ # Send the packed binary data to the device
378
+ send_command(dev, CommandVals.Draw, vals)
379
+
380
+
381
+ def light_leds(dev, leds):
382
+ """Light a specific number of LEDs"""
383
+ # Initialize a byte array with all LEDs off
384
+ vals = [0x00 for _ in range(39)]
385
+
386
+ # Calculate how many complete bytes we need to fill (each byte = 8 LEDs)
387
+ complete_bytes = int(leds / 8)
388
+
389
+ # Set all complete bytes to 0xFF (all 8 bits on)
390
+ for byte in range(complete_bytes):
391
+ vals[byte] = 0xFF
392
+
393
+ # Handle the remaining LEDs (less than 8) in the last partial byte
394
+ remaining_leds = leds % 8
395
+
396
+ # For each remaining LED, set the corresponding bit in the last byte
397
+ # This creates a binary pattern like 00011111 for 5 remaining LEDs
398
+ for i in range(remaining_leds):
399
+ vals[complete_bytes] += 1 << i
400
+
401
+ # Send the command to the device to display the pattern
402
+ send_command(dev, CommandVals.Draw, vals)
403
+
404
+
405
+ def pwm_freq(dev, freq):
406
+ """Display a pattern that's already programmed into the firmware"""
407
+ if freq == "29kHz":
408
+ send_command(dev, CommandVals.PwmFreq, [0])
409
+ elif freq == "3.6kHz":
410
+ send_command(dev, CommandVals.PwmFreq, [1])
411
+ elif freq == "1.8kHz":
412
+ send_command(dev, CommandVals.PwmFreq, [2])
413
+ elif freq == "900Hz":
414
+ send_command(dev, CommandVals.PwmFreq, [3])
415
+
416
+
417
+ def pattern(dev, p):
418
+ """Display a pattern that's already programmed into the firmware"""
419
+ if p == "All LEDs on":
420
+ send_command(dev, CommandVals.Pattern, [PatternVals.FullBrightness])
421
+ elif p == "Gradient (0-13% Brightness)":
422
+ send_command(dev, CommandVals.Pattern, [PatternVals.Gradient])
423
+ elif p == "Double Gradient (0-7-0% Brightness)":
424
+ send_command(dev, CommandVals.Pattern, [PatternVals.DoubleGradient])
425
+ elif p == '"LOTUS" sideways':
426
+ send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus])
427
+ elif p == "Zigzag":
428
+ send_command(dev, CommandVals.Pattern, [PatternVals.ZigZag])
429
+ elif p == '"PANIC"':
430
+ send_command(dev, CommandVals.Pattern, [PatternVals.DisplayPanic])
431
+ elif p == '"LOTUS" Top Down':
432
+ send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus2])
433
+ elif p == "All brightness levels (1 LED each)":
434
+ all_brightnesses(dev)
435
+ elif p == "Every Second Row":
436
+ every_nth_row(dev, 2)
437
+ elif p == "Every Third Row":
438
+ every_nth_row(dev, 3)
439
+ elif p == "Every Fourth Row":
440
+ every_nth_row(dev, 4)
441
+ elif p == "Every Fifth Row":
442
+ every_nth_row(dev, 5)
443
+ elif p == "Every Sixth Row":
444
+ every_nth_row(dev, 6)
445
+ elif p == "Every Second Col":
446
+ every_nth_col(dev, 2)
447
+ elif p == "Every Third Col":
448
+ every_nth_col(dev, 3)
449
+ elif p == "Every Fourth Col":
450
+ every_nth_col(dev, 4)
451
+ elif p == "Every Fifth Col":
452
+ every_nth_col(dev, 4)
453
+ elif p == "Checkerboard":
454
+ checkerboard(dev, 1)
455
+ elif p == "Double Checkerboard":
456
+ checkerboard(dev, 2)
457
+ elif p == "Triple Checkerboard":
458
+ checkerboard(dev, 3)
459
+ elif p == "Quad Checkerboard":
460
+ checkerboard(dev, 4)
461
+ else:
462
+ print("Invalid pattern")
463
+
464
+
465
+ def show_string(dev, s):
466
+ """Render a string with up to five letters"""
467
+ show_font(dev, [font.convert_font(letter) for letter in str(s)[:5]])
468
+
469
+
470
+ def show_font(dev, font_items):
471
+ """Render up to five 5x6 pixel font items"""
472
+ # Initialize a byte array with all pixels off
473
+ vals = [0x00 for _ in range(39)]
474
+
475
+ # Process each font item (character/symbol)
476
+ for digit_i, digit_pixels in enumerate(font_items):
477
+ # Calculate vertical offset for this character
478
+ # Each character is placed 7 pixels apart vertically
479
+ offset = digit_i * 7
480
+
481
+ # Process each pixel in the 5x6 character grid
482
+ for pixel_x in range(5):
483
+ for pixel_y in range(6):
484
+ # Convert 2D coordinates to 1D index in the font data
485
+ # Font data is stored as a 1D array in row-major order
486
+ pixel_value = digit_pixels[pixel_x + pixel_y * 5]
487
+
488
+ # Calculate the position in the LED matrix
489
+ # Characters start at x=2 (to center them) and are stacked vertically
490
+ # with the calculated offset
491
+ i = (2 + pixel_x) + (9 * (pixel_y + offset))
492
+
493
+ # If this pixel should be on
494
+ if pixel_value:
495
+ # Calculate which byte and bit to set
496
+ byte_index = int(i / 8)
497
+ bit_position = i % 8
498
+
499
+ # Set the bit in the appropriate byte
500
+ vals[byte_index] = vals[byte_index] | (1 << bit_position)
501
+
502
+ # Send the command to display the characters
503
+ send_command(dev, CommandVals.Draw, vals)
504
+
505
+
506
+ def show_symbols(dev, symbols):
507
+ """Render a list of up to five symbols
508
+ Can use letters/numbers or symbol names, like 'sun', ':)'"""
509
+ font_items = []
510
+ for symbol in symbols:
511
+ s = font.convert_symbol(symbol)
512
+ if not s:
513
+ s = font.convert_font(symbol)
514
+ font_items.append(s)
515
+
516
+ show_font(dev, font_items)
@@ -0,0 +1,4 @@
1
+ """
2
+ Fake `__init__.py` to make the Scripts directory a sub-package.
3
+ """
4
+
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Battery Monitor Script.
4
+
5
+ This script provides a command-line interface for monitoring the battery status
6
+ and displaying it on an LED matrix. It uses the PowerMonitor class from the
7
+ is_matrix_forge.monitor module to monitor the battery level and power state.
8
+
9
+ Usage:
10
+ python battery_monitor.py [options]
11
+
12
+ Options:
13
+ --interval SECONDS Set the battery check interval in seconds (default: 5)
14
+ --brightness PERCENT Set the LED matrix brightness (0-100, default: 50)
15
+ --device INDEX Specify which LED matrix device to use (default: 0)
16
+ --no-sound Disable sound notifications
17
+ --help Show this help message and exit
18
+ """
19
+
20
+ import argparse
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Import the necessary modules from the is_matrix_forge package
25
+ from is_matrix_forge.monitor import run_power_monitor
26
+ from is_matrix_forge.led_matrix.helpers.device import get_devices
27
+ from is_matrix_forge.notify.sounds import Sound
28
+ from is_matrix_forge.led_matrix.led_matrix import LEDMatrix
29
+
30
+
31
+ def parse_arguments():
32
+ """
33
+ Parse command-line arguments.
34
+
35
+ Returns:
36
+ argparse.Namespace: The parsed command-line arguments.
37
+ """
38
+ parser = argparse.ArgumentParser(
39
+ description="Monitor battery status and display it on an LED matrix."
40
+ )
41
+
42
+ parser.add_argument(
43
+ "--interval",
44
+ type=float,
45
+ default=5.0,
46
+ help="Battery check interval in seconds (default: 5.0)"
47
+ )
48
+
49
+ parser.add_argument(
50
+ "--brightness",
51
+ type=int,
52
+ default=50,
53
+ help="LED matrix brightness (0-100, default: 50)"
54
+ )
55
+
56
+ parser.add_argument(
57
+ "--device",
58
+ type=int,
59
+ default=0,
60
+ help="Index of the LED matrix device to use (default: 0)"
61
+ )
62
+
63
+ parser.add_argument(
64
+ "--no-sound",
65
+ action="store_true",
66
+ help="Disable sound notifications"
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--plugged-sound",
71
+ type=str,
72
+ help="Path to custom sound file for plugged-in notification"
73
+ )
74
+
75
+ parser.add_argument(
76
+ "--unplugged-sound",
77
+ type=str,
78
+ help="Path to custom sound file for unplugged notification"
79
+ )
80
+
81
+ return parser.parse_args()
82
+
83
+
84
+ def main():
85
+ """
86
+ Main function to run the battery monitor.
87
+ """
88
+ # Parse command-line arguments
89
+ args = parse_arguments()
90
+
91
+ # Get available LED matrix devices
92
+ devices = get_devices()
93
+
94
+ if not devices:
95
+ print("Error: No LED matrix devices found.")
96
+ print("Make sure your LED matrix is connected and recognized by the system.")
97
+ sys.exit(1)
98
+
99
+ # Select the specified device or default to the first one
100
+ try:
101
+ device = devices[args.device]
102
+ except IndexError:
103
+ print(f"Error: Device index {args.device} is out of range.")
104
+ print(f"Available devices: {len(devices)}")
105
+ sys.exit(1)
106
+
107
+ # Create sound objects for notifications if not disabled
108
+ plugged_alert = None
109
+ unplugged_alert = None
110
+
111
+ if not args.no_sound:
112
+ if args.plugged_sound:
113
+ plugged_path = Path(args.plugged_sound)
114
+ if plugged_path.exists():
115
+ plugged_alert = Sound(plugged_path)
116
+ else:
117
+ print(f"Warning: Plugged sound file not found: {plugged_path}")
118
+ print("Using default sound instead.")
119
+
120
+ if args.unplugged_sound:
121
+ unplugged_path = Path(args.unplugged_sound)
122
+ if unplugged_path.exists():
123
+ unplugged_alert = Sound(unplugged_path)
124
+ else:
125
+ print(f"Warning: Unplugged sound file not found: {unplugged_path}")
126
+ print("Using default sound instead.")
127
+
128
+ # Initialize the LED matrix with the specified brightness
129
+ led_matrix = LEDMatrix(device, args.brightness)
130
+
131
+ print(f"Starting battery monitor with device: {device.device}")
132
+ print(f"Battery check interval: {args.interval} seconds")
133
+ print(f"LED matrix brightness: {args.brightness}%")
134
+ print("Press Ctrl+C to exit")
135
+
136
+ try:
137
+ # Run the power monitor
138
+ monitor = run_power_monitor(
139
+ device,
140
+ battery_check_interval=args.interval,
141
+ plugged_alert=plugged_alert,
142
+ unplugged_alert=unplugged_alert
143
+ )
144
+ except KeyboardInterrupt:
145
+ print("\nBattery monitor stopped by user.")
146
+ except Exception as e:
147
+ print(f"Error: {e}")
148
+ sys.exit(1)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,2 @@
1
+ from .main import main as install_presets
2
+