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,252 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Tuple, Any, Dict
|
|
3
|
+
import PySimpleGUI as sg
|
|
4
|
+
import copy
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from ...led_matrix.helpers.device import DEVICES
|
|
8
|
+
from is_matrix_forge.led_matrix.display.animations.frame.base import Frame
|
|
9
|
+
from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
|
|
10
|
+
from is_matrix_forge.led_matrix.display.animations.frame.helpers import is_valid_frames
|
|
11
|
+
from .layout import PixelGridLayout
|
|
12
|
+
from is_matrix_forge.led_matrix import render_matrix
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize(key: Any) -> str:
|
|
16
|
+
s = str(key)
|
|
17
|
+
for ch in " <>,'()":
|
|
18
|
+
s = s.replace(ch, '_')
|
|
19
|
+
return s.strip('_')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PixelGrid:
|
|
23
|
+
"""
|
|
24
|
+
LED matrix pixel grid UI with frame-by-frame animation.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, width: int = 9, height: int = 34):
|
|
27
|
+
self.__width = None
|
|
28
|
+
self.__height = None
|
|
29
|
+
self.__frames = None
|
|
30
|
+
self.__current_frame = None
|
|
31
|
+
self.__grid = None
|
|
32
|
+
self.__preferred_device = None
|
|
33
|
+
|
|
34
|
+
self.width = width
|
|
35
|
+
self.height = height
|
|
36
|
+
|
|
37
|
+
base = [[0] * self.height for _ in range(self.width)]
|
|
38
|
+
Frame.__width = self.width
|
|
39
|
+
Frame.__height = self.height
|
|
40
|
+
self.frames = [Frame(copy.deepcopy(base))]
|
|
41
|
+
self.current_frame = 0
|
|
42
|
+
self.grid = self.frames[0].grid
|
|
43
|
+
self.preferred_device = None
|
|
44
|
+
|
|
45
|
+
layout = PixelGridLayout(self.width, self.height).build()
|
|
46
|
+
self.window = sg.Window('Pixel Grid', layout, finalize=True)
|
|
47
|
+
self._init_button_colors()
|
|
48
|
+
self._update_frame_indicator()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def width(self) -> int:
|
|
52
|
+
return self.__width
|
|
53
|
+
|
|
54
|
+
@width.setter
|
|
55
|
+
def width(self, value: Any) -> None:
|
|
56
|
+
if not isinstance(value, int) or value <= 0:
|
|
57
|
+
raise TypeError("Width must be a positive integer")
|
|
58
|
+
self.__width = value
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def height(self) -> int:
|
|
62
|
+
return self.__height
|
|
63
|
+
|
|
64
|
+
@height.setter
|
|
65
|
+
def height(self, value: Any) -> None:
|
|
66
|
+
if not isinstance(value, int) or value <= 0:
|
|
67
|
+
raise TypeError("Height must be a positive integer")
|
|
68
|
+
self.__height = value
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def frames(self) -> List[Frame]:
|
|
72
|
+
return self.__frames
|
|
73
|
+
|
|
74
|
+
@frames.setter
|
|
75
|
+
def frames(self, value: Any) -> None:
|
|
76
|
+
if not isinstance(value, list) or len(value) == 0 or not all(isinstance(f, Frame) for f in value):
|
|
77
|
+
raise TypeError("frames must be a non-empty list of Frame objects")
|
|
78
|
+
self.__frames = value
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def current_frame(self) -> int:
|
|
82
|
+
return self.__current_frame
|
|
83
|
+
|
|
84
|
+
@current_frame.setter
|
|
85
|
+
def current_frame(self, value: Any) -> None:
|
|
86
|
+
if not isinstance(value, int) or not (0 <= value < len(self.frames)):
|
|
87
|
+
raise ValueError("current_frame must be an index into frames list")
|
|
88
|
+
self.__current_frame = value
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def grid(self) -> List[List[int]]:
|
|
92
|
+
return self.__grid
|
|
93
|
+
|
|
94
|
+
@grid.setter
|
|
95
|
+
def grid(self, value: Any) -> None:
|
|
96
|
+
if not is_valid_grid(value, self.width, self.height):
|
|
97
|
+
raise ValueError("Grid must match width/height and be 0/1 values")
|
|
98
|
+
self.__grid = value
|
|
99
|
+
# update frame's grid as well
|
|
100
|
+
if hasattr(self, 'frames') and self.frames:
|
|
101
|
+
self.frames[self.current_frame].grid = value
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def preferred_device(self):
|
|
105
|
+
return self.__preferred_device
|
|
106
|
+
|
|
107
|
+
@preferred_device.setter
|
|
108
|
+
def preferred_device(self, value: Any) -> None:
|
|
109
|
+
# no strict type check, but could enforce presence in DEVICES
|
|
110
|
+
self.__preferred_device = value
|
|
111
|
+
|
|
112
|
+
def _init_button_colors(self) -> None:
|
|
113
|
+
for col in range(self.width):
|
|
114
|
+
for row in range(self.height):
|
|
115
|
+
state = self.grid[col][row]
|
|
116
|
+
color = 'green' if state else 'lightgrey'
|
|
117
|
+
self.window[(col, row)].update(button_color=('black', color))
|
|
118
|
+
|
|
119
|
+
def _update_frame_indicator(self) -> None:
|
|
120
|
+
total = len(self.frames)
|
|
121
|
+
idx = self.current_frame + 1
|
|
122
|
+
dur = self.frames[self.current_frame].duration
|
|
123
|
+
self.window['FRAME_INDICATOR'].update(
|
|
124
|
+
f'Frame {idx}/{total} (Duration: {dur}s)'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def run(self) -> None:
|
|
128
|
+
while True:
|
|
129
|
+
event, _ = self.window.read()
|
|
130
|
+
if event in (sg.WINDOW_CLOSED, 'Exit'):
|
|
131
|
+
break
|
|
132
|
+
handler = getattr(self, f'_handle_{_normalize(event)}', None)
|
|
133
|
+
if callable(handler):
|
|
134
|
+
handler(event)
|
|
135
|
+
elif isinstance(event, tuple):
|
|
136
|
+
self._toggle_pixel(event)
|
|
137
|
+
self.window.close()
|
|
138
|
+
|
|
139
|
+
def _toggle_pixel(self, key: Tuple[int, int]) -> None:
|
|
140
|
+
col, row = key
|
|
141
|
+
new = 1 - self.grid[col][row]
|
|
142
|
+
self.grid = [[new if (c==col and r==row) else self.grid[c][r]
|
|
143
|
+
for r in range(self.height)]
|
|
144
|
+
for c in range(self.width)]
|
|
145
|
+
self.window[key].update(
|
|
146
|
+
button_color=('black', 'green' if new else 'lightgrey')
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Frame management
|
|
150
|
+
def add_frame(self) -> None:
|
|
151
|
+
new_grid = copy.deepcopy(self.grid)
|
|
152
|
+
frm = Frame(new_grid)
|
|
153
|
+
self.frames = self.frames + [frm]
|
|
154
|
+
self.current_frame = len(self.frames) - 1
|
|
155
|
+
self.grid = frm.grid
|
|
156
|
+
self._init_button_colors()
|
|
157
|
+
self._update_frame_indicator()
|
|
158
|
+
|
|
159
|
+
def prev_frame(self) -> None:
|
|
160
|
+
if self.current_frame > 0:
|
|
161
|
+
self.current_frame -= 1
|
|
162
|
+
self._load_frame()
|
|
163
|
+
|
|
164
|
+
def next_frame(self) -> None:
|
|
165
|
+
if self.current_frame < len(self.frames) - 1:
|
|
166
|
+
self.current_frame += 1
|
|
167
|
+
self._load_frame()
|
|
168
|
+
|
|
169
|
+
def _load_frame(self) -> None:
|
|
170
|
+
frm = self.frames[self.current_frame]
|
|
171
|
+
self.grid = frm.grid
|
|
172
|
+
self._init_button_colors()
|
|
173
|
+
self._update_frame_indicator()
|
|
174
|
+
|
|
175
|
+
# Export handlers
|
|
176
|
+
def _handle_Export(self, _event) -> None:
|
|
177
|
+
data = [f.grid for f in self.frames] if len(self.frames) > 1 else self.frames[0].grid
|
|
178
|
+
print(data)
|
|
179
|
+
|
|
180
|
+
def _handle_Export_to_File(self, _event) -> None:
|
|
181
|
+
path = sg.popup_get_file(
|
|
182
|
+
'Save Grid As', save_as=True, no_window=True,
|
|
183
|
+
file_types=(('JSON Files', '*.json'),), default_extension='.json'
|
|
184
|
+
)
|
|
185
|
+
if not path:
|
|
186
|
+
return
|
|
187
|
+
data = [f.grid for f in self.frames] if len(self.frames) > 1 else self.frames[0].grid
|
|
188
|
+
try:
|
|
189
|
+
with open(path, 'w') as f:
|
|
190
|
+
json.dump(data, f)
|
|
191
|
+
sg.popup('Success', f'Saved to {os.path.basename(path)}')
|
|
192
|
+
except Exception as e:
|
|
193
|
+
sg.popup_error('Error', str(e))
|
|
194
|
+
|
|
195
|
+
def _handle_Load_from_File(self, _event) -> None:
|
|
196
|
+
path = sg.popup_get_file(
|
|
197
|
+
'Load Grid From', no_window=True,
|
|
198
|
+
file_types=(('JSON Files', '*.json'),)
|
|
199
|
+
)
|
|
200
|
+
if not path:
|
|
201
|
+
return
|
|
202
|
+
try:
|
|
203
|
+
raw = json.load(open(path))
|
|
204
|
+
if is_valid_grid(raw, self.width, self.height):
|
|
205
|
+
grids = [raw]
|
|
206
|
+
elif is_valid_frames(raw, self.width, self.height):
|
|
207
|
+
grids = raw
|
|
208
|
+
else:
|
|
209
|
+
sg.popup_error('Invalid format')
|
|
210
|
+
return
|
|
211
|
+
self.frames = [Frame(copy.deepcopy(g)) for g in grids]
|
|
212
|
+
self.current_frame = 0
|
|
213
|
+
self._load_frame()
|
|
214
|
+
sg.popup('Success', f'Loaded {len(self.frames)} frame(s)')
|
|
215
|
+
except Exception as e:
|
|
216
|
+
sg.popup_error('Error', str(e))
|
|
217
|
+
|
|
218
|
+
# Send to matrix
|
|
219
|
+
def _handle_Send_to_Matrix(self, _event) -> None:
|
|
220
|
+
devices = DEVICES
|
|
221
|
+
if not devices:
|
|
222
|
+
sg.popup_error('No devices found.')
|
|
223
|
+
return
|
|
224
|
+
if self.preferred_device in devices:
|
|
225
|
+
dev = self.preferred_device
|
|
226
|
+
else:
|
|
227
|
+
descs = [f"{d.device} - {d.description}" for d in devices]
|
|
228
|
+
win = sg.Window('Select Device', [
|
|
229
|
+
[sg.Text('Select device:')],
|
|
230
|
+
[sg.Listbox(descs, size=(40, len(descs)), key='DEV')],
|
|
231
|
+
[sg.Checkbox('Always choose', key='REM')],
|
|
232
|
+
[sg.OK(), sg.Cancel()]
|
|
233
|
+
], modal=True)
|
|
234
|
+
ev, vals = win.read(); win.close()
|
|
235
|
+
if ev != 'OK' or not vals['DEV']:
|
|
236
|
+
return
|
|
237
|
+
dev = devices[descs.index(vals['DEV'][0])]
|
|
238
|
+
if vals['REM']:
|
|
239
|
+
self.preferred_device = dev
|
|
240
|
+
try:
|
|
241
|
+
render_matrix(dev, self.grid)
|
|
242
|
+
sg.popup('Success', f'Sent to {dev.device}')
|
|
243
|
+
except Exception as e:
|
|
244
|
+
sg.popup_error('Error sending:', str(e))
|
|
245
|
+
|
|
246
|
+
# Button-mapped handlers
|
|
247
|
+
def _handle_Add_Frame(self, e): self.add_frame()
|
|
248
|
+
def _handle_Prev(self, e): self.prev_frame()
|
|
249
|
+
def _handle_Next(self, e): self.next_frame()
|
|
250
|
+
|
|
251
|
+
def _validate(self, grid: Any) -> bool:
|
|
252
|
+
return is_valid_grid(grid, self.width, self.height)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import PySimpleGUI as sg
|
|
2
|
+
from typing import List, Tuple, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PixelGridLayout:
|
|
6
|
+
"""
|
|
7
|
+
Builds the PySimpleGUI layout for the pixel grid, control panel,
|
|
8
|
+
and frame animation controls.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
width: int = 9,
|
|
13
|
+
height: int = 34,
|
|
14
|
+
button_list: List[str] = None,
|
|
15
|
+
pad: Tuple[int, int] = (0, 0),
|
|
16
|
+
button_size: Tuple[int, int] = (2, 1)
|
|
17
|
+
):
|
|
18
|
+
# initialize backing fields
|
|
19
|
+
self.__width = None
|
|
20
|
+
self.__height = None
|
|
21
|
+
self.__pad = None
|
|
22
|
+
self.__button_size = None
|
|
23
|
+
self.__button_list = None
|
|
24
|
+
# assign via properties
|
|
25
|
+
self.width = width
|
|
26
|
+
self.height = height
|
|
27
|
+
self.pad = pad
|
|
28
|
+
self.button_size = button_size
|
|
29
|
+
self.button_list = button_list or [
|
|
30
|
+
'Export', 'Export to File', 'Load from File',
|
|
31
|
+
'Send to Matrix', 'Add Frame', 'Exit'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def width(self) -> int:
|
|
36
|
+
return self.__width
|
|
37
|
+
|
|
38
|
+
@width.setter
|
|
39
|
+
def width(self, value: Any) -> None:
|
|
40
|
+
if not isinstance(value, int) or value <= 0:
|
|
41
|
+
raise TypeError("Width must be a positive integer")
|
|
42
|
+
self.__width = value
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def height(self) -> int:
|
|
46
|
+
return self.__height
|
|
47
|
+
|
|
48
|
+
@height.setter
|
|
49
|
+
def height(self, value: Any) -> None:
|
|
50
|
+
if not isinstance(value, int) or value <= 0:
|
|
51
|
+
raise TypeError("Height must be a positive integer")
|
|
52
|
+
self.__height = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def pad(self) -> Tuple[int, int]:
|
|
56
|
+
return self.__pad
|
|
57
|
+
|
|
58
|
+
@pad.setter
|
|
59
|
+
def pad(self, value: Any) -> None:
|
|
60
|
+
if (
|
|
61
|
+
not isinstance(value, tuple)
|
|
62
|
+
or len(value) != 2
|
|
63
|
+
or not all(isinstance(v, int) for v in value)
|
|
64
|
+
):
|
|
65
|
+
raise TypeError("pad must be a tuple of two integers")
|
|
66
|
+
self.__pad = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def button_size(self) -> Tuple[int, int]:
|
|
70
|
+
return self.__button_size
|
|
71
|
+
|
|
72
|
+
@button_size.setter
|
|
73
|
+
def button_size(self, value: Any) -> None:
|
|
74
|
+
if (
|
|
75
|
+
not isinstance(value, tuple)
|
|
76
|
+
or len(value) != 2
|
|
77
|
+
or not all(isinstance(v, int) for v in value)
|
|
78
|
+
):
|
|
79
|
+
raise TypeError("button_size must be a tuple of two integers")
|
|
80
|
+
self.__button_size = value
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def button_list(self) -> List[str]:
|
|
84
|
+
return self.__button_list
|
|
85
|
+
|
|
86
|
+
@button_list.setter
|
|
87
|
+
def button_list(self, value: Any) -> None:
|
|
88
|
+
if (
|
|
89
|
+
not isinstance(value, list)
|
|
90
|
+
or not all(isinstance(v, str) for v in value)
|
|
91
|
+
):
|
|
92
|
+
raise TypeError("button_list must be a list of strings")
|
|
93
|
+
self.__button_list = value
|
|
94
|
+
|
|
95
|
+
def build(self) -> List[List[Any]]:
|
|
96
|
+
grid = [
|
|
97
|
+
[sg.Button(' ', size=self.button_size, key=(col, row), pad=self.pad)
|
|
98
|
+
for col in range(self.width)]
|
|
99
|
+
for row in range(self.height)
|
|
100
|
+
]
|
|
101
|
+
grid_column = sg.Column(grid)
|
|
102
|
+
|
|
103
|
+
controls = [[sg.Button(btn)] for btn in self.button_list]
|
|
104
|
+
control_column = sg.Column(controls, vertical_alignment='top')
|
|
105
|
+
|
|
106
|
+
frame_controls = [
|
|
107
|
+
sg.Button('< Prev', key='<Prev>'),
|
|
108
|
+
sg.Text('', key='FRAME_INDICATOR'),
|
|
109
|
+
sg.Button('Next >', key='Next>')
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
layout = [
|
|
113
|
+
[grid_column, sg.VerticalSeparator(), control_column],
|
|
114
|
+
[sg.HorizontalSeparator(pad=(0, 5))],
|
|
115
|
+
[sg.Push(), *frame_controls, sg.Push()]
|
|
116
|
+
]
|
|
117
|
+
return layout
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from platformdirs import PlatformDirs
|
|
3
|
+
from easy_exit_calls import ExitCallHandler
|
|
4
|
+
from is_matrix_forge.log_engine import ROOT_LOGGER as PARENT_LOGGER
|
|
5
|
+
|
|
6
|
+
MOD_LOGGER = PARENT_LOGGER.get_child('monitor.gui.debug')
|
|
7
|
+
|
|
8
|
+
DEBUG_MODE_KEYFILE_NAME = 'marauder.map'
|
|
9
|
+
|
|
10
|
+
ECH: ExitCallHandler
|
|
11
|
+
|
|
12
|
+
PLATFORM_DIRS = PlatformDirs('LEDMatrixPowerMonitor', 'Inspyre-Softworks')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_debug_mode_keyfile_path():
|
|
16
|
+
return PLATFORM_DIRS.user_data_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MARAUDER_FILE_PATH = get_debug_mode_keyfile_path()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_marauders_map(mm_path=MARAUDER_FILE_PATH):
|
|
23
|
+
log = MOD_LOGGER.get_child('create_marauders_map')
|
|
24
|
+
mm_path = Path(mm_path).expanduser().resolve()
|
|
25
|
+
log.debug(f'Creating Marauder\'s Map at {mm_path}')
|
|
26
|
+
|
|
27
|
+
if not mm_path.parent.exists():
|
|
28
|
+
log.debug(f'Creating parent directory for Marauder\'s Map: {mm_path.parent}')
|
|
29
|
+
mm_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
with open(mm_path, 'w') as f:
|
|
32
|
+
f.write('Mooney, Wormtail, Padfoot, and Prongs are proud to present the Marauder\'s Map.')
|
|
33
|
+
|
|
34
|
+
log.debug(f'Marauder\'s Map created at {mm_path}')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def disable_debug_mode(mm_file_path=MARAUDER_FILE_PATH):
|
|
38
|
+
log = MOD_LOGGER.get_child('disable_debug_mode')
|
|
39
|
+
mm_file_path = Path(mm_file_path).expanduser().resolve()
|
|
40
|
+
log.debug(f'Writing magic words to Marauder\'s Map at {mm_file_path}')
|
|
41
|
+
|
|
42
|
+
if not mm_file_path.exists():
|
|
43
|
+
log.warning(f'Marauder\'s Map file does not exist: {mm_file_path}. (No debug mode to disable)')
|
|
44
|
+
raise RuntimeError(f'Marauder\'s Map file does not exist: {mm_file_path}. (No debug mode to disable)')
|
|
45
|
+
|
|
46
|
+
with open(MARAUDER_FILE_PATH, 'w') as f:
|
|
47
|
+
f.write('Mischief Managed.')
|
|
48
|
+
log.debug(f'Magic words written to Marauder\'s Map at {mm_file_path}')
|
|
49
|
+
|
|
50
|
+
if 'ECH' in globals() and globals()['ECH'].function_registered(disable_debug_mode):
|
|
51
|
+
log.debug(f'Found ExitCallHandler instance with registered function: {disable_debug_mode}. Unregistering it.')
|
|
52
|
+
globals()['ECH'].unregister_handler(disable_debug_mode)
|
|
53
|
+
log.debug('Unregistered ExitCallHandler instance with registered function: {disable_debug_mode}.')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enable_debug_mode(
|
|
57
|
+
mm_file_path=MARAUDER_FILE_PATH,
|
|
58
|
+
disable_on_exit=False,
|
|
59
|
+
):
|
|
60
|
+
mm_file_path = Path(mm_file_path).expanduser().resolve()
|
|
61
|
+
|
|
62
|
+
if not mm_file_path.exists():
|
|
63
|
+
create_marauders_map(mm_file_path)
|
|
64
|
+
|
|
65
|
+
with open(MARAUDER_FILE_PATH, 'w') as f:
|
|
66
|
+
f.write('I solemnly swear that I am up to no good.')
|
|
67
|
+
|
|
68
|
+
if disable_on_exit:
|
|
69
|
+
from easy_exit_calls import ExitCallHandler
|
|
70
|
+
global ECH
|
|
71
|
+
ech = ExitCallHandler()
|
|
72
|
+
ech.register_handler(disable_debug_mode, mm_file_path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_debug_mode(portkey_file_path=MARAUDER_FILE_PATH):
|
|
76
|
+
try:
|
|
77
|
+
with open(portkey_file_path, 'r') as f:
|
|
78
|
+
# Check if the file contains the correct content
|
|
79
|
+
content = f.read()
|
|
80
|
+
return content.strip() == 'I solemnly swear that I am up to no good.'
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
return False
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Author:
|
|
5
|
+
Inspyre Softworks
|
|
6
|
+
|
|
7
|
+
Project:
|
|
8
|
+
led-matrix-battery
|
|
9
|
+
|
|
10
|
+
File:
|
|
11
|
+
is_matrix_forge/dev_tools/presets.py
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Description:
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Union, Optional
|
|
20
|
+
from is_matrix_forge.common.dirs import PRESETS_DIR
|
|
21
|
+
from is_matrix_forge.led_matrix.constants import PROJECT_URLS
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MANIFEST_FILE_NAME = 'manifest.json'
|
|
26
|
+
REMOTE_MANIFEST_URL = PROJECT_URLS['github_api'] + '/' + MANIFEST_FILE_NAME
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_remote_manifest():
|
|
30
|
+
"""
|
|
31
|
+
Download the remote preset manifest file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict:
|
|
35
|
+
The JSON content of the remote manifest file.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
requests.RequestException:
|
|
39
|
+
If there is an error during the HTTP request.
|
|
40
|
+
"""
|
|
41
|
+
res = requests.get(REMOTE_MANIFEST_URL)
|
|
42
|
+
res.raise_for_status()
|
|
43
|
+
|
|
44
|
+
data = res.json()
|
|
45
|
+
|
|
46
|
+
return data
|
|
47
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
import serial
|
|
3
|
+
|
|
4
|
+
DISCONNECTED_DEVS = []
|
|
5
|
+
FWK_MAGIC = [0x32, 0xAC]
|
|
6
|
+
FWK_VID = 0x32AC
|
|
7
|
+
LED_MATRIX_PID = 0x20
|
|
8
|
+
QTPY_PID = 0x001F
|
|
9
|
+
INPUTMODULE_PIDS = [LED_MATRIX_PID, QTPY_PID]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def disconnect_dev(dev):
|
|
13
|
+
"""
|
|
14
|
+
Disconnect the device from the system.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
dev (str):
|
|
18
|
+
The device to disconnect.
|
|
19
|
+
"""
|
|
20
|
+
global DISCONNECTED_DEVS
|
|
21
|
+
if dev in DISCONNECTED_DEVS:
|
|
22
|
+
return
|
|
23
|
+
DISCONNECTED_DEVS.append(dev)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CommandVals(IntEnum):
|
|
27
|
+
Brightness = 0x00
|
|
28
|
+
Pattern = 0x01
|
|
29
|
+
BootloaderReset = 0x02
|
|
30
|
+
Sleep = 0x03
|
|
31
|
+
Animate = 0x04
|
|
32
|
+
Panic = 0x05
|
|
33
|
+
Draw = 0x06
|
|
34
|
+
StageGreyCol = 0x07
|
|
35
|
+
DrawGreyColBuffer = 0x08
|
|
36
|
+
SetText = 0x09
|
|
37
|
+
StartGame = 0x10
|
|
38
|
+
GameControl = 0x11
|
|
39
|
+
GameStatus = 0x12
|
|
40
|
+
SetColor = 0x13
|
|
41
|
+
DisplayOn = 0x14
|
|
42
|
+
InvertScreen = 0x15
|
|
43
|
+
SetPixelColumn = 0x16
|
|
44
|
+
FlushFramebuffer = 0x17
|
|
45
|
+
ClearRam = 0x18
|
|
46
|
+
ScreenSaver = 0x19
|
|
47
|
+
SetFps = 0x1A
|
|
48
|
+
SetPowerMode = 0x1B
|
|
49
|
+
PwmFreq = 0x1E
|
|
50
|
+
DebugMode = 0x1F
|
|
51
|
+
Version = 0x20
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Game(IntEnum):
|
|
55
|
+
Snake = 0x00
|
|
56
|
+
Pong = 0x01
|
|
57
|
+
Tetris = 0x02
|
|
58
|
+
GameOfLife = 0x03
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PatternVals(IntEnum):
|
|
62
|
+
Percentage = 0x00
|
|
63
|
+
Gradient = 0x01
|
|
64
|
+
DoubleGradient = 0x02
|
|
65
|
+
DisplayLotus = 0x03
|
|
66
|
+
ZigZag = 0x04
|
|
67
|
+
FullBrightness = 0x05
|
|
68
|
+
DisplayPanic = 0x06
|
|
69
|
+
DisplayLotus2 = 0x07
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GameOfLifeStartParam(IntEnum):
|
|
73
|
+
Currentmatrix = 0x00
|
|
74
|
+
Pattern1 = 0x01
|
|
75
|
+
Blinker = 0x02
|
|
76
|
+
Toad = 0x03
|
|
77
|
+
Beacon = 0x04
|
|
78
|
+
Glider = 0x05
|
|
79
|
+
|
|
80
|
+
def __str__(self):
|
|
81
|
+
return self.name.lower()
|
|
82
|
+
|
|
83
|
+
def __repr__(self):
|
|
84
|
+
return str(self)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def argparse(s):
|
|
88
|
+
try:
|
|
89
|
+
return GameOfLifeStartParam[s.lower().capitalize()]
|
|
90
|
+
except KeyError:
|
|
91
|
+
return s
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class GameControlVal(IntEnum):
|
|
95
|
+
Up = 0
|
|
96
|
+
Down = 1
|
|
97
|
+
Left = 2
|
|
98
|
+
Right = 3
|
|
99
|
+
Quit = 4
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
RESPONSE_SIZE = 32
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def bootloader_jump(dev):
|
|
106
|
+
"""Reboot into the bootloader to flash new firmware"""
|
|
107
|
+
send_command(dev, CommandVals.BootloaderReset, [0x00])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def brightness(dev, b: int):
|
|
111
|
+
"""Adjust the brightness scaling of the entire screen."""
|
|
112
|
+
send_command(dev, CommandVals.Brightness, [b])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_brightness(dev):
|
|
116
|
+
"""Adjust the brightness scaling of the entire screen."""
|
|
117
|
+
res = send_command(dev, CommandVals.Brightness, with_response=True)
|
|
118
|
+
return int(res[0])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_version(dev):
|
|
122
|
+
"""Get the device's firmware version"""
|
|
123
|
+
res = send_command(dev, CommandVals.Version, with_response=True)
|
|
124
|
+
if not res:
|
|
125
|
+
return 'Unknown'
|
|
126
|
+
major = res[0]
|
|
127
|
+
minor = (res[1] & 0xF0) >> 4
|
|
128
|
+
patch = res[1] & 0xF
|
|
129
|
+
pre_release = res[2]
|
|
130
|
+
|
|
131
|
+
version = f"{major}.{minor}.{patch}"
|
|
132
|
+
if pre_release:
|
|
133
|
+
version += " (Pre-release)"
|
|
134
|
+
return version
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def send_command(dev, command, parameters=[], with_response=False):
|
|
138
|
+
return send_command_raw(dev, FWK_MAGIC + [command] + parameters, with_response)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def send_command_raw(dev, command, with_response=False):
|
|
142
|
+
"""Send a command to the device.
|
|
143
|
+
Opens new serial connection every time"""
|
|
144
|
+
# print(f"Sending command: {command}")
|
|
145
|
+
try:
|
|
146
|
+
with serial.Serial(dev.device, 115200) as s:
|
|
147
|
+
s.write(command)
|
|
148
|
+
|
|
149
|
+
if with_response:
|
|
150
|
+
res = s.read(RESPONSE_SIZE)
|
|
151
|
+
# print(f"Received: {res}")
|
|
152
|
+
return res
|
|
153
|
+
except (IOError, OSError) as _ex:
|
|
154
|
+
disconnect_dev(dev.device)
|
|
155
|
+
# print("Error: ", ex)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def send_serial(dev, s, command):
|
|
159
|
+
"""Send serial command by using existing serial connection"""
|
|
160
|
+
try:
|
|
161
|
+
s.write(command)
|
|
162
|
+
except (IOError, OSError) as _ex:
|
|
163
|
+
disconnect_dev(dev.device)
|
|
164
|
+
# print("Error: ", ex)
|