From 339c9fa1cd8d708194f19769ad23a2150b7710b2 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 14:49:33 +0800 Subject: [PATCH 01/16] python: Move to subfolder Signed-off-by: Daniel Schaefer --- python/framework16_inputmodules/__init__.py | 0 .../ledmatrix_control.py | 0 python/pyproject.toml | 136 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 python/framework16_inputmodules/__init__.py rename ledmatrix_control.py => python/framework16_inputmodules/ledmatrix_control.py (100%) create mode 100644 python/pyproject.toml diff --git a/python/framework16_inputmodules/__init__.py b/python/framework16_inputmodules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ledmatrix_control.py b/python/framework16_inputmodules/ledmatrix_control.py similarity index 100% rename from ledmatrix_control.py rename to python/framework16_inputmodules/ledmatrix_control.py diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..2366f3fa --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,136 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "framework16-inputmodule" +dynamic = ["version"] +description = 'A library to control input modules on the Framework 16 Laptop' +readme = "README.md" +requires-python = ">=3.7" +license = { text = "MIT" } +keywords = [ + "hatch", +] +authors = [ + { name = "Daniel Schaefer", email = "dhs@frame.work" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "pyserial>=3.5", +] + +[project.urls] +Issues = "https://github.com/FrameworkComputer/inputmodule-rs/issues" +Source = "https://github.com/FrameworkComputer/inputmodule-rs" + +# TODO: Figure out how to add a runnable-script +# [project.scripts] +# hatch-showcase = "hatch_showcase.cli:hatch_showcase" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/hatch_showcase/_version.py" + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github", +] + +# TODO: Maybe typing with mypy +# [tool.hatch.build.targets.wheel.hooks.mypyc] +# enable-by-default = false +# dependencies = ["hatch-mypyc>=0.14.1"] +# require-runtime-dependencies = true +# mypy-args = [ +# "--no-warn-unused-ignores", +# ] +# +# [tool.mypy] +# disallow_untyped_defs = false +# follow_imports = "normal" +# ignore_missing_imports = true +# pretty = true +# show_column_numbers = true +# warn_no_return = false +# warn_unused_ignores = true + +# TODO: Code formatting +# [tool.black] +# target-version = ["py37"] +# line-length = 120 +# skip-string-normalization = true +# +# [tool.ruff] +# target-version = "py37" +# line-length = 120 +# select = [ +# "A", +# "B", +# "C", +# "DTZ", +# "E", +# "EM", +# "F", +# "FBT", +# "I", +# "ICN", +# "ISC", +# "N", +# "PLC", +# "PLE", +# "PLR", +# "PLW", +# "Q", +# "RUF", +# "S", +# "SIM", +# "T", +# "TID", +# "UP", +# "W", +# "YTT", +# ] +# ignore = [ +# # Allow non-abstract empty methods in abstract base classes +# "B027", +# # Allow boolean positional values in function calls, like `dict.get(... True)` +# "FBT003", +# # Ignore checks for possible passwords +# "S105", "S106", "S107", +# # Ignore complexity +# "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +# "PLC1901", # empty string comparisons +# "PLW2901", # `for` loop variable overwritten +# "SIM114", # Combine `if` branches using logical `or` operator +# ] +# unfixable = [ +# # Don't touch unused imports +# "F401", +# ] +# +# [tool.ruff.isort] +# known-first-party = ["hatch_showcase"] +# +# [tool.ruff.flake8-quotes] +# inline-quotes = "single" +# +# [tool.ruff.flake8-tidy-imports] +# ban-relative-imports = "all" +# +# [tool.ruff.per-file-ignores] +# # Tests can use relative imports and assertions +# "tests/**/*" = ["TID252", "S101"] From 5c19c6e0f69c4bf026ba2eea8930765dc6a650c9 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 14:53:42 +0800 Subject: [PATCH 02/16] python: Fix ruff checks Signed-off-by: Daniel Schaefer --- .../ledmatrix_control.py | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/python/framework16_inputmodules/ledmatrix_control.py b/python/framework16_inputmodules/ledmatrix_control.py index b9763f88..75b0d7d6 100755 --- a/python/framework16_inputmodules/ledmatrix_control.py +++ b/python/framework16_inputmodules/ledmatrix_control.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import argparse -import math import os import random import sys @@ -602,8 +601,8 @@ def camera(dev): for y in range(0, HEIGHT): vals[y] = cropped[y, x] - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def video(dev, video_file): """Resize and play back a video""" @@ -613,7 +612,6 @@ def video(dev, video_file): capture = cv2.VideoCapture(video_file) ret, frame = capture.read() - scale_x = WIDTH/frame.shape[1] scale_y = HEIGHT/frame.shape[0] # Scale the video to 34 pixels height @@ -648,8 +646,8 @@ def video(dev, video_file): for y in range(0, HEIGHT): vals[y] = frame[y, x] - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def pixel_to_brightness(pixel): @@ -690,21 +688,21 @@ def image_greyscale(dev, image_file): for y in range(HEIGHT): vals[y] = pixel_to_brightness(pixel_values[x+y*WIDTH]) - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) -def send_col(s, x, vals): +def send_col(dev, s, x, vals): """Stage greyscale values for a single column. Must be committed with commit_cols()""" command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals - send_serial(s, command) + send_serial(dev, s, command) -def commit_cols(s): +def commit_cols(dev, s): """Commit the changes from sending individual cols with send_col(), displaying the matrix. This makes sure that the matrix isn't partially updated.""" command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00] - send_serial(s, command) + send_serial(dev, s, command) def get_color(dev): @@ -746,8 +744,8 @@ def checkerboard(dev, n): # Rotate once vals = vals[n:] + vals[:n] - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def every_nth_col(dev, n): @@ -755,8 +753,8 @@ def every_nth_col(dev, n): for x in range(0, WIDTH): vals = [(0xFF if x % n == 0 else 0) for _ in range(HEIGHT)] - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def every_nth_row(dev, n): @@ -764,8 +762,8 @@ def every_nth_row(dev, n): for x in range(0, WIDTH): vals = [(0xFF if y % n == 0 else 0) for y in range(HEIGHT)] - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def all_brightnesses(dev): @@ -782,8 +780,8 @@ def all_brightnesses(dev): else: vals[y] = brightness - send_col(s, x, vals) - commit_cols(s) + send_col(dev, s, x, vals) + commit_cols(dev, s) def countdown(dev, seconds): @@ -1034,7 +1032,7 @@ def snake(dev): def wpm_demo(dev): """Capture keypresses and calculate the WPM of the last 10 seconds TODO: I'm not sure my calculation is right.""" - from getkey import getkey, keys + from getkey import getkey start = datetime.now() keypresses = [] while True: @@ -1236,17 +1234,17 @@ def send_command_raw(dev, command, with_response=False): res = s.read(RESPONSE_SIZE) # print(f"Received: {res}") return res - except (IOError, OSError) as ex: + except (IOError, OSError) as _ex: global DISCONNECTED_DEVS DISCONNECTED_DEVS.append(dev.device) #print("Error: ", ex) -def send_serial(s, command): +def send_serial(dev, s, command): """Send serial command by using existing serial connection""" try: s.write(command) - except (IOError, OSError) as ex: + except (IOError, OSError) as _ex: global DISCONNECTED_DEVS DISCONNECTED_DEVS.append(dev.device) #print("Error: ", ex) @@ -1533,9 +1531,9 @@ def get_power_mode_cmd(dev): res = send_command(dev, CommandVals.SetPowerMode, with_response=True) current_mode = int(res[0]) if current_mode == 0: - print(f"Current Power Mode: Low Power") + print("Current Power Mode: Low Power") elif current_mode == 1: - print(f"Current Power Mode: High Power") + print("Current Power Mode: High Power") def get_fps_cmd(dev): From d9afd620f3a20acb72cee1babbab1f344b32b44f Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 14:58:04 +0800 Subject: [PATCH 03/16] python: Format with ruff Signed-off-by: Daniel Schaefer --- .../ledmatrix_control.py | 846 ++++++++++-------- 1 file changed, 473 insertions(+), 373 deletions(-) diff --git a/python/framework16_inputmodules/ledmatrix_control.py b/python/framework16_inputmodules/ledmatrix_control.py index 75b0d7d6..f0e1372e 100755 --- a/python/framework16_inputmodules/ledmatrix_control.py +++ b/python/framework16_inputmodules/ledmatrix_control.py @@ -98,37 +98,38 @@ class GameControlVal(IntEnum): Right = 3 Quit = 4 + PWM_FREQUENCIES = [ - '29kHz', - '3.6kHz', - '1.8kHz', - '900Hz', + "29kHz", + "3.6kHz", + "1.8kHz", + "900Hz", ] PATTERNS = [ - 'All LEDs on', + "All LEDs on", '"LOTUS" sideways', - 'Gradient (0-13% Brightness)', - 'Double Gradient (0-7-0% Brightness)', - 'Zigzag', + "Gradient (0-13% Brightness)", + "Double Gradient (0-7-0% Brightness)", + "Zigzag", '"PANIC"', '"LOTUS" Top Down', - 'All brightness levels (1 LED each)', - 'Every Second Row', - 'Every Third Row', - 'Every Fourth Row', - 'Every Fifth Row', - 'Every Sixth Row', - 'Every Second Col', - 'Every Third Col', - 'Every Fourth Col', - 'Every Fifth Col', - 'Checkerboard', - 'Double Checkerboard', - 'Triple Checkerboard', - 'Quad Checkerboard' + "All brightness levels (1 LED each)", + "Every Second Row", + "Every Third Row", + "Every Fourth Row", + "Every Fifth Row", + "Every Sixth Row", + "Every Second Col", + "Every Third Col", + "Every Fourth Col", + "Every Fifth Col", + "Checkerboard", + "Double Checkerboard", + "Triple Checkerboard", + "Quad Checkerboard", ] -DRAW_PATTERNS = ['off', 'on', 'foo'] +DRAW_PATTERNS = ["off", "on", "foo"] GREYSCALE_DEPTH = 32 RESPONSE_SIZE = 32 WIDTH = 9 @@ -144,10 +145,8 @@ class GameControlVal(IntEnum): ARG_2LEFT = 5 ARG_2RIGHT = 6 -RGB_COLORS = ['white', 'black', 'red', 'green', - 'blue', 'cyan', 'yellow', 'purple'] -SCREEN_FPS = ['quarter', 'half', 'one', 'two', - 'four', 'eight', 'sixteen', 'thirtytwo'] +RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] +SCREEN_FPS = ["quarter", "half", "one", "two", "four", "eight", "sixteen", "thirtytwo"] HIGH_FPS_MASK = 0b00010000 LOW_FPS_MASK = 0b00000111 @@ -166,99 +165,158 @@ def update_brightness_slider(window, devices): average_brightness += br print(f"Brightness: {br}") if average_brightness: - window['-BRIGHTNESS-'].update(average_brightness / len(devices)) + window["-BRIGHTNESS-"].update(average_brightness / len(devices)) def main(): parser = argparse.ArgumentParser() parser.add_argument( - "-l", "--list", help="List all compatible devices", action="store_true") - parser.add_argument("--bootloader", help="Jump to the bootloader to flash new firmware", - action="store_true") - parser.add_argument('--sleep', help='Simulate the host going to sleep or waking up', - action=argparse.BooleanOptionalAction) - parser.add_argument('--is-sleeping', help='Check current sleep state', - action='store_true') - parser.add_argument("--brightness", help="Adjust the brightness. Value 0-255", - type=int) - parser.add_argument("--get-brightness", help="Get current brightness", - action="store_true") - parser.add_argument('--animate', action=argparse.BooleanOptionalAction, - help='Start/stop vertical scrolling') - parser.add_argument('--get-animate', action='store_true', - help='Check if currently animating') - parser.add_argument("--pwm", help="Adjust the PWM frequency. Value 0-255", - type=int, choices=[29000, 3600, 1800, 900]) - parser.add_argument("--get-pwm", help="Get current PWM Frequency", - action="store_true") - parser.add_argument("--pattern", help='Display a pattern', - type=str, choices=PATTERNS) - parser.add_argument("--image", help="Display a PNG or GIF image in black and white only)", - type=argparse.FileType('rb')) - parser.add_argument("--image-grey", help="Display a PNG or GIF image in greyscale", - type=argparse.FileType('rb')) - parser.add_argument("--camera", help="Stream from the webcam", - action="store_true") - parser.add_argument("--video", help="Play a video", - type=str) - parser.add_argument("--percentage", help="Fill a percentage of the screen", - type=int) - parser.add_argument("--clock", help="Display the current time", - action="store_true") - parser.add_argument("--string", help="Display a string or number, like FPS", - type=str) - parser.add_argument("--symbols", help="Show symbols (degF, degC, :), snow, cloud, ...)", - nargs='+') - parser.add_argument("--gui", help="Launch the graphical version of the program", - action="store_true") - parser.add_argument("--panic", help="Crash the firmware (TESTING ONLY)", - action="store_true") - parser.add_argument("--blink", help="Blink the current pattern", - action="store_true") - parser.add_argument("--breathing", help="Breathing of the current pattern", - action="store_true") - parser.add_argument("--eq", help="Equalizer", nargs='+', type=int) + "-l", "--list", help="List all compatible devices", action="store_true" + ) + parser.add_argument( + "--bootloader", + help="Jump to the bootloader to flash new firmware", + action="store_true", + ) + parser.add_argument( + "--sleep", + help="Simulate the host going to sleep or waking up", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--is-sleeping", help="Check current sleep state", action="store_true" + ) + parser.add_argument( + "--brightness", help="Adjust the brightness. Value 0-255", type=int + ) + parser.add_argument( + "--get-brightness", help="Get current brightness", action="store_true" + ) + parser.add_argument( + "--animate", + action=argparse.BooleanOptionalAction, + help="Start/stop vertical scrolling", + ) + parser.add_argument( + "--get-animate", action="store_true", help="Check if currently animating" + ) + parser.add_argument( + "--pwm", + help="Adjust the PWM frequency. Value 0-255", + type=int, + choices=[29000, 3600, 1800, 900], + ) + parser.add_argument( + "--get-pwm", help="Get current PWM Frequency", action="store_true" + ) parser.add_argument( - "--random-eq", help="Random Equalizer", action="store_true") + "--pattern", help="Display a pattern", type=str, choices=PATTERNS + ) + parser.add_argument( + "--image", + help="Display a PNG or GIF image in black and white only)", + type=argparse.FileType("rb"), + ) + parser.add_argument( + "--image-grey", + help="Display a PNG or GIF image in greyscale", + type=argparse.FileType("rb"), + ) + parser.add_argument("--camera", help="Stream from the webcam", action="store_true") + parser.add_argument("--video", help="Play a video", type=str) + parser.add_argument( + "--percentage", help="Fill a percentage of the screen", type=int + ) + parser.add_argument("--clock", help="Display the current time", action="store_true") + parser.add_argument( + "--string", help="Display a string or number, like FPS", type=str + ) + parser.add_argument( + "--symbols", help="Show symbols (degF, degC, :), snow, cloud, ...)", nargs="+" + ) + parser.add_argument( + "--gui", help="Launch the graphical version of the program", action="store_true" + ) + parser.add_argument( + "--panic", help="Crash the firmware (TESTING ONLY)", action="store_true" + ) + parser.add_argument( + "--blink", help="Blink the current pattern", action="store_true" + ) + parser.add_argument( + "--breathing", help="Breathing of the current pattern", action="store_true" + ) + parser.add_argument("--eq", help="Equalizer", nargs="+", type=int) + parser.add_argument("--random-eq", help="Random Equalizer", action="store_true") parser.add_argument("--wpm", help="WPM Demo", action="store_true") parser.add_argument("--snake", help="Snake", action="store_true") - parser.add_argument("--snake-embedded", - help="Snake on the module", action="store_true") - parser.add_argument("--pong-embedded", - help="Pong on the module", action="store_true") - parser.add_argument("--game-of-life-embedded", - help="Game of Life", type=GameOfLifeStartParam.argparse, choices=list(GameOfLifeStartParam)) - parser.add_argument("--quit-embedded-game", - help="Quit the current game", action="store_true") parser.add_argument( - "--all-brightnesses", help="Show every pixel in a different brightness", action="store_true") + "--snake-embedded", help="Snake on the module", action="store_true" + ) + parser.add_argument( + "--pong-embedded", help="Pong on the module", action="store_true" + ) parser.add_argument( - "--set-color", help="Set RGB color (C1 Minimal Input Module)", choices=RGB_COLORS) + "--game-of-life-embedded", + help="Game of Life", + type=GameOfLifeStartParam.argparse, + choices=list(GameOfLifeStartParam), + ) parser.add_argument( - "--get-color", help="Get RGB color (C1 Minimal Input Module)", action="store_true") - parser.add_argument("-v", "--version", - help="Get device version", action="store_true") + "--quit-embedded-game", help="Quit the current game", action="store_true" + ) parser.add_argument( - "--serial-dev", help="Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows") + "--all-brightnesses", + help="Show every pixel in a different brightness", + action="store_true", + ) + parser.add_argument( + "--set-color", + help="Set RGB color (C1 Minimal Input Module)", + choices=RGB_COLORS, + ) + parser.add_argument( + "--get-color", + help="Get RGB color (C1 Minimal Input Module)", + action="store_true", + ) + parser.add_argument( + "-v", "--version", help="Get device version", action="store_true" + ) + parser.add_argument( + "--serial-dev", + help="Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows", + ) parser.add_argument( - "--disp-str", help="Display a string on the LCD Display", type=str) - parser.add_argument("--display-on", help="Control display power", - action=argparse.BooleanOptionalAction) - parser.add_argument("--invert-screen", help="Invert display", - action=argparse.BooleanOptionalAction) - parser.add_argument("--screen-saver", help="Turn on/off screensaver", - action=argparse.BooleanOptionalAction) - parser.add_argument("--set-fps", help="Set screen FPS", - choices=SCREEN_FPS) - parser.add_argument("--set-power-mode", help="Set screen power mode", - choices=['high', 'low']) - parser.add_argument("--get-fps", help="Set screen FPS", - action='store_true') - parser.add_argument("--get-power-mode", help="Set screen power mode", - action='store_true') - parser.add_argument("--b1image", help="On the B1 display, show a PNG or GIF image in black and white only)", - type=argparse.FileType('rb')) + "--disp-str", help="Display a string on the LCD Display", type=str + ) + parser.add_argument( + "--display-on", + help="Control display power", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--invert-screen", help="Invert display", action=argparse.BooleanOptionalAction + ) + parser.add_argument( + "--screen-saver", + help="Turn on/off screensaver", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument("--set-fps", help="Set screen FPS", choices=SCREEN_FPS) + parser.add_argument( + "--set-power-mode", help="Set screen power mode", choices=["high", "low"] + ) + parser.add_argument("--get-fps", help="Set screen FPS", action="store_true") + parser.add_argument( + "--get-power-mode", help="Set screen power mode", action="store_true" + ) + parser.add_argument( + "--b1image", + help="On the B1 display, show a PNG or GIF image in black and white only)", + type=argparse.FileType("rb"), + ) args = parser.parse_args() @@ -270,7 +328,7 @@ def main(): print_devs(ports) sys.exit(0) - if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): # Force GUI in pyinstaller bundled app args.gui = True @@ -283,8 +341,15 @@ def main(): elif len(ports) == 1: dev = ports[0] elif len(ports) >= 1 and not args.gui: - popup(args.gui, "More than 1 compatibles devices found. Please choose from the commandline with --serial-dev COMX.\nConnected ports:\n- {}".format("\n- ".join([port.device for port in ports]))) - print("More than 1 compatible device found. Please choose with --serial-dev ...") + popup( + args.gui, + "More than 1 compatibles devices found. Please choose from the commandline with --serial-dev COMX.\nConnected ports:\n- {}".format( + "\n- ".join([port.device for port in ports]) + ), + ) + print( + "More than 1 compatible device found. Please choose with --serial-dev ..." + ) print("Example on Windows: --serial-dev COM3") print("Example on Linux: --serial-dev /dev/ttyACM0") print_devs(ports) @@ -316,13 +381,13 @@ def main(): print(f"Current brightness: {br}") elif args.pwm is not None: if args.pwm == 29000: - pwm_freq(dev, '29kHz') + pwm_freq(dev, "29kHz") elif args.pwm == 3600: - pwm_freq(dev, '3.6kHz') + pwm_freq(dev, "3.6kHz") elif args.pwm == 1800: - pwm_freq(dev, '1.8kHz') + pwm_freq(dev, "1.8kHz") elif args.pwm == 900: - pwm_freq(dev, '900Hz') + pwm_freq(dev, "900Hz") elif args.get_pwm: p = get_pwm_freq(dev) print(f"Current PWM Frequency: {p} Hz") @@ -356,7 +421,7 @@ def main(): (red, green, blue) = get_color(dev) print(f"Current color: RGB:({red}, {green}, {blue})") elif args.gui: - devices = find_devs()#show=False, verbose=False) + devices = find_devs() # show=False, verbose=False) print("Found {} devices".format(len(devices))) gui(devices) elif args.blink: @@ -412,7 +477,7 @@ def main(): def resource_path(): - """ Get absolute path to resource, works for dev and for PyInstaller""" + """Get absolute path to resource, works for dev and for PyInstaller""" try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS @@ -424,7 +489,9 @@ def resource_path(): def find_devs(): ports = list_ports.comports() - return [port for port in ports if port.vid == 0x32AC and port.pid in INPUTMODULE_PIDS] + return [ + port for port in ports if port.vid == 0x32AC and port.pid in INPUTMODULE_PIDS + ] def print_devs(ports): @@ -447,21 +514,18 @@ def percentage(dev, p): def brightness(dev, b: int): - """Adjust the brightness scaling of the entire screen. - """ + """Adjust the brightness scaling of the entire screen.""" send_command(dev, CommandVals.Brightness, [b]) def get_brightness(dev): - """Adjust the brightness scaling of the entire screen. - """ + """Adjust the brightness scaling of the entire screen.""" res = send_command(dev, CommandVals.Brightness, with_response=True) return int(res[0]) def get_pwm_freq(dev): - """Adjust the brightness scaling of the entire screen. - """ + """Adjust the brightness scaling of the entire screen.""" res = send_command(dev, CommandVals.PwmFreq, with_response=True) freq = int(res[0]) if freq == 0: @@ -504,17 +568,18 @@ def get_animate(dev): def b1image_bl(dev, image_file): - """ Display an image in black and white + """Display an image in black and white Confirmed working with PNG and GIF. Must be 300x400 in size. Sends one 400px column in a single commands and a flush at the end """ from PIL import Image + im = Image.open(image_file).convert("RGB") width, height = im.size - assert (width == B1_WIDTH) - assert (height == B1_HEIGHT) + assert width == B1_WIDTH + assert height == B1_HEIGHT pixel_values = list(im.getdata()) for x in range(B1_WIDTH): @@ -522,9 +587,9 @@ def b1image_bl(dev, image_file): byte = None for y in range(B1_HEIGHT): - pixel = pixel_values[y*B1_WIDTH + x] + pixel = pixel_values[y * B1_WIDTH + x] brightness = sum(pixel) / 3 - black = brightness < 0xFF/2 + black = brightness < 0xFF / 2 bit = y % 8 @@ -534,9 +599,9 @@ def b1image_bl(dev, image_file): byte |= 1 << bit if bit == 7: - vals[int(y/8)] = byte + vals[int(y / 8)] = byte - column_le = list((x).to_bytes(2, 'little')) + column_le = list((x).to_bytes(2, "little")) command = FWK_MAGIC + [0x16] + column_le + vals send_command(dev, command) @@ -554,47 +619,49 @@ def image_bl(dev, image_file): vals = [0 for _ in range(39)] from PIL import Image + im = Image.open(image_file).convert("RGB") width, height = im.size - assert (width == 9) - assert (height == 34) + assert width == 9 + assert height == 34 pixel_values = list(im.getdata()) for i, pixel in enumerate(pixel_values): brightness = sum(pixel) / 3 - if brightness > 0xFF/2: - vals[int(i/8)] |= (1 << i % 8) + if brightness > 0xFF / 2: + vals[int(i / 8)] |= 1 << i % 8 send_command(dev, CommandVals.Draw, vals) + def camera(dev): """Play a live view from the webcam, for fun""" with serial.Serial(dev.device, 115200) as s: import cv2 - + capture = cv2.VideoCapture(0) ret, frame = capture.read() - - scale_y = HEIGHT/frame.shape[0] - + + scale_y = HEIGHT / frame.shape[0] + # Scale the video to 34 pixels height - dim = (HEIGHT, int(round(frame.shape[1]*scale_y))) + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) # Find the starting position to crop the width to be centered # For very narrow videos, make sure to stay in bounds - start_x = max(0, int(round(dim[1]/2-WIDTH/2))) + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) end_x = min(dim[1], start_x + WIDTH) - + # Pre-process the video into resized, cropped, grayscale frames while True: ret, frame = capture.read() if not ret: print("Failed to capture video frames") break - + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - + resized = cv2.resize(gray, (dim[1], dim[0])) cropped = resized[0:HEIGHT, start_x:end_x] - + for x in range(0, cropped.shape[1]): vals = [0 for _ in range(HEIGHT)] @@ -604,39 +671,40 @@ def camera(dev): send_col(dev, s, x, vals) commit_cols(dev, s) + def video(dev, video_file): """Resize and play back a video""" with serial.Serial(dev.device, 115200) as s: import cv2 - + capture = cv2.VideoCapture(video_file) ret, frame = capture.read() - - scale_y = HEIGHT/frame.shape[0] - + + scale_y = HEIGHT / frame.shape[0] + # Scale the video to 34 pixels height - dim = (HEIGHT, int(round(frame.shape[1]*scale_y))) + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) # Find the starting position to crop the width to be centered # For very narrow videos, make sure to stay in bounds - start_x = max(0, int(round(dim[1]/2-WIDTH/2))) + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) end_x = min(dim[1], start_x + WIDTH) - + processed = [] - + # Pre-process the video into resized, cropped, grayscale frames while True: ret, frame = capture.read() if not ret: print("Failed to read video frames") break - + gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) - + resized = cv2.resize(gray, (dim[1], dim[0])) cropped = resized[0:HEIGHT, start_x:end_x] - + processed.append(cropped) - + # Write it out to the module one frame at a time # TODO: actually control for framerate for frame in processed: @@ -648,11 +716,11 @@ def video(dev, video_file): send_col(dev, s, x, vals) commit_cols(dev, s) - + def pixel_to_brightness(pixel): """Calculate pixel brightness from an RGB triple""" - assert (len(pixel) == 3) + assert len(pixel) == 3 brightness = sum(pixel) / len(pixel) # Poor man's scaling to make the greyscale pop better. @@ -677,16 +745,17 @@ def image_greyscale(dev, image_file): """ with serial.Serial(dev.device, 115200) as s: from PIL import Image + im = Image.open(image_file).convert("RGB") width, height = im.size - assert (width == 9) - assert (height == 34) + assert width == 9 + assert height == 34 pixel_values = list(im.getdata()) for x in range(0, WIDTH): vals = [0 for _ in range(HEIGHT)] for y in range(HEIGHT): - vals[y] = pixel_to_brightness(pixel_values[x+y*WIDTH]) + vals[y] = pixel_to_brightness(pixel_values[x + y * WIDTH]) send_col(dev, s, x, vals) commit_cols(dev, s) @@ -712,21 +781,21 @@ def get_color(dev): def set_color(dev, color): rgb = None - if color == 'white': + if color == "white": rgb = [0xFF, 0xFF, 0xFF] - elif color == 'black': + elif color == "black": rgb = [0x00, 0x00, 0x00] - elif color == 'red': + elif color == "red": rgb = [0xFF, 0x00, 0x00] - elif color == 'green': + elif color == "green": rgb = [0x00, 0xFF, 0x00] - elif color == 'blue': + elif color == "blue": rgb = [0x00, 0x00, 0xFF] - elif color == 'yellow': + elif color == "yellow": rgb = [0xFF, 0xFF, 0x00] - elif color == 'cyan': + elif color == "cyan": rgb = [0x00, 0xFF, 0xFF] - elif color == 'purple': + elif color == "purple": rgb = [0xFF, 0x00, 0xFF] else: print(f"Unknown color: {color}") @@ -739,8 +808,8 @@ def set_color(dev, color): def checkerboard(dev, n): with serial.Serial(dev.device, 115200) as s: for x in range(0, WIDTH): - vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT/2) - if x % (n*2) < n: + vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT / 2) + if x % (n * 2) < n: # Rotate once vals = vals[n:] + vals[:n] @@ -785,7 +854,7 @@ def all_brightnesses(dev): def countdown(dev, seconds): - """ Run a countdown timer. Lighting more LEDs every 100th of a seconds. + """Run a countdown timer. Lighting more LEDs every 100th of a seconds. Until the timer runs out and every LED is lit""" start = datetime.now() target = seconds * 1_000_000 @@ -808,7 +877,7 @@ def countdown(dev, seconds): light_leds(dev, 306) breathing(dev) - #blinking(dev) + # blinking(dev) def blinking(dev): @@ -833,22 +902,22 @@ def breathing(dev): # Go quickly from 250 to 50 for i in range(10): time.sleep(0.03) - brightness(dev, 250 - i*20) + brightness(dev, 250 - i * 20) # Go slowly from 50 to 0 for i in range(10): time.sleep(0.06) - brightness(dev, 50 - i*5) + brightness(dev, 50 - i * 5) # Go slowly from 0 to 50 for i in range(10): time.sleep(0.06) - brightness(dev, i*5) + brightness(dev, i * 5) # Go quickly from 50 to 250 for i in range(10): time.sleep(0.03) - brightness(dev, 50 + i*20) + brightness(dev, 50 + i * 20) direction = None @@ -857,6 +926,7 @@ def breathing(dev): def opposite_direction(direction): from getkey import keys + if direction == keys.RIGHT: return keys.LEFT elif direction == keys.LEFT: @@ -870,6 +940,7 @@ def opposite_direction(direction): def snake_keyscan(): from getkey import getkey, keys + global direction global body @@ -897,7 +968,7 @@ def snake_embedded_keyscan(dev): key_arg = GameControlVal.Left elif key == keys.RIGHT: key_arg = GameControlVal.Right - elif key == 'q': + elif key == "q": # Quit key_arg = GameControlVal.Quit if key_arg is not None: @@ -907,12 +978,12 @@ def snake_embedded_keyscan(dev): def game_over(dev): global body while True: - show_string(dev, 'GAME ') + show_string(dev, "GAME ") time.sleep(0.75) - show_string(dev, 'OVER!') + show_string(dev, "OVER!") time.sleep(0.75) score = len(body) - show_string(dev, f'{score:>3} P') + show_string(dev, f"{score:>3} P") time.sleep(0.75) @@ -929,11 +1000,11 @@ def pong_embedded(dev): key_arg = ARG_LEFT elif key == keys.RIGHT: key_arg = ARG_RIGHT - elif key == 'a': + elif key == "a": key_arg = ARG_2LEFT - elif key == 'd': + elif key == "d": key_arg = ARG_2RIGHT - elif key == 'q': + elif key == "q": # Quit key_arg = ARG_QUIT if key_arg is not None: @@ -956,14 +1027,14 @@ def snake_embedded(dev): def snake(dev): from getkey import keys + global direction global body head = (0, 0) direction = keys.DOWN food = (0, 0) while food == head: - food = (random.randint(0, WIDTH-1), - random.randint(0, HEIGHT-1)) + food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) # Setting WRAP = False @@ -985,13 +1056,13 @@ def snake(dev): (x, y) = head oldhead = head if direction == keys.RIGHT: - head = (x+1, y) + head = (x + 1, y) elif direction == keys.LEFT: - head = (x-1, y) + head = (x - 1, y) elif direction == keys.UP: - head = (x, y-1) + head = (x, y - 1) elif direction == keys.DOWN: - head = (x, y+1) + head = (x, y + 1) # Detect edge condition (x, y) = head @@ -1002,19 +1073,18 @@ def snake(dev): if x >= WIDTH: x = 0 elif x < 0: - x = WIDTH-1 + x = WIDTH - 1 elif y >= HEIGHT: y = 0 elif y < 0: - y = HEIGHT-1 + y = HEIGHT - 1 head = (x, y) else: return game_over(dev) elif head == food: body.insert(0, oldhead) while food == head: - food = (random.randint(0, WIDTH-1), - random.randint(0, HEIGHT-1)) + food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) elif body: body.pop() body.insert(0, oldhead) @@ -1033,6 +1103,7 @@ def wpm_demo(dev): """Capture keypresses and calculate the WPM of the last 10 seconds TODO: I'm not sure my calculation is right.""" from getkey import getkey + start = datetime.now() keypresses = [] while True: @@ -1044,23 +1115,22 @@ def wpm_demo(dev): # Word is five letters wpm = (len(keypresses) / 5) * 6 - total_time = (now-start).total_seconds() + total_time = (now - start).total_seconds() if total_time < 10: wpm = wpm / (total_time / 10) - show_string(dev, ' ' + str(int(wpm))) + show_string(dev, " " + str(int(wpm))) def random_eq(dev): - """Display an equlizer looking animation with random values. - """ + """Display an equlizer looking animation with random values.""" global STOP_THREAD while True: if STOP_THREAD or dev.device in DISCONNECTED_DEVS: STOP_THREAD = False return # Lower values more likely, makes it look nicer - weights = [i*i for i in range(33, 0, -1)] + weights = [i * i for i in range(33, 0, -1)] population = list(range(1, 34)) vals = random.choices(population, weights=weights, k=9) eq(dev, vals) @@ -1071,15 +1141,15 @@ def eq(dev, vals): """Display 9 values in equalizer diagram starting from the middle, going up and down""" matrix = [[0 for _ in range(34)] for _ in range(9)] - for (col, val) in enumerate(vals[:9]): + for col, val in enumerate(vals[:9]): row = int(34 / 2) above = int(val / 2) below = val - above for i in range(above): - matrix[col][row+i] = 0xFF + matrix[col][row + i] = 0xFF for i in range(below): - matrix[col][row-1-i] = 0xFF + matrix[col][row - 1 - i] = 0xFF render_matrix(dev, matrix) @@ -1091,15 +1161,15 @@ def render_matrix(dev, matrix): for x in range(9): for y in range(34): - i = x + 9*y + i = x + 9 * y if matrix[x][y]: - vals[int(i/8)] = vals[int(i/8)] | (1 << i % 8) + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) send_command(dev, CommandVals.Draw, vals) def light_leds(dev, leds): - """ Light a specific number of LEDs """ + """Light a specific number of LEDs""" vals = [0x00 for _ in range(39)] for byte in range(int(leds / 8)): vals[byte] = 0xFF @@ -1110,59 +1180,59 @@ def light_leds(dev, leds): def pwm_freq(dev, freq): """Display a pattern that's already programmed into the firmware""" - if freq == '29kHz': + if freq == "29kHz": send_command(dev, CommandVals.PwmFreq, [0]) - elif freq == '3.6kHz': + elif freq == "3.6kHz": send_command(dev, CommandVals.PwmFreq, [1]) - elif freq == '1.8kHz': + elif freq == "1.8kHz": send_command(dev, CommandVals.PwmFreq, [2]) - elif freq == '900Hz': + elif freq == "900Hz": send_command(dev, CommandVals.PwmFreq, [3]) def pattern(dev, p): """Display a pattern that's already programmed into the firmware""" - if p == 'All LEDs on': + if p == "All LEDs on": send_command(dev, CommandVals.Pattern, [PatternVals.FullBrightness]) - elif p == 'Gradient (0-13% Brightness)': + elif p == "Gradient (0-13% Brightness)": send_command(dev, CommandVals.Pattern, [PatternVals.Gradient]) - elif p == 'Double Gradient (0-7-0% Brightness)': + elif p == "Double Gradient (0-7-0% Brightness)": send_command(dev, CommandVals.Pattern, [PatternVals.DoubleGradient]) elif p == '"LOTUS" sideways': send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus]) - elif p == 'Zigzag': + elif p == "Zigzag": send_command(dev, CommandVals.Pattern, [PatternVals.ZigZag]) elif p == '"PANIC"': send_command(dev, CommandVals.Pattern, [PatternVals.DisplayPanic]) elif p == '"LOTUS" Top Down': send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus2]) - elif p == 'All brightness levels (1 LED each)': + elif p == "All brightness levels (1 LED each)": all_brightnesses(dev) - elif p == 'Every Second Row': + elif p == "Every Second Row": every_nth_row(dev, 2) - elif p == 'Every Third Row': + elif p == "Every Third Row": every_nth_row(dev, 3) - elif p == 'Every Fourth Row': + elif p == "Every Fourth Row": every_nth_row(dev, 4) - elif p == 'Every Fifth Row': + elif p == "Every Fifth Row": every_nth_row(dev, 5) - elif p == 'Every Sixth Row': + elif p == "Every Sixth Row": every_nth_row(dev, 6) - elif p == 'Every Second Col': + elif p == "Every Second Col": every_nth_col(dev, 2) - elif p == 'Every Third Col': + elif p == "Every Third Col": every_nth_col(dev, 3) - elif p == 'Every Fourth Col': + elif p == "Every Fourth Col": every_nth_col(dev, 4) - elif p == 'Every Fifth Col': + elif p == "Every Fifth Col": every_nth_col(dev, 4) - elif p == 'Checkerboard': + elif p == "Checkerboard": checkerboard(dev, 1) - elif p == 'Double Checkerboard': + elif p == "Double Checkerboard": checkerboard(dev, 2) - elif p == 'Triple Checkerboard': + elif p == "Triple Checkerboard": checkerboard(dev, 3) - elif p == 'Quad Checkerboard': + elif p == "Quad Checkerboard": checkerboard(dev, 4) else: print("Invalid pattern") @@ -1181,10 +1251,10 @@ def show_font(dev, font_items): offset = digit_i * 7 for pixel_x in range(5): for pixel_y in range(6): - pixel_value = digit_pixels[pixel_x + pixel_y*5] - i = (2+pixel_x) + (9*(pixel_y+offset)) + pixel_value = digit_pixels[pixel_x + pixel_y * 5] + i = (2 + pixel_x) + (9 * (pixel_y + offset)) if pixel_value: - vals[int(i/8)] = vals[int(i/8)] | (1 << i % 8) + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) send_command(dev, CommandVals.Draw, vals) @@ -1237,7 +1307,7 @@ def send_command_raw(dev, command, with_response=False): except (IOError, OSError) as _ex: global DISCONNECTED_DEVS DISCONNECTED_DEVS.append(dev.device) - #print("Error: ", ex) + # print("Error: ", ex) def send_serial(dev, s, command): @@ -1247,13 +1317,14 @@ def send_serial(dev, s, command): except (IOError, OSError) as _ex: global DISCONNECTED_DEVS DISCONNECTED_DEVS.append(dev.device) - #print("Error: ", ex) + # print("Error: ", ex) def popup(gui, message): if not gui: return import PySimpleGUI as sg + sg.Popup(message, title="Framework Laptop 16 LED Matrix") @@ -1263,96 +1334,109 @@ def gui(devices): device_checkboxes = [] for dev in devices: version = get_version(dev) - device_info = f"{dev.name}\nSerial No: {dev.serial_number}\nFW Version:{version}" - checkbox = sg.Checkbox(device_info, default=True, key=f'-CHECKBOX-{dev.name}-', enable_events=True) + device_info = ( + f"{dev.name}\nSerial No: {dev.serial_number}\nFW Version:{version}" + ) + checkbox = sg.Checkbox( + device_info, default=True, key=f"-CHECKBOX-{dev.name}-", enable_events=True + ) device_checkboxes.append([checkbox]) - - layout = [ - [sg.Text("Detected Devices")], - ] + device_checkboxes + [ - [sg.HorizontalSeparator()], - [sg.Text("Device Control")], - [sg.Button("Bootloader"), sg.Button("Sleep"), sg.Button("Wake")], - - [sg.HorizontalSeparator()], - [sg.Text("Brightness")], - # TODO: Get default from device - [sg.Slider((0, 255), orientation='h', default_value=120, - k='-BRIGHTNESS-', enable_events=True)], - - [sg.HorizontalSeparator()], - [sg.Text("Animation")], - [sg.Button("Start Animation"), sg.Button("Stop Animation")], - - [sg.HorizontalSeparator()], - [sg.Text("Pattern")], - [sg.Combo(PATTERNS, k='-PATTERN-', enable_events=True)], - - [sg.HorizontalSeparator()], - [sg.Text("Fill screen X% (could be volume indicator)")], - [sg.Slider((0, 100), orientation='h', - k='-PERCENTAGE-', enable_events=True)], - - [sg.HorizontalSeparator()], - [sg.Text("Countdown Timer")], - [ - sg.Spin([i for i in range(1, 60)], - initial_value=10, k='-COUNTDOWN-'), - sg.Text("Seconds"), - sg.Button("Start", k='-START-COUNTDOWN-'), - sg.Button("Stop", k='-STOP-COUNTDOWN-'), - ], - - [sg.HorizontalSeparator()], - [ - sg.Column([ - [sg.Text("Black&White Image")], - [sg.Button("Send stripe.gif", k='-SEND-BL-IMAGE-')] - ]), - sg.VSeperator(), - sg.Column([ - [sg.Text("Greyscale Image")], - [sg.Button("Send greyscale.gif", k='-SEND-GREY-IMAGE-')] - ]) - ], - - [sg.HorizontalSeparator()], - [sg.Text("Display Current Time")], - [ - sg.Button("Start", k='-START-TIME-'), - sg.Button("Stop", k='-STOP-TIME-') - ], - - [sg.HorizontalSeparator()], - [ - sg.Column([ - [sg.Text("Custom Text")], - [sg.Input(k='-CUSTOM-TEXT-', s=7), sg.Button("Show", k='SEND-CUSTOM-TEXT')], - ]), - sg.VSeperator(), - sg.Column([ - [sg.Text("Display Text with Symbols")], - [sg.Button("Send '2 5 degC thunder'", k='-SEND-TEXT-')], - ]) - ], - [sg.HorizontalSeparator()], - [sg.Text("PWM Frequency")], - [sg.Combo(PWM_FREQUENCIES, k='-PWM-FREQ-', enable_events=True)], - - - # TODO - # [sg.Text("Play Snake")], - # [sg.Button("Start Game", k='-PLAY-SNAKE-')], - - [sg.HorizontalSeparator()], - [sg.Text("Equalizer")], + layout = ( [ - sg.Button("Start random equalizer", k='-RANDOM-EQ-'), - sg.Button("Stop", k='-STOP-EQ-') - ], - # [sg.Button("Panic")] - ] + [sg.Text("Detected Devices")], + ] + + device_checkboxes + + [ + [sg.HorizontalSeparator()], + [sg.Text("Device Control")], + [sg.Button("Bootloader"), sg.Button("Sleep"), sg.Button("Wake")], + [sg.HorizontalSeparator()], + [sg.Text("Brightness")], + # TODO: Get default from device + [ + sg.Slider( + (0, 255), + orientation="h", + default_value=120, + k="-BRIGHTNESS-", + enable_events=True, + ) + ], + [sg.HorizontalSeparator()], + [sg.Text("Animation")], + [sg.Button("Start Animation"), sg.Button("Stop Animation")], + [sg.HorizontalSeparator()], + [sg.Text("Pattern")], + [sg.Combo(PATTERNS, k="-PATTERN-", enable_events=True)], + [sg.HorizontalSeparator()], + [sg.Text("Fill screen X% (could be volume indicator)")], + [ + sg.Slider( + (0, 100), orientation="h", k="-PERCENTAGE-", enable_events=True + ) + ], + [sg.HorizontalSeparator()], + [sg.Text("Countdown Timer")], + [ + sg.Spin([i for i in range(1, 60)], initial_value=10, k="-COUNTDOWN-"), + sg.Text("Seconds"), + sg.Button("Start", k="-START-COUNTDOWN-"), + sg.Button("Stop", k="-STOP-COUNTDOWN-"), + ], + [sg.HorizontalSeparator()], + [ + sg.Column( + [ + [sg.Text("Black&White Image")], + [sg.Button("Send stripe.gif", k="-SEND-BL-IMAGE-")], + ] + ), + sg.VSeperator(), + sg.Column( + [ + [sg.Text("Greyscale Image")], + [sg.Button("Send greyscale.gif", k="-SEND-GREY-IMAGE-")], + ] + ), + ], + [sg.HorizontalSeparator()], + [sg.Text("Display Current Time")], + [sg.Button("Start", k="-START-TIME-"), sg.Button("Stop", k="-STOP-TIME-")], + [sg.HorizontalSeparator()], + [ + sg.Column( + [ + [sg.Text("Custom Text")], + [ + sg.Input(k="-CUSTOM-TEXT-", s=7), + sg.Button("Show", k="SEND-CUSTOM-TEXT"), + ], + ] + ), + sg.VSeperator(), + sg.Column( + [ + [sg.Text("Display Text with Symbols")], + [sg.Button("Send '2 5 degC thunder'", k="-SEND-TEXT-")], + ] + ), + ], + [sg.HorizontalSeparator()], + [sg.Text("PWM Frequency")], + [sg.Combo(PWM_FREQUENCIES, k="-PWM-FREQ-", enable_events=True)], + # TODO + # [sg.Text("Play Snake")], + # [sg.Button("Start Game", k='-PLAY-SNAKE-')], + [sg.HorizontalSeparator()], + [sg.Text("Equalizer")], + [ + sg.Button("Start random equalizer", k="-RANDOM-EQ-"), + sg.Button("Stop", k="-STOP-EQ-"), + ], + # [sg.Button("Panic")] + ] + ) window = sg.Window("LED Matrix Control", layout, finalize=True) selected_devices = [] @@ -1369,13 +1453,16 @@ def gui(devices): # TODO for dev in devices: - #print("Dev {} disconnected? {}".format(dev.name, dev.device in DISCONNECTED_DEVS)) + # print("Dev {} disconnected? {}".format(dev.name, dev.device in DISCONNECTED_DEVS)) if dev.device in DISCONNECTED_DEVS: - window['-CHECKBOX-{}-'.format(dev.name)].update(False, disabled=True) + window["-CHECKBOX-{}-".format(dev.name)].update( + False, disabled=True + ) selected_devices = [ - dev for dev in devices if - values and values['-CHECKBOX-{}-'.format(dev.name)] + dev + for dev in devices + if values and values["-CHECKBOX-{}-".format(dev.name)] ] # print("Selected {} devices".format(len(selected_devices))) @@ -1383,67 +1470,80 @@ def gui(devices): break if len(selected_devices) == 1: dev = selected_devices[0] - if event == '-START-COUNTDOWN-': - thread = threading.Thread(target=countdown, args=(dev, - int(values['-COUNTDOWN-']),), daemon=True) + if event == "-START-COUNTDOWN-": + thread = threading.Thread( + target=countdown, + args=( + dev, + int(values["-COUNTDOWN-"]), + ), + daemon=True, + ) thread.start() - if event == '-START-TIME-': + if event == "-START-TIME-": thread = threading.Thread(target=clock, args=(dev,), daemon=True) thread.start() - if event == '-PLAY-SNAKE-': + if event == "-PLAY-SNAKE-": snake() - if event == '-RANDOM-EQ-': - thread = threading.Thread(target=random_eq, args=(dev,), daemon=True) + if event == "-RANDOM-EQ-": + thread = threading.Thread( + target=random_eq, args=(dev,), daemon=True + ) thread.start() else: - if event in ['-START-COUNTDOWN-', '-PLAY-SNAKE-', '-RANDOM-EQ-', '-START-TIME-']: - sg.Popup('Select exactly 1 device for this action') - if event in ['-STOP-COUNTDOWN-', '-STOP-EQ-', '-STOP-TIME-']: + if event in [ + "-START-COUNTDOWN-", + "-PLAY-SNAKE-", + "-RANDOM-EQ-", + "-START-TIME-", + ]: + sg.Popup("Select exactly 1 device for this action") + if event in ["-STOP-COUNTDOWN-", "-STOP-EQ-", "-STOP-TIME-"]: STOP_THREAD = True for dev in selected_devices: if event == "Bootloader": bootloader(dev) - if event == '-PATTERN-': - pattern(dev, values['-PATTERN-']) + if event == "-PATTERN-": + pattern(dev, values["-PATTERN-"]) - if event == '-PWM-FREQ-': - pwm_freq(dev, values['-PWM-FREQ-']) + if event == "-PWM-FREQ-": + pwm_freq(dev, values["-PWM-FREQ-"]) - if event == 'Start Animation': + if event == "Start Animation": animate(dev, True) - if event == 'Stop Animation': + if event == "Stop Animation": animate(dev, False) - if event == '-BRIGHTNESS-': - brightness(dev, int(values['-BRIGHTNESS-'])) + if event == "-BRIGHTNESS-": + brightness(dev, int(values["-BRIGHTNESS-"])) - if event == '-PERCENTAGE-': - percentage(dev, int(values['-PERCENTAGE-'])) + if event == "-PERCENTAGE-": + percentage(dev, int(values["-PERCENTAGE-"])) - if event == '-SEND-BL-IMAGE-': - path = os.path.join(resource_path(), 'res', 'stripe.gif') + if event == "-SEND-BL-IMAGE-": + path = os.path.join(resource_path(), "res", "stripe.gif") image_bl(dev, path) - if event == '-SEND-GREY-IMAGE-': - path = os.path.join(resource_path(), 'res', 'greyscale.gif') + if event == "-SEND-GREY-IMAGE-": + path = os.path.join(resource_path(), "res", "greyscale.gif") image_greyscale(dev, path) - if event == '-SEND-TEXT-': - show_symbols(dev, ['2', '5', 'degC', ' ', 'thunder']) + if event == "-SEND-TEXT-": + show_symbols(dev, ["2", "5", "degC", " ", "thunder"]) - if event == 'SEND-CUSTOM-TEXT': - show_string(dev, values['-CUSTOM-TEXT-'].upper()) + if event == "SEND-CUSTOM-TEXT": + show_string(dev, values["-CUSTOM-TEXT-"].upper()) - if event == 'Sleep': + if event == "Sleep": send_command(dev, CommandVals.Sleep, [True]) - if event == 'Wake': + if event == "Wake": send_command(dev, CommandVals.Sleep, [False]) window.close() @@ -1451,7 +1551,7 @@ def gui(devices): print(e) raise e pass - #sg.popup_error_with_traceback(f'An error happened. Here is the info:', e) + # sg.popup_error_with_traceback(f'An error happened. Here is the info:', e) def display_string(dev, disp_str): @@ -1475,52 +1575,52 @@ def set_fps_cmd(dev, mode): res = send_command(dev, CommandVals.SetFps, with_response=True) current_fps = res[0] - if mode == 'quarter': + if mode == "quarter": fps = current_fps & ~LOW_FPS_MASK fps |= 0b000 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'half': + set_power_mode_cmd("low") + elif mode == "half": fps = current_fps & ~LOW_FPS_MASK fps |= 0b001 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'one': + set_power_mode_cmd("low") + elif mode == "one": fps = current_fps & ~LOW_FPS_MASK fps |= 0b010 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'two': + set_power_mode_cmd("low") + elif mode == "two": fps = current_fps & ~LOW_FPS_MASK fps |= 0b011 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'four': + set_power_mode_cmd("low") + elif mode == "four": fps = current_fps & ~LOW_FPS_MASK fps |= 0b100 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'eight': + set_power_mode_cmd("low") + elif mode == "eight": fps = current_fps & ~LOW_FPS_MASK fps |= 0b101 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'sixteen': + set_power_mode_cmd("low") + elif mode == "sixteen": fps = current_fps & ~HIGH_FPS_MASK fps |= 0b00000000 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('high') - elif mode == 'thirtytwo': + set_power_mode_cmd("high") + elif mode == "thirtytwo": fps = current_fps & ~HIGH_FPS_MASK fps |= 0b00010000 send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd('high') + set_power_mode_cmd("high") def set_power_mode_cmd(dev, mode): - if mode == 'low': + if mode == "low": send_command(dev, CommandVals.SetPowerMode, [0]) - elif mode == 'high': + elif mode == "high": send_command(dev, CommandVals.SetPowerMode, [1]) else: print("Unsupported power mode") @@ -1559,11 +1659,11 @@ def get_fps_cmd(dev): print(f"Current FPS: {fps}") -# 5x6 symbol font. Leaves 2 pixels on each side empty -# We can leave one row empty below and then the display fits 5 of these digits. def convert_symbol(symbol): + """ 5x6 symbol font. Leaves 2 pixels on each side empty + We can leave one row empty below and then the display fits 5 of these digits.""" symbols = { 'degC': [ 1, 1, 0, 0, 0, From b8016d6740aea5a76570d1a01f65c7b2d8fcd1b5 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 16:52:59 +0800 Subject: [PATCH 04/16] python: Split into modules Release python library and command. - [x] GUI tested - [x] CLI tested - [ ] Package library for pypi - [ ] Package cli, gui for pypi Linux - [ ] Package GUI for Windows Signed-off-by: Daniel Schaefer --- .gitignore | 3 + python/framework16_inputmodules/font.py | 2164 +++++++++++++++++ .../framework16_inputmodules/gui/__init__.py | 284 +++ python/framework16_inputmodules/gui/games.py | 207 ++ .../gui/gui_threading.py | 28 + .../framework16_inputmodules/gui/ledmatrix.py | 75 + .../inputmodule/__init__.py | 150 ++ .../inputmodule/b1display.py | 159 ++ .../inputmodule/c1minimal.py | 34 + .../inputmodule/ledmatrix.py | 452 ++++ .../ledmatrix_control.py | 1967 +-------------- 11 files changed, 3613 insertions(+), 1910 deletions(-) create mode 100644 python/framework16_inputmodules/font.py create mode 100644 python/framework16_inputmodules/gui/__init__.py create mode 100644 python/framework16_inputmodules/gui/games.py create mode 100644 python/framework16_inputmodules/gui/gui_threading.py create mode 100644 python/framework16_inputmodules/gui/ledmatrix.py create mode 100644 python/framework16_inputmodules/inputmodule/__init__.py create mode 100644 python/framework16_inputmodules/inputmodule/b1display.py create mode 100644 python/framework16_inputmodules/inputmodule/c1minimal.py create mode 100644 python/framework16_inputmodules/inputmodule/ledmatrix.py diff --git a/.gitignore b/.gitignore index 02293cdb..fbde9a3d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ venv # Panic dump message.bin +# Python +__pycache__ + # pyinstaller build/ dist/ diff --git a/python/framework16_inputmodules/font.py b/python/framework16_inputmodules/font.py new file mode 100644 index 00000000..3f349a5d --- /dev/null +++ b/python/framework16_inputmodules/font.py @@ -0,0 +1,2164 @@ +def convert_symbol(symbol): + """5x6 symbol font. Leaves 2 pixels on each side empty + We can leave one row empty below and then the display fits 5 of these digits.""" + symbols = { + "degC": [ + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + ], + "degF": [ + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + ], + "snow": [ + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + ], + "sun": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "cloud": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "rain": [ + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + ], + "thunder": [ + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "batteryLow": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "!!": [ + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + ], + "heart": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "heart0": [ + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "heart2": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + ], + ":)": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + ":|": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + ], + ":(": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + ";)": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + } + if symbol in symbols: + return symbols[symbol] + else: + return None + + +def convert_font(num): + """5x6 font. Leaves 2 pixels on each side empty + We can leave one row empty below and then the display fits 5 of these digits.""" + font = { + "0": [ + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + ], + "1": [ + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "2": [ + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "3": [ + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "4": [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + ], + "5": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "6": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "7": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "8": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "9": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + ":": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + " ": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "?": [ + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + ".": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + ",": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "!": [ + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "/": [ + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "*": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "%": [ + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + ], + "+": [ + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "-": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "=": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "A": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "B": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "C": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "D": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "E": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "F": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "G": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "H": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "I": [ + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + ], + "J": [ + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + ], + "K": [ + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + "L": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "M": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + ], + "N": [ + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + ], + "O": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "P": [ + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "Q": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + ], + "R": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + ], + "S": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "T": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "U": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + ], + "V": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "W": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + ], + "X": [ + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + "Y": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "Z": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "Ä": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "Ö": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "Ü": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + ], + } + if num in font: + return font[num] + else: + return font["?"] diff --git a/python/framework16_inputmodules/gui/__init__.py b/python/framework16_inputmodules/gui/__init__.py new file mode 100644 index 00000000..790cd611 --- /dev/null +++ b/python/framework16_inputmodules/gui/__init__.py @@ -0,0 +1,284 @@ +import os +import threading +import sys + +import PySimpleGUI as sg + +from inputmodule import ( + send_command, + get_version, + brightness, + get_brightness, + bootloader, + CommandVals, +) +from gui.games import snake +from gui.ledmatrix import countdown, random_eq, clock +from gui.gui_threading import stop_thread, is_dev_disconnected +from inputmodule.ledmatrix import ( + percentage, + pattern, + animate, + PATTERNS, + PWM_FREQUENCIES, + show_symbols, + show_string, + pwm_freq, + image_bl, + image_greyscale, +) + + +def update_brightness_slider(window, devices): + average_brightness = None + for dev in devices: + if not average_brightness: + average_brightness = 0 + + br = get_brightness(dev) + average_brightness += br + print(f"Brightness: {br}") + if average_brightness: + window["-BRIGHTNESS-"].update(average_brightness / len(devices)) + + +def popup(has_gui, message): + if not has_gui: + return + import PySimpleGUI as sg + + sg.Popup(message, title="Framework Laptop 16 LED Matrix") + + +def run_gui(devices): + device_checkboxes = [] + for dev in devices: + version = get_version(dev) + device_info = ( + f"{dev.name}\nSerial No: {dev.serial_number}\nFW Version:{version}" + ) + checkbox = sg.Checkbox( + device_info, default=True, key=f"-CHECKBOX-{dev.name}-", enable_events=True + ) + device_checkboxes.append([checkbox]) + + layout = ( + [ + [sg.Text("Detected Devices")], + ] + + device_checkboxes + + [ + [sg.HorizontalSeparator()], + [sg.Text("Device Control")], + [sg.Button("Bootloader"), sg.Button("Sleep"), sg.Button("Wake")], + [sg.HorizontalSeparator()], + [sg.Text("Brightness")], + # TODO: Get default from device + [ + sg.Slider( + (0, 255), + orientation="h", + default_value=120, + k="-BRIGHTNESS-", + enable_events=True, + ) + ], + [sg.HorizontalSeparator()], + [sg.Text("Animation")], + [sg.Button("Start Animation"), sg.Button("Stop Animation")], + [sg.HorizontalSeparator()], + [sg.Text("Pattern")], + [sg.Combo(PATTERNS, k="-PATTERN-", enable_events=True)], + [sg.HorizontalSeparator()], + [sg.Text("Fill screen X% (could be volume indicator)")], + [ + sg.Slider( + (0, 100), orientation="h", k="-PERCENTAGE-", enable_events=True + ) + ], + [sg.HorizontalSeparator()], + [sg.Text("Countdown Timer")], + [ + sg.Spin([i for i in range(1, 60)], initial_value=10, k="-COUNTDOWN-"), + sg.Text("Seconds"), + sg.Button("Start", k="-START-COUNTDOWN-"), + sg.Button("Stop", k="-STOP-COUNTDOWN-"), + ], + [sg.HorizontalSeparator()], + [ + sg.Column( + [ + [sg.Text("Black&White Image")], + [sg.Button("Send stripe.gif", k="-SEND-BL-IMAGE-")], + ] + ), + sg.VSeperator(), + sg.Column( + [ + [sg.Text("Greyscale Image")], + [sg.Button("Send greyscale.gif", k="-SEND-GREY-IMAGE-")], + ] + ), + ], + [sg.HorizontalSeparator()], + [sg.Text("Display Current Time")], + [sg.Button("Start", k="-START-TIME-"), sg.Button("Stop", k="-STOP-TIME-")], + [sg.HorizontalSeparator()], + [ + sg.Column( + [ + [sg.Text("Custom Text")], + [ + sg.Input(k="-CUSTOM-TEXT-", s=7), + sg.Button("Show", k="SEND-CUSTOM-TEXT"), + ], + ] + ), + sg.VSeperator(), + sg.Column( + [ + [sg.Text("Display Text with Symbols")], + [sg.Button("Send '2 5 degC thunder'", k="-SEND-TEXT-")], + ] + ), + ], + [sg.HorizontalSeparator()], + [sg.Text("PWM Frequency")], + [sg.Combo(PWM_FREQUENCIES, k="-PWM-FREQ-", enable_events=True)], + # TODO + # [sg.Text("Play Snake")], + # [sg.Button("Start Game", k='-PLAY-SNAKE-')], + [sg.HorizontalSeparator()], + [sg.Text("Equalizer")], + [ + sg.Button("Start random equalizer", k="-RANDOM-EQ-"), + sg.Button("Stop", k="-STOP-EQ-"), + ], + # [sg.Button("Panic")] + ] + ) + + window = sg.Window("LED Matrix Control", layout, finalize=True) + selected_devices = [] + + update_brightness_slider(window, devices) + + try: + while True: + event, values = window.read() + # print('Event', event) + # print('Values', values) + + # TODO + for dev in devices: + # print("Dev {} disconnected? {}".format(dev.name, dev.device in DISCONNECTED_DEVS)) + if is_dev_disconnected(dev.device): + window["-CHECKBOX-{}-".format(dev.name)].update( + False, disabled=True + ) + + selected_devices = [ + dev + for dev in devices + if values and values["-CHECKBOX-{}-".format(dev.name)] + ] + # print("Selected {} devices".format(len(selected_devices))) + + if event == sg.WIN_CLOSED: + break + if len(selected_devices) == 1: + dev = selected_devices[0] + if event == "-START-COUNTDOWN-": + print("Starting countdown") + thread = threading.Thread( + target=countdown, + args=( + dev, + int(values["-COUNTDOWN-"]), + ), + daemon=True, + ) + thread.start() + + if event == "-START-TIME-": + thread = threading.Thread(target=clock, args=(dev,), daemon=True) + thread.start() + + if event == "-PLAY-SNAKE-": + snake() + + if event == "-RANDOM-EQ-": + thread = threading.Thread( + target=random_eq, args=(dev,), daemon=True + ) + thread.start() + else: + if event in [ + "-START-COUNTDOWN-", + "-PLAY-SNAKE-", + "-RANDOM-EQ-", + "-START-TIME-", + ]: + sg.Popup("Select exactly 1 device for this action") + if event in ["-STOP-COUNTDOWN-", "-STOP-EQ-", "-STOP-TIME-"]: + stop_thread() + + for dev in selected_devices: + if event == "Bootloader": + bootloader(dev) + + if event == "-PATTERN-": + pattern(dev, values["-PATTERN-"]) + + if event == "-PWM-FREQ-": + pwm_freq(dev, values["-PWM-FREQ-"]) + + if event == "Start Animation": + animate(dev, True) + + if event == "Stop Animation": + animate(dev, False) + + if event == "-BRIGHTNESS-": + brightness(dev, int(values["-BRIGHTNESS-"])) + + if event == "-PERCENTAGE-": + percentage(dev, int(values["-PERCENTAGE-"])) + + if event == "-SEND-BL-IMAGE-": + path = os.path.join(resource_path(), "res", "stripe.gif") + image_bl(dev, path) + + if event == "-SEND-GREY-IMAGE-": + path = os.path.join(resource_path(), "res", "greyscale.gif") + image_greyscale(dev, path) + + if event == "-SEND-TEXT-": + show_symbols(dev, ["2", "5", "degC", " ", "thunder"]) + + if event == "SEND-CUSTOM-TEXT": + show_string(dev, values["-CUSTOM-TEXT-"].upper()) + + if event == "Sleep": + send_command(dev, CommandVals.Sleep, [True]) + + if event == "Wake": + send_command(dev, CommandVals.Sleep, [False]) + + window.close() + except Exception as e: + print(e) + raise e + pass + # sg.popup_error_with_traceback(f'An error happened. Here is the info:', e) + + +def resource_path(): + """Get absolute path to resource, works for dev and for PyInstaller""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath("../../") + + return base_path diff --git a/python/framework16_inputmodules/gui/games.py b/python/framework16_inputmodules/gui/games.py new file mode 100644 index 00000000..f940a1e4 --- /dev/null +++ b/python/framework16_inputmodules/gui/games.py @@ -0,0 +1,207 @@ +from getkey import getkey, keys +import random +from datetime import datetime, timedelta +import time +import threading + +from inputmodule import GameControlVal, send_command, CommandVals, Game +from inputmodule.ledmatrix import show_string, WIDTH, HEIGHT, render_matrix + +# Constants +ARG_UP = 0 +ARG_DOWN = 1 +ARG_LEFT = 2 +ARG_RIGHT = 3 +ARG_QUIT = 4 +ARG_2LEFT = 5 +ARG_2RIGHT = 6 + +# Variables +direction = None +body = [] + + +def opposite_direction(direction): + if direction == keys.RIGHT: + return keys.LEFT + elif direction == keys.LEFT: + return keys.RIGHT + elif direction == keys.UP: + return keys.DOWN + elif direction == keys.DOWN: + return keys.UP + return direction + + +def snake_keyscan(): + global direction + global body + + while True: + current_dir = direction + key = getkey() + if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: + # Don't allow accidental suicide if we have a body + if key == opposite_direction(current_dir) and body: + continue + direction = key + + +def snake_embedded_keyscan(dev): + while True: + key_arg = None + key = getkey() + if key == keys.UP: + key_arg = GameControlVal.Up + elif key == keys.DOWN: + key_arg = GameControlVal.Down + elif key == keys.LEFT: + key_arg = GameControlVal.Left + elif key == keys.RIGHT: + key_arg = GameControlVal.Right + elif key == "q": + # Quit + key_arg = GameControlVal.Quit + if key_arg is not None: + send_command(dev, CommandVals.GameControl, [key_arg]) + + +def game_over(dev): + global body + while True: + show_string(dev, "GAME ") + time.sleep(0.75) + show_string(dev, "OVER!") + time.sleep(0.75) + score = len(body) + show_string(dev, f"{score:>3} P") + time.sleep(0.75) + + +def pong_embedded(dev): + # Start game + send_command(dev, CommandVals.StartGame, [Game.Pong]) + + while True: + key_arg = None + key = getkey() + if key == keys.LEFT: + key_arg = ARG_LEFT + elif key == keys.RIGHT: + key_arg = ARG_RIGHT + elif key == "a": + key_arg = ARG_2LEFT + elif key == "d": + key_arg = ARG_2RIGHT + elif key == "q": + # Quit + key_arg = ARG_QUIT + if key_arg is not None: + send_command(dev, CommandVals.GameControl, [key_arg]) + + +def game_of_life_embedded(dev, arg): + # Start game + # TODO: Add a way to stop it + print("Game", int(arg)) + send_command(dev, CommandVals.StartGame, [Game.GameOfLife, int(arg)]) + + +def snake_embedded(dev): + # Start game + send_command(dev, CommandVals.StartGame, [Game.Snake]) + + snake_embedded_keyscan(dev) + + +def snake(dev): + global direction + global body + head = (0, 0) + direction = keys.DOWN + food = (0, 0) + while food == head: + food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) + + # Setting + WRAP = False + + thread = threading.Thread(target=snake_keyscan, args=(), daemon=True) + thread.start() + + prev = datetime.now() + while True: + now = datetime.now() + delta = (now - prev) / timedelta(milliseconds=1) + + if delta > 200: + prev = now + else: + continue + + # Update position + (x, y) = head + oldhead = head + if direction == keys.RIGHT: + head = (x + 1, y) + elif direction == keys.LEFT: + head = (x - 1, y) + elif direction == keys.UP: + head = (x, y - 1) + elif direction == keys.DOWN: + head = (x, y + 1) + + # Detect edge condition + (x, y) = head + if head in body: + return game_over(dev) + elif x >= WIDTH or x < 0 or y >= HEIGHT or y < 0: + if WRAP: + if x >= WIDTH: + x = 0 + elif x < 0: + x = WIDTH - 1 + elif y >= HEIGHT: + y = 0 + elif y < 0: + y = HEIGHT - 1 + head = (x, y) + else: + return game_over(dev) + elif head == food: + body.insert(0, oldhead) + while food == head: + food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) + elif body: + body.pop() + body.insert(0, oldhead) + + # Draw on screen + matrix = [[0 for _ in range(HEIGHT)] for _ in range(WIDTH)] + matrix[x][y] = 1 + matrix[food[0]][food[1]] = 1 + for bodypart in body: + (x, y) = bodypart + matrix[x][y] = 1 + render_matrix(dev, matrix) + + +def wpm_demo(dev): + """Capture keypresses and calculate the WPM of the last 10 seconds + TODO: I'm not sure my calculation is right.""" + start = datetime.now() + keypresses = [] + while True: + _ = getkey() + + now = datetime.now() + keypresses = [x for x in keypresses if (now - x).total_seconds() < 10] + keypresses.append(now) + # Word is five letters + wpm = (len(keypresses) / 5) * 6 + + total_time = (now - start).total_seconds() + if total_time < 10: + wpm = wpm / (total_time / 10) + + show_string(dev, " " + str(int(wpm))) diff --git a/python/framework16_inputmodules/gui/gui_threading.py b/python/framework16_inputmodules/gui/gui_threading.py new file mode 100644 index 00000000..daff8899 --- /dev/null +++ b/python/framework16_inputmodules/gui/gui_threading.py @@ -0,0 +1,28 @@ +# Global GUI variables +STOP_THREAD = False +DISCONNECTED_DEVS = [] + + +def stop_thread(): + global STOP_THREAD + STOP_THREAD = True + + +def reset_thread(): + global STOP_THREAD + STOP_THREAD = False + + +def is_thread_stopped(): + global STOP_THREAD + return STOP_THREAD + + +def is_dev_disconnected(dev): + global DISCONNECTED_DEVS + return dev in DISCONNECTED_DEVS + + +def disconnect_dev(device): + global DISCONNECTED_DEVS + DISCONNECTED_DEVS.append(device) diff --git a/python/framework16_inputmodules/gui/ledmatrix.py b/python/framework16_inputmodules/gui/ledmatrix.py new file mode 100644 index 00000000..0c129c64 --- /dev/null +++ b/python/framework16_inputmodules/gui/ledmatrix.py @@ -0,0 +1,75 @@ +from datetime import datetime, timedelta +import time +import random + +from gui.gui_threading import reset_thread, is_thread_stopped, is_dev_disconnected +from inputmodule.ledmatrix import light_leds, show_string, eq, breathing +from inputmodule import brightness + + +def countdown(dev, seconds): + """Run a countdown timer. Lighting more LEDs every 100th of a seconds. + Until the timer runs out and every LED is lit""" + start = datetime.now() + target = seconds * 1_000_000 + while True: + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + now = datetime.now() + passed_time = (now - start) / timedelta(microseconds=1) + + ratio = passed_time / target + if passed_time >= target: + break + + leds = int(306 * ratio) + light_leds(dev, leds) + + time.sleep(0.01) + + light_leds(dev, 306) + breathing(dev) + # blinking(dev) + + +def blinking(dev): + """Blink brightness high/off every second. + Keeps currently displayed grid""" + while True: + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + brightness(dev, 0) + time.sleep(0.5) + brightness(dev, 200) + time.sleep(0.5) + + +def random_eq(dev): + """Display an equlizer looking animation with random values.""" + while True: + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + # Lower values more likely, makes it look nicer + weights = [i * i for i in range(33, 0, -1)] + population = list(range(1, 34)) + vals = random.choices(population, weights=weights, k=9) + eq(dev, vals) + time.sleep(0.2) + + +def clock(dev): + """Render the current time and display. + Loops forever, updating every second""" + while True: + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + now = datetime.now() + current_time = now.strftime("%H:%M") + print("Current Time =", current_time) + + show_string(dev, current_time) + time.sleep(1) diff --git a/python/framework16_inputmodules/inputmodule/__init__.py b/python/framework16_inputmodules/inputmodule/__init__.py new file mode 100644 index 00000000..9cfefea1 --- /dev/null +++ b/python/framework16_inputmodules/inputmodule/__init__.py @@ -0,0 +1,150 @@ +from enum import IntEnum +import serial + +# TODO: Make independent from GUI +from gui.gui_threading import disconnect_dev + +FWK_MAGIC = [0x32, 0xAC] +FWK_VID = 0x32AC +LED_MATRIX_PID = 0x20 +QTPY_PID = 0x001F +INPUTMODULE_PIDS = [LED_MATRIX_PID, QTPY_PID] + + +class CommandVals(IntEnum): + Brightness = 0x00 + Pattern = 0x01 + BootloaderReset = 0x02 + Sleep = 0x03 + Animate = 0x04 + Panic = 0x05 + Draw = 0x06 + StageGreyCol = 0x07 + DrawGreyColBuffer = 0x08 + SetText = 0x09 + StartGame = 0x10 + GameControl = 0x11 + GameStatus = 0x12 + SetColor = 0x13 + DisplayOn = 0x14 + InvertScreen = 0x15 + SetPixelColumn = 0x16 + FlushFramebuffer = 0x17 + ClearRam = 0x18 + ScreenSaver = 0x19 + SetFps = 0x1A + SetPowerMode = 0x1B + PwmFreq = 0x1E + DebugMode = 0x1F + Version = 0x20 + + +class Game(IntEnum): + Snake = 0x00 + Pong = 0x01 + Tetris = 0x02 + GameOfLife = 0x03 + + +class PatternVals(IntEnum): + Percentage = 0x00 + Gradient = 0x01 + DoubleGradient = 0x02 + DisplayLotus = 0x03 + ZigZag = 0x04 + FullBrightness = 0x05 + DisplayPanic = 0x06 + DisplayLotus2 = 0x07 + + +class GameOfLifeStartParam(IntEnum): + Currentmatrix = 0x00 + Pattern1 = 0x01 + Blinker = 0x02 + Toad = 0x03 + Beacon = 0x04 + Glider = 0x05 + + def __str__(self): + return self.name.lower() + + def __repr__(self): + return str(self) + + @staticmethod + def argparse(s): + try: + return GameOfLifeStartParam[s.lower().capitalize()] + except KeyError: + return s + + +class GameControlVal(IntEnum): + Up = 0 + Down = 1 + Left = 2 + Right = 3 + Quit = 4 + + +RESPONSE_SIZE = 32 + + +def bootloader(dev): + """Reboot into the bootloader to flash new firmware""" + send_command(dev, CommandVals.BootloaderReset, [0x00]) + + +def brightness(dev, b: int): + """Adjust the brightness scaling of the entire screen.""" + send_command(dev, CommandVals.Brightness, [b]) + + +def get_brightness(dev): + """Adjust the brightness scaling of the entire screen.""" + res = send_command(dev, CommandVals.Brightness, with_response=True) + return int(res[0]) + + +def get_version(dev): + """Get the device's firmware version""" + res = send_command(dev, CommandVals.Version, with_response=True) + major = res[0] + minor = (res[1] & 0xF0) >> 4 + patch = res[1] & 0xF + pre_release = res[2] + + version = f"{major}.{minor}.{patch}" + if pre_release: + version += " (Pre-release)" + return version + + +def send_command(dev, command, parameters=[], with_response=False): + return send_command_raw(dev, FWK_MAGIC + [command] + parameters, with_response) + + +def send_command_raw(dev, command, with_response=False): + """Send a command to the device. + Opens new serial connection every time""" + # print(f"Sending command: {command}") + try: + with serial.Serial(dev.device, 115200) as s: + s.write(command) + + if with_response: + res = s.read(RESPONSE_SIZE) + # print(f"Received: {res}") + return res + except (IOError, OSError) as _ex: + disconnect_dev(dev.device) + # print("Error: ", ex) + + +def send_serial(dev, s, command): + """Send serial command by using existing serial connection""" + try: + s.write(command) + except (IOError, OSError) as _ex: + disconnect_dev(dev.device) + # print("Error: ", ex) diff --git a/python/framework16_inputmodules/inputmodule/b1display.py b/python/framework16_inputmodules/inputmodule/b1display.py new file mode 100644 index 00000000..ff178475 --- /dev/null +++ b/python/framework16_inputmodules/inputmodule/b1display.py @@ -0,0 +1,159 @@ +import sys + +from inputmodule import send_command, CommandVals, FWK_MAGIC + +B1_WIDTH = 300 +B1_HEIGHT = 400 +GREYSCALE_DEPTH = 32 + +SCREEN_FPS = ["quarter", "half", "one", "two", "four", "eight", "sixteen", "thirtytwo"] +HIGH_FPS_MASK = 0b00010000 +LOW_FPS_MASK = 0b00000111 + + +def b1image_bl(dev, image_file): + """Display an image in black and white + Confirmed working with PNG and GIF. + Must be 300x400 in size. + Sends one 400px column in a single commands and a flush at the end + """ + + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == B1_WIDTH + assert height == B1_HEIGHT + pixel_values = list(im.getdata()) + + for x in range(B1_WIDTH): + vals = [0 for _ in range(50)] + + byte = None + for y in range(B1_HEIGHT): + pixel = pixel_values[y * B1_WIDTH + x] + brightness = sum(pixel) / 3 + black = brightness < 0xFF / 2 + + bit = y % 8 + + if bit == 0: + byte = 0 + if black: + byte |= 1 << bit + + if bit == 7: + vals[int(y / 8)] = byte + + column_le = list((x).to_bytes(2, "little")) + command = FWK_MAGIC + [0x16] + column_le + vals + send_command(dev, command) + + # Flush + command = FWK_MAGIC + [0x17] + send_command(dev, command) + + +def display_string(dev, disp_str): + b = [ord(x) for x in disp_str] + send_command(dev, CommandVals.SetText, [len(disp_str)] + b) + + +def display_on_cmd(dev, on): + send_command(dev, CommandVals.DisplayOn, [on]) + + +def invert_screen_cmd(dev, invert): + send_command(dev, CommandVals.InvertScreen, [invert]) + + +def screen_saver_cmd(dev, on): + send_command(dev, CommandVals.ScreenSaver, [on]) + + +def set_fps_cmd(dev, mode): + res = send_command(dev, CommandVals.SetFps, with_response=True) + current_fps = res[0] + + if mode == "quarter": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "half": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b001 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "one": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b010 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "two": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b011 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "four": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b100 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "eight": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b101 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "sixteen": + fps = current_fps & ~HIGH_FPS_MASK + fps |= 0b00000000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("high") + elif mode == "thirtytwo": + fps = current_fps & ~HIGH_FPS_MASK + fps |= 0b00010000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("high") + + +def set_power_mode_cmd(dev, mode): + if mode == "low": + send_command(dev, CommandVals.SetPowerMode, [0]) + elif mode == "high": + send_command(dev, CommandVals.SetPowerMode, [1]) + else: + print("Unsupported power mode") + sys.exit(1) + + +def get_power_mode_cmd(dev): + res = send_command(dev, CommandVals.SetPowerMode, with_response=True) + current_mode = int(res[0]) + if current_mode == 0: + print("Current Power Mode: Low Power") + elif current_mode == 1: + print("Current Power Mode: High Power") + + +def get_fps_cmd(dev): + res = send_command(dev, CommandVals.SetFps, with_response=True) + current_fps = res[0] + res = send_command(dev, CommandVals.SetPowerMode, with_response=True) + current_mode = int(res[0]) + + if current_mode == 0: + current_fps &= LOW_FPS_MASK + if current_fps == 0: + fps = 0.25 + elif current_fps == 1: + fps = 0.5 + else: + fps = 2 ** (current_fps - 2) + elif current_mode == 1: + if current_fps & HIGH_FPS_MASK: + fps = 32 + else: + fps = 16 + + print(f"Current FPS: {fps}") diff --git a/python/framework16_inputmodules/inputmodule/c1minimal.py b/python/framework16_inputmodules/inputmodule/c1minimal.py new file mode 100644 index 00000000..5c7cd8a9 --- /dev/null +++ b/python/framework16_inputmodules/inputmodule/c1minimal.py @@ -0,0 +1,34 @@ +from inputmodule import send_command, CommandVals + +RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] + + +def get_color(dev): + res = send_command(dev, CommandVals.SetColor, with_response=True) + return (int(res[0]), int(res[1]), int(res[2])) + + +def set_color(dev, color): + rgb = None + if color == "white": + rgb = [0xFF, 0xFF, 0xFF] + elif color == "black": + rgb = [0x00, 0x00, 0x00] + elif color == "red": + rgb = [0xFF, 0x00, 0x00] + elif color == "green": + rgb = [0x00, 0xFF, 0x00] + elif color == "blue": + rgb = [0x00, 0x00, 0xFF] + elif color == "yellow": + rgb = [0xFF, 0xFF, 0x00] + elif color == "cyan": + rgb = [0x00, 0xFF, 0xFF] + elif color == "purple": + rgb = [0xFF, 0x00, 0xFF] + else: + print(f"Unknown color: {color}") + return + + if rgb: + send_command(dev, CommandVals.SetColor, rgb) diff --git a/python/framework16_inputmodules/inputmodule/ledmatrix.py b/python/framework16_inputmodules/inputmodule/ledmatrix.py new file mode 100644 index 00000000..599cf68a --- /dev/null +++ b/python/framework16_inputmodules/inputmodule/ledmatrix.py @@ -0,0 +1,452 @@ +import time + +import serial + +import font +from inputmodule import ( + send_command, + CommandVals, + PatternVals, + FWK_MAGIC, + send_serial, + brightness, +) + +WIDTH = 9 +HEIGHT = 34 +PATTERNS = [ + "All LEDs on", + '"LOTUS" sideways', + "Gradient (0-13% Brightness)", + "Double Gradient (0-7-0% Brightness)", + "Zigzag", + '"PANIC"', + '"LOTUS" Top Down', + "All brightness levels (1 LED each)", + "Every Second Row", + "Every Third Row", + "Every Fourth Row", + "Every Fifth Row", + "Every Sixth Row", + "Every Second Col", + "Every Third Col", + "Every Fourth Col", + "Every Fifth Col", + "Checkerboard", + "Double Checkerboard", + "Triple Checkerboard", + "Quad Checkerboard", +] +PWM_FREQUENCIES = [ + "29kHz", + "3.6kHz", + "1.8kHz", + "900Hz", +] + + +def get_pwm_freq(dev): + """Adjust the brightness scaling of the entire screen.""" + res = send_command(dev, CommandVals.PwmFreq, with_response=True) + freq = int(res[0]) + if freq == 0: + return 29000 + elif freq == 1: + return 3600 + elif freq == 2: + return 1800 + elif freq == 3: + return 900 + else: + return None + + +def percentage(dev, p): + """Fill a percentage of the screen. Bottom to top""" + send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p]) + + +def animate(dev, b: bool): + """Tell the firmware to start/stop animation. + Scrolls the currently saved grid vertically down.""" + send_command(dev, CommandVals.Animate, [b]) + + +def get_animate(dev): + """Tell the firmware to start/stop animation. + Scrolls the currently saved grid vertically down.""" + res = send_command(dev, CommandVals.Animate, with_response=True) + return bool(res[0]) + + +def image_bl(dev, image_file): + """Display an image in black and white + Confirmed working with PNG and GIF. + Must be 9x34 in size. + Sends everything in a single command + """ + vals = [0 for _ in range(39)] + + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == 9 + assert height == 34 + pixel_values = list(im.getdata()) + for i, pixel in enumerate(pixel_values): + brightness = sum(pixel) / 3 + if brightness > 0xFF / 2: + vals[int(i / 8)] |= 1 << i % 8 + + send_command(dev, CommandVals.Draw, vals) + + +def camera(dev): + """Play a live view from the webcam, for fun""" + with serial.Serial(dev.device, 115200) as s: + import cv2 + + capture = cv2.VideoCapture(0) + ret, frame = capture.read() + + scale_y = HEIGHT / frame.shape[0] + + # Scale the video to 34 pixels height + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) + # Find the starting position to crop the width to be centered + # For very narrow videos, make sure to stay in bounds + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) + end_x = min(dim[1], start_x + WIDTH) + + # Pre-process the video into resized, cropped, grayscale frames + while True: + ret, frame = capture.read() + if not ret: + print("Failed to capture video frames") + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + resized = cv2.resize(gray, (dim[1], dim[0])) + cropped = resized[0:HEIGHT, start_x:end_x] + + for x in range(0, cropped.shape[1]): + vals = [0 for _ in range(HEIGHT)] + + for y in range(0, HEIGHT): + vals[y] = cropped[y, x] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def video(dev, video_file): + """Resize and play back a video""" + with serial.Serial(dev.device, 115200) as s: + import cv2 + + capture = cv2.VideoCapture(video_file) + ret, frame = capture.read() + + scale_y = HEIGHT / frame.shape[0] + + # Scale the video to 34 pixels height + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) + # Find the starting position to crop the width to be centered + # For very narrow videos, make sure to stay in bounds + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) + end_x = min(dim[1], start_x + WIDTH) + + processed = [] + + # Pre-process the video into resized, cropped, grayscale frames + while True: + ret, frame = capture.read() + if not ret: + print("Failed to read video frames") + break + + gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + + resized = cv2.resize(gray, (dim[1], dim[0])) + cropped = resized[0:HEIGHT, start_x:end_x] + + processed.append(cropped) + + # Write it out to the module one frame at a time + # TODO: actually control for framerate + for frame in processed: + for x in range(0, cropped.shape[1]): + vals = [0 for _ in range(HEIGHT)] + + for y in range(0, HEIGHT): + vals[y] = frame[y, x] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def pixel_to_brightness(pixel): + """Calculate pixel brightness from an RGB triple""" + assert len(pixel) == 3 + brightness = sum(pixel) / len(pixel) + + # Poor man's scaling to make the greyscale pop better. + # Should find a good function. + if brightness > 200: + brightness = brightness + elif brightness > 150: + brightness = brightness * 0.8 + elif brightness > 100: + brightness = brightness * 0.5 + elif brightness > 50: + brightness = brightness + else: + brightness = brightness * 2 + + return int(brightness) + + +def image_greyscale(dev, image_file): + """Display an image in greyscale + Sends each 1x34 column and then commits => 10 commands + """ + with serial.Serial(dev.device, 115200) as s: + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == 9 + assert height == 34 + pixel_values = list(im.getdata()) + for x in range(0, WIDTH): + vals = [0 for _ in range(HEIGHT)] + + for y in range(HEIGHT): + vals[y] = pixel_to_brightness(pixel_values[x + y * WIDTH]) + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def send_col(dev, s, x, vals): + """Stage greyscale values for a single column. Must be committed with commit_cols()""" + command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals + send_serial(dev, s, command) + + +def commit_cols(dev, s): + """Commit the changes from sending individual cols with send_col(), displaying the matrix. + This makes sure that the matrix isn't partially updated.""" + command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00] + send_serial(dev, s, command) + + +def checkerboard(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT / 2) + if x % (n * 2) < n: + # Rotate once + vals = vals[n:] + vals[:n] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def every_nth_col(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [(0xFF if x % n == 0 else 0) for _ in range(HEIGHT)] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def every_nth_row(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [(0xFF if y % n == 0 else 0) for y in range(HEIGHT)] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def all_brightnesses(dev): + """Increase the brightness with each pixel. + Only 0-255 available, so it can't fill all 306 LEDs""" + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [0 for _ in range(HEIGHT)] + + for y in range(HEIGHT): + brightness = x + WIDTH * y + if brightness > 255: + vals[y] = 0 + else: + vals[y] = brightness + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def breathing(dev): + """Animate breathing brightness. + Keeps currently displayed grid""" + # Bright ranges appear similar, so we have to go through those faster + while True: + # Go quickly from 250 to 50 + for i in range(10): + time.sleep(0.03) + brightness(dev, 250 - i * 20) + + # Go slowly from 50 to 0 + for i in range(10): + time.sleep(0.06) + brightness(dev, 50 - i * 5) + + # Go slowly from 0 to 50 + for i in range(10): + time.sleep(0.06) + brightness(dev, i * 5) + + # Go quickly from 50 to 250 + for i in range(10): + time.sleep(0.03) + brightness(dev, 50 + i * 20) + + +def eq(dev, vals): + """Display 9 values in equalizer diagram starting from the middle, going up and down""" + matrix = [[0 for _ in range(34)] for _ in range(9)] + + for col, val in enumerate(vals[:9]): + row = int(34 / 2) + above = int(val / 2) + below = val - above + + for i in range(above): + matrix[col][row + i] = 0xFF + for i in range(below): + matrix[col][row - 1 - i] = 0xFF + + render_matrix(dev, matrix) + + +def render_matrix(dev, matrix): + """Show a black/white matrix + Send everything in a single command""" + vals = [0x00 for _ in range(39)] + + for x in range(9): + for y in range(34): + i = x + 9 * y + if matrix[x][y]: + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) + + send_command(dev, CommandVals.Draw, vals) + + +def light_leds(dev, leds): + """Light a specific number of LEDs""" + vals = [0x00 for _ in range(39)] + for byte in range(int(leds / 8)): + vals[byte] = 0xFF + for i in range(leds % 8): + vals[int(leds / 8)] += 1 << i + send_command(dev, CommandVals.Draw, vals) + + +def pwm_freq(dev, freq): + """Display a pattern that's already programmed into the firmware""" + if freq == "29kHz": + send_command(dev, CommandVals.PwmFreq, [0]) + elif freq == "3.6kHz": + send_command(dev, CommandVals.PwmFreq, [1]) + elif freq == "1.8kHz": + send_command(dev, CommandVals.PwmFreq, [2]) + elif freq == "900Hz": + send_command(dev, CommandVals.PwmFreq, [3]) + + +def pattern(dev, p): + """Display a pattern that's already programmed into the firmware""" + if p == "All LEDs on": + send_command(dev, CommandVals.Pattern, [PatternVals.FullBrightness]) + elif p == "Gradient (0-13% Brightness)": + send_command(dev, CommandVals.Pattern, [PatternVals.Gradient]) + elif p == "Double Gradient (0-7-0% Brightness)": + send_command(dev, CommandVals.Pattern, [PatternVals.DoubleGradient]) + elif p == '"LOTUS" sideways': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus]) + elif p == "Zigzag": + send_command(dev, CommandVals.Pattern, [PatternVals.ZigZag]) + elif p == '"PANIC"': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayPanic]) + elif p == '"LOTUS" Top Down': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus2]) + elif p == "All brightness levels (1 LED each)": + all_brightnesses(dev) + elif p == "Every Second Row": + every_nth_row(dev, 2) + elif p == "Every Third Row": + every_nth_row(dev, 3) + elif p == "Every Fourth Row": + every_nth_row(dev, 4) + elif p == "Every Fifth Row": + every_nth_row(dev, 5) + elif p == "Every Sixth Row": + every_nth_row(dev, 6) + elif p == "Every Second Col": + every_nth_col(dev, 2) + elif p == "Every Third Col": + every_nth_col(dev, 3) + elif p == "Every Fourth Col": + every_nth_col(dev, 4) + elif p == "Every Fifth Col": + every_nth_col(dev, 4) + elif p == "Checkerboard": + checkerboard(dev, 1) + elif p == "Double Checkerboard": + checkerboard(dev, 2) + elif p == "Triple Checkerboard": + checkerboard(dev, 3) + elif p == "Quad Checkerboard": + checkerboard(dev, 4) + else: + print("Invalid pattern") + + +def show_string(dev, s): + """Render a string with up to five letters""" + show_font(dev, [font.convert_font(letter) for letter in str(s)[:5]]) + + +def show_font(dev, font_items): + """Render up to five 5x6 pixel font items""" + vals = [0x00 for _ in range(39)] + + for digit_i, digit_pixels in enumerate(font_items): + offset = digit_i * 7 + for pixel_x in range(5): + for pixel_y in range(6): + pixel_value = digit_pixels[pixel_x + pixel_y * 5] + i = (2 + pixel_x) + (9 * (pixel_y + offset)) + if pixel_value: + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) + + send_command(dev, CommandVals.Draw, vals) + + +def show_symbols(dev, symbols): + """Render a list of up to five symbols + Can use letters/numbers or symbol names, like 'sun', ':)'""" + font_items = [] + for symbol in symbols: + s = font.convert_symbol(symbol) + if not s: + s = font.convert_font(symbol) + font_items.append(s) + + show_font(dev, font_items) diff --git a/python/framework16_inputmodules/ledmatrix_control.py b/python/framework16_inputmodules/ledmatrix_control.py index f0e1372e..cb41c86b 100755 --- a/python/framework16_inputmodules/ledmatrix_control.py +++ b/python/framework16_inputmodules/ledmatrix_control.py @@ -1,172 +1,67 @@ #!/usr/bin/env python3 import argparse -import os -import random import sys -import threading -import time -from datetime import datetime, timedelta -from enum import IntEnum # Need to install -import serial from serial.tools import list_ports +# Local dependencies +import gui +from inputmodule import ( + INPUTMODULE_PIDS, + send_command, + get_version, + brightness, + get_brightness, + CommandVals, + bootloader, + GameOfLifeStartParam, + GameControlVal, +) +from gui.games import ( + snake, + snake_embedded, + pong_embedded, + game_of_life_embedded, + wpm_demo, +) +from gui.ledmatrix import random_eq, clock, blinking +from inputmodule.ledmatrix import ( + eq, + breathing, + camera, + video, + all_brightnesses, + percentage, + pattern, + animate, + get_animate, + pwm_freq, + get_pwm_freq, + show_string, + show_symbols, + PATTERNS, + image_bl, + image_greyscale, +) +from inputmodule.b1display import ( + b1image_bl, + invert_screen_cmd, + screen_saver_cmd, + set_fps_cmd, + set_power_mode_cmd, + get_power_mode_cmd, + get_fps_cmd, + SCREEN_FPS, + display_on_cmd, + display_string, +) +from inputmodule.c1minimal import set_color, get_color, RGB_COLORS + # Optional dependencies: # from PIL import Image # import PySimpleGUI as sg -FWK_MAGIC = [0x32, 0xAC] -FWK_VID = 0x32AC -LED_MATRIX_PID = 0x20 -QTPY_PID = 0x001F -INPUTMODULE_PIDS = [LED_MATRIX_PID, QTPY_PID] - - -class CommandVals(IntEnum): - Brightness = 0x00 - Pattern = 0x01 - BootloaderReset = 0x02 - Sleep = 0x03 - Animate = 0x04 - Panic = 0x05 - Draw = 0x06 - StageGreyCol = 0x07 - DrawGreyColBuffer = 0x08 - SetText = 0x09 - StartGame = 0x10 - GameControl = 0x11 - GameStatus = 0x12 - SetColor = 0x13 - DisplayOn = 0x14 - InvertScreen = 0x15 - SetPixelColumn = 0x16 - FlushFramebuffer = 0x17 - ClearRam = 0x18 - ScreenSaver = 0x19 - SetFps = 0x1A - SetPowerMode = 0x1B - PwmFreq = 0x1E - DebugMode = 0x1F - Version = 0x20 - - -class Game(IntEnum): - Snake = 0x00 - Pong = 0x01 - Tetris = 0x02 - GameOfLife = 0x03 - - -class PatternVals(IntEnum): - Percentage = 0x00 - Gradient = 0x01 - DoubleGradient = 0x02 - DisplayLotus = 0x03 - ZigZag = 0x04 - FullBrightness = 0x05 - DisplayPanic = 0x06 - DisplayLotus2 = 0x07 - - -class GameOfLifeStartParam(IntEnum): - Currentmatrix = 0x00 - Pattern1 = 0x01 - Blinker = 0x02 - Toad = 0x03 - Beacon = 0x04 - Glider = 0x05 - - def __str__(self): - return self.name.lower() - - def __repr__(self): - return str(self) - - @staticmethod - def argparse(s): - try: - return GameOfLifeStartParam[s.lower().capitalize()] - except KeyError: - return s - - -class GameControlVal(IntEnum): - Up = 0 - Down = 1 - Left = 2 - Right = 3 - Quit = 4 - - -PWM_FREQUENCIES = [ - "29kHz", - "3.6kHz", - "1.8kHz", - "900Hz", -] - -PATTERNS = [ - "All LEDs on", - '"LOTUS" sideways', - "Gradient (0-13% Brightness)", - "Double Gradient (0-7-0% Brightness)", - "Zigzag", - '"PANIC"', - '"LOTUS" Top Down', - "All brightness levels (1 LED each)", - "Every Second Row", - "Every Third Row", - "Every Fourth Row", - "Every Fifth Row", - "Every Sixth Row", - "Every Second Col", - "Every Third Col", - "Every Fourth Col", - "Every Fifth Col", - "Checkerboard", - "Double Checkerboard", - "Triple Checkerboard", - "Quad Checkerboard", -] -DRAW_PATTERNS = ["off", "on", "foo"] -GREYSCALE_DEPTH = 32 -RESPONSE_SIZE = 32 -WIDTH = 9 -HEIGHT = 34 -B1_WIDTH = 300 -B1_HEIGHT = 400 - -ARG_UP = 0 -ARG_DOWN = 1 -ARG_LEFT = 2 -ARG_RIGHT = 3 -ARG_QUIT = 4 -ARG_2LEFT = 5 -ARG_2RIGHT = 6 - -RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] -SCREEN_FPS = ["quarter", "half", "one", "two", "four", "eight", "sixteen", "thirtytwo"] -HIGH_FPS_MASK = 0b00010000 -LOW_FPS_MASK = 0b00000111 - -# Global variables -STOP_THREAD = False -DISCONNECTED_DEVS = [] - - -def update_brightness_slider(window, devices): - average_brightness = None - for dev in devices: - if not average_brightness: - average_brightness = 0 - - br = get_brightness(dev) - average_brightness += br - print(f"Brightness: {br}") - if average_brightness: - window["-BRIGHTNESS-"].update(average_brightness / len(devices)) - def main(): parser = argparse.ArgumentParser() @@ -334,14 +229,14 @@ def main(): if not ports: print("No device found") - popup(args.gui, "No device found") + gui.popup(args.gui, "No device found") sys.exit(1) elif args.serial_dev is not None: dev = [port for port in ports if port.name == args.serial_dev][0] elif len(ports) == 1: dev = ports[0] elif len(ports) >= 1 and not args.gui: - popup( + gui.popup( args.gui, "More than 1 compatibles devices found. Please choose from the commandline with --serial-dev COMX.\nConnected ports:\n- {}".format( "\n- ".join([port.device for port in ports]) @@ -360,7 +255,7 @@ def main(): if not args.gui and dev is None: print("No device selected") - popup(args.gui, "No device selected") + gui.popup(args.gui, "No device selected") sys.exit(1) if args.bootloader: @@ -423,7 +318,7 @@ def main(): elif args.gui: devices = find_devs() # show=False, verbose=False) print("Found {} devices".format(len(devices))) - gui(devices) + gui.run_gui(devices) elif args.blink: blinking(dev) elif args.breathing: @@ -476,17 +371,6 @@ def main(): sys.exit(1) -def resource_path(): - """Get absolute path to resource, works for dev and for PyInstaller""" - try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS - except Exception: - base_path = os.path.abspath(".") - - return base_path - - def find_devs(): ports = list_ports.comports() return [ @@ -503,1742 +387,5 @@ def print_devs(ports): print(f" Product: {port.product}") -def bootloader(dev): - """Reboot into the bootloader to flash new firmware""" - send_command(dev, CommandVals.BootloaderReset, [0x00]) - - -def percentage(dev, p): - """Fill a percentage of the screen. Bottom to top""" - send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p]) - - -def brightness(dev, b: int): - """Adjust the brightness scaling of the entire screen.""" - send_command(dev, CommandVals.Brightness, [b]) - - -def get_brightness(dev): - """Adjust the brightness scaling of the entire screen.""" - res = send_command(dev, CommandVals.Brightness, with_response=True) - return int(res[0]) - - -def get_pwm_freq(dev): - """Adjust the brightness scaling of the entire screen.""" - res = send_command(dev, CommandVals.PwmFreq, with_response=True) - freq = int(res[0]) - if freq == 0: - return 29000 - elif freq == 1: - return 3600 - elif freq == 2: - return 1800 - elif freq == 3: - return 900 - else: - return None - - -def get_version(dev): - """Get the device's firmware version""" - res = send_command(dev, CommandVals.Version, with_response=True) - major = res[0] - minor = (res[1] & 0xF0) >> 4 - patch = res[1] & 0xF - pre_release = res[2] - - version = f"{major}.{minor}.{patch}" - if pre_release: - version += " (Pre-release)" - return version - - -def animate(dev, b: bool): - """Tell the firmware to start/stop animation. - Scrolls the currently saved grid vertically down.""" - send_command(dev, CommandVals.Animate, [b]) - - -def get_animate(dev): - """Tell the firmware to start/stop animation. - Scrolls the currently saved grid vertically down.""" - res = send_command(dev, CommandVals.Animate, with_response=True) - return bool(res[0]) - - -def b1image_bl(dev, image_file): - """Display an image in black and white - Confirmed working with PNG and GIF. - Must be 300x400 in size. - Sends one 400px column in a single commands and a flush at the end - """ - - from PIL import Image - - im = Image.open(image_file).convert("RGB") - width, height = im.size - assert width == B1_WIDTH - assert height == B1_HEIGHT - pixel_values = list(im.getdata()) - - for x in range(B1_WIDTH): - vals = [0 for _ in range(50)] - - byte = None - for y in range(B1_HEIGHT): - pixel = pixel_values[y * B1_WIDTH + x] - brightness = sum(pixel) / 3 - black = brightness < 0xFF / 2 - - bit = y % 8 - - if bit == 0: - byte = 0 - if black: - byte |= 1 << bit - - if bit == 7: - vals[int(y / 8)] = byte - - column_le = list((x).to_bytes(2, "little")) - command = FWK_MAGIC + [0x16] + column_le + vals - send_command(dev, command) - - # Flush - command = FWK_MAGIC + [0x17] - send_command(dev, command) - - -def image_bl(dev, image_file): - """Display an image in black and white - Confirmed working with PNG and GIF. - Must be 9x34 in size. - Sends everything in a single command - """ - vals = [0 for _ in range(39)] - - from PIL import Image - - im = Image.open(image_file).convert("RGB") - width, height = im.size - assert width == 9 - assert height == 34 - pixel_values = list(im.getdata()) - for i, pixel in enumerate(pixel_values): - brightness = sum(pixel) / 3 - if brightness > 0xFF / 2: - vals[int(i / 8)] |= 1 << i % 8 - - send_command(dev, CommandVals.Draw, vals) - - -def camera(dev): - """Play a live view from the webcam, for fun""" - with serial.Serial(dev.device, 115200) as s: - import cv2 - - capture = cv2.VideoCapture(0) - ret, frame = capture.read() - - scale_y = HEIGHT / frame.shape[0] - - # Scale the video to 34 pixels height - dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) - # Find the starting position to crop the width to be centered - # For very narrow videos, make sure to stay in bounds - start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) - end_x = min(dim[1], start_x + WIDTH) - - # Pre-process the video into resized, cropped, grayscale frames - while True: - ret, frame = capture.read() - if not ret: - print("Failed to capture video frames") - break - - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - - resized = cv2.resize(gray, (dim[1], dim[0])) - cropped = resized[0:HEIGHT, start_x:end_x] - - for x in range(0, cropped.shape[1]): - vals = [0 for _ in range(HEIGHT)] - - for y in range(0, HEIGHT): - vals[y] = cropped[y, x] - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def video(dev, video_file): - """Resize and play back a video""" - with serial.Serial(dev.device, 115200) as s: - import cv2 - - capture = cv2.VideoCapture(video_file) - ret, frame = capture.read() - - scale_y = HEIGHT / frame.shape[0] - - # Scale the video to 34 pixels height - dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) - # Find the starting position to crop the width to be centered - # For very narrow videos, make sure to stay in bounds - start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) - end_x = min(dim[1], start_x + WIDTH) - - processed = [] - - # Pre-process the video into resized, cropped, grayscale frames - while True: - ret, frame = capture.read() - if not ret: - print("Failed to read video frames") - break - - gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) - - resized = cv2.resize(gray, (dim[1], dim[0])) - cropped = resized[0:HEIGHT, start_x:end_x] - - processed.append(cropped) - - # Write it out to the module one frame at a time - # TODO: actually control for framerate - for frame in processed: - for x in range(0, cropped.shape[1]): - vals = [0 for _ in range(HEIGHT)] - - for y in range(0, HEIGHT): - vals[y] = frame[y, x] - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def pixel_to_brightness(pixel): - """Calculate pixel brightness from an RGB triple""" - assert len(pixel) == 3 - brightness = sum(pixel) / len(pixel) - - # Poor man's scaling to make the greyscale pop better. - # Should find a good function. - if brightness > 200: - brightness = brightness - elif brightness > 150: - brightness = brightness * 0.8 - elif brightness > 100: - brightness = brightness * 0.5 - elif brightness > 50: - brightness = brightness - else: - brightness = brightness * 2 - - return int(brightness) - - -def image_greyscale(dev, image_file): - """Display an image in greyscale - Sends each 1x34 column and then commits => 10 commands - """ - with serial.Serial(dev.device, 115200) as s: - from PIL import Image - - im = Image.open(image_file).convert("RGB") - width, height = im.size - assert width == 9 - assert height == 34 - pixel_values = list(im.getdata()) - for x in range(0, WIDTH): - vals = [0 for _ in range(HEIGHT)] - - for y in range(HEIGHT): - vals[y] = pixel_to_brightness(pixel_values[x + y * WIDTH]) - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def send_col(dev, s, x, vals): - """Stage greyscale values for a single column. Must be committed with commit_cols()""" - command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals - send_serial(dev, s, command) - - -def commit_cols(dev, s): - """Commit the changes from sending individual cols with send_col(), displaying the matrix. - This makes sure that the matrix isn't partially updated.""" - command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00] - send_serial(dev, s, command) - - -def get_color(dev): - res = send_command(dev, CommandVals.SetColor, with_response=True) - return (int(res[0]), int(res[1]), int(res[2])) - - -def set_color(dev, color): - rgb = None - if color == "white": - rgb = [0xFF, 0xFF, 0xFF] - elif color == "black": - rgb = [0x00, 0x00, 0x00] - elif color == "red": - rgb = [0xFF, 0x00, 0x00] - elif color == "green": - rgb = [0x00, 0xFF, 0x00] - elif color == "blue": - rgb = [0x00, 0x00, 0xFF] - elif color == "yellow": - rgb = [0xFF, 0xFF, 0x00] - elif color == "cyan": - rgb = [0x00, 0xFF, 0xFF] - elif color == "purple": - rgb = [0xFF, 0x00, 0xFF] - else: - print(f"Unknown color: {color}") - return - - if rgb: - send_command(dev, CommandVals.SetColor, rgb) - - -def checkerboard(dev, n): - with serial.Serial(dev.device, 115200) as s: - for x in range(0, WIDTH): - vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT / 2) - if x % (n * 2) < n: - # Rotate once - vals = vals[n:] + vals[:n] - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def every_nth_col(dev, n): - with serial.Serial(dev.device, 115200) as s: - for x in range(0, WIDTH): - vals = [(0xFF if x % n == 0 else 0) for _ in range(HEIGHT)] - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def every_nth_row(dev, n): - with serial.Serial(dev.device, 115200) as s: - for x in range(0, WIDTH): - vals = [(0xFF if y % n == 0 else 0) for y in range(HEIGHT)] - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def all_brightnesses(dev): - """Increase the brightness with each pixel. - Only 0-255 available, so it can't fill all 306 LEDs""" - with serial.Serial(dev.device, 115200) as s: - for x in range(0, WIDTH): - vals = [0 for _ in range(HEIGHT)] - - for y in range(HEIGHT): - brightness = x + WIDTH * y - if brightness > 255: - vals[y] = 0 - else: - vals[y] = brightness - - send_col(dev, s, x, vals) - commit_cols(dev, s) - - -def countdown(dev, seconds): - """Run a countdown timer. Lighting more LEDs every 100th of a seconds. - Until the timer runs out and every LED is lit""" - start = datetime.now() - target = seconds * 1_000_000 - global STOP_THREAD - while True: - if STOP_THREAD or dev.device in DISCONNECTED_DEVS: - STOP_THREAD = False - return - now = datetime.now() - passed_time = (now - start) / timedelta(microseconds=1) - - ratio = passed_time / target - if passed_time >= target: - break - - leds = int(306 * ratio) - light_leds(dev, leds) - - time.sleep(0.01) - - light_leds(dev, 306) - breathing(dev) - # blinking(dev) - - -def blinking(dev): - """Blink brightness high/off every second. - Keeps currently displayed grid""" - global STOP_THREAD - while True: - if STOP_THREAD or dev.device in DISCONNECTED_DEVS: - STOP_THREAD = False - return - brightness(dev, 0) - time.sleep(0.5) - brightness(dev, 200) - time.sleep(0.5) - - -def breathing(dev): - """Animate breathing brightness. - Keeps currently displayed grid""" - # Bright ranges appear similar, so we have to go through those faster - while True: - # Go quickly from 250 to 50 - for i in range(10): - time.sleep(0.03) - brightness(dev, 250 - i * 20) - - # Go slowly from 50 to 0 - for i in range(10): - time.sleep(0.06) - brightness(dev, 50 - i * 5) - - # Go slowly from 0 to 50 - for i in range(10): - time.sleep(0.06) - brightness(dev, i * 5) - - # Go quickly from 50 to 250 - for i in range(10): - time.sleep(0.03) - brightness(dev, 50 + i * 20) - - -direction = None -body = [] - - -def opposite_direction(direction): - from getkey import keys - - if direction == keys.RIGHT: - return keys.LEFT - elif direction == keys.LEFT: - return keys.RIGHT - elif direction == keys.UP: - return keys.DOWN - elif direction == keys.DOWN: - return keys.UP - return direction - - -def snake_keyscan(): - from getkey import getkey, keys - - global direction - global body - - while True: - current_dir = direction - key = getkey() - if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: - # Don't allow accidental suicide if we have a body - if key == opposite_direction(current_dir) and body: - continue - direction = key - - -def snake_embedded_keyscan(dev): - from getkey import getkey, keys - - while True: - key_arg = None - key = getkey() - if key == keys.UP: - key_arg = GameControlVal.Up - elif key == keys.DOWN: - key_arg = GameControlVal.Down - elif key == keys.LEFT: - key_arg = GameControlVal.Left - elif key == keys.RIGHT: - key_arg = GameControlVal.Right - elif key == "q": - # Quit - key_arg = GameControlVal.Quit - if key_arg is not None: - send_command(dev, CommandVals.GameControl, [key_arg]) - - -def game_over(dev): - global body - while True: - show_string(dev, "GAME ") - time.sleep(0.75) - show_string(dev, "OVER!") - time.sleep(0.75) - score = len(body) - show_string(dev, f"{score:>3} P") - time.sleep(0.75) - - -def pong_embedded(dev): - # Start game - send_command(dev, CommandVals.StartGame, [Game.Pong]) - - from getkey import getkey, keys - - while True: - key_arg = None - key = getkey() - if key == keys.LEFT: - key_arg = ARG_LEFT - elif key == keys.RIGHT: - key_arg = ARG_RIGHT - elif key == "a": - key_arg = ARG_2LEFT - elif key == "d": - key_arg = ARG_2RIGHT - elif key == "q": - # Quit - key_arg = ARG_QUIT - if key_arg is not None: - send_command(dev, CommandVals.GameControl, [key_arg]) - - -def game_of_life_embedded(dev, arg): - # Start game - # TODO: Add a way to stop it - print("Game", int(arg)) - send_command(dev, CommandVals.StartGame, [Game.GameOfLife, int(arg)]) - - -def snake_embedded(dev): - # Start game - send_command(dev, CommandVals.StartGame, [Game.Snake]) - - snake_embedded_keyscan(dev) - - -def snake(dev): - from getkey import keys - - global direction - global body - head = (0, 0) - direction = keys.DOWN - food = (0, 0) - while food == head: - food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) - - # Setting - WRAP = False - - thread = threading.Thread(target=snake_keyscan, args=(), daemon=True) - thread.start() - - prev = datetime.now() - while True: - now = datetime.now() - delta = (now - prev) / timedelta(milliseconds=1) - - if delta > 200: - prev = now - else: - continue - - # Update position - (x, y) = head - oldhead = head - if direction == keys.RIGHT: - head = (x + 1, y) - elif direction == keys.LEFT: - head = (x - 1, y) - elif direction == keys.UP: - head = (x, y - 1) - elif direction == keys.DOWN: - head = (x, y + 1) - - # Detect edge condition - (x, y) = head - if head in body: - return game_over(dev) - elif x >= WIDTH or x < 0 or y >= HEIGHT or y < 0: - if WRAP: - if x >= WIDTH: - x = 0 - elif x < 0: - x = WIDTH - 1 - elif y >= HEIGHT: - y = 0 - elif y < 0: - y = HEIGHT - 1 - head = (x, y) - else: - return game_over(dev) - elif head == food: - body.insert(0, oldhead) - while food == head: - food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) - elif body: - body.pop() - body.insert(0, oldhead) - - # Draw on screen - matrix = [[0 for _ in range(HEIGHT)] for _ in range(WIDTH)] - matrix[x][y] = 1 - matrix[food[0]][food[1]] = 1 - for bodypart in body: - (x, y) = bodypart - matrix[x][y] = 1 - render_matrix(dev, matrix) - - -def wpm_demo(dev): - """Capture keypresses and calculate the WPM of the last 10 seconds - TODO: I'm not sure my calculation is right.""" - from getkey import getkey - - start = datetime.now() - keypresses = [] - while True: - _ = getkey() - - now = datetime.now() - keypresses = [x for x in keypresses if (now - x).total_seconds() < 10] - keypresses.append(now) - # Word is five letters - wpm = (len(keypresses) / 5) * 6 - - total_time = (now - start).total_seconds() - if total_time < 10: - wpm = wpm / (total_time / 10) - - show_string(dev, " " + str(int(wpm))) - - -def random_eq(dev): - """Display an equlizer looking animation with random values.""" - global STOP_THREAD - while True: - if STOP_THREAD or dev.device in DISCONNECTED_DEVS: - STOP_THREAD = False - return - # Lower values more likely, makes it look nicer - weights = [i * i for i in range(33, 0, -1)] - population = list(range(1, 34)) - vals = random.choices(population, weights=weights, k=9) - eq(dev, vals) - time.sleep(0.2) - - -def eq(dev, vals): - """Display 9 values in equalizer diagram starting from the middle, going up and down""" - matrix = [[0 for _ in range(34)] for _ in range(9)] - - for col, val in enumerate(vals[:9]): - row = int(34 / 2) - above = int(val / 2) - below = val - above - - for i in range(above): - matrix[col][row + i] = 0xFF - for i in range(below): - matrix[col][row - 1 - i] = 0xFF - - render_matrix(dev, matrix) - - -def render_matrix(dev, matrix): - """Show a black/white matrix - Send everything in a single command""" - vals = [0x00 for _ in range(39)] - - for x in range(9): - for y in range(34): - i = x + 9 * y - if matrix[x][y]: - vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) - - send_command(dev, CommandVals.Draw, vals) - - -def light_leds(dev, leds): - """Light a specific number of LEDs""" - vals = [0x00 for _ in range(39)] - for byte in range(int(leds / 8)): - vals[byte] = 0xFF - for i in range(leds % 8): - vals[int(leds / 8)] += 1 << i - send_command(dev, CommandVals.Draw, vals) - - -def pwm_freq(dev, freq): - """Display a pattern that's already programmed into the firmware""" - if freq == "29kHz": - send_command(dev, CommandVals.PwmFreq, [0]) - elif freq == "3.6kHz": - send_command(dev, CommandVals.PwmFreq, [1]) - elif freq == "1.8kHz": - send_command(dev, CommandVals.PwmFreq, [2]) - elif freq == "900Hz": - send_command(dev, CommandVals.PwmFreq, [3]) - - -def pattern(dev, p): - """Display a pattern that's already programmed into the firmware""" - if p == "All LEDs on": - send_command(dev, CommandVals.Pattern, [PatternVals.FullBrightness]) - elif p == "Gradient (0-13% Brightness)": - send_command(dev, CommandVals.Pattern, [PatternVals.Gradient]) - elif p == "Double Gradient (0-7-0% Brightness)": - send_command(dev, CommandVals.Pattern, [PatternVals.DoubleGradient]) - elif p == '"LOTUS" sideways': - send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus]) - elif p == "Zigzag": - send_command(dev, CommandVals.Pattern, [PatternVals.ZigZag]) - elif p == '"PANIC"': - send_command(dev, CommandVals.Pattern, [PatternVals.DisplayPanic]) - elif p == '"LOTUS" Top Down': - send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus2]) - elif p == "All brightness levels (1 LED each)": - all_brightnesses(dev) - elif p == "Every Second Row": - every_nth_row(dev, 2) - elif p == "Every Third Row": - every_nth_row(dev, 3) - elif p == "Every Fourth Row": - every_nth_row(dev, 4) - elif p == "Every Fifth Row": - every_nth_row(dev, 5) - elif p == "Every Sixth Row": - every_nth_row(dev, 6) - elif p == "Every Second Col": - every_nth_col(dev, 2) - elif p == "Every Third Col": - every_nth_col(dev, 3) - elif p == "Every Fourth Col": - every_nth_col(dev, 4) - elif p == "Every Fifth Col": - every_nth_col(dev, 4) - elif p == "Checkerboard": - checkerboard(dev, 1) - elif p == "Double Checkerboard": - checkerboard(dev, 2) - elif p == "Triple Checkerboard": - checkerboard(dev, 3) - elif p == "Quad Checkerboard": - checkerboard(dev, 4) - else: - print("Invalid pattern") - - -def show_string(dev, s): - """Render a string with up to five letters""" - show_font(dev, [convert_font(letter) for letter in str(s)[:5]]) - - -def show_font(dev, font_items): - """Render up to five 5x6 pixel font items""" - vals = [0x00 for _ in range(39)] - - for digit_i, digit_pixels in enumerate(font_items): - offset = digit_i * 7 - for pixel_x in range(5): - for pixel_y in range(6): - pixel_value = digit_pixels[pixel_x + pixel_y * 5] - i = (2 + pixel_x) + (9 * (pixel_y + offset)) - if pixel_value: - vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) - - send_command(dev, CommandVals.Draw, vals) - - -def show_symbols(dev, symbols): - """Render a list of up to five symbols - Can use letters/numbers or symbol names, like 'sun', ':)'""" - font_items = [] - for symbol in symbols: - s = convert_symbol(symbol) - if not s: - s = convert_font(symbol) - font_items.append(s) - - show_font(dev, font_items) - - -def clock(dev): - """Render the current time and display. - Loops forever, updating every second""" - global STOP_THREAD - while True: - if STOP_THREAD or dev.device in DISCONNECTED_DEVS: - STOP_THREAD = False - return - now = datetime.now() - current_time = now.strftime("%H:%M") - print("Current Time =", current_time) - - show_string(dev, current_time) - time.sleep(1) - - -def send_command(dev, command, parameters=[], with_response=False): - return send_command_raw(dev, FWK_MAGIC + [command] + parameters, with_response) - - -def send_command_raw(dev, command, with_response=False): - """Send a command to the device. - Opens new serial connection every time""" - # print(f"Sending command: {command}") - try: - with serial.Serial(dev.device, 115200) as s: - s.write(command) - - if with_response: - res = s.read(RESPONSE_SIZE) - # print(f"Received: {res}") - return res - except (IOError, OSError) as _ex: - global DISCONNECTED_DEVS - DISCONNECTED_DEVS.append(dev.device) - # print("Error: ", ex) - - -def send_serial(dev, s, command): - """Send serial command by using existing serial connection""" - try: - s.write(command) - except (IOError, OSError) as _ex: - global DISCONNECTED_DEVS - DISCONNECTED_DEVS.append(dev.device) - # print("Error: ", ex) - - -def popup(gui, message): - if not gui: - return - import PySimpleGUI as sg - - sg.Popup(message, title="Framework Laptop 16 LED Matrix") - - -def gui(devices): - import PySimpleGUI as sg - - device_checkboxes = [] - for dev in devices: - version = get_version(dev) - device_info = ( - f"{dev.name}\nSerial No: {dev.serial_number}\nFW Version:{version}" - ) - checkbox = sg.Checkbox( - device_info, default=True, key=f"-CHECKBOX-{dev.name}-", enable_events=True - ) - device_checkboxes.append([checkbox]) - - layout = ( - [ - [sg.Text("Detected Devices")], - ] - + device_checkboxes - + [ - [sg.HorizontalSeparator()], - [sg.Text("Device Control")], - [sg.Button("Bootloader"), sg.Button("Sleep"), sg.Button("Wake")], - [sg.HorizontalSeparator()], - [sg.Text("Brightness")], - # TODO: Get default from device - [ - sg.Slider( - (0, 255), - orientation="h", - default_value=120, - k="-BRIGHTNESS-", - enable_events=True, - ) - ], - [sg.HorizontalSeparator()], - [sg.Text("Animation")], - [sg.Button("Start Animation"), sg.Button("Stop Animation")], - [sg.HorizontalSeparator()], - [sg.Text("Pattern")], - [sg.Combo(PATTERNS, k="-PATTERN-", enable_events=True)], - [sg.HorizontalSeparator()], - [sg.Text("Fill screen X% (could be volume indicator)")], - [ - sg.Slider( - (0, 100), orientation="h", k="-PERCENTAGE-", enable_events=True - ) - ], - [sg.HorizontalSeparator()], - [sg.Text("Countdown Timer")], - [ - sg.Spin([i for i in range(1, 60)], initial_value=10, k="-COUNTDOWN-"), - sg.Text("Seconds"), - sg.Button("Start", k="-START-COUNTDOWN-"), - sg.Button("Stop", k="-STOP-COUNTDOWN-"), - ], - [sg.HorizontalSeparator()], - [ - sg.Column( - [ - [sg.Text("Black&White Image")], - [sg.Button("Send stripe.gif", k="-SEND-BL-IMAGE-")], - ] - ), - sg.VSeperator(), - sg.Column( - [ - [sg.Text("Greyscale Image")], - [sg.Button("Send greyscale.gif", k="-SEND-GREY-IMAGE-")], - ] - ), - ], - [sg.HorizontalSeparator()], - [sg.Text("Display Current Time")], - [sg.Button("Start", k="-START-TIME-"), sg.Button("Stop", k="-STOP-TIME-")], - [sg.HorizontalSeparator()], - [ - sg.Column( - [ - [sg.Text("Custom Text")], - [ - sg.Input(k="-CUSTOM-TEXT-", s=7), - sg.Button("Show", k="SEND-CUSTOM-TEXT"), - ], - ] - ), - sg.VSeperator(), - sg.Column( - [ - [sg.Text("Display Text with Symbols")], - [sg.Button("Send '2 5 degC thunder'", k="-SEND-TEXT-")], - ] - ), - ], - [sg.HorizontalSeparator()], - [sg.Text("PWM Frequency")], - [sg.Combo(PWM_FREQUENCIES, k="-PWM-FREQ-", enable_events=True)], - # TODO - # [sg.Text("Play Snake")], - # [sg.Button("Start Game", k='-PLAY-SNAKE-')], - [sg.HorizontalSeparator()], - [sg.Text("Equalizer")], - [ - sg.Button("Start random equalizer", k="-RANDOM-EQ-"), - sg.Button("Stop", k="-STOP-EQ-"), - ], - # [sg.Button("Panic")] - ] - ) - - window = sg.Window("LED Matrix Control", layout, finalize=True) - selected_devices = [] - global STOP_THREAD - global DISCONNECTED_DEVS - - update_brightness_slider(window, devices) - - try: - while True: - event, values = window.read() - # print('Event', event) - # print('Values', values) - - # TODO - for dev in devices: - # print("Dev {} disconnected? {}".format(dev.name, dev.device in DISCONNECTED_DEVS)) - if dev.device in DISCONNECTED_DEVS: - window["-CHECKBOX-{}-".format(dev.name)].update( - False, disabled=True - ) - - selected_devices = [ - dev - for dev in devices - if values and values["-CHECKBOX-{}-".format(dev.name)] - ] - # print("Selected {} devices".format(len(selected_devices))) - - if event == sg.WIN_CLOSED: - break - if len(selected_devices) == 1: - dev = selected_devices[0] - if event == "-START-COUNTDOWN-": - thread = threading.Thread( - target=countdown, - args=( - dev, - int(values["-COUNTDOWN-"]), - ), - daemon=True, - ) - thread.start() - - if event == "-START-TIME-": - thread = threading.Thread(target=clock, args=(dev,), daemon=True) - thread.start() - - if event == "-PLAY-SNAKE-": - snake() - - if event == "-RANDOM-EQ-": - thread = threading.Thread( - target=random_eq, args=(dev,), daemon=True - ) - thread.start() - else: - if event in [ - "-START-COUNTDOWN-", - "-PLAY-SNAKE-", - "-RANDOM-EQ-", - "-START-TIME-", - ]: - sg.Popup("Select exactly 1 device for this action") - if event in ["-STOP-COUNTDOWN-", "-STOP-EQ-", "-STOP-TIME-"]: - STOP_THREAD = True - - for dev in selected_devices: - if event == "Bootloader": - bootloader(dev) - - if event == "-PATTERN-": - pattern(dev, values["-PATTERN-"]) - - if event == "-PWM-FREQ-": - pwm_freq(dev, values["-PWM-FREQ-"]) - - if event == "Start Animation": - animate(dev, True) - - if event == "Stop Animation": - animate(dev, False) - - if event == "-BRIGHTNESS-": - brightness(dev, int(values["-BRIGHTNESS-"])) - - if event == "-PERCENTAGE-": - percentage(dev, int(values["-PERCENTAGE-"])) - - if event == "-SEND-BL-IMAGE-": - path = os.path.join(resource_path(), "res", "stripe.gif") - image_bl(dev, path) - - if event == "-SEND-GREY-IMAGE-": - path = os.path.join(resource_path(), "res", "greyscale.gif") - image_greyscale(dev, path) - - if event == "-SEND-TEXT-": - show_symbols(dev, ["2", "5", "degC", " ", "thunder"]) - - if event == "SEND-CUSTOM-TEXT": - show_string(dev, values["-CUSTOM-TEXT-"].upper()) - - if event == "Sleep": - send_command(dev, CommandVals.Sleep, [True]) - - if event == "Wake": - send_command(dev, CommandVals.Sleep, [False]) - - window.close() - except Exception as e: - print(e) - raise e - pass - # sg.popup_error_with_traceback(f'An error happened. Here is the info:', e) - - -def display_string(dev, disp_str): - b = [ord(x) for x in disp_str] - send_command(dev, CommandVals.SetText, [len(disp_str)] + b) - - -def display_on_cmd(dev, on): - send_command(dev, CommandVals.DisplayOn, [on]) - - -def invert_screen_cmd(dev, invert): - send_command(dev, CommandVals.InvertScreen, [invert]) - - -def screen_saver_cmd(dev, on): - send_command(dev, CommandVals.ScreenSaver, [on]) - - -def set_fps_cmd(dev, mode): - res = send_command(dev, CommandVals.SetFps, with_response=True) - current_fps = res[0] - - if mode == "quarter": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b000 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "half": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b001 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "one": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b010 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "two": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b011 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "four": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b100 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "eight": - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b101 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("low") - elif mode == "sixteen": - fps = current_fps & ~HIGH_FPS_MASK - fps |= 0b00000000 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("high") - elif mode == "thirtytwo": - fps = current_fps & ~HIGH_FPS_MASK - fps |= 0b00010000 - send_command(dev, CommandVals.SetFps, [fps]) - set_power_mode_cmd("high") - - -def set_power_mode_cmd(dev, mode): - if mode == "low": - send_command(dev, CommandVals.SetPowerMode, [0]) - elif mode == "high": - send_command(dev, CommandVals.SetPowerMode, [1]) - else: - print("Unsupported power mode") - sys.exit(1) - - -def get_power_mode_cmd(dev): - res = send_command(dev, CommandVals.SetPowerMode, with_response=True) - current_mode = int(res[0]) - if current_mode == 0: - print("Current Power Mode: Low Power") - elif current_mode == 1: - print("Current Power Mode: High Power") - - -def get_fps_cmd(dev): - res = send_command(dev, CommandVals.SetFps, with_response=True) - current_fps = res[0] - res = send_command(dev, CommandVals.SetPowerMode, with_response=True) - current_mode = int(res[0]) - - if current_mode == 0: - current_fps &= LOW_FPS_MASK - if current_fps == 0: - fps = 0.25 - elif current_fps == 1: - fps = 0.5 - else: - fps = 2 ** (current_fps - 2) - elif current_mode == 1: - if current_fps & HIGH_FPS_MASK: - fps = 32 - else: - fps = 16 - - print(f"Current FPS: {fps}") - - - - -def convert_symbol(symbol): - """ 5x6 symbol font. Leaves 2 pixels on each side empty - We can leave one row empty below and then the display fits 5 of these digits.""" - symbols = { - 'degC': [ - 1, 1, 0, 0, 0, - 1, 1, 0, 0, 0, - 0, 0, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 1, 1, - ], - 'degF': [ - 1, 1, 0, 0, 0, - 1, 1, 0, 0, 0, - 0, 0, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 1, 1, - 0, 0, 1, 0, 0, - ], - 'snow': [ - 0, 0, 0, 0, 0, - 1, 0, 1, 0, 1, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 1, 0, 1, 0, 1, - ], - 'sun': [ - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - ], - 'cloud': [ - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'rain': [ - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 0, 0, 1, - 0, 0, 1, 0, 0, - 1, 0, 0, 1, 0, - ], - 'thunder': [ - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - 'batteryLow': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 0, - 1, 0, 0, 1, 1, - 1, 0, 0, 1, 1, - 1, 1, 1, 1, 0, - ], - '!!': [ - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - ], - 'heart': [ - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - 'heart0': [ - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'heart2': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - ], - ':)': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - ':|': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - ], - ':(': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - ], - ';)': [ - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - } - if symbol in symbols: - return symbols[symbol] - else: - return None - - -def convert_font(num): - """ 5x6 font. Leaves 2 pixels on each side empty - We can leave one row empty below and then the display fits 5 of these digits.""" - font = { - '0': [ - 0, 1, 1, 0, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 0, 1, 1, 0, 0, - ], - - '1': [ - 0, 0, 1, 0, 0, - 0, 1, 1, 0, 0, - 1, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - ], - - '2': [ - 1, 1, 1, 1, 0, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - - '3': [ - 1, 1, 1, 1, 0, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - - '4': [ - 0, 0, 0, 1, 0, - 0, 0, 1, 1, 0, - 0, 1, 0, 1, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 0, - 0, 0, 0, 1, 0, - ], - - '5': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - - '6': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - '7': [ - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - - '8': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - '9': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - ':': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - - ' ': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '?': [ - 0, 1, 1, 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - - '.': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - ',': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '!': [ - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - - '/': [ - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 1, - 0, 0, 1, 1, 0, - 0, 1, 1, 0, 0, - 1, 1, 0, 0, 0, - 1, 0, 0, 0, 0, - ], - - '*': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '%': [ - 1, 1, 0, 0, 1, - 1, 1, 0, 1, 1, - 0, 0, 1, 1, 0, - 0, 1, 1, 0, 0, - 1, 1, 0, 1, 1, - 1, 0, 0, 1, 1, - ], - - '+': [ - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - - '-': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '=': [ - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'A': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - ], - 'B': [ - 1, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - 'C': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - 'D': [ - 1, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - 'E': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - 'F': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - ], - 'G': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 0, - 1, 0, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - 'H': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - ], - 'I': [ - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - ], - 'J': [ - 0, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 1, 0, 0, 1, - 0, 0, 1, 1, 0, - ], - 'K': [ - 1, 0, 0, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 0, 0, 0, - 1, 0, 1, 0, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 0, 1, - ], - 'L': [ - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - 'M': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - ], - 'N': [ - 1, 0, 0, 0, 1, - 1, 1, 0, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 0, 1, 1, - ], - 'O': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - 'P': [ - 1, 1, 1, 0, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 1, 1, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - ], - 'Q': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 0, 1, 0, - 0, 1, 1, 0, 1, - ], - 'R': [ - 1, 1, 1, 1, 0, - 1, 0, 0, 1, 0, - 1, 1, 1, 1, 0, - 1, 1, 0, 0, 0, - 1, 0, 1, 0, 0, - 1, 0, 0, 1, 0, - ], - 'S': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - 'T': [ - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - 'U': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - ], - 'V': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 1, - 0, 1, 0, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - 'W': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - ], - 'X': [ - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 0, 0, 1, - ], - 'Y': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - 'Z': [ - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 1, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - 'Ä': [ - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - ], - 'Ö': [ - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - 'Ü': [ - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - ], - } - if num in font: - return font[num] - else: - return font['?'] - - if __name__ == "__main__": main() From 4af5289911b052eb30093b87efe7f14775dba670 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 18:01:41 +0800 Subject: [PATCH 05/16] python: Enable packaging via hatch ``` > cd python > python3 -m build > ls -1 dist framework16_inputmodule-0.1.0-py3-none-any.whl framework16_inputmodule-0.1.0.tar.gz mkdir temp && temp python3 -m venv venv source venv/bin/activate > python3 -m pip install ../dist/framework16_inputmodule-0.1.0.tar.gz > inputmodulectl > inputmodulegui ``` Signed-off-by: Daniel Schaefer --- .gitignore | 3 + python/README.md | 197 ++++++++++++++++++ .../{ledmatrix_control.py => cli.py} | 28 ++- .../framework16_inputmodules/gui/__init__.py | 10 +- python/framework16_inputmodules/gui/games.py | 14 +- .../framework16_inputmodules/gui/ledmatrix.py | 15 +- .../inputmodule/__init__.py | 2 +- .../inputmodule/b1display.py | 2 +- .../inputmodule/c1minimal.py | 2 +- .../inputmodule/ledmatrix.py | 4 +- python/pyproject.toml | 99 ++------- 11 files changed, 271 insertions(+), 105 deletions(-) create mode 100644 python/README.md rename python/framework16_inputmodules/{ledmatrix_control.py => cli.py} (94%) diff --git a/.gitignore b/.gitignore index fbde9a3d..579047c8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ message.bin # Python __pycache__ +# Hatch +_version.py + # pyinstaller build/ dist/ diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..8eb911a4 --- /dev/null +++ b/python/README.md @@ -0,0 +1,197 @@ +# Framework Laptop 16 - Input Module Firmware/Software + +This repository contains both the firmware for the Framework Laptop 16 input modules, +as well as the tool to control them. + +Rust firmware project setup based off of: https://github.com/rp-rs/rp2040-project-template + +## Modules + +See pages of the individual modules for details about how they work and how +they're controlled. + +- [LED Matrix](ledmatrix/README.md) +- [Minimal C1 Input Module](c1minimal/README.md) +- [2nd Display](b1display/README.md) +- [QT PY RP2040](qtpy/README.md) + +## Generic Features + +All modules are built with an RP2040 microcontroller +Features that all modules share + +- Firmware written in bare-metal Rust +- Reset into RP2040 bootloader when firmware crashes/panics +- Sleep Mode to save power +- API over USB ACM Serial Port - Requires no Drivers on Windows and Linux + - Go to sleep + - Reset into bootloader + - Control and read module state (brightness, displayed image, ...) + +## Control from the host + +To build your own application see the: [API command documentation](commands.md) + +Or use our `inputmodule-control` app, which you can download from the latest +[GH Actions](https://github.com/FrameworkComputer/led_matrix_fw/actions) run or +the [release page](https://github.com/FrameworkComputer/led_matrix_fw/releases). +Optionally there are is also a [Python script](python.md). + +For device specific commands, see their individual documentation pages. + +###### Permissions on Linux +To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: + +``` +sudo cp release/50-framework-inputmodule.rules /etc/udev/rules.d/ +sudo udevadm control --reload && sudo udevadm trigger +``` + +##### Common commands: + +###### Listing available devices + +```sh +> inputmodule-control --list +/dev/ttyACM0 + VID 0x32AC + PID 0x0020 + SN FRAKDEAM0020110001 + Product LED_Matrix +/dev/ttyACM1 + VID 0x32AC + PID 0x0021 + SN FRAKDEAM0000000000 + Product B1_Display +``` + +###### Apply command to single device + +By default a command will be sent to all devices that can be found, to apply it +to a single device, specify the COM port. +In this example the command is targeted at `b1-display`, so it will only apply +to this module type. + +``` +# Example on Linux +> inputmodule-control --serial-dev /dev/ttyACM0 b1-display --pattern black + +# Example on Windows +> inputmodule-control.exe --serial-dev COM5 b1-display --pattern black +``` + +###### Send command when device connects + +By default the app tries to connect with the device and aborts if it can't +connect. But you might want to start the app, have it wait until the device is +connected and then send the command. + +``` +> inputmodule-control b1-display --pattern black +Failed to find serial devivce. Please manually specify with --serial-dev + +# No failure, waits until the device is connected, sends command and exits +> inputmodule-control --wait-for-device b1-display --pattern black + +# If the device is already connected, it does nothing, just wait 1s. +# This means you can run this command by a system service and restart it when +# it finishes. Then it will only ever do anything if the device reconnects. +> inputmodule-control --wait-for-device b1-display --pattern black +Device already present. No need to wait. Not executing command. +``` + +## Update the Firmware + +First, put the module into bootloader mode. + +This can be done either by pressing the bootsel button while plugging it in or +by using one of the following commands: + +```sh +inputmodule-control led-matrix --bootloader +inputmodule-control b1-display --bootloader +inputmodule-control c1-minimal --bootloader +``` + +Then the module will present itself in the same way as a USB thumb drive. +Copy the UF2 firmware file onto it and the device will flash and reset automatically. +Alternatively when building from source, run one of the following commands: + +```sh +cargo run -p ledmatrix +cargo run -p b1display +cargo run -p c1minimal +``` + +## Building the firmware + +Dependencies: Rust + +Prepare Rust toolchain (once): + +```sh +rustup target install thumbv6m-none-eabi +cargo install flip-link +``` + +Build: + +```sh +cargo make --cwd ledmatrix +cargo make --cwd b1display +cargo make --cwd c1minimal +``` + +Generate the UF2 update file: + +```sh +cargo make --cwd ledmatrix uf2 +cargo make --cwd b1display uf2 +cargo make --cwd c1minimal uf2 +``` + +## Building the Application + +Dependencies: Rust, pkg-config, libudev + +Currently have to specify the build target because it's not possible to specify a per package build target. +Tracking issue: https://github.com/rust-lang/cargo/issues/9406 + +``` +# Build it +> cargo make --cwd inputmodule-control + +# Build and run it, showing the tool version +> cargo make --cwd inputmodule-control run -- --version +``` + +### Check the firmware version of the device + +###### In-band using commandline + +```sh +> inputmodule-control b1-display --version +Device Version: 0.1.3 +``` + +###### By looking at the USB descriptor + +On Linux: + +```sh +> lsusb -d 32ac: -v 2> /dev/null | grep -P 'ID 32ac|bcdDevice' +Bus 003 Device 078: ID 32ac:0021 Framework Laptop 16 B1 Display + bcdDevice 0.10 +``` + +## Rust Panic + +When the Rust code panics, the RP2040 resets itself into bootloader mode. +This means a new firmware can be written to overwrite the old one. + +Additionally the panic message is written to XIP RAM, which can be read with [picotool](https://github.com/raspberrypi/picotool): + +```sh +sudo picotool save -r 0x15000000 0x15004000 message.bin +strings message.bin | head +``` diff --git a/python/framework16_inputmodules/ledmatrix_control.py b/python/framework16_inputmodules/cli.py similarity index 94% rename from python/framework16_inputmodules/ledmatrix_control.py rename to python/framework16_inputmodules/cli.py index cb41c86b..16732a61 100755 --- a/python/framework16_inputmodules/ledmatrix_control.py +++ b/python/framework16_inputmodules/cli.py @@ -6,8 +6,8 @@ from serial.tools import list_ports # Local dependencies -import gui -from inputmodule import ( +from framework16_inputmodules import gui +from framework16_inputmodules.inputmodule import ( INPUTMODULE_PIDS, send_command, get_version, @@ -18,15 +18,15 @@ GameOfLifeStartParam, GameControlVal, ) -from gui.games import ( +from framework16_inputmodules.gui.games import ( snake, snake_embedded, pong_embedded, game_of_life_embedded, wpm_demo, ) -from gui.ledmatrix import random_eq, clock, blinking -from inputmodule.ledmatrix import ( +from framework16_inputmodules.gui.ledmatrix import random_eq, clock, blinking +from framework16_inputmodules.inputmodule.ledmatrix import ( eq, breathing, camera, @@ -44,7 +44,7 @@ image_bl, image_greyscale, ) -from inputmodule.b1display import ( +from framework16_inputmodules.inputmodule.b1display import ( b1image_bl, invert_screen_cmd, screen_saver_cmd, @@ -56,14 +56,18 @@ display_on_cmd, display_string, ) -from inputmodule.c1minimal import set_color, get_color, RGB_COLORS +from framework16_inputmodules.inputmodule.c1minimal import ( + set_color, + get_color, + RGB_COLORS, +) # Optional dependencies: # from PIL import Image # import PySimpleGUI as sg -def main(): +def main_cli(): parser = argparse.ArgumentParser() parser.add_argument( "-l", "--list", help="List all compatible devices", action="store_true" @@ -387,5 +391,11 @@ def print_devs(ports): print(f" Product: {port.product}") +def main_gui(): + devices = find_devs() # show=False, verbose=False) + print("Found {} devices".format(len(devices))) + gui.run_gui(devices) + + if __name__ == "__main__": - main() + main_cli() diff --git a/python/framework16_inputmodules/gui/__init__.py b/python/framework16_inputmodules/gui/__init__.py index 790cd611..44142cac 100644 --- a/python/framework16_inputmodules/gui/__init__.py +++ b/python/framework16_inputmodules/gui/__init__.py @@ -4,7 +4,7 @@ import PySimpleGUI as sg -from inputmodule import ( +from framework16_inputmodules.inputmodule import ( send_command, get_version, brightness, @@ -12,10 +12,10 @@ bootloader, CommandVals, ) -from gui.games import snake -from gui.ledmatrix import countdown, random_eq, clock -from gui.gui_threading import stop_thread, is_dev_disconnected -from inputmodule.ledmatrix import ( +from framework16_inputmodules.gui.games import snake +from framework16_inputmodules.gui.ledmatrix import countdown, random_eq, clock +from framework16_inputmodules.gui.gui_threading import stop_thread, is_dev_disconnected +from framework16_inputmodules.inputmodule.ledmatrix import ( percentage, pattern, animate, diff --git a/python/framework16_inputmodules/gui/games.py b/python/framework16_inputmodules/gui/games.py index f940a1e4..67e87fb4 100644 --- a/python/framework16_inputmodules/gui/games.py +++ b/python/framework16_inputmodules/gui/games.py @@ -4,8 +4,18 @@ import time import threading -from inputmodule import GameControlVal, send_command, CommandVals, Game -from inputmodule.ledmatrix import show_string, WIDTH, HEIGHT, render_matrix +from framework16_inputmodules.inputmodule import ( + GameControlVal, + send_command, + CommandVals, + Game, +) +from framework16_inputmodules.inputmodule.ledmatrix import ( + show_string, + WIDTH, + HEIGHT, + render_matrix, +) # Constants ARG_UP = 0 diff --git a/python/framework16_inputmodules/gui/ledmatrix.py b/python/framework16_inputmodules/gui/ledmatrix.py index 0c129c64..b0dbbba5 100644 --- a/python/framework16_inputmodules/gui/ledmatrix.py +++ b/python/framework16_inputmodules/gui/ledmatrix.py @@ -2,9 +2,18 @@ import time import random -from gui.gui_threading import reset_thread, is_thread_stopped, is_dev_disconnected -from inputmodule.ledmatrix import light_leds, show_string, eq, breathing -from inputmodule import brightness +from framework16_inputmodules.gui.gui_threading import ( + reset_thread, + is_thread_stopped, + is_dev_disconnected, +) +from framework16_inputmodules.inputmodule.ledmatrix import ( + light_leds, + show_string, + eq, + breathing, +) +from framework16_inputmodules.inputmodule import brightness def countdown(dev, seconds): diff --git a/python/framework16_inputmodules/inputmodule/__init__.py b/python/framework16_inputmodules/inputmodule/__init__.py index 9cfefea1..6357a8c1 100644 --- a/python/framework16_inputmodules/inputmodule/__init__.py +++ b/python/framework16_inputmodules/inputmodule/__init__.py @@ -2,7 +2,7 @@ import serial # TODO: Make independent from GUI -from gui.gui_threading import disconnect_dev +from framework16_inputmodules.gui.gui_threading import disconnect_dev FWK_MAGIC = [0x32, 0xAC] FWK_VID = 0x32AC diff --git a/python/framework16_inputmodules/inputmodule/b1display.py b/python/framework16_inputmodules/inputmodule/b1display.py index ff178475..4db439ae 100644 --- a/python/framework16_inputmodules/inputmodule/b1display.py +++ b/python/framework16_inputmodules/inputmodule/b1display.py @@ -1,6 +1,6 @@ import sys -from inputmodule import send_command, CommandVals, FWK_MAGIC +from framework16_inputmodules.inputmodule import send_command, CommandVals, FWK_MAGIC B1_WIDTH = 300 B1_HEIGHT = 400 diff --git a/python/framework16_inputmodules/inputmodule/c1minimal.py b/python/framework16_inputmodules/inputmodule/c1minimal.py index 5c7cd8a9..1f576ac2 100644 --- a/python/framework16_inputmodules/inputmodule/c1minimal.py +++ b/python/framework16_inputmodules/inputmodule/c1minimal.py @@ -1,4 +1,4 @@ -from inputmodule import send_command, CommandVals +from framework16_inputmodules.inputmodule import send_command, CommandVals RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] diff --git a/python/framework16_inputmodules/inputmodule/ledmatrix.py b/python/framework16_inputmodules/inputmodule/ledmatrix.py index 599cf68a..1bf339cf 100644 --- a/python/framework16_inputmodules/inputmodule/ledmatrix.py +++ b/python/framework16_inputmodules/inputmodule/ledmatrix.py @@ -2,8 +2,8 @@ import serial -import font -from inputmodule import ( +from framework16_inputmodules import font +from framework16_inputmodules.inputmodule import ( send_command, CommandVals, PatternVals, diff --git a/python/pyproject.toml b/python/pyproject.toml index 2366f3fa..e2fcc702 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,11 +1,14 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "framework16-inputmodule" -dynamic = ["version"] +# TODO: Dynamic version from git (requires tags) +#dynamic = ["version"] +version = "0.1.0" description = 'A library to control input modules on the Framework 16 Laptop' +# TODO: Custom README for python project readme = "README.md" requires-python = ">=3.7" license = { text = "MIT" } @@ -18,17 +21,15 @@ authors = [ classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3", ] dependencies = [ - "pyserial>=3.5", + "pyserial", + # Optional for GUI + "getkey", + "PySimpleGUI", + # Optional for image operations + "Pillow", ] [project.urls] @@ -36,14 +37,17 @@ Issues = "https://github.com/FrameworkComputer/inputmodule-rs/issues" Source = "https://github.com/FrameworkComputer/inputmodule-rs" # TODO: Figure out how to add a runnable-script -# [project.scripts] -# hatch-showcase = "hatch_showcase.cli:hatch_showcase" +[project.scripts] +ledmatrixctl = "framework16_inputmodules.cli:main_cli" + +[project.gui-scripts] +ledmatrixgui = "framework16_inputmodules.cli:main_gui" [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] -version-file = "src/hatch_showcase/_version.py" +version-file = "framework16_inputmodules/_version.py" [tool.hatch.build.targets.sdist] exclude = [ @@ -67,70 +71,3 @@ exclude = [ # show_column_numbers = true # warn_no_return = false # warn_unused_ignores = true - -# TODO: Code formatting -# [tool.black] -# target-version = ["py37"] -# line-length = 120 -# skip-string-normalization = true -# -# [tool.ruff] -# target-version = "py37" -# line-length = 120 -# select = [ -# "A", -# "B", -# "C", -# "DTZ", -# "E", -# "EM", -# "F", -# "FBT", -# "I", -# "ICN", -# "ISC", -# "N", -# "PLC", -# "PLE", -# "PLR", -# "PLW", -# "Q", -# "RUF", -# "S", -# "SIM", -# "T", -# "TID", -# "UP", -# "W", -# "YTT", -# ] -# ignore = [ -# # Allow non-abstract empty methods in abstract base classes -# "B027", -# # Allow boolean positional values in function calls, like `dict.get(... True)` -# "FBT003", -# # Ignore checks for possible passwords -# "S105", "S106", "S107", -# # Ignore complexity -# "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", -# "PLC1901", # empty string comparisons -# "PLW2901", # `for` loop variable overwritten -# "SIM114", # Combine `if` branches using logical `or` operator -# ] -# unfixable = [ -# # Don't touch unused imports -# "F401", -# ] -# -# [tool.ruff.isort] -# known-first-party = ["hatch_showcase"] -# -# [tool.ruff.flake8-quotes] -# inline-quotes = "single" -# -# [tool.ruff.flake8-tidy-imports] -# ban-relative-imports = "all" -# -# [tool.ruff.per-file-ignores] -# # Tests can use relative imports and assertions -# "tests/**/*" = ["TID252", "S101"] From d5e141eff21d9d46105acff0de3dd2a1445b7eaa Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 18:14:11 +0800 Subject: [PATCH 06/16] python: Rename to framework16_inputmodule Signed-off-by: Daniel Schaefer --- .../__init__.py | 0 .../cli.py | 14 +++++++------- .../font.py | 0 .../gui/__init__.py | 10 +++++----- .../gui/games.py | 4 ++-- .../gui/gui_threading.py | 0 .../gui/ledmatrix.py | 6 +++--- .../inputmodule/__init__.py | 2 +- .../inputmodule/b1display.py | 2 +- .../inputmodule/c1minimal.py | 2 +- .../inputmodule/ledmatrix.py | 4 ++-- python/pyproject.toml | 8 ++++---- 12 files changed, 26 insertions(+), 26 deletions(-) rename python/{framework16_inputmodules => framework16_inputmodule}/__init__.py (100%) rename python/{framework16_inputmodules => framework16_inputmodule}/cli.py (96%) rename python/{framework16_inputmodules => framework16_inputmodule}/font.py (100%) rename python/{framework16_inputmodules => framework16_inputmodule}/gui/__init__.py (96%) rename python/{framework16_inputmodules => framework16_inputmodule}/gui/games.py (97%) rename python/{framework16_inputmodules => framework16_inputmodule}/gui/gui_threading.py (100%) rename python/{framework16_inputmodules => framework16_inputmodule}/gui/ledmatrix.py (92%) rename python/{framework16_inputmodules => framework16_inputmodule}/inputmodule/__init__.py (97%) rename python/{framework16_inputmodules => framework16_inputmodule}/inputmodule/b1display.py (98%) rename python/{framework16_inputmodules => framework16_inputmodule}/inputmodule/c1minimal.py (92%) rename python/{framework16_inputmodules => framework16_inputmodule}/inputmodule/ledmatrix.py (99%) diff --git a/python/framework16_inputmodules/__init__.py b/python/framework16_inputmodule/__init__.py similarity index 100% rename from python/framework16_inputmodules/__init__.py rename to python/framework16_inputmodule/__init__.py diff --git a/python/framework16_inputmodules/cli.py b/python/framework16_inputmodule/cli.py similarity index 96% rename from python/framework16_inputmodules/cli.py rename to python/framework16_inputmodule/cli.py index 16732a61..f38c3d9f 100755 --- a/python/framework16_inputmodules/cli.py +++ b/python/framework16_inputmodule/cli.py @@ -6,8 +6,8 @@ from serial.tools import list_ports # Local dependencies -from framework16_inputmodules import gui -from framework16_inputmodules.inputmodule import ( +from framework16_inputmodule import gui +from framework16_inputmodule.inputmodule import ( INPUTMODULE_PIDS, send_command, get_version, @@ -18,15 +18,15 @@ GameOfLifeStartParam, GameControlVal, ) -from framework16_inputmodules.gui.games import ( +from framework16_inputmodule.gui.games import ( snake, snake_embedded, pong_embedded, game_of_life_embedded, wpm_demo, ) -from framework16_inputmodules.gui.ledmatrix import random_eq, clock, blinking -from framework16_inputmodules.inputmodule.ledmatrix import ( +from framework16_inputmodule.gui.ledmatrix import random_eq, clock, blinking +from framework16_inputmodule.inputmodule.ledmatrix import ( eq, breathing, camera, @@ -44,7 +44,7 @@ image_bl, image_greyscale, ) -from framework16_inputmodules.inputmodule.b1display import ( +from framework16_inputmodule.inputmodule.b1display import ( b1image_bl, invert_screen_cmd, screen_saver_cmd, @@ -56,7 +56,7 @@ display_on_cmd, display_string, ) -from framework16_inputmodules.inputmodule.c1minimal import ( +from framework16_inputmodule.inputmodule.c1minimal import ( set_color, get_color, RGB_COLORS, diff --git a/python/framework16_inputmodules/font.py b/python/framework16_inputmodule/font.py similarity index 100% rename from python/framework16_inputmodules/font.py rename to python/framework16_inputmodule/font.py diff --git a/python/framework16_inputmodules/gui/__init__.py b/python/framework16_inputmodule/gui/__init__.py similarity index 96% rename from python/framework16_inputmodules/gui/__init__.py rename to python/framework16_inputmodule/gui/__init__.py index 44142cac..9a702ff6 100644 --- a/python/framework16_inputmodules/gui/__init__.py +++ b/python/framework16_inputmodule/gui/__init__.py @@ -4,7 +4,7 @@ import PySimpleGUI as sg -from framework16_inputmodules.inputmodule import ( +from framework16_inputmodule.inputmodule import ( send_command, get_version, brightness, @@ -12,10 +12,10 @@ bootloader, CommandVals, ) -from framework16_inputmodules.gui.games import snake -from framework16_inputmodules.gui.ledmatrix import countdown, random_eq, clock -from framework16_inputmodules.gui.gui_threading import stop_thread, is_dev_disconnected -from framework16_inputmodules.inputmodule.ledmatrix import ( +from framework16_inputmodule.gui.games import snake +from framework16_inputmodule.gui.ledmatrix import countdown, random_eq, clock +from framework16_inputmodule.gui.gui_threading import stop_thread, is_dev_disconnected +from framework16_inputmodule.inputmodule.ledmatrix import ( percentage, pattern, animate, diff --git a/python/framework16_inputmodules/gui/games.py b/python/framework16_inputmodule/gui/games.py similarity index 97% rename from python/framework16_inputmodules/gui/games.py rename to python/framework16_inputmodule/gui/games.py index 67e87fb4..01ead75b 100644 --- a/python/framework16_inputmodules/gui/games.py +++ b/python/framework16_inputmodule/gui/games.py @@ -4,13 +4,13 @@ import time import threading -from framework16_inputmodules.inputmodule import ( +from framework16_inputmodule.inputmodule import ( GameControlVal, send_command, CommandVals, Game, ) -from framework16_inputmodules.inputmodule.ledmatrix import ( +from framework16_inputmodule.inputmodule.ledmatrix import ( show_string, WIDTH, HEIGHT, diff --git a/python/framework16_inputmodules/gui/gui_threading.py b/python/framework16_inputmodule/gui/gui_threading.py similarity index 100% rename from python/framework16_inputmodules/gui/gui_threading.py rename to python/framework16_inputmodule/gui/gui_threading.py diff --git a/python/framework16_inputmodules/gui/ledmatrix.py b/python/framework16_inputmodule/gui/ledmatrix.py similarity index 92% rename from python/framework16_inputmodules/gui/ledmatrix.py rename to python/framework16_inputmodule/gui/ledmatrix.py index b0dbbba5..4413e158 100644 --- a/python/framework16_inputmodules/gui/ledmatrix.py +++ b/python/framework16_inputmodule/gui/ledmatrix.py @@ -2,18 +2,18 @@ import time import random -from framework16_inputmodules.gui.gui_threading import ( +from framework16_inputmodule.gui.gui_threading import ( reset_thread, is_thread_stopped, is_dev_disconnected, ) -from framework16_inputmodules.inputmodule.ledmatrix import ( +from framework16_inputmodule.inputmodule.ledmatrix import ( light_leds, show_string, eq, breathing, ) -from framework16_inputmodules.inputmodule import brightness +from framework16_inputmodule.inputmodule import brightness def countdown(dev, seconds): diff --git a/python/framework16_inputmodules/inputmodule/__init__.py b/python/framework16_inputmodule/inputmodule/__init__.py similarity index 97% rename from python/framework16_inputmodules/inputmodule/__init__.py rename to python/framework16_inputmodule/inputmodule/__init__.py index 6357a8c1..8e00d189 100644 --- a/python/framework16_inputmodules/inputmodule/__init__.py +++ b/python/framework16_inputmodule/inputmodule/__init__.py @@ -2,7 +2,7 @@ import serial # TODO: Make independent from GUI -from framework16_inputmodules.gui.gui_threading import disconnect_dev +from framework16_inputmodule.gui.gui_threading import disconnect_dev FWK_MAGIC = [0x32, 0xAC] FWK_VID = 0x32AC diff --git a/python/framework16_inputmodules/inputmodule/b1display.py b/python/framework16_inputmodule/inputmodule/b1display.py similarity index 98% rename from python/framework16_inputmodules/inputmodule/b1display.py rename to python/framework16_inputmodule/inputmodule/b1display.py index 4db439ae..4c8d7963 100644 --- a/python/framework16_inputmodules/inputmodule/b1display.py +++ b/python/framework16_inputmodule/inputmodule/b1display.py @@ -1,6 +1,6 @@ import sys -from framework16_inputmodules.inputmodule import send_command, CommandVals, FWK_MAGIC +from framework16_inputmodule.inputmodule import send_command, CommandVals, FWK_MAGIC B1_WIDTH = 300 B1_HEIGHT = 400 diff --git a/python/framework16_inputmodules/inputmodule/c1minimal.py b/python/framework16_inputmodule/inputmodule/c1minimal.py similarity index 92% rename from python/framework16_inputmodules/inputmodule/c1minimal.py rename to python/framework16_inputmodule/inputmodule/c1minimal.py index 1f576ac2..818578b1 100644 --- a/python/framework16_inputmodules/inputmodule/c1minimal.py +++ b/python/framework16_inputmodule/inputmodule/c1minimal.py @@ -1,4 +1,4 @@ -from framework16_inputmodules.inputmodule import send_command, CommandVals +from framework16_inputmodule.inputmodule import send_command, CommandVals RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] diff --git a/python/framework16_inputmodules/inputmodule/ledmatrix.py b/python/framework16_inputmodule/inputmodule/ledmatrix.py similarity index 99% rename from python/framework16_inputmodules/inputmodule/ledmatrix.py rename to python/framework16_inputmodule/inputmodule/ledmatrix.py index 1bf339cf..1fef7270 100644 --- a/python/framework16_inputmodules/inputmodule/ledmatrix.py +++ b/python/framework16_inputmodule/inputmodule/ledmatrix.py @@ -2,8 +2,8 @@ import serial -from framework16_inputmodules import font -from framework16_inputmodules.inputmodule import ( +from framework16_inputmodule import font +from framework16_inputmodule.inputmodule import ( send_command, CommandVals, PatternVals, diff --git a/python/pyproject.toml b/python/pyproject.toml index e2fcc702..36ebe9ab 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] -name = "framework16-inputmodule" +name = "framework16_inputmodule" # TODO: Dynamic version from git (requires tags) #dynamic = ["version"] version = "0.1.0" @@ -38,16 +38,16 @@ Source = "https://github.com/FrameworkComputer/inputmodule-rs" # TODO: Figure out how to add a runnable-script [project.scripts] -ledmatrixctl = "framework16_inputmodules.cli:main_cli" +ledmatrixctl = "framework16_inputmodule.cli:main_cli" [project.gui-scripts] -ledmatrixgui = "framework16_inputmodules.cli:main_gui" +ledmatrixgui = "framework16_inputmodule.cli:main_gui" [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] -version-file = "framework16_inputmodules/_version.py" +version-file = "framework16_inputmodule/_version.py" [tool.hatch.build.targets.sdist] exclude = [ From 1bccc83b5b30dded75f39a643bfbff70c4194d7b Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 13 Nov 2023 18:20:06 +0800 Subject: [PATCH 07/16] python: Use hardcoded version for now Signed-off-by: Daniel Schaefer --- python/pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 36ebe9ab..79ce32e4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -43,11 +43,11 @@ ledmatrixctl = "framework16_inputmodule.cli:main_cli" [project.gui-scripts] ledmatrixgui = "framework16_inputmodule.cli:main_gui" -[tool.hatch.version] -source = "vcs" - -[tool.hatch.build.hooks.vcs] -version-file = "framework16_inputmodule/_version.py" +#[tool.hatch.version] +#source = "vcs" +# +#[tool.hatch.build.hooks.vcs] +#version-file = "framework16_inputmodule/_version.py" [tool.hatch.build.targets.sdist] exclude = [ From 2c0647018c498b6d765d0976cf32a176ddf89221 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 14 Nov 2023 16:05:15 +0800 Subject: [PATCH 08/16] python: Improvements for multiple ledmatrices - Can filter by /dev/tty... not just tty... - Better error message when not found Signed-off-by: Daniel Schaefer --- python/framework16_inputmodule/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/framework16_inputmodule/cli.py b/python/framework16_inputmodule/cli.py index f38c3d9f..059d0b36 100755 --- a/python/framework16_inputmodule/cli.py +++ b/python/framework16_inputmodule/cli.py @@ -236,7 +236,11 @@ def main_cli(): gui.popup(args.gui, "No device found") sys.exit(1) elif args.serial_dev is not None: - dev = [port for port in ports if port.name == args.serial_dev][0] + filtered_devs = [port for port in ports if port.name in args.serial_dev] + if not filtered_devs: + print("Failed to find requested device") + sys.exit(1) + dev = filtered_devs[0] elif len(ports) == 1: dev = ports[0] elif len(ports) >= 1 and not args.gui: From 2325ec3226f6a5994834d90af807eb013d10c55b Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 14 Nov 2023 16:05:55 +0800 Subject: [PATCH 09/16] python: Don't print brightness on GUI startup Signed-off-by: Daniel Schaefer --- python/framework16_inputmodule/gui/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/framework16_inputmodule/gui/__init__.py b/python/framework16_inputmodule/gui/__init__.py index 9a702ff6..8e32ecb0 100644 --- a/python/framework16_inputmodule/gui/__init__.py +++ b/python/framework16_inputmodule/gui/__init__.py @@ -37,7 +37,6 @@ def update_brightness_slider(window, devices): br = get_brightness(dev) average_brightness += br - print(f"Brightness: {br}") if average_brightness: window["-BRIGHTNESS-"].update(average_brightness / len(devices)) From 69f09cc810d9c3955b7da81ff717782c75327308 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 14 Nov 2023 16:18:54 +0800 Subject: [PATCH 10/16] python: Update README Signed-off-by: Daniel Schaefer --- python.md | 58 ------------ python/README.md | 232 +++++++++++++++++++++-------------------------- 2 files changed, 101 insertions(+), 189 deletions(-) delete mode 100644 python.md diff --git a/python.md b/python.md deleted file mode 100644 index 749fe28b..00000000 --- a/python.md +++ /dev/null @@ -1,58 +0,0 @@ -# Python script to control Framework Laptop 16 Input Modules - -Requirements: Python, pyserial, [PySimpleGUI](https://www.pysimplegui.org) and optionally [pillow](https://pillow.readthedocs.io/en/stable/index.html) -For convenience install dependencies via: `python3 -m pip install -r requirements.txt`. -Use `ledmatrix_control.py`. Either the commandline, see `ledmatrix_control.py --help` or the graphical version: `ledmatrix_control.py --gui` - -``` -options: - -h, --help show this help message and exit - --bootloader Jump to the bootloader to flash new firmware - --sleep, --no-sleep Simulate the host going to sleep or waking up - --brightness BRIGHTNESS - Adjust the brightness. Value 0-255 - --animate, --no-animate - Start/stop vertical scrolling - --pattern {full,lotus,gradient,double-gradient,zigzag,panic,lotus2} - Display a pattern - --image IMAGE Display a PNG or GIF image in black and white only) - --image-grey IMAGE_GREY - Display a PNG or GIF image in greyscale - --percentage PERCENTAGE - Fill a percentage of the screen - --clock Display the current time - --string STRING Display a string or number, like FPS - --symbols SYMBOLS [SYMBOLS ...] - Show symbols (degF, degC, :), snow, cloud, ...) - --gui Launch the graphical version of the program - --blink Blink the current pattern - --breathing Breathing of the current pattern - --eq EQ [EQ ...] Equalizer - --random-eq Random Equalizer - --wpm WPM Demo - --snake Snake - --all-brightnesses Show every pixel in a different brightness - --set-color {white,black,red,green,blue,cyan,yellow,purple} - Set RGB color (C1 Minimal Input Module) - --get-color Get RGB color (C1 Minimal Input Module) - -v, --version Get device version - --serial-dev SERIAL_DEV - Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows -``` - -Examples - -```sh -# Launch graphical application -./ledmatrix_control.py --gui - -# Show current time and keep updating it -./ledmatrix_control.py --clock - -# Draw PNG or GIF -./ledmatrix_control.py --image stripe.gif -./ledmatrix_control.py --image stripe.png - -# Change brightness (0-255) -./ledmatrix_control.py --brightness 50 -``` diff --git a/python/README.md b/python/README.md index 8eb911a4..8c4725fc 100644 --- a/python/README.md +++ b/python/README.md @@ -1,43 +1,19 @@ -# Framework Laptop 16 - Input Module Firmware/Software +# Framework Laptop 16 - Input Module Software -This repository contains both the firmware for the Framework Laptop 16 input modules, -as well as the tool to control them. +This repository contains a python library and scripts to control the +(non-keyboard) input modules, which is currently just the LED Matrix. -Rust firmware project setup based off of: https://github.com/rp-rs/rp2040-project-template +## Installing -## Modules +Pre-requisites: Python with pip -See pages of the individual modules for details about how they work and how -they're controlled. - -- [LED Matrix](ledmatrix/README.md) -- [Minimal C1 Input Module](c1minimal/README.md) -- [2nd Display](b1display/README.md) -- [QT PY RP2040](qtpy/README.md) - -## Generic Features - -All modules are built with an RP2040 microcontroller -Features that all modules share - -- Firmware written in bare-metal Rust -- Reset into RP2040 bootloader when firmware crashes/panics -- Sleep Mode to save power -- API over USB ACM Serial Port - Requires no Drivers on Windows and Linux - - Go to sleep - - Reset into bootloader - - Control and read module state (brightness, displayed image, ...) +```sh +python3 -m pip install framework16_inputmodule +``` ## Control from the host -To build your own application see the: [API command documentation](commands.md) - -Or use our `inputmodule-control` app, which you can download from the latest -[GH Actions](https://github.com/FrameworkComputer/led_matrix_fw/actions) run or -the [release page](https://github.com/FrameworkComputer/led_matrix_fw/releases). -Optionally there are is also a [Python script](python.md). - -For device specific commands, see their individual documentation pages. +To build your own application see the: [API command documentation](https://github.com/FrameworkComputer/inputmodule-rs/tree/main/commands.md) ###### Permissions on Linux To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: @@ -52,126 +28,131 @@ sudo udevadm control --reload && sudo udevadm trigger ###### Listing available devices ```sh -> inputmodule-control --list -/dev/ttyACM0 - VID 0x32AC - PID 0x0020 - SN FRAKDEAM0020110001 - Product LED_Matrix +> ledmatrixctl +More than 1 compatible device found. Please choose with --serial-dev ... +Example on Windows: --serial-dev COM3 +Example on Linux: --serial-dev /dev/ttyACM0 /dev/ttyACM1 - VID 0x32AC - PID 0x0021 - SN FRAKDEAM0000000000 - Product B1_Display + VID: 0x32AC + PID: 0x0020 + SN: FRAKDEBZ0100000000 + Product: LED Matrix Input Module +/dev/ttyACM0 + VID: 0x32AC + PID: 0x0020 + SN: FRAKDEBZ0100000000 + Product: LED Matrix Input Module ``` ###### Apply command to single device -By default a command will be sent to all devices that can be found, to apply it -to a single device, specify the COM port. -In this example the command is targeted at `b1-display`, so it will only apply -to this module type. +When there are multiple devices you need to select which one to control. ``` # Example on Linux -> inputmodule-control --serial-dev /dev/ttyACM0 b1-display --pattern black +> ledmatrixctl --serial-dev /dev/ttyACM0 --percentage 33 # Example on Windows -> inputmodule-control.exe --serial-dev COM5 b1-display --pattern black +> ledmatrixctl --serial-dev COM5 --percentage 33 ``` -###### Send command when device connects +### Graphical Application + +Launch the graphical application -By default the app tries to connect with the device and aborts if it can't -connect. But you might want to start the app, have it wait until the device is -connected and then send the command. +```sh +# Either via the commandline +ledmatrixctl --gui +# Or using the standanlone application +ledmatrixgui ``` -> inputmodule-control b1-display --pattern black -Failed to find serial devivce. Please manually specify with --serial-dev -# No failure, waits until the device is connected, sends command and exits -> inputmodule-control --wait-for-device b1-display --pattern black +### Other example commands + +```sh -# If the device is already connected, it does nothing, just wait 1s. -# This means you can run this command by a system service and restart it when -# it finishes. Then it will only ever do anything if the device reconnects. -> inputmodule-control --wait-for-device b1-display --pattern black -Device already present. No need to wait. Not executing command. +# Show current time and keep updating it +ledmatrixctl --clock + +# Draw PNG or GIF +ledmatrixctl --image stripe.gif +ledmatrixctl --image stripe.png + +# Change brightness (0-255) +ledmatrixctl --brightness 50 +``` + +### All commandline options + +``` +> ledmatrixctl --help +options: + -h, --help show this help message and exit + -l, --list List all compatible devices + --bootloader Jump to the bootloader to flash new firmware + --sleep, --no-sleep Simulate the host going to sleep or waking up + --is-sleeping Check current sleep state + --brightness BRIGHTNESS + Adjust the brightness. Value 0-255 + --get-brightness Get current brightness + --animate, --no-animate + Start/stop vertical scrolling + --get-animate Check if currently animating + --pwm {29000,3600,1800,900} + Adjust the PWM frequency. Value 0-255 + --get-pwm Get current PWM Frequency + --pattern {...} Display a pattern + --image IMAGE Display a PNG or GIF image in black and white only) + --image-grey IMAGE_GREY + Display a PNG or GIF image in greyscale + --camera Stream from the webcam + --video VIDEO Play a video + --percentage PERCENTAGE + Fill a percentage of the screen + --clock Display the current time + --string STRING Display a string or number, like FPS + --symbols SYMBOLS [SYMBOLS ...] + Show symbols (degF, degC, :), snow, cloud, ...) + --gui Launch the graphical version of the program + --panic Crash the firmware (TESTING ONLY) + --blink Blink the current pattern + --breathing Breathing of the current pattern + --eq EQ [EQ ...] Equalizer + --random-eq Random Equalizer + --wpm WPM Demo + --snake Snake + --snake-embedded Snake on the module + --pong-embedded Pong on the module + --game-of-life-embedded {currentmatrix,pattern1,blinker,toad,beacon,glider} + Game of Life + --quit-embedded-game Quit the current game + --all-brightnesses Show every pixel in a different brightness + -v, --version Get device version + --serial-dev SERIAL_DEV + Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows ``` ## Update the Firmware First, put the module into bootloader mode. -This can be done either by pressing the bootsel button while plugging it in or +This can be done either by flipping DIP switch #2 or by using one of the following commands: ```sh -inputmodule-control led-matrix --bootloader -inputmodule-control b1-display --bootloader -inputmodule-control c1-minimal --bootloader +> ledmatrixctl --bootloader ``` Then the module will present itself in the same way as a USB thumb drive. Copy the UF2 firmware file onto it and the device will flash and reset automatically. -Alternatively when building from source, run one of the following commands: - -```sh -cargo run -p ledmatrix -cargo run -p b1display -cargo run -p c1minimal -``` - -## Building the firmware - -Dependencies: Rust - -Prepare Rust toolchain (once): - -```sh -rustup target install thumbv6m-none-eabi -cargo install flip-link -``` - -Build: - -```sh -cargo make --cwd ledmatrix -cargo make --cwd b1display -cargo make --cwd c1minimal -``` - -Generate the UF2 update file: - -```sh -cargo make --cwd ledmatrix uf2 -cargo make --cwd b1display uf2 -cargo make --cwd c1minimal uf2 -``` - -## Building the Application - -Dependencies: Rust, pkg-config, libudev - -Currently have to specify the build target because it's not possible to specify a per package build target. -Tracking issue: https://github.com/rust-lang/cargo/issues/9406 - -``` -# Build it -> cargo make --cwd inputmodule-control - -# Build and run it, showing the tool version -> cargo make --cwd inputmodule-control run -- --version ``` ### Check the firmware version of the device -###### In-band using commandline - ```sh -> inputmodule-control b1-display --version -Device Version: 0.1.3 +> ledmatrixctl --version +Device Version: 0.1.7 ``` ###### By looking at the USB descriptor @@ -180,18 +161,7 @@ On Linux: ```sh > lsusb -d 32ac: -v 2> /dev/null | grep -P 'ID 32ac|bcdDevice' -Bus 003 Device 078: ID 32ac:0021 Framework Laptop 16 B1 Display - bcdDevice 0.10 +Bus 003 Device 078: ID 32ac:0020 Framework Computer Inc LED Matrix Input Module + bcdDevice 0.17 ``` -## Rust Panic - -When the Rust code panics, the RP2040 resets itself into bootloader mode. -This means a new firmware can be written to overwrite the old one. - -Additionally the panic message is written to XIP RAM, which can be read with [picotool](https://github.com/raspberrypi/picotool): - -```sh -sudo picotool save -r 0x15000000 0x15004000 message.bin -strings message.bin | head -``` From d4c60edc1bc8f251f61bcf68e24121dd329273f7 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 14 Nov 2023 16:28:55 +0800 Subject: [PATCH 11/16] python: Bump to v0.1.1 Signed-off-by: Daniel Schaefer --- python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 79ce32e4..9750d39d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "framework16_inputmodule" # TODO: Dynamic version from git (requires tags) #dynamic = ["version"] -version = "0.1.0" +version = "0.1.1" description = 'A library to control input modules on the Framework 16 Laptop' # TODO: Custom README for python project readme = "README.md" From 7deffa0412a2953a7d990040b55029c64018941a Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 14 Nov 2023 16:34:49 +0800 Subject: [PATCH 12/16] python: Update for GH Actions Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index 1df8f6e4..f8616bf1 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -102,7 +102,6 @@ jobs: build-gui: name: Build GUI runs-on: windows-latest -# runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -110,11 +109,24 @@ jobs: uses: Martin005/pyinstaller-action@main with: python_ver: '3.11' - spec: ledmatrix_control.py #'src/build.spec' + spec: python/framework16_inputmodule/cli.py #'src/build.spec' requirements: 'requirements.txt' - upload_exe_with_name: 'ledmatrix_control.py' + upload_exe_with_name: 'ledmatrixgui' options: --onefile, --windowed, --add-data 'res;res' + package-python: + name: Package Python + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - run: | + cd python + python3 -m pip install --upgrade build + python3 -m pip install --upgrade hatch + python3 -m pip install --upgrade twine + python3 -m build + lints: name: Lints runs-on: ubuntu-22.04 From 61e45b145a2d0479affef4e2e88f16961af9585f Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 27 Nov 2023 21:07:28 +0800 Subject: [PATCH 13/16] python: Rename module to just `inputmodule` Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 2 +- python/README.md | 2 +- .../__init__.py | 0 .../cli.py | 29 +++++++++++-------- .../font.py | 0 .../gui/__init__.py | 25 +++++++++------- .../gui/games.py | 7 +++-- .../gui/gui_threading.py | 0 .../gui/ledmatrix.py | 6 ++-- .../inputmodule/__init__.py | 2 +- .../inputmodule/b1display.py | 5 ++-- .../inputmodule/c1minimal.py | 5 ++-- .../inputmodule/ledmatrix.py | 4 +-- python/pyproject.toml | 8 ++--- 14 files changed, 54 insertions(+), 41 deletions(-) rename python/{framework16_inputmodule => inputmodule}/__init__.py (100%) rename python/{framework16_inputmodule => inputmodule}/cli.py (93%) rename python/{framework16_inputmodule => inputmodule}/font.py (100%) rename python/{framework16_inputmodule => inputmodule}/gui/__init__.py (91%) rename python/{framework16_inputmodule => inputmodule}/gui/games.py (96%) rename python/{framework16_inputmodule => inputmodule}/gui/gui_threading.py (100%) rename python/{framework16_inputmodule => inputmodule}/gui/ledmatrix.py (92%) rename python/{framework16_inputmodule => inputmodule}/inputmodule/__init__.py (97%) rename python/{framework16_inputmodule => inputmodule}/inputmodule/b1display.py (96%) rename python/{framework16_inputmodule => inputmodule}/inputmodule/c1minimal.py (83%) rename python/{framework16_inputmodule => inputmodule}/inputmodule/ledmatrix.py (99%) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index f8616bf1..448832fe 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -109,7 +109,7 @@ jobs: uses: Martin005/pyinstaller-action@main with: python_ver: '3.11' - spec: python/framework16_inputmodule/cli.py #'src/build.spec' + spec: python/inputmodule/cli.py #'src/build.spec' requirements: 'requirements.txt' upload_exe_with_name: 'ledmatrixgui' options: --onefile, --windowed, --add-data 'res;res' diff --git a/python/README.md b/python/README.md index 8c4725fc..80b3fd2d 100644 --- a/python/README.md +++ b/python/README.md @@ -8,7 +8,7 @@ This repository contains a python library and scripts to control the Pre-requisites: Python with pip ```sh -python3 -m pip install framework16_inputmodule +python3 -m pip install inputmodule ``` ## Control from the host diff --git a/python/framework16_inputmodule/__init__.py b/python/inputmodule/__init__.py similarity index 100% rename from python/framework16_inputmodule/__init__.py rename to python/inputmodule/__init__.py diff --git a/python/framework16_inputmodule/cli.py b/python/inputmodule/cli.py similarity index 93% rename from python/framework16_inputmodule/cli.py rename to python/inputmodule/cli.py index 059d0b36..5f97615d 100755 --- a/python/framework16_inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -6,8 +6,8 @@ from serial.tools import list_ports # Local dependencies -from framework16_inputmodule import gui -from framework16_inputmodule.inputmodule import ( +from inputmodule import gui +from inputmodule.inputmodule import ( INPUTMODULE_PIDS, send_command, get_version, @@ -18,15 +18,15 @@ GameOfLifeStartParam, GameControlVal, ) -from framework16_inputmodule.gui.games import ( +from inputmodule.gui.games import ( snake, snake_embedded, pong_embedded, game_of_life_embedded, wpm_demo, ) -from framework16_inputmodule.gui.ledmatrix import random_eq, clock, blinking -from framework16_inputmodule.inputmodule.ledmatrix import ( +from inputmodule.gui.ledmatrix import random_eq, clock, blinking +from inputmodule.inputmodule.ledmatrix import ( eq, breathing, camera, @@ -44,7 +44,7 @@ image_bl, image_greyscale, ) -from framework16_inputmodule.inputmodule.b1display import ( +from inputmodule.inputmodule.b1display import ( b1image_bl, invert_screen_cmd, screen_saver_cmd, @@ -56,7 +56,7 @@ display_on_cmd, display_string, ) -from framework16_inputmodule.inputmodule.c1minimal import ( +from inputmodule.inputmodule.c1minimal import ( set_color, get_color, RGB_COLORS, @@ -121,12 +121,14 @@ def main_cli(): help="Display a PNG or GIF image in greyscale", type=argparse.FileType("rb"), ) - parser.add_argument("--camera", help="Stream from the webcam", action="store_true") + parser.add_argument( + "--camera", help="Stream from the webcam", action="store_true") parser.add_argument("--video", help="Play a video", type=str) parser.add_argument( "--percentage", help="Fill a percentage of the screen", type=int ) - parser.add_argument("--clock", help="Display the current time", action="store_true") + parser.add_argument( + "--clock", help="Display the current time", action="store_true") parser.add_argument( "--string", help="Display a string or number, like FPS", type=str ) @@ -146,7 +148,8 @@ def main_cli(): "--breathing", help="Breathing of the current pattern", action="store_true" ) parser.add_argument("--eq", help="Equalizer", nargs="+", type=int) - parser.add_argument("--random-eq", help="Random Equalizer", action="store_true") + parser.add_argument( + "--random-eq", help="Random Equalizer", action="store_true") parser.add_argument("--wpm", help="WPM Demo", action="store_true") parser.add_argument("--snake", help="Snake", action="store_true") parser.add_argument( @@ -207,7 +210,8 @@ def main_cli(): parser.add_argument( "--set-power-mode", help="Set screen power mode", choices=["high", "low"] ) - parser.add_argument("--get-fps", help="Set screen FPS", action="store_true") + parser.add_argument("--get-fps", help="Set screen FPS", + action="store_true") parser.add_argument( "--get-power-mode", help="Set screen power mode", action="store_true" ) @@ -236,7 +240,8 @@ def main_cli(): gui.popup(args.gui, "No device found") sys.exit(1) elif args.serial_dev is not None: - filtered_devs = [port for port in ports if port.name in args.serial_dev] + filtered_devs = [ + port for port in ports if port.name in args.serial_dev] if not filtered_devs: print("Failed to find requested device") sys.exit(1) diff --git a/python/framework16_inputmodule/font.py b/python/inputmodule/font.py similarity index 100% rename from python/framework16_inputmodule/font.py rename to python/inputmodule/font.py diff --git a/python/framework16_inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py similarity index 91% rename from python/framework16_inputmodule/gui/__init__.py rename to python/inputmodule/gui/__init__.py index 8e32ecb0..ba0f1e1a 100644 --- a/python/framework16_inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -4,7 +4,7 @@ import PySimpleGUI as sg -from framework16_inputmodule.inputmodule import ( +from inputmodule.inputmodule import ( send_command, get_version, brightness, @@ -12,10 +12,10 @@ bootloader, CommandVals, ) -from framework16_inputmodule.gui.games import snake -from framework16_inputmodule.gui.ledmatrix import countdown, random_eq, clock -from framework16_inputmodule.gui.gui_threading import stop_thread, is_dev_disconnected -from framework16_inputmodule.inputmodule.ledmatrix import ( +from inputmodule.gui.games import snake +from inputmodule.gui.ledmatrix import countdown, random_eq, clock +from inputmodule.gui.gui_threading import stop_thread, is_dev_disconnected +from inputmodule.inputmodule.ledmatrix import ( percentage, pattern, animate, @@ -98,7 +98,8 @@ def run_gui(devices): [sg.HorizontalSeparator()], [sg.Text("Countdown Timer")], [ - sg.Spin([i for i in range(1, 60)], initial_value=10, k="-COUNTDOWN-"), + sg.Spin([i for i in range(1, 60)], + initial_value=10, k="-COUNTDOWN-"), sg.Text("Seconds"), sg.Button("Start", k="-START-COUNTDOWN-"), sg.Button("Stop", k="-STOP-COUNTDOWN-"), @@ -115,13 +116,15 @@ def run_gui(devices): sg.Column( [ [sg.Text("Greyscale Image")], - [sg.Button("Send greyscale.gif", k="-SEND-GREY-IMAGE-")], + [sg.Button("Send greyscale.gif", + k="-SEND-GREY-IMAGE-")], ] ), ], [sg.HorizontalSeparator()], [sg.Text("Display Current Time")], - [sg.Button("Start", k="-START-TIME-"), sg.Button("Stop", k="-STOP-TIME-")], + [sg.Button("Start", k="-START-TIME-"), + sg.Button("Stop", k="-STOP-TIME-")], [sg.HorizontalSeparator()], [ sg.Column( @@ -200,7 +203,8 @@ def run_gui(devices): thread.start() if event == "-START-TIME-": - thread = threading.Thread(target=clock, args=(dev,), daemon=True) + thread = threading.Thread( + target=clock, args=(dev,), daemon=True) thread.start() if event == "-PLAY-SNAKE-": @@ -249,7 +253,8 @@ def run_gui(devices): image_bl(dev, path) if event == "-SEND-GREY-IMAGE-": - path = os.path.join(resource_path(), "res", "greyscale.gif") + path = os.path.join( + resource_path(), "res", "greyscale.gif") image_greyscale(dev, path) if event == "-SEND-TEXT-": diff --git a/python/framework16_inputmodule/gui/games.py b/python/inputmodule/gui/games.py similarity index 96% rename from python/framework16_inputmodule/gui/games.py rename to python/inputmodule/gui/games.py index 01ead75b..46df99dd 100644 --- a/python/framework16_inputmodule/gui/games.py +++ b/python/inputmodule/gui/games.py @@ -4,13 +4,13 @@ import time import threading -from framework16_inputmodule.inputmodule import ( +from inputmodule.inputmodule import ( GameControlVal, send_command, CommandVals, Game, ) -from framework16_inputmodule.inputmodule.ledmatrix import ( +from inputmodule.inputmodule.ledmatrix import ( show_string, WIDTH, HEIGHT, @@ -181,7 +181,8 @@ def snake(dev): elif head == food: body.insert(0, oldhead) while food == head: - food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) + food = (random.randint(0, WIDTH - 1), + random.randint(0, HEIGHT - 1)) elif body: body.pop() body.insert(0, oldhead) diff --git a/python/framework16_inputmodule/gui/gui_threading.py b/python/inputmodule/gui/gui_threading.py similarity index 100% rename from python/framework16_inputmodule/gui/gui_threading.py rename to python/inputmodule/gui/gui_threading.py diff --git a/python/framework16_inputmodule/gui/ledmatrix.py b/python/inputmodule/gui/ledmatrix.py similarity index 92% rename from python/framework16_inputmodule/gui/ledmatrix.py rename to python/inputmodule/gui/ledmatrix.py index 4413e158..9a369e7c 100644 --- a/python/framework16_inputmodule/gui/ledmatrix.py +++ b/python/inputmodule/gui/ledmatrix.py @@ -2,18 +2,18 @@ import time import random -from framework16_inputmodule.gui.gui_threading import ( +from inputmodule.gui.gui_threading import ( reset_thread, is_thread_stopped, is_dev_disconnected, ) -from framework16_inputmodule.inputmodule.ledmatrix import ( +from inputmodule.inputmodule.ledmatrix import ( light_leds, show_string, eq, breathing, ) -from framework16_inputmodule.inputmodule import brightness +from inputmodule.inputmodule import brightness def countdown(dev, seconds): diff --git a/python/framework16_inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py similarity index 97% rename from python/framework16_inputmodule/inputmodule/__init__.py rename to python/inputmodule/inputmodule/__init__.py index 8e00d189..2ceb9de6 100644 --- a/python/framework16_inputmodule/inputmodule/__init__.py +++ b/python/inputmodule/inputmodule/__init__.py @@ -2,7 +2,7 @@ import serial # TODO: Make independent from GUI -from framework16_inputmodule.gui.gui_threading import disconnect_dev +from inputmodule.gui.gui_threading import disconnect_dev FWK_MAGIC = [0x32, 0xAC] FWK_VID = 0x32AC diff --git a/python/framework16_inputmodule/inputmodule/b1display.py b/python/inputmodule/inputmodule/b1display.py similarity index 96% rename from python/framework16_inputmodule/inputmodule/b1display.py rename to python/inputmodule/inputmodule/b1display.py index 4c8d7963..f471edf4 100644 --- a/python/framework16_inputmodule/inputmodule/b1display.py +++ b/python/inputmodule/inputmodule/b1display.py @@ -1,12 +1,13 @@ import sys -from framework16_inputmodule.inputmodule import send_command, CommandVals, FWK_MAGIC +from inputmodule.inputmodule import send_command, CommandVals, FWK_MAGIC B1_WIDTH = 300 B1_HEIGHT = 400 GREYSCALE_DEPTH = 32 -SCREEN_FPS = ["quarter", "half", "one", "two", "four", "eight", "sixteen", "thirtytwo"] +SCREEN_FPS = ["quarter", "half", "one", "two", + "four", "eight", "sixteen", "thirtytwo"] HIGH_FPS_MASK = 0b00010000 LOW_FPS_MASK = 0b00000111 diff --git a/python/framework16_inputmodule/inputmodule/c1minimal.py b/python/inputmodule/inputmodule/c1minimal.py similarity index 83% rename from python/framework16_inputmodule/inputmodule/c1minimal.py rename to python/inputmodule/inputmodule/c1minimal.py index 818578b1..7b7fbf49 100644 --- a/python/framework16_inputmodule/inputmodule/c1minimal.py +++ b/python/inputmodule/inputmodule/c1minimal.py @@ -1,6 +1,7 @@ -from framework16_inputmodule.inputmodule import send_command, CommandVals +from inputmodule.inputmodule import send_command, CommandVals -RGB_COLORS = ["white", "black", "red", "green", "blue", "cyan", "yellow", "purple"] +RGB_COLORS = ["white", "black", "red", "green", + "blue", "cyan", "yellow", "purple"] def get_color(dev): diff --git a/python/framework16_inputmodule/inputmodule/ledmatrix.py b/python/inputmodule/inputmodule/ledmatrix.py similarity index 99% rename from python/framework16_inputmodule/inputmodule/ledmatrix.py rename to python/inputmodule/inputmodule/ledmatrix.py index 1fef7270..f1ead13e 100644 --- a/python/framework16_inputmodule/inputmodule/ledmatrix.py +++ b/python/inputmodule/inputmodule/ledmatrix.py @@ -2,8 +2,8 @@ import serial -from framework16_inputmodule import font -from framework16_inputmodule.inputmodule import ( +from inputmodule import font +from inputmodule.inputmodule import ( send_command, CommandVals, PatternVals, diff --git a/python/pyproject.toml b/python/pyproject.toml index 9750d39d..0111e850 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] -name = "framework16_inputmodule" +name = "inputmodule" # TODO: Dynamic version from git (requires tags) #dynamic = ["version"] version = "0.1.1" @@ -38,16 +38,16 @@ Source = "https://github.com/FrameworkComputer/inputmodule-rs" # TODO: Figure out how to add a runnable-script [project.scripts] -ledmatrixctl = "framework16_inputmodule.cli:main_cli" +ledmatrixctl = "inputmodule.cli:main_cli" [project.gui-scripts] -ledmatrixgui = "framework16_inputmodule.cli:main_gui" +ledmatrixgui = "inputmodule.cli:main_gui" #[tool.hatch.version] #source = "vcs" # #[tool.hatch.build.hooks.vcs] -#version-file = "framework16_inputmodule/_version.py" +#version-file = "inputmodule/_version.py" [tool.hatch.build.targets.sdist] exclude = [ From 3ec546455a365952bbb397dd75f4fda0699ecf0c Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 28 Nov 2023 00:26:09 +0800 Subject: [PATCH 14/16] python: Sample usage of a new python API Signed-off-by: Daniel Schaefer --- python/sample.py | 138 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 python/sample.py diff --git a/python/sample.py b/python/sample.py new file mode 100644 index 00000000..2780e260 --- /dev/null +++ b/python/sample.py @@ -0,0 +1,138 @@ +import time +from datetime import datetime +#from inputmodule.ledmatrix import LedMatrix, Pattern + +# TODO: Import +from enum import IntEnum +class PatternVals(IntEnum): + FullBrightness = 0x05 +class CommandVals(IntEnum): + Brightness = 0x00 + Pattern = 0x01 + +class Pattern: + width = 9 + height = 34 + vals = [] + + def percentage(p): + Pattern() + + def from_string(s): + Pattern() + + def set(self, x, y, val): + pass + +class InputModule: + pass + +class LedMatrix(object): + def __init__(self, name): + self.name = name + self.fw_version = "0.1.9" + self.sleeping = True + self.brightness = 100 + + def find_all(): + return [1, 2] + + def __enter__(self): + #print('entering') + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + #print('leaving') + + def enter_bootloader(self): + pass + + def raw_command(self, command, vals): + pass + + def set_bw(self, pattern): + pass + + def set_grayscale(self, pattern): + pass + + # TODO: Properties for things like sleeping, brightness, ... + @property + def radius(self): + """The radius property.""" + print("Get radius") + return self._radius + + @radius.setter + def radius(self, value): + print("Set radius") + self._radius = value + + +# +matrices = LedMatrix.find_all() +print(f"{len(matrices)} LED Matrices connected to the system") + +with LedMatrix("COM1") as matrix: + print(f"Firmware version: {matrix.fw_version}") + + print(f"Sleep status: {matrix.sleeping}") + print(f"Going to sleep and back") + matrix.sleeping = True + matrix.sleeping = False + + print(f"Current brightness: {matrix.brightness}") + print("Setting 100% brightness") + matrix.brightness = 100 + print("Setting 50% brightness") + matrix.brightness = 50 + + # TODO + # matrix.pwm_freq + # matrix.animate + # matrix.animate = True + # matrix.animate = False + + # Enter bootloader to prepare for flashing + # To exit bootloader, either flash the firmware or re-plug the device + # matrix.enter_bootloader() + + print("Iterating through a couple of built-in patterns, 1s each") + pattern_commands = [ + PatternVals.FullBrightness, + #PatternVals.Gradient, + #PatternVals.DoubleGradient, + #PatternVals.DisplayLotus, + #PatternVals.ZigZag, + #PatternVals.DisplayPanic, + #PatternVals.DisplayLotus2 + ] + for pattern in pattern_commands: + matrix.raw_command(CommandVals.Pattern, [pattern]) + time.sleep(1) + + print("Iterating through a couple of black/white patterns, 1s each") + bw_patterns = [ + Pattern.percentage(50), + ] + for pattern in bw_patterns: + matrix.set_bw(pattern) + time.sleep(1) + + # Demonstrate gray-scale pattern + print("Show all 255 brightness levels, one per LED") + pattern = Pattern() + for x in range(0, pattern.width): + for y in range(pattern.height): + brightness = x + pattern.width * y + if brightness > 255: + pattern.set(x, y, 0) + else: + pattern.set(x, y, brightness) + matrix.set_grayscale(pattern) + + # Show current time + current_time = datetime.now().strftime("%H:%M") + print("Current Time =", current_time) + matrix.set_bw(Pattern.from_string(current_time)) From 631ffa78a5eff061f8f6b244bf95a0338240a2fb Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 28 Nov 2023 14:48:17 +0800 Subject: [PATCH 15/16] python: decouple thread sync from gui Would cause circular import in some cases. Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 2 +- python/inputmodule/gui/ledmatrix.py | 2 +- python/inputmodule/inputmodule/__init__.py | 3 +-- python/inputmodule/{gui/gui_threading.py => thread_sync.py} | 0 python/sample.py | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) rename python/inputmodule/{gui/gui_threading.py => thread_sync.py} (100%) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index ba0f1e1a..8a29e994 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -14,7 +14,7 @@ ) from inputmodule.gui.games import snake from inputmodule.gui.ledmatrix import countdown, random_eq, clock -from inputmodule.gui.gui_threading import stop_thread, is_dev_disconnected +from inputmodule.thread_sync import stop_thread, is_dev_disconnected from inputmodule.inputmodule.ledmatrix import ( percentage, pattern, diff --git a/python/inputmodule/gui/ledmatrix.py b/python/inputmodule/gui/ledmatrix.py index 9a369e7c..4b046b3b 100644 --- a/python/inputmodule/gui/ledmatrix.py +++ b/python/inputmodule/gui/ledmatrix.py @@ -2,7 +2,7 @@ import time import random -from inputmodule.gui.gui_threading import ( +from inputmodule.thread_sync import ( reset_thread, is_thread_stopped, is_dev_disconnected, diff --git a/python/inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py index 2ceb9de6..ff8dd108 100644 --- a/python/inputmodule/inputmodule/__init__.py +++ b/python/inputmodule/inputmodule/__init__.py @@ -1,8 +1,7 @@ from enum import IntEnum import serial -# TODO: Make independent from GUI -from inputmodule.gui.gui_threading import disconnect_dev +from inputmodule.thread_sync import disconnect_dev FWK_MAGIC = [0x32, 0xAC] FWK_VID = 0x32AC diff --git a/python/inputmodule/gui/gui_threading.py b/python/inputmodule/thread_sync.py similarity index 100% rename from python/inputmodule/gui/gui_threading.py rename to python/inputmodule/thread_sync.py diff --git a/python/sample.py b/python/sample.py index 2780e260..7292a1eb 100644 --- a/python/sample.py +++ b/python/sample.py @@ -1,6 +1,6 @@ import time from datetime import datetime -#from inputmodule.ledmatrix import LedMatrix, Pattern +#from inputmodule.inputmodule.ledmatrix import LedMatrix, Pattern # TODO: Import from enum import IntEnum From cd6e329886f246bb8cc87faae39fe186222975ac Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 28 Nov 2023 16:02:46 +0800 Subject: [PATCH 16/16] python: Refine sample API Signed-off-by: Daniel Schaefer --- python/sample.py | 226 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 187 insertions(+), 39 deletions(-) diff --git a/python/sample.py b/python/sample.py index 7292a1eb..4ba03d3f 100644 --- a/python/sample.py +++ b/python/sample.py @@ -1,80 +1,198 @@ import time from datetime import datetime -#from inputmodule.inputmodule.ledmatrix import LedMatrix, Pattern +# from inputmodule.inputmodule.ledmatrix import LedMatrix, Pattern + +FWK_MAGIC = [0x32, 0xAC] +FWK_VID = 0x32AC +LED_MATRIX_PID = 0x20 +QTPY_PID = 0x001F +INPUTMODULE_PIDS = [LED_MATRIX_PID, QTPY_PID] +RESPONSE_SIZE = 32 + +import serial # TODO: Import from enum import IntEnum + + class PatternVals(IntEnum): + Percentage = 0x00 + Gradient = 0x01 + DoubleGradient = 0x02 + DisplayLotus = 0x03 + ZigZag = 0x04 FullBrightness = 0x05 + DisplayPanic = 0x06 + DisplayLotus2 = 0x07 + + class CommandVals(IntEnum): Brightness = 0x00 Pattern = 0x01 + BootloaderReset = 0x02 + Sleep = 0x03 + Animate = 0x04 + Panic = 0x05 + Draw = 0x06 + StageGreyCol = 0x07 + DrawGreyColBuffer = 0x08 + SetText = 0x09 + StartGame = 0x10 + GameControl = 0x11 + GameStatus = 0x12 + SetColor = 0x13 + DisplayOn = 0x14 + InvertScreen = 0x15 + SetPixelColumn = 0x16 + FlushFramebuffer = 0x17 + ClearRam = 0x18 + ScreenSaver = 0x19 + SetFps = 0x1A + SetPowerMode = 0x1B + PwmFreq = 0x1E + DebugMode = 0x1F + Version = 0x20 + class Pattern: width = 9 height = 34 - vals = [] + + def __init__(self): + """Empty pattern with all LEDs off""" + self._vals = [[0 for _ in range(self.height)] for _ in range(self.width)] def percentage(p): - Pattern() + """A percentage of LEDs on, increasing vertically from the bottom""" + pattern = Pattern() + pattern._vals = [ + [(0xFF if (y * 100 / 34 > p) else 0) for y in range(pattern.height)] + for _ in range(pattern.width) + ] + return pattern def from_string(s): - Pattern() + # TODO + return Pattern() def set(self, x, y, val): - pass + """Set a specific LED to a brightness value""" + assert val >= 0 and val <= 0xFF + assert x >= 0 and x <= self.width + assert y >= 0 and y <= self.height + self._vals[x][y] = val + + def to_bw_vals(self): + """To list of 39 byte values [Int]""" + vals = [0x00 for _ in range(39)] + for x, col in enumerate(self._vals): + for y, val in enumerate(col): + if val == 0xFF: + i = x + self.width * y + vals[int(i / 8)] |= 1 << i % 8 + return vals + + def to_gray_vals(self): + """To [[]]""" + return self._vals + -class InputModule: +class ModuleNotFoundException(Exception): pass + class LedMatrix(object): - def __init__(self, name): - self.name = name + def __init__(self, dev_path=None): + self.dev_path = dev_path + + if dev_path is None: + pass + self.fw_version = "0.1.9" self.sleeping = True - self.brightness = 100 + # self.brightness = 100 + self.dev_path = "/dev/tty0" + # self.dev_path = None + self.dev = None + # TODO: Check if it's there + # raise ModuleNotFoundException(f"Module {port} not found") + if False: + raise ModuleNotFoundException("No Module found") - def find_all(): - return [1, 2] + def from_port(port): + """Connect to an LED matrix by specifying the serial port name/path""" + return LedMatrix(port) + + def left(): + """Connect to the left LED matrix""" + # TODO + raise ModuleNotFoundException("Left Module not found") + + def right(): + """Connect to the right LED matrix""" + # TODO + raise ModuleNotFoundException("Right Module not found") + + def list_ports(): + """List all serial ports with LED matrices""" + return ["/dev/ttyACM0"] def __enter__(self): - #print('entering') + self.dev = serial.Serial(self.dev_path, 115200) return self def __exit__(self, exc_type, exc_val, exc_tb): - pass - #print('leaving') + self.dev.close() def enter_bootloader(self): - pass + """Put the module in bootloader mode to update the firmware""" + self.raw_command(CommandVals.BootloaderReset, [0x00]) - def raw_command(self, command, vals): - pass + def raw_command(self, command, parameters=[], with_response=False): + """Send a raw command with command ID and payload bytes""" + vals = FWK_MAGIC + [command] + parameters + self.dev.write(command) + if with_response: + res = self.dev.read(RESPONSE_SIZE) + return res def set_bw(self, pattern): - pass + """Draw a black and white pattern on the LED matrix""" + vals = pattern.to_bw_vals() + self.raw_command(CommandVals.Draw, vals) def set_grayscale(self, pattern): - pass + """Draw a greyscale pattern on the LED matrix""" + for x in range(0, pattern.width): + vals = pattern.to_gray_vals()[x] + self._send_col(x, vals) + self._commit_cols() + + def _send_col(self, x, vals): + """Stage greyscale values for a single column. Must be committed with commit_cols()""" + command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals + self.dev.write(command) + + def _commit_cols(self): + """Commit the changes from sending individual cols with send_col(), displaying the matrix. + This makes sure that the matrix isn't partially updated.""" + command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00] + self.dev.write(command) # TODO: Properties for things like sleeping, brightness, ... @property - def radius(self): - """The radius property.""" - print("Get radius") - return self._radius - - @radius.setter - def radius(self, value): - print("Set radius") - self._radius = value + def brightness(self): + """Get current module brightness""" + res = self.raw_command(CommandVals.Brightness, with_response=True) + return int(res[0]) + @brightness.setter + def brightness(self, value): + """Change brightness""" + self.raw_command(CommandVals.Brightness, [value]) -# -matrices = LedMatrix.find_all() -print(f"{len(matrices)} LED Matrices connected to the system") -with LedMatrix("COM1") as matrix: +def demo_interaction(matrix): print(f"Firmware version: {matrix.fw_version}") print(f"Sleep status: {matrix.sleeping}") @@ -82,7 +200,7 @@ def radius(self, value): matrix.sleeping = True matrix.sleeping = False - print(f"Current brightness: {matrix.brightness}") + # print(f"Current brightness: {matrix.brightness}") print("Setting 100% brightness") matrix.brightness = 100 print("Setting 50% brightness") @@ -101,12 +219,12 @@ def radius(self, value): print("Iterating through a couple of built-in patterns, 1s each") pattern_commands = [ PatternVals.FullBrightness, - #PatternVals.Gradient, - #PatternVals.DoubleGradient, - #PatternVals.DisplayLotus, - #PatternVals.ZigZag, - #PatternVals.DisplayPanic, - #PatternVals.DisplayLotus2 + PatternVals.Gradient, + PatternVals.DoubleGradient, + PatternVals.DisplayLotus, + PatternVals.ZigZag, + PatternVals.DisplayPanic, + PatternVals.DisplayLotus2, ] for pattern in pattern_commands: matrix.raw_command(CommandVals.Pattern, [pattern]) @@ -136,3 +254,33 @@ def radius(self, value): current_time = datetime.now().strftime("%H:%M") print("Current Time =", current_time) matrix.set_bw(Pattern.from_string(current_time)) + + +def demo(): + matrices = LedMatrix.list_ports() + print(f"{len(matrices)} LED Matrices connected to the system") + + try: + # Open specific + with LedMatrix.from_port("COM1") as matrix: + pass + except ModuleNotFoundException as e: + print(e) + + # Left and right matrix, fails if only one is connected + try: + with LedMatrix.left() as left_matrix, LedMatrix.right() as right_matrix: + print(f"Left: {left_matrix}, Right: {right_matrix}") + except ModuleNotFoundException as e: + print(e) + + # Choose first available matrix (best if just one is connected) + try: + with LedMatrix() as matrix: + demo_interaction(matrix) + except ModuleNotFoundException as e: + print(e) + + +if __name__ == "__main__": + demo()