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.
- is_matrix_forge/__init__.py +8 -0
- is_matrix_forge/assets/__init__.py +8 -0
- is_matrix_forge/assets/audio/__init__.py +36 -0
- is_matrix_forge/assets/audio/files/__init__.py +53 -0
- is_matrix_forge/assets/audio/files/plugged.wav +0 -0
- is_matrix_forge/assets/audio/files/unplugged.wav +0 -0
- is_matrix_forge/assets/char_map.json +2150 -0
- is_matrix_forge/assets/font_map.py +119 -0
- is_matrix_forge/common/__init__.py +0 -0
- is_matrix_forge/common/decorators/__init__.py +152 -0
- is_matrix_forge/common/dirs.py +20 -0
- is_matrix_forge/common/helpers/__init__.py +132 -0
- is_matrix_forge/common/helpers/github_api.py +67 -0
- is_matrix_forge/designer_gui/__init__.py +8 -0
- is_matrix_forge/designer_gui/main_window/__init__.py +252 -0
- is_matrix_forge/designer_gui/main_window/layout.py +117 -0
- is_matrix_forge/dev_tools/__init__.py +8 -0
- is_matrix_forge/dev_tools/debug.py +82 -0
- is_matrix_forge/dev_tools/presets.py +47 -0
- is_matrix_forge/inputmodule/__init__.py +164 -0
- is_matrix_forge/inputmodule/b1display.py +160 -0
- is_matrix_forge/inputmodule/c1minimal.py +35 -0
- is_matrix_forge/inputmodule/font.py +2166 -0
- is_matrix_forge/inputmodule/helpers/__init__.py +16 -0
- is_matrix_forge/inputmodule/ledmatrix.py +516 -0
- is_matrix_forge/led_matrix/Scripts/__init__.py +4 -0
- is_matrix_forge/led_matrix/Scripts/battery_monitor.py +152 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/__init__.py +2 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/arguments.py +84 -0
- is_matrix_forge/led_matrix/Scripts/install_presets/main.py +192 -0
- is_matrix_forge/led_matrix/__init__.py +7 -0
- is_matrix_forge/led_matrix/commands/__init__.py +84 -0
- is_matrix_forge/led_matrix/commands/map.py +46 -0
- is_matrix_forge/led_matrix/constants.py +82 -0
- is_matrix_forge/led_matrix/controller/__init__.py +26 -0
- is_matrix_forge/led_matrix/controller/controller.py +635 -0
- is_matrix_forge/led_matrix/controller/helpers/__init__.py +7 -0
- is_matrix_forge/led_matrix/controller/helpers/threading.py +41 -0
- is_matrix_forge/led_matrix/controller/multiton.py +32 -0
- is_matrix_forge/led_matrix/controller/threaded.py +72 -0
- is_matrix_forge/led_matrix/display/__init__.py +13 -0
- is_matrix_forge/led_matrix/display/animations/__init__.py +29 -0
- is_matrix_forge/led_matrix/display/animations/animation.py +593 -0
- is_matrix_forge/led_matrix/display/animations/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/errors.py +23 -0
- is_matrix_forge/led_matrix/display/animations/font/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/animations/font/font.py +4 -0
- is_matrix_forge/led_matrix/display/animations/font/map_builder.py +123 -0
- is_matrix_forge/led_matrix/display/animations/frame/__init__.py +47 -0
- is_matrix_forge/led_matrix/display/animations/frame/base.py +245 -0
- is_matrix_forge/led_matrix/display/animations/frame/helpers.py +67 -0
- is_matrix_forge/led_matrix/display/animations/keep_awake.py +1 -0
- is_matrix_forge/led_matrix/display/animations/plugged.py +0 -0
- is_matrix_forge/led_matrix/display/animations/text_scroller.py +104 -0
- is_matrix_forge/led_matrix/display/effects/__init__.py +17 -0
- is_matrix_forge/led_matrix/display/effects/breather.py +206 -0
- is_matrix_forge/led_matrix/display/effects/fade.py +0 -0
- is_matrix_forge/led_matrix/display/effects/scroller/__init__.py +92 -0
- is_matrix_forge/led_matrix/display/effects/scroller/base.py +76 -0
- is_matrix_forge/led_matrix/display/effects/scroller/helpers.py +62 -0
- is_matrix_forge/led_matrix/display/grid/__init__.py +9 -0
- is_matrix_forge/led_matrix/display/grid/grid.py +208 -0
- is_matrix_forge/led_matrix/display/grid/helpers.py +130 -0
- is_matrix_forge/led_matrix/display/grid/presets/__init__.py +64 -0
- is_matrix_forge/led_matrix/display/grid/presets/manifest.py +172 -0
- is_matrix_forge/led_matrix/display/helpers/__init__.py +110 -0
- is_matrix_forge/led_matrix/display/helpers/columns.py +33 -0
- is_matrix_forge/led_matrix/display/media.py +199 -0
- is_matrix_forge/led_matrix/display/patterns/__init__.py +59 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +91 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +3 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +29 -0
- is_matrix_forge/led_matrix/display/patterns/built_in/stencils.py +70 -0
- is_matrix_forge/led_matrix/display/patterns/utils.py +2 -0
- is_matrix_forge/led_matrix/display/text/__init__.py +66 -0
- is_matrix_forge/led_matrix/display/text/scroller.py +136 -0
- is_matrix_forge/led_matrix/errors/__init__.py +4 -0
- is_matrix_forge/led_matrix/errors/base.py +14 -0
- is_matrix_forge/led_matrix/errors/grid.py +37 -0
- is_matrix_forge/led_matrix/errors/matrix.py +68 -0
- is_matrix_forge/led_matrix/errors/misc.py +36 -0
- is_matrix_forge/led_matrix/fonts.py +20 -0
- is_matrix_forge/led_matrix/hardware.py +172 -0
- is_matrix_forge/led_matrix/helpers/__init__.py +165 -0
- is_matrix_forge/led_matrix/helpers/core/__init__.py +17 -0
- is_matrix_forge/led_matrix/helpers/core/lazy_mod_load.py +84 -0
- is_matrix_forge/led_matrix/helpers/device.py +118 -0
- is_matrix_forge/led_matrix/helpers/status_handler.py +14 -0
- is_matrix_forge/led_matrix/helpers/stream/__init__.py +70 -0
- is_matrix_forge/led_matrix/helpers/stream/errors.py +17 -0
- is_matrix_forge/led_matrix/led_matrix.py +25 -0
- is_matrix_forge/led_matrix/status.py +8 -0
- is_matrix_forge/led_matrix/tools/__init__.py +8 -0
- is_matrix_forge/led_matrix/tools/identify.py +27 -0
- is_matrix_forge/led_matrix/utils/__init__.py +41 -0
- is_matrix_forge/log_engine.py +25 -0
- is_matrix_forge/monitor/__init__.py +68 -0
- is_matrix_forge/monitor/errors.py +35 -0
- is_matrix_forge/monitor/events.py +78 -0
- is_matrix_forge/monitor/gui/__init__.py +0 -0
- is_matrix_forge/monitor/gui/errors.py +35 -0
- is_matrix_forge/monitor/gui/event/__init__.py +8 -0
- is_matrix_forge/monitor/gui/event/collection.py +153 -0
- is_matrix_forge/monitor/gui/event/errors.py +58 -0
- is_matrix_forge/monitor/gui/event/event.py +226 -0
- is_matrix_forge/monitor/gui/layout/__init__.py +8 -0
- is_matrix_forge/monitor/gui/layout/base.py +147 -0
- is_matrix_forge/monitor/gui/metaclasses/__init__.py +20 -0
- is_matrix_forge/monitor/gui/windows/__init__.py +0 -0
- is_matrix_forge/monitor/gui/windows/base.py +325 -0
- is_matrix_forge/monitor/gui/windows/main/__init__.py +77 -0
- is_matrix_forge/monitor/gui/windows/main/layout.py +21 -0
- is_matrix_forge/monitor/helpers.py +55 -0
- is_matrix_forge/monitor/monitor.py +431 -0
- is_matrix_forge/notify/__init__.py +0 -0
- is_matrix_forge/notify/sounds/__init__.py +6 -0
- is_matrix_forge/notify/sounds/base.py +106 -0
- is_matrix_forge/progress.py +99 -0
- is_matrix_forge-1.0.0.dev1.dist-info/LICENSE.md +19 -0
- is_matrix_forge-1.0.0.dev1.dist-info/METADATA +190 -0
- is_matrix_forge-1.0.0.dev1.dist-info/RECORD +123 -0
- is_matrix_forge-1.0.0.dev1.dist-info/WHEEL +4 -0
- 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,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()
|