diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml index 02e2a2b..cba0a1a 100644 --- a/.github/workflows/firmware.yml +++ b/.github/workflows/firmware.yml @@ -24,7 +24,7 @@ jobs: name: Building runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show @@ -66,7 +66,7 @@ jobs: cargo make --cwd ledmatrix bin - name: Upload ledmatrix files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ledmatrix_fw_${{github.sha}} path: | @@ -79,7 +79,7 @@ jobs: target/thumbv6m-none-eabi/release/ledmatrix_evt.uf2 - name: Upload b1display files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: b1display_fw_${{github.sha}} path: | @@ -87,7 +87,7 @@ jobs: target/thumbv6m-none-eabi/release/b1display.uf2 - name: Upload c1minimal files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: c1minimal_fw_${{github.sha}} path: | @@ -98,7 +98,7 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show @@ -130,7 +130,7 @@ jobs: name: Formatting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index 448832f..105dde7 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -29,7 +29,7 @@ jobs: # name: Cross-Build for FreeBSD # runs-on: 'ubuntu-22.04' # steps: - # - uses: actions/checkout@v3 + # - uses: actions/checkout@v4 # - name: Setup Rust toolchain # run: rustup show @@ -41,7 +41,7 @@ jobs: # run: cross build --target=x86_64-unknown-freebsd # - name: Upload FreeBSD App - # uses: actions/upload-artifact@v3 + # uses: actions/upload-artifact@v4 # with: # name: qmk_hid_freebsd # path: target/x86_64-unknown-freebsd/debug/qmk_hid @@ -50,7 +50,7 @@ jobs: name: Build Linux runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -69,7 +69,7 @@ jobs: run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' - name: Upload Linux tool - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: inputmodule-control path: target/x86_64-unknown-linux-gnu/release/inputmodule-control @@ -78,7 +78,7 @@ jobs: name: Build Windows runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show @@ -92,7 +92,7 @@ jobs: run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' - name: Upload Windows App - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: inputmodule-control.exe path: target/x86_64-pc-windows-msvc/release/inputmodule-control.exe @@ -103,22 +103,30 @@ jobs: name: Build GUI runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Download releases to bundle + run: | + mkdir releases + mkdir releases\0.2.0 + Invoke-WebRequest -Uri https://github.com/FrameworkComputer/inputmodule-rs/releases/download/v0.2.0/ledmatrix.uf2 -OutFile releases\0.2.0\ledmatrix.uf2 + + # To run locally, need to make sure to include the pywin32 DLL + # pyinstaller --onefile, --name "python/inputmodule/cli.py", --windowed, --add-data "releases;releases" --icon=res\framework_startmenuicon.ico --path C:\users\skype\appdata\local\packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\localcache\local-packages\Python312\site-packages\pywin32_system32 --add-data 'res;res' -p python/inputmodule python/inputmodule/cli.py - name: Create Executable - uses: Martin005/pyinstaller-action@main + uses: JohnAZoidberg/pyinstaller-action@dont-clean with: - python_ver: '3.11' + python_ver: '3.12' spec: python/inputmodule/cli.py #'src/build.spec' - requirements: 'requirements.txt' - upload_exe_with_name: 'ledmatrixgui' - options: --onefile, --windowed, --add-data 'res;res' + requirements: 'python/requirements.txt' + upload_exe_with_name: 'ledmatrixgui.exe' + options: --onefile, --name "ledmatrixgui", --windowed, --add-data "releases;releases" --icon=res/framework_startmenuicon.ico --add-data 'res;res' -p python/inputmodule package-python: name: Package Python runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | cd python @@ -131,7 +139,7 @@ jobs: name: Lints runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/traditional-cargo.yml b/.github/workflows/traditional-cargo.yml index 8df7106..231ead0 100644 --- a/.github/workflows/traditional-cargo.yml +++ b/.github/workflows/traditional-cargo.yml @@ -24,7 +24,7 @@ jobs: name: Build firmware runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show @@ -41,7 +41,7 @@ jobs: name: Build Linux runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -61,7 +61,7 @@ jobs: name: Build Windows runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show @@ -78,7 +78,7 @@ jobs: name: Lint and format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/README.md b/README.md index ad2a53b..a6c102b 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,14 @@ 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/inputmodule-rs/actions) run or the [release page](https://github.com/FrameworkComputer/inputmodule-rs/releases). -Optionally there are is also a [Python script](python/README.md). For device specific commands, see their individual documentation pages. +### GUI and Python +There are also a python library and GUI tool. See their [README](python/README.md). + +![](res/ledmatrixgui-home.png) + ###### Permissions on Linux To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index 5f97615..852641f 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -14,11 +14,11 @@ brightness, get_brightness, CommandVals, - bootloader, + bootloader_jump, GameOfLifeStartParam, GameControlVal, ) -from inputmodule.gui.games import ( +from inputmodule.games import ( snake, snake_embedded, pong_embedded, @@ -62,10 +62,6 @@ RGB_COLORS, ) -# Optional dependencies: -# from PIL import Image -# import PySimpleGUI as sg - def main_cli(): parser = argparse.ArgumentParser() @@ -237,7 +233,7 @@ def main_cli(): if not ports: print("No device found") - gui.popup(args.gui, "No device found") + gui.popup("No device found", gui=args.gui) sys.exit(1) elif args.serial_dev is not None: filtered_devs = [ @@ -250,10 +246,10 @@ def main_cli(): dev = ports[0] elif len(ports) >= 1 and not args.gui: 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]) ), + gui=args.gui, ) print( "More than 1 compatible device found. Please choose with --serial-dev ..." @@ -268,11 +264,11 @@ def main_cli(): if not args.gui and dev is None: print("No device selected") - gui.popup(args.gui, "No device selected") + gui.popup("No device selected", gui=args.gui) sys.exit(1) if args.bootloader: - bootloader(dev) + bootloader_jump(dev) elif args.sleep is not None: send_command(dev, CommandVals.Sleep, [args.sleep]) elif args.is_sleeping: @@ -394,6 +390,7 @@ def find_devs(): def print_devs(ports): for port in ports: print(f"{port.device}") + print(f" {port.name}") print(f" VID: 0x{port.vid:04X}") print(f" PID: 0x{port.pid:04X}") print(f" SN: {port.serial_number}") @@ -407,4 +404,4 @@ def main_gui(): if __name__ == "__main__": - main_cli() + main_gui() diff --git a/python/inputmodule/firmware_update.py b/python/inputmodule/firmware_update.py new file mode 100644 index 0000000..7d2e752 --- /dev/null +++ b/python/inputmodule/firmware_update.py @@ -0,0 +1,80 @@ +import os +import time + +from inputmodule.inputmodule import bootloader_jump +from inputmodule import uf2conv + +def dev_to_str(dev): + return dev.name + +def flash_firmware(dev, fw_path): + print(f"Flashing {fw_path} onto {dev_to_str(dev)}") + + # First jump to bootloader + drives = uf2conv.list_drives() + if not drives: + print("Jump to bootloader") + bootloader_jump(dev) + + timeout = 10 # 5s + while not drives: + if timeout == 0: + print("Failed to find device in bootloader") + # TODO: Handle return value + return False + # Wait for it to appear + time.sleep(0.5) + timeout -= 1 + drives = uf2conv.get_drives() + + + if len(drives) == 0: + print("No drive to deploy.") + return False + + # Firmware is pretty small, can just fit it all into memory + with open(fw_path, 'rb') as f: + fw_buf = f.read() + + for d in drives: + print("Flashing {} ({})".format(d, uf2conv.board_id(d))) + uf2conv.write_file(d + "/NEW.UF2", fw_buf) + + print("Flashing finished") + +# Example return value +# { +# '0.1.7': { +# 'ansi': 'framework_ansi_default_v0.1.7.uf2', +# 'gridpad': 'framework_gridpad_default_v0.1.7.uf2' +# }, +# '0.1.8': { +# 'ansi': 'framework_ansi_default.uf2', +# 'gridpad': 'framework_gridpad_default.uf2', +# } +# } +def find_releases(res_path, filename_format): + from os import listdir + from os.path import isfile, join + import re + + releases = {} + try: + versions = listdir(os.path.join(res_path, "releases")) + except FileNotFoundError: + return releases + + for version in versions: + path = join(res_path, "releases", version) + releases[version] = {} + for filename in listdir(path): + if not isfile(join(path, filename)): + continue + type_search = re.search(filename_format, filename) + if not type_search: + print(f"Filename '{filename}' not matching patten!") + sys.exit(1) + continue + fw_type = type_search.group(1) + releases[version][fw_type] = os.path.join(res_path, "releases", version, filename) + return releases diff --git a/python/inputmodule/gui/games.py b/python/inputmodule/games.py similarity index 99% rename from python/inputmodule/gui/games.py rename to python/inputmodule/games.py index 46df99d..f6bf1d9 100644 --- a/python/inputmodule/gui/games.py +++ b/python/inputmodule/games.py @@ -43,6 +43,7 @@ def opposite_direction(direction): return direction + def snake_keyscan(): global direction global body diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index ba0f1e1..f7e53a6 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -1,18 +1,23 @@ import os -import threading +import platform import sys +import threading +import webbrowser -import PySimpleGUI as sg +import tkinter as tk +from tkinter import ttk, messagebox +from inputmodule import firmware_update from inputmodule.inputmodule import ( send_command, get_version, brightness, get_brightness, - bootloader, + bootloader_jump, CommandVals, + Game, + GameControlVal ) -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 ( @@ -28,8 +33,7 @@ image_greyscale, ) - -def update_brightness_slider(window, devices): +def update_brightness_slider(devices): average_brightness = None for dev in devices: if not average_brightness: @@ -38,244 +42,284 @@ def update_brightness_slider(window, devices): br = get_brightness(dev) average_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") + brightness_scale.set(average_brightness) +def popup(message, gui=True): + if gui: + messagebox.showinfo("Framework Laptop 16 LED Matrix", message) def run_gui(devices): - device_checkboxes = [] + root = tk.Tk() + root.title("LED Matrix Control") + + ico = "framework_startmenuicon.ico" + res_path = resource_path() + if os.name == 'nt': + root.iconbitmap(f"{res_path}/res/{ico}") + + tabControl = ttk.Notebook(root) + tab1 = ttk.Frame(tabControl) + tab_games = ttk.Frame(tabControl) + tab2 = ttk.Frame(tabControl) + tab_fw = ttk.Frame(tabControl) + tab3 = ttk.Frame(tabControl) + tabControl.add(tab1, text="Home") + tabControl.add(tab_games, text="Games") + tabControl.add(tab2, text="Dynamic Controls") + tabControl.add(tab_fw, text="Firmware Update") + tabControl.add(tab3, text="Advanced") + tabControl.pack(expand=1, fill="both") + + # Device Checkboxes + detected_devices_frame = ttk.LabelFrame(root, text="Detected Devices", style="TLabelframe") + detected_devices_frame.pack(fill="x", padx=10, pady=5) + + global device_checkboxes + 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) - + checkbox_var = tk.BooleanVar(value=True) + checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton") + checkbox.pack(anchor="w") + device_checkboxes[dev.name] = (checkbox_var, checkbox) + + # Online Info + info_frame = ttk.LabelFrame(tab1, text="Online Info", style="TLabelframe") + info_frame.pack(fill="x", padx=10, pady=5) + infos = { + "Web Interface": "https://ledmatrix.frame.work", + "Latest Releases": "https://github.com/FrameworkComputer/inputmodule-rs/releases", + "Hardware Info": "https://github.com/FrameworkComputer/InputModules", + } + for (i, (text, url)) in enumerate(infos.items()): + # Organize in columns of three + row = int(i / 3) + column = i % 3 + btn = ttk.Button(info_frame, text=text, command=lambda url=url: webbrowser.open(url), style="TButton") + btn.grid(row=row, column=column) + + # Brightness Slider + brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") + brightness_frame.pack(fill="x", padx=10, pady=5) + global brightness_scale + brightness_scale = tk.Scale(brightness_frame, from_=0, to=255, orient='horizontal', command=lambda value: set_brightness(devices, value)) + brightness_scale.set(120) # Default value + brightness_scale.pack(fill="x", padx=5, pady=5) + + # Animation Control + animation_frame = ttk.LabelFrame(tab1, text="Animation", style="TLabelframe") + animation_frame.pack(fill="x", padx=10, pady=5) + animation_buttons = { + "Start Animation": "start_animation", + "Stop Animation": "stop_animation" + } + for text, action in animation_buttons.items(): + ttk.Button(animation_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + + # Pattern Combo Box + pattern_frame = ttk.LabelFrame(tab1, text="Pattern", style="TLabelframe") + pattern_frame.pack(fill="x", padx=10, pady=5) + pattern_combo = ttk.Combobox(pattern_frame, values=PATTERNS, style="TCombobox", state="readonly") + pattern_combo.pack(fill="x", padx=5, pady=5) + pattern_combo.bind("<>", lambda event: set_pattern(devices, pattern_combo.get())) + + # Percentage Slider + percentage_frame = ttk.LabelFrame(tab1, text="Fill screen X% (could be volume indicator)", style="TLabelframe") + percentage_frame.pack(fill="x", padx=10, pady=5) + percentage_scale = tk.Scale(percentage_frame, from_=0, to=100, orient='horizontal', command=lambda value: set_percentage(devices, value)) + percentage_scale.pack(fill="x", padx=5, pady=5) + + # Games tab + games_frame = ttk.LabelFrame(tab_games, text="Interactive", style="TLabelframe") + games_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(games_frame, text="Snake", command=lambda: perform_action(devices, 'game_snake'), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(games_frame, text="Ledris", command=lambda: perform_action(devices, 'game_ledris'), style="TButton").pack(side="left", padx=5, pady=5) + gol_frame = ttk.LabelFrame(tab_games, text="Game of Life", style="TLabelframe") + gol_frame.pack(fill="x", padx=10, pady=5) + animation_buttons = { + "Current": "gol_current", + "Pattern 1": "gol_pattern1", + "Blinker": "gol_blinker", + "Toad": "gol_toad", + "Beacon": "gol_beacon", + "Glider": "gol_glider", + "Stop": "game_stop", + } + for (i, (text, action)) in enumerate(animation_buttons.items()): + # Organize in columns of three + row = int(i / 3) + column = i % 3 + if action == "game_stop": + column = 0 + row += 1 + btn = ttk.Button(gol_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton") + btn.grid(row=row, column=column) + + # Countdown Timer + countdown_frame = ttk.LabelFrame(tab2, text="Countdown Timer", style="TLabelframe") + countdown_frame.pack(fill="x", padx=10, pady=5) + countdown_spinbox = tk.Spinbox(countdown_frame, from_=1, to=60, width=5, textvariable=tk.StringVar(value=10)) + countdown_spinbox.pack(side="left", padx=5, pady=5) + ttk.Label(countdown_frame, text="Seconds", style="TLabel").pack(side="left") + ttk.Button(countdown_frame, text="Start", command=lambda: start_countdown(devices, countdown_spinbox.get()), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(countdown_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Black & White and Greyscale Images in same row + image_frame = ttk.LabelFrame(tab1, text="Black&White Images / Greyscale Images", style="TLabelframe") + image_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(image_frame, text="Send stripe.gif", command=lambda: send_image(devices, "stripe.gif", image_bl), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(image_frame, text="Send greyscale.gif", command=lambda: send_image(devices, "greyscale.gif", image_greyscale), style="TButton").pack(side="left", padx=5, pady=5) + + # Display Current Time + time_frame = ttk.LabelFrame(tab2, text="Display Current Time", style="TLabelframe") + time_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(time_frame, text="Start", command=lambda: perform_action(devices, "start_time"), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(time_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Custom Text + custom_text_frame = ttk.LabelFrame(tab1, text="Custom Text", style="TLabelframe") + custom_text_frame.pack(fill="x", padx=10, pady=5) + custom_text_entry = ttk.Entry(custom_text_frame, width=20, style="TEntry") + custom_text_entry.pack(side="left", padx=5, pady=5) + ttk.Button(custom_text_frame, text="Show", command=lambda: show_custom_text(devices, custom_text_entry.get()), style="TButton").pack(side="left", padx=5, pady=5) + + # Display Text with Symbols + symbols_frame = ttk.LabelFrame(tab1, text="Display Text with Symbols", style="TLabelframe") + symbols_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(symbols_frame, text="Send '2 5 degC thunder'", command=lambda: send_symbols(devices), style="TButton").pack(side="left", padx=5, pady=5) + + # Firmware Update + bootloader_frame = ttk.LabelFrame(tab_fw, text="Bootloader", style="TLabelframe") + bootloader_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(bootloader_frame, text="Enter Bootloader", command=lambda: perform_action(devices, "bootloader"), style="TButton").pack(side="left", padx=5, pady=5) + + bundled_fw_frame = ttk.LabelFrame(tab_fw, text="Bundled Updates", style="TLabelframe") + bundled_fw_frame.pack(fill="x", padx=10, pady=5) + releases = firmware_update.find_releases(resource_path(), r'(ledmatrix).uf2') + if not releases: + tk.Label(bundled_fw_frame, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5) + else: + versions = sorted(list(releases.keys()), reverse=True) + + #tk.Label(fw_update_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) + fw_ver_combo = ttk.Combobox(bundled_fw_frame, values=versions, style="TCombobox", state="readonly") + fw_ver_combo.pack(side=tk.LEFT, padx=5, pady=5) + fw_ver_combo.current(0) + flash_btn = ttk.Button(bundled_fw_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), 'ledmatrix'), style="TButton") + flash_btn.pack(side="left", padx=5, pady=5) + + # PWM Frequency Combo Box + pwm_freq_frame = ttk.LabelFrame(tab3, text="PWM Frequency", style="TLabelframe") + pwm_freq_frame.pack(fill="x", padx=10, pady=5) + pwm_freq_combo = ttk.Combobox(pwm_freq_frame, values=PWM_FREQUENCIES, style="TCombobox", state="readonly") + pwm_freq_combo.pack(fill="x", padx=5, pady=5) + pwm_freq_combo.bind("<>", lambda: set_pwm_freq(devices, pwm_freq_combo.get())) + + # Equalizer + equalizer_frame = ttk.LabelFrame(tab2, text="Equalizer", style="TLabelframe") + equalizer_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(equalizer_frame, text="Start random equalizer", command=lambda: perform_action(devices, "start_eq"), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(equalizer_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Device Control Buttons + device_control_frame = ttk.LabelFrame(tab3, text="Device Control", style="TLabelframe") + device_control_frame.pack(fill="x", padx=10, pady=5) + control_buttons = { + "Sleep": "sleep", + "Wake": "wake" + } + for text, action in control_buttons.items(): + ttk.Button(device_control_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + + root.mainloop() + +def perform_action(devices, action): + if action.startswith("game_"): + from inputmodule.gui.pygames import snake, ledris + action_map = { + "game_snake": snake.main_devices, + "game_ledris": ledris.main_devices, + } + if action in action_map: + threading.Thread(target=action_map[action], args=(devices,), daemon=True).start(), + + if action == "bootloader": + disable_devices(devices) + restart_hint() + + action_map = { + "bootloader": bootloader_jump, + "sleep": lambda dev: send_command(dev, CommandVals.Sleep, [True]), + "wake": lambda dev: send_command(dev, CommandVals.Sleep, [False]), + "start_animation": lambda dev: animate(dev, True), + "stop_animation": lambda dev: animate(dev, False), + "start_time": lambda dev: threading.Thread(target=clock, args=(dev,), daemon=True).start(), + "start_eq": lambda dev: threading.Thread(target=random_eq, args=(dev,), daemon=True).start(), + "gol_current": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 0]), + "gol_pattern1": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 1]), + "gol_blinker": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 2]), + "gol_toad": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 3]), + "gol_beacon": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 4]), + "gol_glider": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 5]), + "game_stop": lambda dev: send_command(dev, CommandVals.GameControl, [GameControlVal.Quit]), + } + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + if action in action_map: + action_map[action](dev) + +def set_brightness(devices, value): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + brightness(dev, int(value)) + +def set_pattern(devices, pattern_name): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + pattern(dev, pattern_name) + +def set_percentage(devices, value): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + percentage(dev, int(value)) + +def show_custom_text(devices, text): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + show_string(dev, text.upper()) + +def send_image(devices, image_name, image_function): + selected_devices = get_selected_devices(devices) + path = os.path.join(resource_path(), "res", image_name) + if not os.path.exists(path): + popup(f"Image file {image_name} not found.") + return + for dev in selected_devices: + image_function(dev, path) + +def send_symbols(devices): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + show_symbols(dev, ["2", "5", "degC", " ", "thunder"]) + +def start_countdown(devices, countdown_time): + selected_devices = get_selected_devices(devices) + if len(selected_devices) == 1: + dev = selected_devices[0] + threading.Thread(target=countdown, args=(dev, int(countdown_time)), daemon=True).start() + else: + popup("Select exactly 1 device for this action") + +def set_pwm_freq(devices, freq): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + pwm_freq(dev, freq) + +def get_selected_devices(devices): + return [dev for dev in devices if dev.name in device_checkboxes and device_checkboxes[dev.name][0].get()] def resource_path(): """Get absolute path to resource, works for dev and for PyInstaller""" @@ -283,6 +327,39 @@ def resource_path(): # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: - base_path = os.path.abspath("../../") + base_path = os.path.abspath(".") return base_path + +def info_popup(msg): + parent = tk.Tk() + parent.title("Info") + message = tk.Message(parent, text=msg, width=800) + message.pack(padx=20, pady=20) + parent.mainloop() + +def tk_flash_firmware(devices, releases, version, fw_type): + selected_devices = get_selected_devices(devices) + if len(selected_devices) != 1: + info_popup('To flash select exactly 1 device.') + return + dev = selected_devices[0] + firmware_update.flash_firmware(dev, releases[version][fw_type]) + # Disable device that we just flashed + disable_devices(devices) + restart_hint() + +def restart_hint(): + parent = tk.Tk() + parent.title("Restart Application") + message = tk.Message(parent, text="After updating a device,\n restart the application to reload the connections.", width=800) + message.pack(padx=20, pady=20) + parent.mainloop() + +def disable_devices(devices): + # Disable checkbox of selected devices + for dev in devices: + for name, (checkbox_var, checkbox) in device_checkboxes.items(): + if name == dev.name: + checkbox_var.set(False) + checkbox.config(state=tk.DISABLED) diff --git a/python/inputmodule/gui/pygames/__init__.py b/python/inputmodule/gui/pygames/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/inputmodule/gui/pygames/ledris.py b/python/inputmodule/gui/pygames/ledris.py new file mode 100644 index 0000000..469acfe --- /dev/null +++ b/python/inputmodule/gui/pygames/ledris.py @@ -0,0 +1,254 @@ +# Run like +# python3 ledris.py + +import pygame +import random +import time + +from inputmodule import cli +from inputmodule.gui.ledmatrix import show_string +from inputmodule.inputmodule import ledmatrix + +# Set the screen width and height for a 34 x 9 block Ledris game +block_width = 20 +block_height = 20 +cols = 9 +rows = 34 + +width = cols * block_width +height = rows * block_height + +# Colors +black = (0, 0, 0) +white = (255, 255, 255) + +# Ledrimino shapes +shapes = [ + [[1, 1, 1, 1]], # I shape + [[1, 1], [1, 1]], # O shape + [[0, 1, 0], [1, 1, 1]], # T shape + [[1, 1, 0], [0, 1, 1]], # S shape + [[0, 1, 1], [1, 1, 0]], # Z shape + [[1, 1, 1], [1, 0, 0]], # L shape + [[1, 1, 1], [0, 0, 1]] # J shape +] + +# Function to get the current board state +def get_board_state(board, current_shape, current_pos): + temp_board = [row[:] for row in board] + off_x, off_y = current_pos + for y, row in enumerate(current_shape): + for x, cell in enumerate(row): + if cell: + if 0 <= off_y + y < rows and 0 <= off_x + x < cols: + temp_board[off_y + y][off_x + x] = 1 + return temp_board + +def draw_ledmatrix(board, devices): + for dev in devices: + matrix = [[0 for _ in range(34)] for _ in range(9)] + for y in range(rows): + for x in range(cols): + matrix[x][y] = board[y][x] + ledmatrix.render_matrix(dev, matrix) + #vals = [0 for _ in range(39)] + #send_command(dev, CommandVals.Draw, vals) + +# Function to check if the position is valid +def check_collision(board, shape, offset): + off_x, off_y = offset + for y, row in enumerate(shape): + for x, cell in enumerate(row): + if cell: + if x + off_x < 0 or x + off_x >= cols or y + off_y >= rows: + return True + if y + off_y >= 0 and board[y + off_y][x + off_x]: + return True + return False + +# Function to merge the shape into the board +def merge_shape(board, shape, offset): + off_x, off_y = offset + for y, row in enumerate(shape): + for x, cell in enumerate(row): + if cell: + if 0 <= off_y + y < rows and 0 <= off_x + x < cols: + board[off_y + y][off_x + x] = 1 + +# Function to clear complete rows +def clear_rows(board): + new_board = [row for row in board if any(cell == 0 for cell in row)] + cleared_rows = rows - len(new_board) + while len(new_board) < rows: + new_board.insert(0, [0 for _ in range(cols)]) + return new_board, cleared_rows + +# Function to display the score using blocks +def display_score(board, score): + score_str = str(score) + start_x = cols - len(score_str) * 4 + for i, digit in enumerate(score_str): + if digit.isdigit(): + digit = int(digit) + for y in range(5): + for x in range(3): + if digit_blocks[digit][y][x]: + if y < rows and start_x + i * 4 + x < cols: + board[y][start_x + i * 4 + x] = 1 + +# Digit blocks for representing score +# Each number is represented in a 5x3 block matrix +digit_blocks = [ + [[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0 + [[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3 + [[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6 + [[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9 +] + + +class Ledris: + # Function to draw a grid + def draw_grid(self): + for y in range(rows): + for x in range(cols): + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect, 1) + + # Function to draw the game based on the board state + def draw_board(self, board, devices): + draw_ledmatrix(board, devices) + self.screen.fill(white) + for y in range(rows): + for x in range(cols): + if board[y][x]: + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect) + self.draw_grid() + pygame.display.update() + + # Main game function + def gameLoop(self, devices): + board = [[0 for _ in range(cols)] for _ in range(rows)] + current_shape = random.choice(shapes) + current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display + game_over = False + fall_time = 0 + fall_speed = 500 # Falling speed in milliseconds + score = 0 + + while not game_over: + # Adjust falling speed based on score + fall_speed = max(100, 500 - (score * 10)) + + # Draw the current board state + board_state = get_board_state(board, current_shape, current_pos) + display_score(board_state, score) + self.draw_board(board_state, devices) + + # Event handling + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game_over = True + + if event.type == pygame.KEYDOWN: + if event.key in [pygame.K_LEFT, pygame.K_h]: + new_pos = [current_pos[0] - 1, current_pos[1]] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_RIGHT, pygame.K_l]: + new_pos = [current_pos[0] + 1, current_pos[1]] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_DOWN, pygame.K_j]: + new_pos = [current_pos[0], current_pos[1] + 1] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_UP, pygame.K_k]: + rotated_shape = list(zip(*current_shape[::-1])) + if not check_collision(board, rotated_shape, current_pos): + current_shape = rotated_shape + elif event.key == pygame.K_SPACE: # Hard drop + while not check_collision(board, current_shape, [current_pos[0], current_pos[1] + 1]): + current_pos[1] += 1 + + # Automatic falling + fall_time += self.clock.get_time() + if fall_time >= fall_speed: + fall_time = 0 + new_pos = [current_pos[0], current_pos[1] + 1] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + else: + merge_shape(board, current_shape, current_pos) + board, cleared_rows = clear_rows(board) + score += cleared_rows # Increase score by one for each row cleared + current_shape = random.choice(shapes) + current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display + if check_collision(board, current_shape, current_pos): + game_over = True + + self.clock.tick(30) + + # Flash the screen twice before waiting for restart + for _ in range(2): + for dev in devices: + ledmatrix.percentage(dev, 0) + self.screen.fill(black) + pygame.display.update() + time.sleep(0.3) + + for dev in devices: + ledmatrix.percentage(dev, 100) + self.screen.fill(white) + pygame.display.update() + time.sleep(0.3) + + # Display final score and wait for restart without clearing the screen + board_state = get_board_state(board, current_shape, current_pos) + display_score(board_state, score) + self.draw_board(board_state, devices) + + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + waiting = False + game_over = True + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + waiting = False + if event.key == pygame.K_r: + board = [[0 for _ in range(cols)] for _ in range(rows)] + gameLoop() + + pygame.quit() + quit() + + def __init__(self): + # Initialize pygame + pygame.init() + + # Create the screen + self.screen = pygame.display.set_mode((width, height)) + + # Clock to control the speed of the game + self.clock = pygame.time.Clock() + +def main_devices(devices): + ledris = Ledris() + ledris.gameLoop(devices) + +def main(): + devices = cli.find_devs() + + ledris = Ledris() + ledris.gameLoop(devices) + +if __name__ == "__main__": + main() diff --git a/python/inputmodule/gui/pygames/snake.py b/python/inputmodule/gui/pygames/snake.py new file mode 100644 index 0000000..6568639 --- /dev/null +++ b/python/inputmodule/gui/pygames/snake.py @@ -0,0 +1,254 @@ +import pygame +import random +import time + +from inputmodule import cli +from inputmodule.inputmodule import ledmatrix + +# Set the screen width and height for a 34 x 9 block game +block_width = 20 +block_height = 20 +COLS = 9 +ROWS = 34 + +WIDTH = COLS * block_width +HEIGHT = ROWS * block_height + +# Colors +black = (0, 0, 0) +white = (255, 255, 255) + +def opposite_direction(direction): + if direction == pygame.K_RIGHT: + return pygame.K_LEFT + elif direction == pygame.K_LEFT: + return pygame.K_RIGHT + elif direction == pygame.K_UP: + return pygame.K_DOWN + elif direction == pygame.K_DOWN: + return pygame.K_UP + return direction + +# Function to get the current board state +def get_board_state(board): + temp_board = [row[:] for row in board] + #off_x, off_y = current_pos + #for y, row in enumerate(current_shape): + # for x, cell in enumerate(row): + # if cell: + # if 0 <= off_y + y < ROWS and 0 <= off_x + x < COLS: + # temp_board[off_y + y][off_x + x] = 1 + return temp_board + +def draw_ledmatrix(board, devices): + for dev in devices: + matrix = [[0 for _ in range(34)] for _ in range(9)] + for y in range(ROWS): + for x in range(COLS): + matrix[x][y] = board[y][x] + ledmatrix.render_matrix(dev, matrix) + #vals = [0 for _ in range(39)] + #send_command(dev, CommandVals.Draw, vals) + +# Function to display the score using blocks +def display_score(board, score): + return + score_str = str(score) + start_x = COLS - len(score_str) * 4 + for i, digit in enumerate(score_str): + if digit.isdigit(): + digit = int(digit) + for y in range(5): + for x in range(3): + if digit_blocks[digit][y][x]: + if y < ROWS and start_x + i * 4 + x < COLS: + board[y][start_x + i * 4 + x] = 1 + +# Digit blocks for representing score +# Each number is represented in a 5x3 block matrix +digit_blocks = [ + [[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0 + [[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3 + [[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6 + [[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9 +] + + +class Snake: + # Function to draw a grid + def draw_grid(self): + for y in range(ROWS): + for x in range(COLS): + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect, 1) + + # Function to draw the game based on the board state + def draw_board(self, board, devices): + draw_ledmatrix(board, devices) + self.screen.fill(white) + for y in range(ROWS): + for x in range(COLS): + if board[y][x]: + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect) + self.draw_grid() + pygame.display.update() + + # Main game function + def gameLoop(self, devices): + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + + game_over = False + body = [] + score = 0 + head = (0, 0) + direction = pygame.K_DOWN + food = (0, 0) + while food == head: + food = (random.randint(0, COLS - 1), random.randint(0, ROWS - 1)) + move_time = 0 + + # Setting + # Wrap and let the snake come out the other side + WRAP = False + MOVE_PERIOD = 200 + + while not game_over: + # Draw the current board state + board_state = get_board_state(board) + display_score(board_state, score) + self.draw_board(board_state, devices) + + # Event handling + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game_over = True + + if event.type == pygame.KEYDOWN: + if event.key == opposite_direction(direction) and body: + continue + if event.key in [pygame.K_LEFT, pygame.K_h]: + direction = pygame.K_LEFT + elif event.key in [pygame.K_RIGHT, pygame.K_l]: + direction = pygame.K_RIGHT + elif event.key in [pygame.K_DOWN, pygame.K_j]: + direction = pygame.K_DOWN + elif event.key in [pygame.K_UP, pygame.K_k]: + direction = pygame.K_UP + + move_time += self.clock.get_time() + if move_time >= MOVE_PERIOD: + move_time = 0 + + # Update position + (x, y) = head + oldhead = head + if direction == pygame.K_LEFT: + head = (x - 1, y) + elif direction == pygame.K_RIGHT: + head = (x + 1, y) + elif direction == pygame.K_DOWN: + head = (x, y + 1) + elif direction == pygame.K_UP: + head = (x, y - 1) + + # Detect edge condition + (x, y) = head + if head in body: + game_over = True + elif x >= COLS or x < 0 or y >= ROWS or y < 0: + if WRAP: + if x >= COLS: + x = 0 + elif x < 0: + x = COLS - 1 + elif y >= ROWS: + y = 0 + elif y < 0: + y = ROWS - 1 + head = (x, y) + else: + game_over = True + elif head == food: + body.insert(0, oldhead) + while food == head: + food = (random.randint(0, COLS - 1), + random.randint(0, ROWS - 1)) + elif body: + body.pop() + body.insert(0, oldhead) + + # Draw on screen + if not game_over: + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + board[y][x] = 1 + board[food[1]][food[0]] = 1 + for bodypart in body: + (x, y) = bodypart + board[y][x] = 1 + + self.clock.tick(30) + + # Flash the screen twice before waiting for restart + for _ in range(2): + for dev in devices: + ledmatrix.percentage(dev, 0) + self.screen.fill(black) + pygame.display.update() + time.sleep(0.3) + + for dev in devices: + ledmatrix.percentage(dev, 100) + self.screen.fill(white) + pygame.display.update() + time.sleep(0.3) + + # Display final score and wait for restart without clearing the screen + board_state = get_board_state(board) + display_score(board_state, score) + self.draw_board(board_state, devices) + + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + waiting = False + game_over = True + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + waiting = False + if event.key == pygame.K_r: + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + gameLoop() + + pygame.quit() + quit() + + def __init__(self): + # Initialize pygame + pygame.init() + + # Create the screen + self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) + + # Clock to control the speed of the game + self.clock = pygame.time.Clock() + +def main_devices(devices): + snake = Snake() + snake.gameLoop(devices) + +def main(): + devices = cli.find_devs() + + snake = Snake() + snake.gameLoop(devices) + +if __name__ == "__main__": + main() diff --git a/python/inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py index 616e726..7fb67f8 100644 --- a/python/inputmodule/inputmodule/__init__.py +++ b/python/inputmodule/inputmodule/__init__.py @@ -90,7 +90,7 @@ class GameControlVal(IntEnum): RESPONSE_SIZE = 32 -def bootloader(dev): +def bootloader_jump(dev): """Reboot into the bootloader to flash new firmware""" send_command(dev, CommandVals.BootloaderReset, [0x00]) diff --git a/python/inputmodule/uf2conv.py b/python/inputmodule/uf2conv.py new file mode 100644 index 0000000..e545b69 --- /dev/null +++ b/python/inputmodule/uf2conv.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: MIT +# Copyright (c) Microsoft Corporation and others +# Taken from: https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py +# And modified, some changes already upstreamed + +# yapf: disable +import sys +import struct +import subprocess +import re +import os +import os.path +import argparse +import json + +# Don't even need -b. hex has this embedded +# > ./util/uf2conv.py .build/framework_ansi_default.hex -o ansi.uf2 -b 0x10000000 -f rp2040 --convert --blocks-reserved 1 +# Converted to 222 blocks +# Converted to uf2, output size: 113664, start address: 0x10000000 +# Wrote 113664 bytes to ansi.uf2 +# # 113664 / 512 = 222 +# +# > ./util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert --blocks-offset 222 +# Converted to 1 blocks +# Converted to uf2, output size: 512, start address: 0x100ff000 +# Wrote 512 bytes to serial.uf2 + + + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if (hd[2] & 0x2000) and (currfamilyid == None): + currfamilyid = hd[7] + if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): + currfamilyid = hd[7] + curraddr = newaddr + if familyid == 0x0 or familyid == hd[7]: + appstartaddr = newaddr + print(f" flags: 0x{hd[2]:02x}") + print(f" addr: 0x{hd[3]:02x}") + print(f" len: {hd[4]}") + print(f" block no: {hd[5]}") + print(f" blocks: {hd[6]}") + print(f" size/famid: {hd[7]}") + print() + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10*1024*1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp.append(b"\x00\x00\x00\x00") + if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): + outp.append(block[32 : 32 + datalen]) + curraddr = newaddr + datalen + if hd[2] & 0x2000: + if hd[7] in families_found.keys(): + if families_found[hd[7]] > newaddr: + families_found[hd[7]] = newaddr + else: + families_found[hd[7]] = newaddr + if prev_flag == None: + prev_flag = hd[2] + if prev_flag != hd[2]: + all_flags_same = False + if blockno == (numblocks - 1): + print("--- UF2 File Header Info ---") + families = load_families() + for family_hex in families_found.keys(): + family_short_name = "" + for name, value in families.items(): + if value == family_hex: + family_short_name = name + print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex)) + print("Target Address is 0x{:08x}".format(families_found[family_hex])) + if all_flags_same: + print("All block flag values consistent, 0x{:04x}".format(hd[2])) + else: + print("Flags were not all the same") + print("----------------------------") + if len(families_found) > 1 and familyid == 0x0: + outp = [] + appstartaddr = 0x0 + return b"".join(outp) + +def convert_to_carray(file_content): + outp = "const unsigned long bindata_len = %d;\n" % len(file_content) + outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % file_content[i] + outp += "\n};\n" + return bytes(outp, "utf-8") + +def convert_to_uf2(file_content, blocks_reserved=0, blocks_offset=0): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = [] + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr:ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + rootpath = "/media" + if sys.platform == "darwin": + rootpath = "/Volumes" + elif sys.platform == "linux": + tmp = rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + tmp = "/run" + rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + for d in os.listdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode='r') as file: + file_content = file.read() + return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def load_families(): + # The expectation is that the `uf2families.json` file is in the same + # directory as this script. Make a path that works using `__file__` + # which contains the full path to this script. + filename = "uf2families.json" + pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + with open(pathname) as f: + raw_families = json.load(f) + + families = {} + for family in raw_families: + families[family["short_name"]] = int(family["id"], 0) + + return families + + +def main(): + global appstartaddr, familyid + def error(msg): + print(msg, file=sys.stderr) + sys.exit(1) + parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='input file (HEX, BIN or UF2)') + parser.add_argument('-b' , '--base', dest='base', type=str, + default="0x2000", + help='set base address of application for BIN format (default: 0x2000)') + parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') + parser.add_argument('-d' , '--device', dest="device_path", + help='select a device path to flash') + parser.add_argument('-l' , '--list', action='store_true', + help='list connected devices') + parser.add_argument('-c' , '--convert', action='store_true', + help='do not flash, just convert') + parser.add_argument('-D' , '--deploy', action='store_true', + help='just flash, do not convert') + parser.add_argument('-f' , '--family', dest='family', type=str, + default="0x0", + help='specify familyID - number or name (default: 0x0)') + parser.add_argument('--blocks-offset', dest='blocks_offset', type=str, + default="0x0", + help='TODO') + parser.add_argument('--blocks-reserved', dest='blocks_reserved', type=str, + default="0x0", + help='TODO') + parser.add_argument('-C' , '--carray', action='store_true', + help='convert binary file to a C array, not UF2') + parser.add_argument('-i', '--info', action='store_true', + help='display header information from UF2, do not convert') + args = parser.parse_args() + appstartaddr = int(args.base, 0) + blocks_offset = int(args.blocks_offset, 0) + blocks_reserved = int(args.blocks_reserved, 0) + + families = load_families() + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode='rb') as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2 and not args.info: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif from_uf2 and args.info: + outbuf = "" + convert_from_uf2(inpbuf) + + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), blocks_reserved, blocks_offset) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf, blocks_reserved, blocks_offset) + if not args.deploy and not args.info: + print("Converted to %s, output size: %d, start address: 0x%x" % + (ext, len(outbuf), appstartaddr)) + if args.convert or ext != "uf2": + drives = [] + if args.output == None: + args.output = "flash." + ext + else: + drives = get_drives() + + if args.output: + write_file(args.output, outbuf) + else: + if len(drives) == 0: + error("No drive to deploy.") + if outbuf: + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main() diff --git a/python/pyproject.toml b/python/pyproject.toml index 0111e85..32b4108 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -25,9 +25,10 @@ classifiers = [ ] dependencies = [ "pyserial", - # Optional for GUI + # Optional for CLI "getkey", - "PySimpleGUI", + # Optional for GUI + "pygame", # Optional for image operations "Pillow", ] @@ -36,7 +37,6 @@ dependencies = [ 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] ledmatrixctl = "inputmodule.cli:main_cli" diff --git a/requirements.txt b/python/requirements.txt similarity index 69% rename from requirements.txt rename to python/requirements.txt index 1972de4..52366a8 100644 --- a/requirements.txt +++ b/python/requirements.txt @@ -1,4 +1,4 @@ get-key==1.60.0 Pillow==10.0.0 pyserial==3.5 -PySimpleGUI==4.60.5 +pygame==2.6.1 diff --git a/res/framework_startmenuicon.ico b/res/framework_startmenuicon.ico new file mode 100644 index 0000000..8c6be29 Binary files /dev/null and b/res/framework_startmenuicon.ico differ diff --git a/res/ledmatrixgui-home.png b/res/ledmatrixgui-home.png new file mode 100644 index 0000000..c0a5ca4 Binary files /dev/null and b/res/ledmatrixgui-home.png differ