diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml index 02e2a2b0..cba0a1a5 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 448832fe..105dde7b 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 8df7106b..231ead0d 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/Cargo.lock b/Cargo.lock index 50a4c9f4..afe9fa55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1033,9 +1033,9 @@ dependencies = [ [[package]] name = "is31fl3741" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d02b7bcb58e9f5ef8562cec1914198f71a35aa8f90e23326b77d12502ab860" +checksum = "dc9abf02bbdd939fe7f46924002c004d8ab811f093392d8e354e8f583a40badf" dependencies = [ "embedded-graphics-core", "embedded-hal", diff --git a/Cargo.toml b/Cargo.toml index a8e11c48..cf32392a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,7 @@ heapless = "0.7.16" usbd-serial = "0.1.1" usbd-hid = "0.6.1" fugit = "0.3.7" -# LED Matrix -is31fl3741 = "0.3.0" +is31fl3741 = { version = "0.4.0", features = ["framework_ledmatrix"] } # B1 Display st7306 = { git = "https://github.com/FrameworkComputer/st7306-rs", branch = "update-deps" } embedded-graphics = "0.8" diff --git a/README.md b/README.md index 53bb6c0a..a6c102b9 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,20 @@ Features that all modules share 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). +[GH Actions](https://github.com/FrameworkComputer/inputmodule-rs/actions) run or +the [release page](https://github.com/FrameworkComputer/inputmodule-rs/releases). 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: -``` +```sh sudo cp release/50-framework-inputmodule.rules /etc/udev/rules.d/ sudo udevadm control --reload && sudo udevadm trigger ``` @@ -72,7 +76,7 @@ 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. -``` +```sh # Example on Linux > inputmodule-control --serial-dev /dev/ttyACM0 b1-display --pattern black @@ -88,7 +92,7 @@ connected and then send the command. ``` > inputmodule-control b1-display --pattern black -Failed to find serial devivce. Please manually specify with --serial-dev +Failed to find serial device. 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 @@ -105,7 +109,7 @@ Device already present. No need to wait. Not executing command. 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: +by using one of the following commands: ```sh inputmodule-control led-matrix --bootloader @@ -125,13 +129,15 @@ cargo run -p c1minimal ## Building the firmware -Dependencies: Rust +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev Prepare Rust toolchain (once): ```sh rustup target install thumbv6m-none-eabi cargo install flip-link +cargo install cargo-make +cargo install elf2uf2-rs ``` Build: @@ -142,7 +148,7 @@ cargo make --cwd b1display cargo make --cwd c1minimal ``` -Generate the UF2 update file: +Generate the UF2 update file into `target/thumbv6m-none-eabi/release/`: ```sh cargo make --cwd ledmatrix uf2 @@ -152,12 +158,15 @@ cargo make --cwd c1minimal uf2 ## Building the Application -Dependencies: Rust, pkg-config, libudev +Dependencies: [Rust/rustup](https://rustup.rs/), 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 -``` +```sh +# Install cargo-make to help build it +cargo install cargo-make + # Build it > cargo make --cwd inputmodule-control diff --git a/commands.md b/commands.md index 00ddfb66..46322edb 100644 --- a/commands.md +++ b/commands.md @@ -9,8 +9,8 @@ Simple example in Python: ```python import serial -def send_command(command_id, parameters, with_response=False) - with serial.Serial(/dev/ttyACM0, 115200) as s: +def send_command(command_id, parameters, with_response=False): + with serial.Serial("/dev/ttyACM0", 115200) as s: s.write([0x32, 0xAC, command_id] + parameters) if with_response: @@ -20,7 +20,7 @@ def send_command(command_id, parameters, with_response=False) # Go to sleep and check the status send_command(0x03, [True]) res = send_command(0x03, [], with_response=True) -print(f"Is currently sleeping: {bool(res[0])}) +print(f"Is currently sleeping: {bool(res[0])}") ``` Many commands support setting and writing a value, with the same command ID. @@ -56,7 +56,7 @@ When no parameters are given, the current value is queried and returned. | InvertScreen | 0x15 | ` D ` | | bool | Invert scren on/off | | SetPxCol | 0x16 | ` D ` | | 50 Bytes | Send a column of pixels | | FlushFB | 0x17 | ` D ` | | | Flush all columns | -| Version | 0x20 | ` D ` | 3 Bytes | | Get firmware version | +| Version | 0x20 | `LDM` | 3 Bytes | | Get firmware version | #### Pattern (0x01) @@ -93,8 +93,15 @@ TODO Response: -``` +```plain Byte 0: USB bcdDevice MSB Byte 1: USB bcdDevice LSB Byte 2: 1 if pre-release version, 0 otherwise + ++-- Major version +| +-- Minor version +| | +-- Patch version +| | | +-- 1 if is pre-release, +| | | | 0 otherwise +MMMMMMMM mmmmPPPP 0000000p ``` diff --git a/fl16-inputmodules/src/control.rs b/fl16-inputmodules/src/control.rs index 23875bdd..ed1fbd32 100644 --- a/fl16-inputmodules/src/control.rs +++ b/fl16-inputmodules/src/control.rs @@ -202,7 +202,9 @@ pub enum Command { GetFps, SetPowerMode(u8), GetPowerMode, + /// Set the animation period in milliseconds SetAnimationPeriod(u16), + /// Get the animation period in milliseconds GetAnimationPeriod, #[cfg(feature = "ledmatrix")] SetPwmFreq(PwmFreqArg), @@ -249,6 +251,7 @@ pub struct B1DIsplayState { pub screensaver: Option, pub power_mode: PowerMode, pub fps_config: FpsConfig, + /// Animation period in microseconds pub animation_period: u64, } diff --git a/fl16-inputmodules/src/fl16.rs b/fl16-inputmodules/src/fl16.rs index 8b9f042c..4a6bdd43 100644 --- a/fl16-inputmodules/src/fl16.rs +++ b/fl16-inputmodules/src/fl16.rs @@ -1,13 +1,3 @@ -// #[cfg_attr(docsrs, doc(cfg(feature = "adafruit_rgb_13x9")))] -#[allow(unused_imports)] -use core::convert::TryFrom; -#[allow(unused_imports)] -use embedded_hal::blocking::delay::DelayMs; -use embedded_hal::blocking::i2c::Read; -use embedded_hal::blocking::i2c::Write; -#[allow(unused_imports)] -use is31fl3741::{Error, IS31FL3741}; - pub const EVT_CALC_PIXEL: fn(x: u8, y: u8) -> (u8, u8) = |x: u8, y: u8| -> (u8, u8) { // Generated by led-matrix.py let lookup: [(u8, u8); 34 * 9] = [ @@ -325,365 +315,3 @@ pub const EVT_CALC_PIXEL: fn(x: u8, y: u8) -> (u8, u8) = |x: u8, y: u8| -> (u8, (0x00, 0) } }; -pub const DVT2_CALC_PIXEL: fn(x: u8, y: u8) -> (u8, u8) = |x: u8, y: u8| -> (u8, u8) { - // Generated by led-matrix.py - let lookup: [(u8, u8); 34 * 9] = [ - (0x00, 0), // x: 1, y: 1, sw: 1, cs: 1, id: 1 - (0x1e, 0), // x: 2, y: 1, sw: 2, cs: 1, id: 2 - (0x3c, 0), // x: 3, y: 1, sw: 3, cs: 1, id: 3 - (0x5a, 0), // x: 4, y: 1, sw: 4, cs: 1, id: 4 - (0x78, 0), // x: 5, y: 1, sw: 5, cs: 1, id: 5 - (0x96, 0), // x: 6, y: 1, sw: 6, cs: 1, id: 6 - (0x00, 1), // x: 7, y: 1, sw: 7, cs: 1, id: 7 - (0x1e, 1), // x: 8, y: 1, sw: 8, cs: 1, id: 8 - (0x5f, 1), // x: 9, y: 1, sw: 1, cs:36, id: 9 - (0x01, 0), // x: 1, y: 2, sw: 1, cs: 2, id: 10 - (0x1f, 0), // x: 2, y: 2, sw: 2, cs: 2, id: 11 - (0x3d, 0), // x: 3, y: 2, sw: 3, cs: 2, id: 12 - (0x5b, 0), // x: 4, y: 2, sw: 4, cs: 2, id: 13 - (0x79, 0), // x: 5, y: 2, sw: 5, cs: 2, id: 14 - (0x97, 0), // x: 6, y: 2, sw: 6, cs: 2, id: 15 - (0x01, 1), // x: 7, y: 2, sw: 7, cs: 2, id: 16 - (0x1f, 1), // x: 8, y: 2, sw: 8, cs: 2, id: 17 - (0x60, 1), // x: 9, y: 2, sw: 1, cs:37, id: 18 - (0x02, 0), // x: 1, y: 3, sw: 1, cs: 3, id: 19 - (0x20, 0), // x: 2, y: 3, sw: 2, cs: 3, id: 20 - (0x3e, 0), // x: 3, y: 3, sw: 3, cs: 3, id: 21 - (0x5c, 0), // x: 4, y: 3, sw: 4, cs: 3, id: 22 - (0x7a, 0), // x: 5, y: 3, sw: 5, cs: 3, id: 23 - (0x98, 0), // x: 6, y: 3, sw: 6, cs: 3, id: 24 - (0x02, 1), // x: 7, y: 3, sw: 7, cs: 3, id: 25 - (0x20, 1), // x: 8, y: 3, sw: 8, cs: 3, id: 26 - (0x61, 1), // x: 9, y: 3, sw: 1, cs:38, id: 27 - (0x03, 0), // x: 1, y: 4, sw: 1, cs: 4, id: 28 - (0x21, 0), // x: 2, y: 4, sw: 2, cs: 4, id: 29 - (0x3f, 0), // x: 3, y: 4, sw: 3, cs: 4, id: 30 - (0x5d, 0), // x: 4, y: 4, sw: 4, cs: 4, id: 31 - (0x7b, 0), // x: 5, y: 4, sw: 5, cs: 4, id: 32 - (0x99, 0), // x: 6, y: 4, sw: 6, cs: 4, id: 33 - (0x03, 1), // x: 7, y: 4, sw: 7, cs: 4, id: 34 - (0x21, 1), // x: 8, y: 4, sw: 8, cs: 4, id: 35 - (0x62, 1), // x: 9, y: 4, sw: 1, cs:39, id: 36 - (0x04, 0), // x: 1, y: 5, sw: 1, cs: 5, id: 37 - (0x22, 0), // x: 2, y: 5, sw: 2, cs: 5, id: 41 - (0x40, 0), // x: 3, y: 5, sw: 3, cs: 5, id: 45 - (0x5e, 0), // x: 4, y: 5, sw: 4, cs: 5, id: 49 - (0x7c, 0), // x: 5, y: 5, sw: 5, cs: 5, id: 53 - (0x9a, 0), // x: 6, y: 5, sw: 6, cs: 5, id: 57 - (0x04, 1), // x: 7, y: 5, sw: 7, cs: 5, id: 61 - (0x22, 1), // x: 8, y: 5, sw: 8, cs: 5, id: 65 - (0x5e, 1), // x: 9, y: 5, sw: 1, cs:35, id: 69 - (0x05, 0), // x: 1, y: 6, sw: 1, cs: 6, id: 38 - (0x23, 0), // x: 2, y: 6, sw: 2, cs: 6, id: 42 - (0x41, 0), // x: 3, y: 6, sw: 3, cs: 6, id: 46 - (0x5f, 0), // x: 4, y: 6, sw: 4, cs: 6, id: 50 - (0x7d, 0), // x: 5, y: 6, sw: 5, cs: 6, id: 54 - (0x9b, 0), // x: 6, y: 6, sw: 6, cs: 6, id: 58 - (0x05, 1), // x: 7, y: 6, sw: 7, cs: 6, id: 62 - (0x23, 1), // x: 8, y: 6, sw: 8, cs: 6, id: 66 - (0x68, 1), // x: 9, y: 6, sw: 2, cs:36, id: 70 - (0x06, 0), // x: 1, y: 7, sw: 1, cs: 7, id: 39 - (0x24, 0), // x: 2, y: 7, sw: 2, cs: 7, id: 43 - (0x42, 0), // x: 3, y: 7, sw: 3, cs: 7, id: 47 - (0x60, 0), // x: 4, y: 7, sw: 4, cs: 7, id: 51 - (0x7e, 0), // x: 5, y: 7, sw: 5, cs: 7, id: 55 - (0x9c, 0), // x: 6, y: 7, sw: 6, cs: 7, id: 59 - (0x06, 1), // x: 7, y: 7, sw: 7, cs: 7, id: 63 - (0x24, 1), // x: 8, y: 7, sw: 8, cs: 7, id: 67 - (0x69, 1), // x: 9, y: 7, sw: 2, cs:37, id: 71 - (0x07, 0), // x: 1, y: 8, sw: 1, cs: 8, id: 40 - (0x25, 0), // x: 2, y: 8, sw: 2, cs: 8, id: 44 - (0x43, 0), // x: 3, y: 8, sw: 3, cs: 8, id: 48 - (0x61, 0), // x: 4, y: 8, sw: 4, cs: 8, id: 52 - (0x7f, 0), // x: 5, y: 8, sw: 5, cs: 8, id: 56 - (0x9d, 0), // x: 6, y: 8, sw: 6, cs: 8, id: 60 - (0x07, 1), // x: 7, y: 8, sw: 7, cs: 8, id: 64 - (0x25, 1), // x: 8, y: 8, sw: 8, cs: 8, id: 68 - (0x6a, 1), // x: 9, y: 8, sw: 2, cs:38, id: 72 - (0x08, 0), // x: 1, y: 9, sw: 1, cs: 9, id: 73 - (0x26, 0), // x: 2, y: 9, sw: 2, cs: 9, id: 81 - (0x44, 0), // x: 3, y: 9, sw: 3, cs: 9, id: 89 - (0x62, 0), // x: 4, y: 9, sw: 4, cs: 9, id: 97 - (0x80, 0), // x: 5, y: 9, sw: 5, cs: 9, id:105 - (0x9e, 0), // x: 6, y: 9, sw: 6, cs: 9, id:113 - (0x08, 1), // x: 7, y: 9, sw: 7, cs: 9, id:121 - (0x26, 1), // x: 8, y: 9, sw: 8, cs: 9, id:129 - (0x6b, 1), // x: 9, y: 9, sw: 2, cs:39, id:137 - (0x09, 0), // x: 1, y:10, sw: 1, cs:10, id: 74 - (0x27, 0), // x: 2, y:10, sw: 2, cs:10, id: 82 - (0x45, 0), // x: 3, y:10, sw: 3, cs:10, id: 90 - (0x63, 0), // x: 4, y:10, sw: 4, cs:10, id: 98 - (0x81, 0), // x: 5, y:10, sw: 5, cs:10, id:106 - (0x9f, 0), // x: 6, y:10, sw: 6, cs:10, id:114 - (0x09, 1), // x: 7, y:10, sw: 7, cs:10, id:122 - (0x27, 1), // x: 8, y:10, sw: 8, cs:10, id:130 - (0x67, 1), // x: 9, y:10, sw: 2, cs:35, id:138 - (0x0a, 0), // x: 1, y:11, sw: 1, cs:11, id: 75 - (0x28, 0), // x: 2, y:11, sw: 2, cs:11, id: 83 - (0x46, 0), // x: 3, y:11, sw: 3, cs:11, id: 91 - (0x64, 0), // x: 4, y:11, sw: 4, cs:11, id: 99 - (0x82, 0), // x: 5, y:11, sw: 5, cs:11, id:107 - (0xa0, 0), // x: 6, y:11, sw: 6, cs:11, id:115 - (0x0a, 1), // x: 7, y:11, sw: 7, cs:11, id:123 - (0x28, 1), // x: 8, y:11, sw: 8, cs:11, id:131 - (0x71, 1), // x: 9, y:11, sw: 3, cs:36, id:139 - (0x0b, 0), // x: 1, y:12, sw: 1, cs:12, id: 76 - (0x29, 0), // x: 2, y:12, sw: 2, cs:12, id: 84 - (0x47, 0), // x: 3, y:12, sw: 3, cs:12, id: 92 - (0x65, 0), // x: 4, y:12, sw: 4, cs:12, id:100 - (0x83, 0), // x: 5, y:12, sw: 5, cs:12, id:108 - (0xa1, 0), // x: 6, y:12, sw: 6, cs:12, id:116 - (0x0b, 1), // x: 7, y:12, sw: 7, cs:12, id:124 - (0x29, 1), // x: 8, y:12, sw: 8, cs:12, id:132 - (0x72, 1), // x: 9, y:12, sw: 3, cs:37, id:140 - (0x0c, 0), // x: 1, y:13, sw: 1, cs:13, id: 77 - (0x2a, 0), // x: 2, y:13, sw: 2, cs:13, id: 85 - (0x48, 0), // x: 3, y:13, sw: 3, cs:13, id: 93 - (0x66, 0), // x: 4, y:13, sw: 4, cs:13, id:101 - (0x84, 0), // x: 5, y:13, sw: 5, cs:13, id:109 - (0xa2, 0), // x: 6, y:13, sw: 6, cs:13, id:117 - (0x0c, 1), // x: 7, y:13, sw: 7, cs:13, id:125 - (0x2a, 1), // x: 8, y:13, sw: 8, cs:13, id:133 - (0x73, 1), // x: 9, y:13, sw: 3, cs:38, id:141 - (0x0d, 0), // x: 1, y:14, sw: 1, cs:14, id: 78 - (0x2b, 0), // x: 2, y:14, sw: 2, cs:14, id: 86 - (0x49, 0), // x: 3, y:14, sw: 3, cs:14, id: 94 - (0x67, 0), // x: 4, y:14, sw: 4, cs:14, id:102 - (0x85, 0), // x: 5, y:14, sw: 5, cs:14, id:110 - (0xa3, 0), // x: 6, y:14, sw: 6, cs:14, id:118 - (0x0d, 1), // x: 7, y:14, sw: 7, cs:14, id:126 - (0x2b, 1), // x: 8, y:14, sw: 8, cs:14, id:134 - (0x70, 1), // x: 9, y:14, sw: 3, cs:35, id:142 - (0x0e, 0), // x: 1, y:15, sw: 1, cs:15, id: 79 - (0x2c, 0), // x: 2, y:15, sw: 2, cs:15, id: 87 - (0x4a, 0), // x: 3, y:15, sw: 3, cs:15, id: 95 - (0x68, 0), // x: 4, y:15, sw: 4, cs:15, id:103 - (0x86, 0), // x: 5, y:15, sw: 5, cs:15, id:111 - (0xa4, 0), // x: 6, y:15, sw: 6, cs:15, id:119 - (0x0e, 1), // x: 7, y:15, sw: 7, cs:15, id:127 - (0x2c, 1), // x: 8, y:15, sw: 8, cs:15, id:135 - (0x7a, 1), // x: 9, y:15, sw: 4, cs:36, id:143 - (0x0f, 0), // x: 1, y:16, sw: 1, cs:16, id: 80 - (0x2d, 0), // x: 2, y:16, sw: 2, cs:16, id: 88 - (0x4b, 0), // x: 3, y:16, sw: 3, cs:16, id: 96 - (0x69, 0), // x: 4, y:16, sw: 4, cs:16, id:104 - (0x87, 0), // x: 5, y:16, sw: 5, cs:16, id:112 - (0xa5, 0), // x: 6, y:16, sw: 6, cs:16, id:120 - (0x0f, 1), // x: 7, y:16, sw: 7, cs:16, id:128 - (0x2d, 1), // x: 8, y:16, sw: 8, cs:16, id:136 - (0x7b, 1), // x: 9, y:16, sw: 4, cs:37, id:144 - (0x10, 0), // x: 1, y:17, sw: 1, cs:17, id:145 - (0x2e, 0), // x: 2, y:17, sw: 2, cs:17, id:161 - (0x4c, 0), // x: 3, y:17, sw: 3, cs:17, id:177 - (0x6a, 0), // x: 4, y:17, sw: 4, cs:17, id:193 - (0x88, 0), // x: 5, y:17, sw: 5, cs:17, id:209 - (0xa6, 0), // x: 6, y:17, sw: 6, cs:17, id:225 - (0x10, 1), // x: 7, y:17, sw: 7, cs:17, id:241 - (0x2e, 1), // x: 8, y:17, sw: 8, cs:17, id:257 - (0x7c, 1), // x: 9, y:17, sw: 4, cs:38, id:273 - (0x11, 0), // x: 1, y:18, sw: 1, cs:18, id:146 - (0x2f, 0), // x: 2, y:18, sw: 2, cs:18, id:162 - (0x4d, 0), // x: 3, y:18, sw: 3, cs:18, id:178 - (0x6b, 0), // x: 4, y:18, sw: 4, cs:18, id:194 - (0x89, 0), // x: 5, y:18, sw: 5, cs:18, id:210 - (0xa7, 0), // x: 6, y:18, sw: 6, cs:18, id:226 - (0x11, 1), // x: 7, y:18, sw: 7, cs:18, id:242 - (0x2f, 1), // x: 8, y:18, sw: 8, cs:18, id:258 - (0x79, 1), // x: 9, y:18, sw: 4, cs:35, id:274 - (0x12, 0), // x: 1, y:19, sw: 1, cs:19, id:147 - (0x30, 0), // x: 2, y:19, sw: 2, cs:19, id:163 - (0x4e, 0), // x: 3, y:19, sw: 3, cs:19, id:179 - (0x6c, 0), // x: 4, y:19, sw: 4, cs:19, id:195 - (0x8a, 0), // x: 5, y:19, sw: 5, cs:19, id:211 - (0xa8, 0), // x: 6, y:19, sw: 6, cs:19, id:227 - (0x12, 1), // x: 7, y:19, sw: 7, cs:19, id:243 - (0x30, 1), // x: 8, y:19, sw: 8, cs:19, id:259 - (0x83, 1), // x: 9, y:19, sw: 5, cs:36, id:275 - (0x13, 0), // x: 1, y:20, sw: 1, cs:20, id:148 - (0x31, 0), // x: 2, y:20, sw: 2, cs:20, id:164 - (0x4f, 0), // x: 3, y:20, sw: 3, cs:20, id:180 - (0x6d, 0), // x: 4, y:20, sw: 4, cs:20, id:196 - (0x8b, 0), // x: 5, y:20, sw: 5, cs:20, id:212 - (0xa9, 0), // x: 6, y:20, sw: 6, cs:20, id:228 - (0x13, 1), // x: 7, y:20, sw: 7, cs:20, id:244 - (0x31, 1), // x: 8, y:20, sw: 8, cs:20, id:260 - (0x84, 1), // x: 9, y:20, sw: 5, cs:37, id:276 - (0x14, 0), // x: 1, y:21, sw: 1, cs:21, id:149 - (0x32, 0), // x: 2, y:21, sw: 2, cs:21, id:165 - (0x50, 0), // x: 3, y:21, sw: 3, cs:21, id:181 - (0x6e, 0), // x: 4, y:21, sw: 4, cs:21, id:197 - (0x8c, 0), // x: 5, y:21, sw: 5, cs:21, id:213 - (0xaa, 0), // x: 6, y:21, sw: 6, cs:21, id:229 - (0x14, 1), // x: 7, y:21, sw: 7, cs:21, id:245 - (0x32, 1), // x: 8, y:21, sw: 8, cs:21, id:261 - (0x85, 1), // x: 9, y:21, sw: 5, cs:38, id:277 - (0x15, 0), // x: 1, y:22, sw: 1, cs:22, id:150 - (0x33, 0), // x: 2, y:22, sw: 2, cs:22, id:166 - (0x51, 0), // x: 3, y:22, sw: 3, cs:22, id:182 - (0x6f, 0), // x: 4, y:22, sw: 4, cs:22, id:198 - (0x8d, 0), // x: 5, y:22, sw: 5, cs:22, id:214 - (0xab, 0), // x: 6, y:22, sw: 6, cs:22, id:230 - (0x15, 1), // x: 7, y:22, sw: 7, cs:22, id:246 - (0x33, 1), // x: 8, y:22, sw: 8, cs:22, id:262 - (0x82, 1), // x: 9, y:22, sw: 5, cs:35, id:278 - (0x16, 0), // x: 1, y:23, sw: 1, cs:23, id:151 - (0x34, 0), // x: 2, y:23, sw: 2, cs:23, id:167 - (0x52, 0), // x: 3, y:23, sw: 3, cs:23, id:183 - (0x70, 0), // x: 4, y:23, sw: 4, cs:23, id:199 - (0x8e, 0), // x: 5, y:23, sw: 5, cs:23, id:215 - (0xac, 0), // x: 6, y:23, sw: 6, cs:23, id:231 - (0x16, 1), // x: 7, y:23, sw: 7, cs:23, id:247 - (0x34, 1), // x: 8, y:23, sw: 8, cs:23, id:263 - (0x8c, 1), // x: 9, y:23, sw: 6, cs:36, id:279 - (0x17, 0), // x: 1, y:24, sw: 1, cs:24, id:152 - (0x35, 0), // x: 2, y:24, sw: 2, cs:24, id:168 - (0x53, 0), // x: 3, y:24, sw: 3, cs:24, id:184 - (0x71, 0), // x: 4, y:24, sw: 4, cs:24, id:200 - (0x8f, 0), // x: 5, y:24, sw: 5, cs:24, id:216 - (0xad, 0), // x: 6, y:24, sw: 6, cs:24, id:232 - (0x17, 1), // x: 7, y:24, sw: 7, cs:24, id:248 - (0x35, 1), // x: 8, y:24, sw: 8, cs:24, id:264 - (0x8d, 1), // x: 9, y:24, sw: 6, cs:37, id:280 - (0x18, 0), // x: 1, y:25, sw: 1, cs:25, id:153 - (0x36, 0), // x: 2, y:25, sw: 2, cs:25, id:169 - (0x54, 0), // x: 3, y:25, sw: 3, cs:25, id:185 - (0x72, 0), // x: 4, y:25, sw: 4, cs:25, id:201 - (0x90, 0), // x: 5, y:25, sw: 5, cs:25, id:217 - (0xae, 0), // x: 6, y:25, sw: 6, cs:25, id:233 - (0x18, 1), // x: 7, y:25, sw: 7, cs:25, id:249 - (0x36, 1), // x: 8, y:25, sw: 8, cs:25, id:265 - (0x8e, 1), // x: 9, y:25, sw: 6, cs:38, id:281 - (0x19, 0), // x: 1, y:26, sw: 1, cs:26, id:154 - (0x37, 0), // x: 2, y:26, sw: 2, cs:26, id:170 - (0x55, 0), // x: 3, y:26, sw: 3, cs:26, id:186 - (0x73, 0), // x: 4, y:26, sw: 4, cs:26, id:202 - (0x91, 0), // x: 5, y:26, sw: 5, cs:26, id:218 - (0xaf, 0), // x: 6, y:26, sw: 6, cs:26, id:234 - (0x19, 1), // x: 7, y:26, sw: 7, cs:26, id:250 - (0x37, 1), // x: 8, y:26, sw: 8, cs:26, id:266 - (0x8b, 1), // x: 9, y:26, sw: 6, cs:35, id:282 - (0x1a, 0), // x: 1, y:27, sw: 1, cs:27, id:155 - (0x38, 0), // x: 2, y:27, sw: 2, cs:27, id:171 - (0x56, 0), // x: 3, y:27, sw: 3, cs:27, id:187 - (0x74, 0), // x: 4, y:27, sw: 4, cs:27, id:203 - (0x92, 0), // x: 5, y:27, sw: 5, cs:27, id:219 - (0xb0, 0), // x: 6, y:27, sw: 6, cs:27, id:235 - (0x1a, 1), // x: 7, y:27, sw: 7, cs:27, id:251 - (0x38, 1), // x: 8, y:27, sw: 8, cs:27, id:267 - (0x95, 1), // x: 9, y:27, sw: 7, cs:36, id:283 - (0x1b, 0), // x: 1, y:28, sw: 1, cs:28, id:156 - (0x39, 0), // x: 2, y:28, sw: 2, cs:28, id:172 - (0x57, 0), // x: 3, y:28, sw: 3, cs:28, id:188 - (0x75, 0), // x: 4, y:28, sw: 4, cs:28, id:204 - (0x93, 0), // x: 5, y:28, sw: 5, cs:28, id:220 - (0xb1, 0), // x: 6, y:28, sw: 6, cs:28, id:236 - (0x1b, 1), // x: 7, y:28, sw: 7, cs:28, id:252 - (0x39, 1), // x: 8, y:28, sw: 8, cs:28, id:268 - (0x96, 1), // x: 9, y:28, sw: 7, cs:37, id:284 - (0x1c, 0), // x: 1, y:29, sw: 1, cs:29, id:157 - (0x3a, 0), // x: 2, y:29, sw: 2, cs:29, id:173 - (0x58, 0), // x: 3, y:29, sw: 3, cs:29, id:189 - (0x76, 0), // x: 4, y:29, sw: 4, cs:29, id:205 - (0x94, 0), // x: 5, y:29, sw: 5, cs:29, id:221 - (0xb2, 0), // x: 6, y:29, sw: 6, cs:29, id:237 - (0x1c, 1), // x: 7, y:29, sw: 7, cs:29, id:253 - (0x3a, 1), // x: 8, y:29, sw: 8, cs:29, id:269 - (0x97, 1), // x: 9, y:29, sw: 7, cs:38, id:285 - (0x1d, 0), // x: 1, y:30, sw: 1, cs:30, id:158 - (0x3b, 0), // x: 2, y:30, sw: 2, cs:30, id:174 - (0x59, 0), // x: 3, y:30, sw: 3, cs:30, id:190 - (0x77, 0), // x: 4, y:30, sw: 4, cs:30, id:206 - (0x95, 0), // x: 5, y:30, sw: 5, cs:30, id:222 - (0xb3, 0), // x: 6, y:30, sw: 6, cs:30, id:238 - (0x1d, 1), // x: 7, y:30, sw: 7, cs:30, id:254 - (0x3b, 1), // x: 8, y:30, sw: 8, cs:30, id:270 - (0x94, 1), // x: 9, y:30, sw: 7, cs:35, id:286 - (0x5a, 1), // x: 1, y:31, sw: 1, cs:31, id:159 - (0x63, 1), // x: 2, y:31, sw: 2, cs:31, id:175 - (0x6c, 1), // x: 3, y:31, sw: 3, cs:31, id:191 - (0x75, 1), // x: 4, y:31, sw: 4, cs:31, id:207 - (0x7e, 1), // x: 5, y:31, sw: 5, cs:31, id:223 - (0x87, 1), // x: 6, y:31, sw: 6, cs:31, id:239 - (0x90, 1), // x: 7, y:31, sw: 7, cs:31, id:255 - (0x99, 1), // x: 8, y:31, sw: 8, cs:31, id:271 - (0x9e, 1), // x: 9, y:31, sw: 8, cs:36, id:287 - (0x5b, 1), // x: 1, y:32, sw: 1, cs:32, id:160 - (0x64, 1), // x: 2, y:32, sw: 2, cs:32, id:176 - (0x6d, 1), // x: 3, y:32, sw: 3, cs:32, id:192 - (0x76, 1), // x: 4, y:32, sw: 4, cs:32, id:208 - (0x7f, 1), // x: 5, y:32, sw: 5, cs:32, id:224 - (0x88, 1), // x: 6, y:32, sw: 6, cs:32, id:240 - (0x91, 1), // x: 7, y:32, sw: 7, cs:32, id:256 - (0x9a, 1), // x: 8, y:32, sw: 8, cs:32, id:272 - (0x9f, 1), // x: 9, y:32, sw: 8, cs:37, id:288 - (0x5c, 1), // x: 1, y:33, sw: 1, cs:33, id:289 - (0x65, 1), // x: 2, y:33, sw: 2, cs:33, id:290 - (0x6e, 1), // x: 3, y:33, sw: 3, cs:33, id:291 - (0x77, 1), // x: 4, y:33, sw: 4, cs:33, id:292 - (0x80, 1), // x: 5, y:33, sw: 5, cs:33, id:293 - (0x89, 1), // x: 6, y:33, sw: 6, cs:33, id:294 - (0x92, 1), // x: 7, y:33, sw: 7, cs:33, id:295 - (0x9b, 1), // x: 8, y:33, sw: 8, cs:33, id:296 - (0xa0, 1), // x: 9, y:33, sw: 8, cs:38, id:297 - (0x5d, 1), // x: 1, y:34, sw: 1, cs:34, id:298 - (0x66, 1), // x: 2, y:34, sw: 2, cs:34, id:299 - (0x6f, 1), // x: 3, y:34, sw: 3, cs:34, id:300 - (0x78, 1), // x: 4, y:34, sw: 4, cs:34, id:301 - (0x81, 1), // x: 5, y:34, sw: 5, cs:34, id:302 - (0x8a, 1), // x: 6, y:34, sw: 6, cs:34, id:303 - (0x93, 1), // x: 7, y:34, sw: 7, cs:34, id:304 - (0x9c, 1), // x: 8, y:34, sw: 8, cs:34, id:305 - (0x9d, 1), // x: 9, y:34, sw: 8, cs:35, id:306 - ]; - let index: usize = (x as usize) + (y as usize) * 9; - if index < lookup.len() { - lookup[index] - } else { - (0x00, 0) - } -}; - -pub struct LedMatrix { - pub device: IS31FL3741, -} - -impl LedMatrix -where - I2C: Write, - I2C: Read, -{ - pub fn unwrap(self) -> I2C { - self.device.i2c - } - - // TODO: Maybe make this private and set it once in the constructor - pub fn set_scaling(&mut self, scale: u8) -> Result<(), I2cError> { - self.device.set_scaling(scale) - } - - pub fn new(i2c: I2C, calc_pixel: fn(x: u8, y: u8) -> (u8, u8)) -> LedMatrix { - LedMatrix { - device: IS31FL3741 { - i2c, - address: 0x30, - width: 9, - height: 34, - calc_pixel, - }, - } - } - - pub fn setup>(&mut self, delay: &mut DEL) -> Result<(), Error> { - self.device.setup(delay)?; - Ok(()) - } - - pub fn fill_brightness(&mut self, brightness: u8) -> Result<(), Error> { - for x in 0..self.device.width { - for y in 0..self.device.height { - self.device.pixel(x, y, brightness)?; - } - } - Ok(()) - } -} diff --git a/fl16-inputmodules/src/matrix.rs b/fl16-inputmodules/src/matrix.rs index 8046db40..71a625fd 100644 --- a/fl16-inputmodules/src/matrix.rs +++ b/fl16-inputmodules/src/matrix.rs @@ -37,6 +37,7 @@ pub struct LedmatrixState { pub sleeping: SleepState, /// State of the current game, if any pub game: Option, + /// Animation period in microseconds pub animation_period: u64, /// Current LED PWM frequency pub pwm_freq: PwmFreqArg, diff --git a/fl16-inputmodules/src/patterns.rs b/fl16-inputmodules/src/patterns.rs index 37f239b9..5ed6fc9b 100644 --- a/fl16-inputmodules/src/patterns.rs +++ b/fl16-inputmodules/src/patterns.rs @@ -3,10 +3,10 @@ use rp2040_hal::{ pac::I2C1, }; -use crate::fl16::LedMatrix; use crate::led_hal as bsp; use crate::mapping::*; use crate::matrix::*; +use is31fl3741::devices::LedMatrix; /// Bytes needed to represent all LEDs with a single bit /// math.ceil(WIDTH * HEIGHT / 8) diff --git a/inputmodule-control/src/font.rs b/inputmodule-control/src/font.rs index fc06f30b..49c9db8d 100644 --- a/inputmodule-control/src/font.rs +++ b/inputmodule-control/src/font.rs @@ -388,7 +388,7 @@ pub fn convert_font(c: char) -> Vec { 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, ], 'G' => vec![ 0, 1, 1, 1, 0, diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 25baa57e..d2d4e83c 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -90,6 +90,12 @@ fn match_serialdevs( // Find all supported Framework devices for p in ports { if let SerialPortType::UsbPort(usbinfo) = &p.port_type { + // macOS creates a /dev/cu.* and /dev/tty.* device. + // The latter can only be used for reading, not writing, so we have to ignore it. + #[cfg(target_os = "macos")] + if !p.port_name.starts_with("/dev/tty.") { + continue; + } if usbinfo.vid == FRAMEWORK_VID && pids.contains(&usbinfo.pid) { compatible_devs.push(p.port_name.clone()); } @@ -154,7 +160,7 @@ pub fn find_serialdevs(args: &crate::ClapCli, wait_for_device: bool) -> (Vec, bool) = find_serialdevs(args, args.wait_for_device); if serialdevs.is_empty() { - println!("Failed to find serial devivce. Please manually specify with --serial-dev"); + println!("Failed to find serial device. Please manually specify with --serial-dev"); return; } else if args.wait_for_device && !waited { println!("Device already present. No need to wait. Not executing command. Sleep 1s"); @@ -1000,7 +1006,13 @@ fn animation_fps_cmd(serialdev: &str, arg: Option) { .expect("Failed to open port"); if let Some(fps) = arg { - let period = (1000 / fps).to_le_bytes(); + const MS: u16 = 1000; + if fps < MS { + // It would need to set the animation period lower than 1ms + println!("Unable to set FPS over 1000"); + return; + } + let period = (MS / fps).to_le_bytes(); simple_cmd_port(&mut port, Command::AnimationPeriod, &[period[0], period[1]]); } else { simple_cmd_port(&mut port, Command::AnimationPeriod, &[]); diff --git a/ledmatrix/README.md b/ledmatrix/README.md index ff26164f..d2f6cc25 100644 --- a/ledmatrix/README.md +++ b/ledmatrix/README.md @@ -182,7 +182,7 @@ If you want to display something else, either reset the module (unplugging) or run the stop command. ```sh -inputmodule-control led-amtrix --stop-game +inputmodule-control led-matrix --stop-game ``` ## Sleep Behavior diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs index c09b7027..c4082c72 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -110,13 +110,14 @@ const MAX_BRIGHTNESS: u8 = 50; // Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. use bsp::entry; use fl16_inputmodules::animations::*; -#[cfg(not(feature = "evt"))] -use fl16_inputmodules::fl16::DVT2_CALC_PIXEL; #[cfg(feature = "evt")] use fl16_inputmodules::fl16::EVT_CALC_PIXEL; use fl16_inputmodules::games::pong_animation::*; use fl16_inputmodules::games::snake_animation::*; use fl16_inputmodules::{games::game_of_life, led_hal as bsp}; +use is31fl3741::devices::LedMatrix; +#[cfg(not(feature = "evt"))] +use is31fl3741::devices::CALC_PIXEL; //use rp_pico as bsp; // use sparkfun_pro_micro_rp2040 as bsp; @@ -141,7 +142,6 @@ use core::fmt::Write; use heapless::String; use fl16_inputmodules::control::*; -use fl16_inputmodules::fl16::LedMatrix; use fl16_inputmodules::games::{pong, snake}; use fl16_inputmodules::matrix::*; use fl16_inputmodules::patterns::*; @@ -266,7 +266,7 @@ fn main() -> ! { #[cfg(feature = "evt")] let mut matrix = LedMatrix::new(i2c, EVT_CALC_PIXEL); #[cfg(not(feature = "evt"))] - let mut matrix = LedMatrix::new(i2c, DVT2_CALC_PIXEL); + let mut matrix = LedMatrix::new(i2c, CALC_PIXEL); matrix .setup(&mut delay) .expect("failed to setup RGB controller"); diff --git a/python/README.md b/python/README.md index 80b3fd2d..921e9ccb 100644 --- a/python/README.md +++ b/python/README.md @@ -165,3 +165,38 @@ Bus 003 Device 078: ID 32ac:0020 Framework Computer Inc LED Matrix Input Module bcdDevice 0.17 ``` +## Developing + +One time setup + +``` +# Install dependencies on Ubuntu +sudo apt install python3 python3-tk + +# Install dependencies on Fedora +sudo dnf install python3 python3-tkinter + +# Create local venv and enter it +python3 -m venv venv +source venv/bin/activate + +# Install package into local env +python3 -m pip install -e . +``` + +Developing + +``` +# In every new shell, source the virtual environment +source venv/bin/activate + +# Launch GUI or commandline +ledmatrixgui +ledmatrixctl + +# Launch Python REPL and import the library +# As example, launch the GUI +> python3 +>>> from inputmodule import cli +>>> cli.main_gui() +``` diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index 5f97615d..852641f2 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 00000000..7d2e752e --- /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 46df99dd..f6bf1d9a 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 ba0f1e1a..f7e53a60 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/gui_threading.py b/python/inputmodule/gui/gui_threading.py index daff8899..0f7fec20 100644 --- a/python/inputmodule/gui/gui_threading.py +++ b/python/inputmodule/gui/gui_threading.py @@ -1,21 +1,29 @@ # Global GUI variables -STOP_THREAD = False DISCONNECTED_DEVS = [] +STATUS = '' +def set_status(status): + global STATUS + STATUS = status + +def get_status(): + global STATUS + return STATUS def stop_thread(): - global STOP_THREAD - STOP_THREAD = True + global STATUS + STATUS = 'STOP_THREAD' def reset_thread(): - global STOP_THREAD - STOP_THREAD = False + global STATUS + if STATUS == 'STOP_THREAD': + STATUS = '' def is_thread_stopped(): - global STOP_THREAD - return STOP_THREAD + global STATUS + return STATUS == 'STOP_THREAD' def is_dev_disconnected(dev): diff --git a/python/inputmodule/gui/ledmatrix.py b/python/inputmodule/gui/ledmatrix.py index 9a369e7c..dddd1659 100644 --- a/python/inputmodule/gui/ledmatrix.py +++ b/python/inputmodule/gui/ledmatrix.py @@ -6,22 +6,26 @@ reset_thread, is_thread_stopped, is_dev_disconnected, + set_status, + get_status, ) from inputmodule.inputmodule.ledmatrix import ( light_leds, show_string, eq, breathing, + animate, ) from inputmodule.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""" + animate(dev, False) + set_status('countdown') start = datetime.now() target = seconds * 1_000_000 - while True: + while get_status() == 'countdown': if is_thread_stopped() or is_dev_disconnected(dev.device): reset_thread() return @@ -37,15 +41,17 @@ def countdown(dev, seconds): time.sleep(0.01) - light_leds(dev, 306) - breathing(dev) - # blinking(dev) + if get_status() == 'countdown': + light_leds(dev, 306) + breathing(dev) + # blinking(dev) def blinking(dev): """Blink brightness high/off every second. Keeps currently displayed grid""" - while True: + set_status('blinking') + while get_status() == 'blinking': if is_thread_stopped() or is_dev_disconnected(dev.device): reset_thread() return @@ -57,7 +63,9 @@ def blinking(dev): def random_eq(dev): """Display an equlizer looking animation with random values.""" - while True: + animate(dev, False) + set_status('random_eq') + while get_status() == 'random_eq': if is_thread_stopped() or is_dev_disconnected(dev.device): reset_thread() return @@ -72,7 +80,9 @@ def random_eq(dev): def clock(dev): """Render the current time and display. Loops forever, updating every second""" - while True: + animate(dev, False) + set_status('clock') + while get_status() == 'clock': if is_thread_stopped() or is_dev_disconnected(dev.device): reset_thread() return diff --git a/python/inputmodule/gui/pygames/__init__.py b/python/inputmodule/gui/pygames/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/inputmodule/gui/pygames/ledris.py b/python/inputmodule/gui/pygames/ledris.py new file mode 100644 index 00000000..469acfe9 --- /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 00000000..6568639c --- /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 2ceb9de6..7fb67f8b 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]) @@ -109,6 +109,8 @@ def get_brightness(dev): def get_version(dev): """Get the device's firmware version""" res = send_command(dev, CommandVals.Version, with_response=True) + if not res: + return 'Unknown' major = res[0] minor = (res[1] & 0xF0) >> 4 patch = res[1] & 0xF diff --git a/python/inputmodule/inputmodule/ledmatrix.py b/python/inputmodule/inputmodule/ledmatrix.py index f1ead13e..7b351bdc 100644 --- a/python/inputmodule/inputmodule/ledmatrix.py +++ b/python/inputmodule/inputmodule/ledmatrix.py @@ -11,6 +11,7 @@ send_serial, brightness, ) +from inputmodule.gui.gui_threading import get_status, set_status WIDTH = 9 HEIGHT = 34 @@ -69,6 +70,8 @@ def percentage(dev, p): def animate(dev, b: bool): """Tell the firmware to start/stop animation. Scrolls the currently saved grid vertically down.""" + if b: + set_status('animate') send_command(dev, CommandVals.Animate, [b]) @@ -104,6 +107,7 @@ def image_bl(dev, image_file): def camera(dev): """Play a live view from the webcam, for fun""" + set_status('camera') with serial.Serial(dev.device, 115200) as s: import cv2 @@ -120,7 +124,7 @@ def camera(dev): end_x = min(dim[1], start_x + WIDTH) # Pre-process the video into resized, cropped, grayscale frames - while True: + while get_status() == 'camera': ret, frame = capture.read() if not ret: print("Failed to capture video frames") @@ -142,6 +146,7 @@ def camera(dev): def video(dev, video_file): + set_status('video') """Resize and play back a video""" with serial.Serial(dev.device, 115200) as s: import cv2 @@ -161,7 +166,7 @@ def video(dev, video_file): processed = [] # Pre-process the video into resized, cropped, grayscale frames - while True: + while get_status() == 'video': ret, frame = capture.read() if not ret: print("Failed to read video frames") @@ -294,8 +299,9 @@ def all_brightnesses(dev): def breathing(dev): """Animate breathing brightness. Keeps currently displayed grid""" + set_status('breathing') # Bright ranges appear similar, so we have to go through those faster - while True: + while get_status() == 'breathing': # Go quickly from 250 to 50 for i in range(10): time.sleep(0.03) diff --git a/python/inputmodule/uf2conv.py b/python/inputmodule/uf2conv.py new file mode 100644 index 00000000..e545b69c --- /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 0111e850..32b41083 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 1972de45..52366a88 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 00000000..8c6be29a 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 00000000..c0a5ca49 Binary files /dev/null and b/res/ledmatrixgui-home.png differ