IS-Matrix-Forge 1.0.0.dev2__py3-none-any.whl → 1.0.0.dev5__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 +1 -1
- is_matrix_forge/assets/__init__.py +1 -1
- is_matrix_forge/common/helpers/__init__.py +32 -0
- is_matrix_forge/designer_gui/__init__.py +1 -1
- is_matrix_forge/designer_gui/main_window/__init__.py +11 -3
- is_matrix_forge/designer_gui/main_window/{layout.py → layout/__init__.py} +2 -6
- is_matrix_forge/designer_gui/main_window/layout/button_grid/__init__.py +0 -0
- is_matrix_forge/designer_gui/main_window/layout/button_grid/column.py +17 -0
- is_matrix_forge/designer_gui/main_window/layout/button_grid/grid.py +15 -0
- is_matrix_forge/designer_gui/main_window/streamlit_app.py +93 -0
- is_matrix_forge/dev_tools/__init__.py +1 -1
- is_matrix_forge/inputmodule/ledmatrix.py +0 -16
- is_matrix_forge/led_matrix/Scripts/__init__.py +1 -1
- is_matrix_forge/led_matrix/Scripts/identify_matrices.py +228 -0
- is_matrix_forge/led_matrix/__init__.py +55 -4
- is_matrix_forge/led_matrix/commands/__init__.py +1 -66
- is_matrix_forge/led_matrix/constants.py +1 -0
- is_matrix_forge/led_matrix/controller/__init__.py +2 -0
- is_matrix_forge/led_matrix/controller/controller.py +75 -17
- is_matrix_forge/led_matrix/display/animations/__init__.py +0 -1
- is_matrix_forge/led_matrix/display/animations/animation.py +14 -2
- is_matrix_forge/led_matrix/display/animations/effects/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/animations/errors.py +10 -0
- is_matrix_forge/led_matrix/display/animations/font/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/animations/frame/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/animations/frame/base.py +1 -1
- is_matrix_forge/led_matrix/display/effects/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/effects/breather.py +27 -11
- is_matrix_forge/led_matrix/display/grid/presets/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/helpers/__init__.py +7 -1
- is_matrix_forge/led_matrix/display/helpers/columns.py +1 -1
- is_matrix_forge/led_matrix/display/media.py +1 -2
- is_matrix_forge/led_matrix/display/patterns/built_in/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/patterns/built_in/commands.py +2 -2
- is_matrix_forge/led_matrix/display/patterns/built_in/constants.py +1 -27
- is_matrix_forge/led_matrix/display/patterns/built_in/stencils/res.py +64 -0
- is_matrix_forge/led_matrix/display/text/__init__.py +1 -1
- is_matrix_forge/led_matrix/display/text/scroller.py +22 -0
- is_matrix_forge/led_matrix/hardware.py +153 -54
- is_matrix_forge/led_matrix/helpers/__init__.py +5 -1
- is_matrix_forge/led_matrix/helpers/core/__init__.py +1 -1
- is_matrix_forge/led_matrix/tools/__init__.py +1 -1
- is_matrix_forge/led_matrix/utils/__init__.py +1 -1
- is_matrix_forge/monitor/gui/layout/__init__.py +1 -1
- is_matrix_forge/monitor/monitor.py +3 -2
- is_matrix_forge/scripts/__init__.py +1 -0
- is_matrix_forge/scripts/preset_installer.py +184 -0
- is_matrix_forge/serial_com/__init__.py +1 -0
- is_matrix_forge/serial_com/base/__init__.py +18 -0
- is_matrix_forge/serial_com/base/answer.py +12 -0
- is_matrix_forge/serial_com/base/message.py +13 -0
- is_matrix_forge/serial_com/base/request.py +8 -0
- is_matrix_forge/serial_com/exchange.py +47 -0
- {is_matrix_forge-1.0.0.dev2.dist-info → is_matrix_forge-1.0.0.dev5.dist-info}/METADATA +52 -52
- {is_matrix_forge-1.0.0.dev2.dist-info → is_matrix_forge-1.0.0.dev5.dist-info}/RECORD +59 -47
- is_matrix_forge/led_matrix/display/patterns/__init__.py +0 -59
- is_matrix_forge/led_matrix/display/patterns/utils.py +0 -2
- /is_matrix_forge/led_matrix/display/patterns/built_in/{stencils.py → stencils/__init__.py} +0 -0
- {is_matrix_forge-1.0.0.dev2.dist-info → is_matrix_forge-1.0.0.dev5.dist-info}/LICENSE.md +0 -0
- {is_matrix_forge-1.0.0.dev2.dist-info → is_matrix_forge-1.0.0.dev5.dist-info}/WHEEL +0 -0
- {is_matrix_forge-1.0.0.dev2.dist-info → is_matrix_forge-1.0.0.dev5.dist-info}/entry_points.txt +0 -0
is_matrix_forge/__init__.py
CHANGED
|
@@ -3,6 +3,6 @@ Author: Taylor B. tayjaybabee@gmail.com
|
|
|
3
3
|
Date: 2025-04-15 09:15:25
|
|
4
4
|
LastEditors: Taylor B. tayjaybabee@gmail.com
|
|
5
5
|
LastEditTime: 2025-04-15 09:24:40
|
|
6
|
-
FilePath: is_matrix_forge/
|
|
6
|
+
FilePath: is_matrix_forge/grid.py
|
|
7
7
|
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
|
|
8
8
|
"""
|
|
@@ -3,6 +3,6 @@ Author: Taylor B. tayjaybabee@gmail.com
|
|
|
3
3
|
Date: 2025-04-15 09:15:37
|
|
4
4
|
LastEditors: Taylor B. tayjaybabee@gmail.com
|
|
5
5
|
LastEditTime: 2025-04-15 09:24:40
|
|
6
|
-
FilePath: is_matrix_forge/assets/
|
|
6
|
+
FilePath: is_matrix_forge/assets/grid.py
|
|
7
7
|
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
|
|
8
8
|
"""
|
|
@@ -3,6 +3,10 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
import requests
|
|
5
5
|
from inspyre_toolbox.path_man import provision_path
|
|
6
|
+
from is_matrix_forge.log_engine import ROOT_LOGGER
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MOD_LOGGER = ROOT_LOGGER.get_child(__name__)
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def download_presets():
|
|
@@ -24,9 +28,13 @@ def calculate_checksum(file_path):
|
|
|
24
28
|
str:
|
|
25
29
|
The SHA-256 checksum of the file.
|
|
26
30
|
"""
|
|
31
|
+
log = MOD_LOGGER.get_child('calculate_checksum')
|
|
32
|
+
|
|
27
33
|
if not isinstance(file_path, Path):
|
|
28
34
|
file_path = provision_path(file_path)
|
|
29
35
|
|
|
36
|
+
log.debug(f'Calculating checksum for file: {file_path}')
|
|
37
|
+
|
|
30
38
|
sha256 = hashlib.sha256()
|
|
31
39
|
|
|
32
40
|
with open(file_path, 'rb') as f:
|
|
@@ -78,6 +86,30 @@ def percentage_to_value(percent, max_value=255):
|
|
|
78
86
|
return int(round(percent * max_value / 100))
|
|
79
87
|
|
|
80
88
|
|
|
89
|
+
def value_to_percentage(value, max_value=255):
|
|
90
|
+
"""
|
|
91
|
+
Convert a value to its percentage representation relative to `max_value`.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
value (int or float):
|
|
95
|
+
The value to convert.
|
|
96
|
+
|
|
97
|
+
max_value (int, optional):
|
|
98
|
+
The maximum possible value. Defaults to 255.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
float:
|
|
102
|
+
The percentage value between 0 and 100.
|
|
103
|
+
"""
|
|
104
|
+
if not isinstance(value, (int, float)):
|
|
105
|
+
raise TypeError(f'value must be of type int or float, not {type(value)}')
|
|
106
|
+
|
|
107
|
+
if value < 0 or value > max_value:
|
|
108
|
+
raise ValueError(f'value must be between 0 and {max_value}, not {value}')
|
|
109
|
+
|
|
110
|
+
return (value / max_value) * 100
|
|
111
|
+
|
|
112
|
+
|
|
81
113
|
def verify_checksum(remote_checksum, local_file_path):
|
|
82
114
|
"""
|
|
83
115
|
Verify the checksum of a local file against a remote checksum.
|
|
@@ -3,6 +3,6 @@ Author: Taylor B. tayjaybabee@gmail.com
|
|
|
3
3
|
Date: 2025-04-30 16:01:10
|
|
4
4
|
LastEditors: Taylor B. tayjaybabee@gmail.com
|
|
5
5
|
LastEditTime: 2025-04-30 16:01:49
|
|
6
|
-
FilePath: is_matrix_forge/designer_gui/
|
|
6
|
+
FilePath: is_matrix_forge/designer_gui/grid.py
|
|
7
7
|
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
|
|
8
8
|
"""
|
|
@@ -4,12 +4,15 @@ import PySimpleGUI as sg
|
|
|
4
4
|
import copy
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from is_matrix_forge.led_matrix.helpers.device import DEVICES
|
|
8
8
|
from is_matrix_forge.led_matrix.display.animations.frame.base import Frame
|
|
9
9
|
from is_matrix_forge.led_matrix.display.grid.helpers import is_valid_grid
|
|
10
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
|
|
11
|
+
from is_matrix_forge.designer_gui.main_window.layout import PixelGridLayout
|
|
12
|
+
from is_matrix_forge.led_matrix.display.helpers import render_matrix
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
sg.theme('DarkTeal2')
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def _normalize(key: Any) -> str:
|
|
@@ -139,6 +142,7 @@ class PixelGrid:
|
|
|
139
142
|
def _toggle_pixel(self, key: Tuple[int, int]) -> None:
|
|
140
143
|
col, row = key
|
|
141
144
|
new = 1 - self.grid[col][row]
|
|
145
|
+
print(f'Pixel {col},{row} toggled from {self.grid[col][row]} to {new}')
|
|
142
146
|
self.grid = [[new if (c==col and r==row) else self.grid[c][r]
|
|
143
147
|
for r in range(self.height)]
|
|
144
148
|
for c in range(self.width)]
|
|
@@ -250,3 +254,7 @@ class PixelGrid:
|
|
|
250
254
|
|
|
251
255
|
def _validate(self, grid: Any) -> bool:
|
|
252
256
|
return is_valid_grid(grid, self.width, self.height)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == '__main__':
|
|
260
|
+
PixelGrid().run()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import PySimpleGUI as sg
|
|
2
2
|
from typing import List, Tuple, Any
|
|
3
|
+
from is_matrix_forge.designer_gui.main_window.layout.button_grid.column import build_grid_frame
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class PixelGridLayout:
|
|
@@ -93,12 +94,7 @@ class PixelGridLayout:
|
|
|
93
94
|
self.__button_list = value
|
|
94
95
|
|
|
95
96
|
def build(self) -> List[List[Any]]:
|
|
96
|
-
|
|
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)
|
|
97
|
+
grid_column = build_grid_frame(self.width, self.height, button_size=self.button_size, pad=self.pad)
|
|
102
98
|
|
|
103
99
|
controls = [[sg.Button(btn)] for btn in self.button_list]
|
|
104
100
|
control_column = sg.Column(controls, vertical_alignment='top')
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import PySimpleGUI as sg
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def build_grid_frame(num_cols=9, num_rows=34, button_size=(2,1), pad=(1,1)):
|
|
5
|
+
"""Return a layout of 9 vertical columns (of 34 buttons each), side by side."""
|
|
6
|
+
return [
|
|
7
|
+
[
|
|
8
|
+
sg.Column(
|
|
9
|
+
[
|
|
10
|
+
[sg.Button(' ', key=(col, row), size=button_size, pad=pad)]
|
|
11
|
+
for row in range(num_rows)
|
|
12
|
+
],
|
|
13
|
+
pad=(0,0)
|
|
14
|
+
)
|
|
15
|
+
for col in range(num_cols)
|
|
16
|
+
]
|
|
17
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PySimpleGUI as sg
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def build_button_grid(num_cols, num_rows, button_size=(1, 1), pad=(1, 1)):
|
|
5
|
+
"""
|
|
6
|
+
Returns a list of `num_cols` lists, each containing `num_rows` buttons.
|
|
7
|
+
So: 9 columns of 34 rows each.
|
|
8
|
+
"""
|
|
9
|
+
return [
|
|
10
|
+
[sg.Button(' ', key=(col, row), size=button_size, pad=pad)]
|
|
11
|
+
for row in range(num_rows)
|
|
12
|
+
],
|
|
13
|
+
for col in range(num_cols)
|
|
14
|
+
]
|
|
15
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
import json
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
class Frame:
|
|
6
|
+
def __init__(self, grid, duration=1.0):
|
|
7
|
+
self.grid = grid
|
|
8
|
+
self.duration = duration
|
|
9
|
+
|
|
10
|
+
class PixelGridWeb:
|
|
11
|
+
def __init__(self, width=9, height=34):
|
|
12
|
+
self.width = width
|
|
13
|
+
self.height = height
|
|
14
|
+
if "frames" not in st.session_state:
|
|
15
|
+
st.session_state.frames = [Frame(self._new_grid())]
|
|
16
|
+
if "current_frame" not in st.session_state:
|
|
17
|
+
st.session_state.current_frame = 0
|
|
18
|
+
|
|
19
|
+
def _new_grid(self):
|
|
20
|
+
return [[0 for _ in range(self.height)] for _ in range(self.width)]
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def frames(self):
|
|
24
|
+
return st.session_state.frames
|
|
25
|
+
|
|
26
|
+
@frames.setter
|
|
27
|
+
def frames(self, value):
|
|
28
|
+
st.session_state.frames = value
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def current_frame(self):
|
|
32
|
+
return st.session_state.current_frame
|
|
33
|
+
|
|
34
|
+
@current_frame.setter
|
|
35
|
+
def current_frame(self, value):
|
|
36
|
+
st.session_state.current_frame = value
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def grid(self):
|
|
40
|
+
return self.frames[self.current_frame].grid
|
|
41
|
+
|
|
42
|
+
def draw(self):
|
|
43
|
+
st.title("PixelGrid Animator")
|
|
44
|
+
st.write(f"Frame {self.current_frame+1}/{len(self.frames)}")
|
|
45
|
+
# Render grid
|
|
46
|
+
grid = self.grid
|
|
47
|
+
for row in range(self.height):
|
|
48
|
+
cols = st.columns(self.width)
|
|
49
|
+
for col in range(self.width):
|
|
50
|
+
key = f"pix_{col}_{row}_{self.current_frame}"
|
|
51
|
+
is_on = grid[col][row]
|
|
52
|
+
label = "🟩" if is_on else "⬛"
|
|
53
|
+
if cols[col].button(label, key=key, use_container_width=True):
|
|
54
|
+
grid[col][row] ^= 1 # Toggle
|
|
55
|
+
|
|
56
|
+
# Frame controls
|
|
57
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
58
|
+
if c1.button("Prev", disabled=self.current_frame == 0):
|
|
59
|
+
self.current_frame -= 1
|
|
60
|
+
if c2.button("Next", disabled=self.current_frame == len(self.frames)-1):
|
|
61
|
+
self.current_frame += 1
|
|
62
|
+
if c3.button("Add Frame"):
|
|
63
|
+
self.frames.append(Frame(copy.deepcopy(self.grid)))
|
|
64
|
+
self.current_frame = len(self.frames) - 1
|
|
65
|
+
if c4.button("Delete Frame", disabled=len(self.frames) == 1):
|
|
66
|
+
self.frames.pop(self.current_frame)
|
|
67
|
+
self.current_frame = max(0, self.current_frame-1)
|
|
68
|
+
# Export/import
|
|
69
|
+
st.markdown("---")
|
|
70
|
+
exp1, exp2, exp3 = st.columns(3)
|
|
71
|
+
if exp1.button("Export JSON"):
|
|
72
|
+
st.download_button(
|
|
73
|
+
"Download JSON",
|
|
74
|
+
data=json.dumps([f.grid for f in self.frames]),
|
|
75
|
+
file_name="frames.json",
|
|
76
|
+
mime="application/json"
|
|
77
|
+
)
|
|
78
|
+
uploaded = exp2.file_uploader("Load JSON", type="json")
|
|
79
|
+
if uploaded:
|
|
80
|
+
data = json.load(uploaded)
|
|
81
|
+
if isinstance(data, list) and all(isinstance(frame, list) for frame in data):
|
|
82
|
+
self.frames = [Frame(copy.deepcopy(g)) for g in data]
|
|
83
|
+
self.current_frame = 0
|
|
84
|
+
# Placeholder for "Send to Matrix"
|
|
85
|
+
if exp3.button("Send to Matrix"):
|
|
86
|
+
st.info("This would send to your matrix (not implemented here)!")
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
pg = PixelGridWeb()
|
|
90
|
+
pg.draw()
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -3,6 +3,6 @@ Author: Taylor B. tayjaybabee@gmail.com
|
|
|
3
3
|
Date: 2025-05-05 15:02:58
|
|
4
4
|
LastEditors: Taylor B. tayjaybabee@gmail.com
|
|
5
5
|
LastEditTime: 2025-05-05 15:24:08
|
|
6
|
-
FilePath: is_matrix_forge/dev_tools/
|
|
6
|
+
FilePath: is_matrix_forge/dev_tools/grid.py
|
|
7
7
|
Description: 这是默认设置,可以在设置》工具》File Description中进行配置
|
|
8
8
|
"""
|
|
@@ -40,22 +40,6 @@ PWM_FREQUENCIES = [
|
|
|
40
40
|
]
|
|
41
41
|
|
|
42
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
43
|
def percentage(dev, p):
|
|
60
44
|
"""Fill a percentage of the screen. Bottom to top"""
|
|
61
45
|
send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p])
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The main entrypoint for the `led-matrix-identify` command.
|
|
3
|
+
|
|
4
|
+
Description:
|
|
5
|
+
This module contains the entrypoint for the `led-matrix-identify` command, which
|
|
6
|
+
is used to identify:
|
|
7
|
+
|
|
8
|
+
* Which LED matrix can be communicated with by which serial port
|
|
9
|
+
* The physical location of each LED matrix
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
See `main()` for more information.
|
|
13
|
+
|
|
14
|
+
Author:
|
|
15
|
+
Taylor B. <t.blackstone@inspyre.tech>
|
|
16
|
+
|
|
17
|
+
Since:
|
|
18
|
+
v1.0.0
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
from argparse import ArgumentParser
|
|
22
|
+
from threading import Thread
|
|
23
|
+
from typing import List, Optional, Union
|
|
24
|
+
|
|
25
|
+
from is_matrix_forge.led_matrix.controller.controller import LEDMatrixController
|
|
26
|
+
from is_matrix_forge.led_matrix.constants import DEVICES
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DEFAULT_RUNTIME = 30
|
|
30
|
+
DEFAULT_CYCLES = 3
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_leftmost_matrix(matrices: List[LEDMatrixController]) -> Optional[LEDMatrixController]:
|
|
34
|
+
"""
|
|
35
|
+
Determine the leftmost (physically) LED matrix.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
matrices (List[LEDMatrixController]):
|
|
39
|
+
The controller objects for all LED matrices.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Optional[LEDMatrixController]:
|
|
43
|
+
The controller object representing the determined leftmost matrix, `None`
|
|
44
|
+
if none found.
|
|
45
|
+
"""
|
|
46
|
+
if left_devices := [m for m in matrices if m.side_of_keyboard == 'left']:
|
|
47
|
+
return min(left_devices, key=lambda m: m.slot)
|
|
48
|
+
|
|
49
|
+
# Fallback: use right-side devices if no left-side
|
|
50
|
+
right_devices = [m for m in matrices if m.side_of_keyboard == 'right']
|
|
51
|
+
|
|
52
|
+
return min(right_devices, key=lambda m: m.slot) if right_devices else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_rightmost_matrix(matrices: List[LEDMatrixController]) -> Optional[LEDMatrixController]:
|
|
56
|
+
"""
|
|
57
|
+
Determine the rightmost (physically) LED matrix.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
matrices (List[LEDMatrixController]):
|
|
61
|
+
The controller objects for alll LED matrices.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Optional[LEDMatrixController]:
|
|
65
|
+
The controller object representing the determined rightmost matrix, `None`
|
|
66
|
+
if none found.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if right_devices := [m for m in matrices if m.side_of_keyboard == 'right']:
|
|
70
|
+
return max(right_devices, key=lambda m: m.slot)
|
|
71
|
+
|
|
72
|
+
# Fallback: use left-side devices if no right-side
|
|
73
|
+
left_devices = [m for m in matrices if m.side_of_keyboard == 'left']
|
|
74
|
+
|
|
75
|
+
return max(left_devices, key=lambda m: m.slot) if left_devices else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Arguments(ArgumentParser):
|
|
79
|
+
def __init__(self):
|
|
80
|
+
super().__init__('IdentifyLEDMatrices')
|
|
81
|
+
self.__parsed = None
|
|
82
|
+
|
|
83
|
+
# Set up the arguments
|
|
84
|
+
self.add_argument(
|
|
85
|
+
'--runtime', '-t',
|
|
86
|
+
action='store',
|
|
87
|
+
default=DEFAULT_RUNTIME,
|
|
88
|
+
help='The total runtime'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.add_argument(
|
|
92
|
+
'--skip-clear',
|
|
93
|
+
action='store_true',
|
|
94
|
+
default=False,
|
|
95
|
+
help='Skip clearing LEDs'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.add_argument(
|
|
99
|
+
'-c', '--cycle-count',
|
|
100
|
+
action='store',
|
|
101
|
+
type=int,
|
|
102
|
+
default=DEFAULT_CYCLES,
|
|
103
|
+
help='The number of cycles to run per message, for each selected device.'
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Set up the mutually exclusive arguments for the left and right matrices
|
|
107
|
+
left_right = self.add_mutually_exclusive_group()
|
|
108
|
+
|
|
109
|
+
left_right.add_argument(
|
|
110
|
+
'-R', '--only-right',
|
|
111
|
+
action='store_true',
|
|
112
|
+
default=False,
|
|
113
|
+
help='Only display identifying information for/on the rightmost matrix.'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
left_right.add_argument(
|
|
117
|
+
'-L', '--only-left',
|
|
118
|
+
action='store_true',
|
|
119
|
+
default=False,
|
|
120
|
+
help='Only display identifying information for/on the leftmost matrix.'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def parsed(self) -> Optional[argparse.Namespace]:
|
|
125
|
+
"""
|
|
126
|
+
The parsed arguments (if they've been parsed).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
argparse.Namespace:
|
|
130
|
+
The parsed arguments.
|
|
131
|
+
|
|
132
|
+
None:
|
|
133
|
+
If the arguments have not yet been parsed.
|
|
134
|
+
"""
|
|
135
|
+
return self.__parsed
|
|
136
|
+
|
|
137
|
+
def parse_args(self, *args, **kwargs):
|
|
138
|
+
"""
|
|
139
|
+
(Overrides the parent method, since we need to cache the parsed arguments. Does only that and then calls the
|
|
140
|
+
parent method.)
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
*args:
|
|
144
|
+
**kwargs:
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
argparse.Namespace:
|
|
148
|
+
The parsed arguments
|
|
149
|
+
"""
|
|
150
|
+
self.__parsed = super().parse_args(*args, **kwargs)
|
|
151
|
+
return self.__parsed
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main(
|
|
155
|
+
runtime: Optional[Union[int, float]] = None,
|
|
156
|
+
only_left: Optional[bool] = None,
|
|
157
|
+
only_right: Optional[bool] = None,
|
|
158
|
+
skip_clear: Optional[bool] = None,
|
|
159
|
+
cycle_count: Optional[int] = None
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
The main function of the script.
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
runtime (Optional[Union[int, float]]):
|
|
166
|
+
The total runtime in seconds.
|
|
167
|
+
|
|
168
|
+
only_left (Optional[bool]):
|
|
169
|
+
Only display identifying information for/on the leftmost matrix.
|
|
170
|
+
|
|
171
|
+
only_right (Optional[bool]):
|
|
172
|
+
Only display identifying information for/on the rightmost matrix.
|
|
173
|
+
|
|
174
|
+
skip_clear (Optional[bool]):
|
|
175
|
+
Skip clearing LEDs.
|
|
176
|
+
|
|
177
|
+
cycle_count (Optional[int]):
|
|
178
|
+
The number of cycles to run per message, for each selected device.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List[Thread]:
|
|
182
|
+
A list of threads, one for each LED matrix being identified.
|
|
183
|
+
"""
|
|
184
|
+
selected = [LEDMatrixController(device, 100, thread_safe=True) for device in DEVICES]
|
|
185
|
+
|
|
186
|
+
if not skip_clear:
|
|
187
|
+
skip_clear = ARGS.parsed.skip_clear
|
|
188
|
+
|
|
189
|
+
if only_left is None:
|
|
190
|
+
only_left = ARGS.parsed.only_left
|
|
191
|
+
|
|
192
|
+
if only_right is None:
|
|
193
|
+
only_right = ARGS.parsed.only_right
|
|
194
|
+
|
|
195
|
+
if only_right:
|
|
196
|
+
selected = [find_rightmost_matrix(selected)]
|
|
197
|
+
elif only_left:
|
|
198
|
+
selected = [find_leftmost_matrix(selected)]
|
|
199
|
+
|
|
200
|
+
cycle_count = ARGS.parsed.cycle_count if cycle_count is None else cycle_count
|
|
201
|
+
|
|
202
|
+
if runtime is None:
|
|
203
|
+
runtime = ARGS.parsed.runtime
|
|
204
|
+
|
|
205
|
+
threads = []
|
|
206
|
+
|
|
207
|
+
threads.extend(
|
|
208
|
+
Thread(
|
|
209
|
+
target=device.identify,
|
|
210
|
+
kwargs={
|
|
211
|
+
'skip_clear': skip_clear,
|
|
212
|
+
'duration': runtime,
|
|
213
|
+
'cycles': cycle_count
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
for device in selected
|
|
217
|
+
)
|
|
218
|
+
for t in threads:
|
|
219
|
+
t.start()
|
|
220
|
+
|
|
221
|
+
return threads
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
ARGS = Arguments()
|
|
225
|
+
PARSED = ARGS.parse_args()
|
|
226
|
+
|
|
227
|
+
if __name__ == '__main__':
|
|
228
|
+
threads = main()
|
|
@@ -1,7 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from is_matrix_forge.led_matrix.controller import LEDMatrixController, get_controllers
|
|
3
|
+
from is_matrix_forge.led_matrix.display.text.scroller import scroll_text_on_multiple_matrices
|
|
4
|
+
from platformdirs import PlatformDirs
|
|
1
5
|
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
PLATFORM_DIRS = PlatformDirs("IS-Matrix-Forge", "Inspyre Softworks")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
APP_DIR = PLATFORM_DIRS.user_data_path
|
|
11
|
+
MEMORY_FILE = APP_DIR / 'memory.ini'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MEMORY_FILE_TEMPLATE = {
|
|
15
|
+
'first_run': True
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
first_run = False
|
|
19
|
+
|
|
20
|
+
APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
if not MEMORY_FILE.exists():
|
|
23
|
+
MEMORY_FILE.write_text(json.dumps(MEMORY_FILE_TEMPLATE))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_first_run():
|
|
27
|
+
global first_run
|
|
28
|
+
with open(MEMORY_FILE, 'r') as f:
|
|
29
|
+
memory = json.load(f)
|
|
30
|
+
first_run = memory['first_run']
|
|
31
|
+
return first_run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_first_run():
|
|
35
|
+
global first_run
|
|
36
|
+
with open(MEMORY_FILE, 'r') as f:
|
|
37
|
+
memory = json.load(f)
|
|
38
|
+
|
|
39
|
+
memory['first_run'] = False
|
|
40
|
+
|
|
41
|
+
with open(MEMORY_FILE, 'w') as f:
|
|
42
|
+
json.dump(memory, f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def process_first_run():
|
|
46
|
+
global first_run
|
|
47
|
+
fr = get_first_run()
|
|
48
|
+
|
|
49
|
+
if fr:
|
|
50
|
+
controllers = get_controllers(threaded=True)
|
|
51
|
+
scroll_text_on_multiple_matrices(controllers, 'Welcome!', threaded=True)
|
|
52
|
+
|
|
53
|
+
set_first_run()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
process_first_run()
|
|
57
|
+
|
|
6
58
|
|
|
7
|
-
return [LEDMatrixController(device, 100) for device in DEVICES]
|
|
@@ -8,77 +8,12 @@ Project:
|
|
|
8
8
|
led-matrix-battery
|
|
9
9
|
|
|
10
10
|
File:
|
|
11
|
-
is_matrix_forge/led_matrix/commands/
|
|
11
|
+
is_matrix_forge/led_matrix/commands/grid.py
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
Description:
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
"""
|
|
18
|
-
from typing import List, Optional, ByteString
|
|
19
18
|
|
|
20
|
-
import serial
|
|
21
|
-
from serial.tools.list_ports_common import ListPortInfo
|
|
22
19
|
|
|
23
|
-
from is_matrix_forge.led_matrix.constants import RESPONSE_SIZE, FWK_MAGIC
|
|
24
|
-
from is_matrix_forge.led_matrix.helpers import disconnect_dev
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def send_command_raw(dev: ListPortInfo, command: List[int], with_response: bool = False, response_size: Optional[int] = None) -> Optional[ByteString]:
|
|
28
|
-
"""
|
|
29
|
-
Send a command to the device using a new serial connection.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
dev (ListPortInfo): The device to send the command to.
|
|
33
|
-
command (List[int]): The command to send.
|
|
34
|
-
with_response (bool, optional): Whether to wait for a response from the device. Defaults to False.
|
|
35
|
-
response_size (Optional[int], optional): The size of the response to expect. Defaults to None.
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
Optional[ByteString]: The response from the device, if any, or None if no response or an error occurred.
|
|
39
|
-
|
|
40
|
-
Raises:
|
|
41
|
-
IOError, OSError: If there is an error communicating with the device.
|
|
42
|
-
"""
|
|
43
|
-
# print(f"Sending command: {command}")
|
|
44
|
-
res_size = response_size or RESPONSE_SIZE
|
|
45
|
-
try:
|
|
46
|
-
with serial.Serial(dev.device, 115200) as s:
|
|
47
|
-
s.write(command)
|
|
48
|
-
|
|
49
|
-
return s.read(res_size) if with_response else None
|
|
50
|
-
except (IOError, OSError) as _ex:
|
|
51
|
-
disconnect_dev(dev.device)
|
|
52
|
-
return None
|
|
53
|
-
# print("Error: ", ex)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def send_command(
|
|
57
|
-
dev: ListPortInfo,
|
|
58
|
-
command: int,
|
|
59
|
-
parameters: Optional[List[int]] = None,
|
|
60
|
-
with_response: bool = False
|
|
61
|
-
) -> Optional[ByteString]:
|
|
62
|
-
"""
|
|
63
|
-
Send a command to the device using a new serial connection.
|
|
64
|
-
|
|
65
|
-
Parameters:
|
|
66
|
-
dev (ListPortInfo):
|
|
67
|
-
The device to send the command to.
|
|
68
|
-
|
|
69
|
-
command (int):
|
|
70
|
-
The command to send.
|
|
71
|
-
|
|
72
|
-
parameters (Optional[List[int]], optional):
|
|
73
|
-
The parameters to send with the command. Defaults to None.
|
|
74
|
-
|
|
75
|
-
with_response (bool, optional):
|
|
76
|
-
Whether to wait for a response from the device. Defaults to False.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Optional[ByteString]:
|
|
80
|
-
The response from the device, if any, or None if no response or an error occurred.
|
|
81
|
-
"""
|
|
82
|
-
if parameters is None:
|
|
83
|
-
parameters = []
|
|
84
|
-
return send_command_raw(dev, FWK_MAGIC + [command] + parameters, with_response)
|