From 8edaeecc3af2c746e46a6f02d71dd331fd255ef0 Mon Sep 17 00:00:00 2001 From: Ilia Baranov <90713890+iliabaranov@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:47:20 -0800 Subject: [PATCH 01/43] Update font.rs to fix "F" character The "F" character was incorrectly drawn as an "E" --- inputmodule-control/src/font.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 13efc56a0bc0b93495195197c64e6d7bf22cd119 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 13 Feb 2024 07:43:43 +0800 Subject: [PATCH 02/43] python: Avoid exception when getting FW version Signed-off-by: Daniel Schaefer --- python/inputmodule/inputmodule/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py index 2ceb9de6..616e7267 100644 --- a/python/inputmodule/inputmodule/__init__.py +++ b/python/inputmodule/inputmodule/__init__.py @@ -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 From 6a35a7a3d8a93259f3008b92a4ed3147030bcf92 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Thu, 20 Jun 2024 07:50:52 -1000 Subject: [PATCH 03/43] Fix two outdated links to the previous led_matrix_fw name of this repo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 53bb6c0a..6cdd9556 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ 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). +[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.md). For device specific commands, see their individual documentation pages. From 83f63d49501f01c317211809ac8cb7619f3c5e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20Kla=C5=A1ka?= Date: Mon, 17 Jun 2024 22:29:27 +0200 Subject: [PATCH 04/43] Ledmatrix: Fix typo in stop game command. --- ledmatrix/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From db67a86b56ce3b008cff33c5b95b6ce70bef2e1b Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Apr 2024 01:24:11 -0700 Subject: [PATCH 05/43] Update commands.md Fixed typos in example code --- commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands.md b/commands.md index 00ddfb66..853a687e 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("COM4", 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. From c44040c299569169a99628e05c4849a6dae09561 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Apr 2024 01:24:58 -0700 Subject: [PATCH 06/43] Update commands.md --- commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.md b/commands.md index 853a687e..c1186609 100644 --- a/commands.md +++ b/commands.md @@ -10,7 +10,7 @@ Simple example in Python: import serial def send_command(command_id, parameters, with_response=False): - with serial.Serial("COM4", 115200) as s: + with serial.Serial("/dev/ttyACM0", 115200) as s: s.write([0x32, 0xAC, command_id] + parameters) if with_response: From aa3229c4747348e2a7bd098cdb352b291db5f572 Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Tue, 30 Jan 2024 13:36:47 -0500 Subject: [PATCH 07/43] README: Add additional dependencies Add rustup, cargo-make and elf2uf2-rs as dependencies to ensure that a user with no prior rust installation can build the firmware. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6cdd9556..7e2b777c 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,15 @@ cargo run -p c1minimal Dependencies: Rust +Download and install [rustup](https://rustup.rs/) if necessary. + Prepare Rust toolchain (once): ```sh rustup target install thumbv6m-none-eabi cargo install flip-link +cargo install cargo-make +cargo install elf2uf2-rs ``` Build: From ad3a034b9fe08cf662e57ee013a6b4e9afe75fd2 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 13 Oct 2024 13:54:21 +0800 Subject: [PATCH 08/43] README: Add cargo and native dependencies Signed-off-by: Daniel Schaefer --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e2b777c..5388cef5 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,7 @@ cargo run -p c1minimal ## Building the firmware -Dependencies: Rust - -Download and install [rustup](https://rustup.rs/) if necessary. +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev Prepare Rust toolchain (once): @@ -156,12 +154,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 ``` +# Install cargo-make to help build it +cargo install cargo-make + # Build it > cargo make --cwd inputmodule-control From e87571b94cc52037a17ea7bcac7024ba3fd3ae51 Mon Sep 17 00:00:00 2001 From: MoonJam Date: Sun, 13 Oct 2024 13:36:49 +0800 Subject: [PATCH 09/43] inputmodule-control: Ignore /dev/tty. on macOS macOS has two instances of the serial console. /dev/tty.* and /dev/cu.*. The former we can't use. Right now it'll select the wrong one by default. --- inputmodule-control/src/inputmodule.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 25baa57e..e2779239 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()); } From 53606f4d2fb6298ac0ce85aff5932318adb23ed7 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 13 Oct 2024 15:32:37 +0800 Subject: [PATCH 10/43] inputmodule-control: Limit FPS to 1000 Otherwise the division in the CLI results in 0 and tells the firmware to set the animation frequency to 0ms. Fixes #113 Signed-off-by: Daniel Schaefer --- fl16-inputmodules/src/control.rs | 3 +++ fl16-inputmodules/src/matrix.rs | 1 + inputmodule-control/src/inputmodule.rs | 8 +++++++- 3 files changed, 11 insertions(+), 1 deletion(-) 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/matrix.rs b/fl16-inputmodules/src/matrix.rs index 8046db40..48252cb2 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 perios in microseconds pub animation_period: u64, /// Current LED PWM frequency pub pwm_freq: PwmFreqArg, diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 25baa57e..cb037d87 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -1000,7 +1000,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, &[]); From 50bf0ca68a8d52264048122b3f7f73bfbeb91082 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 22 Oct 2024 01:44:55 +0800 Subject: [PATCH 11/43] ledmatrix: Upgrade to v0.4.0 Simplify the code. Signed-off-by: Daniel Schaefer --- Cargo.lock | 4 +- Cargo.toml | 3 +- fl16-inputmodules/src/fl16.rs | 372 ------------------------------ fl16-inputmodules/src/patterns.rs | 2 +- ledmatrix/src/main.rs | 8 +- 5 files changed, 8 insertions(+), 381 deletions(-) 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/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/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/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"); From 42f127c8abff93a05228300bc842d2af55ce8b71 Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Wed, 23 Oct 2024 19:22:39 -0400 Subject: [PATCH 12/43] Fix doc comment typo --- fl16-inputmodules/src/matrix.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fl16-inputmodules/src/matrix.rs b/fl16-inputmodules/src/matrix.rs index 48252cb2..71a625fd 100644 --- a/fl16-inputmodules/src/matrix.rs +++ b/fl16-inputmodules/src/matrix.rs @@ -37,7 +37,7 @@ pub struct LedmatrixState { pub sleeping: SleepState, /// State of the current game, if any pub game: Option, - /// Animation perios in microseconds + /// Animation period in microseconds pub animation_period: u64, /// Current LED PWM frequency pub pwm_freq: PwmFreqArg, From 865e46b4e357c66171902eae50128bee6abf6bbe Mon Sep 17 00:00:00 2001 From: Michael Mior Date: Fri, 25 Oct 2024 14:41:04 -0400 Subject: [PATCH 13/43] Fix link to Python documentation in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5388cef5..cf5fa107 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ 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.md). +Optionally there are is also a [Python script](python/README.md). For device specific commands, see their individual documentation pages. From 262edffd35ad6aa7976aee38632f6b75695bc0a8 Mon Sep 17 00:00:00 2001 From: Jonas Wanke Date: Sun, 17 Nov 2024 09:07:46 +0100 Subject: [PATCH 14/43] doc: version command cleanup (#119) Co-authored-by: Daniel Schaefer --- README.md | 12 ++++++------ commands.md | 11 +++++++++-- inputmodule-control/src/inputmodule.rs | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cf5fa107..ad2a53bd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ For device specific commands, see their individual documentation pages. ###### Permissions on Linux To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: -``` +```sh sudo cp release/50-framework-inputmodule.rules /etc/udev/rules.d/ sudo udevadm control --reload && sudo udevadm trigger ``` @@ -72,7 +72,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 +88,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 +105,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 @@ -144,7 +144,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 @@ -159,7 +159,7 @@ 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 diff --git a/commands.md b/commands.md index c1186609..46322edb 100644 --- a/commands.md +++ b/commands.md @@ -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/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 4908839d..d2d4e83c 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -160,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"); From 9e4830f1e2d304c65b2701cc46ace3fc9bf2cab3 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 17 Nov 2024 16:11:54 +0800 Subject: [PATCH 15/43] docs: Add details about developing python Signed-off-by: Daniel Schaefer --- python/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) 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() +``` From 585d33b1891a965342712d0d1f3b45401678effd Mon Sep 17 00:00:00 2001 From: Panda Date: Mon, 25 Nov 2024 12:15:38 +0800 Subject: [PATCH 16/43] python: Fix threading in GUI (#116) Stopping and starting threads didn't really work before. --- python/inputmodule/gui/gui_threading.py | 22 +++++++++++------ python/inputmodule/gui/ledmatrix.py | 26 ++++++++++++++------- python/inputmodule/inputmodule/ledmatrix.py | 12 +++++++--- 3 files changed, 42 insertions(+), 18 deletions(-) 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/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) From 6daeedbf3833b5e666ac5b3ff0fd30d7385bae59 Mon Sep 17 00:00:00 2001 From: Moon Jam Date: Fri, 25 Oct 2024 03:20:26 +0800 Subject: [PATCH 17/43] python: Use tkinter build GUI instead of PySympleGUI --- python/inputmodule/gui/__init__.py | 422 +++++++++++++---------------- 1 file changed, 188 insertions(+), 234 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index ba0f1e1a..542d963e 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -1,8 +1,8 @@ import os import threading import sys - -import PySimpleGUI as sg +import tkinter as tk +from tkinter import ttk, messagebox from inputmodule.inputmodule import ( send_command, @@ -28,8 +28,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 +37,199 @@ 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): + messagebox.showinfo("Framework Laptop 16 LED Matrix", message) def run_gui(devices): - device_checkboxes = [] + root = tk.Tk() + root.title("LED Matrix Control") + root.geometry("400x900") + + # Configure dark theme + style = ttk.Style() + root.configure(bg="#2b2b2b") + style.configure("TLabelframe", background="#2b2b2b", foreground="white") + style.configure("TLabelframe.Label", background="#2b2b2b", foreground="white") + style.configure("TCheckbutton", background="#2b2b2b", foreground="white") + style.configure("TButton", background="white", foreground="#2b2b2b") + style.configure("TEntry", fieldbackground="#2b2b2b", foreground="white") + style.configure("TCombobox", fieldbackground="#2b2b2b", foreground="white") + style.configure("TScale", background="#2b2b2b", troughcolor="gray") + style.configure("TSpinbox", background="#2b2b2b", foreground="white") + style.map("TButton", background=[("active", "gray"), ("!active", "#2b2b2b")]) + + # 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 + + # Device Control Buttons + device_control_frame = ttk.LabelFrame(root, text="Device Control", style="TLabelframe") + device_control_frame.pack(fill="x", padx=10, pady=5) + control_buttons = { + "Bootloader": "bootloader", + "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) + + # Brightness Slider + brightness_frame = ttk.LabelFrame(root, 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") + brightness_scale.set(120) # Default value + brightness_scale.pack(fill="x", padx=5, pady=5) + + # Animation Control + animation_frame = ttk.LabelFrame(root, 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(root, text="Pattern", style="TLabelframe") + pattern_frame.pack(fill="x", padx=10, pady=5) + pattern_combo = ttk.Combobox(pattern_frame, values=PATTERNS, style="TCombobox") + 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(root, 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") + percentage_scale.pack(fill="x", padx=5, pady=5) + + # Countdown Timer + countdown_frame = ttk.LabelFrame(root, 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, bg="#2b2b2b", fg="white", 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(root, 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(root, 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(root, 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(root, 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) + + # PWM Frequency Combo Box + pwm_freq_frame = ttk.LabelFrame(root, 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") + 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(root, 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) + + root.mainloop() + +def perform_action(devices, action): + action_map = { + "bootloader": bootloader, + "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() + } + 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].get()] def resource_path(): """Get absolute path to resource, works for dev and for PyInstaller""" From be455f7681174374b4f833a0f28df1617e829cc0 Mon Sep 17 00:00:00 2001 From: Moon Jam Date: Wed, 6 Nov 2024 22:59:47 +0800 Subject: [PATCH 18/43] python: Fix weird btn color in different OS --- python/inputmodule/gui/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 542d963e..d5e510cc 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -1,6 +1,7 @@ import os import threading import sys +import platform import tkinter as tk from tkinter import ttk, messagebox @@ -53,7 +54,10 @@ def run_gui(devices): style.configure("TLabelframe", background="#2b2b2b", foreground="white") style.configure("TLabelframe.Label", background="#2b2b2b", foreground="white") style.configure("TCheckbutton", background="#2b2b2b", foreground="white") - style.configure("TButton", background="white", foreground="#2b2b2b") + if platform.system() == "Windows": # On Windows, I don't know why background always stays white even if I set it to black + style.configure("TButton", background="white", foreground="#2b2b2b") + else: + style.configure("TButton", background="#2b2b2b", foreground="white") style.configure("TEntry", fieldbackground="#2b2b2b", foreground="white") style.configure("TCombobox", fieldbackground="#2b2b2b", foreground="white") style.configure("TScale", background="#2b2b2b", troughcolor="gray") From c958eaa0946e352edda94e721dc36fe95073741a Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 18 Nov 2024 19:14:43 +0800 Subject: [PATCH 19/43] python: Split UI into tabs Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 34 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index d5e510cc..4399cf27 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -46,7 +46,15 @@ def popup(message): def run_gui(devices): root = tk.Tk() root.title("LED Matrix Control") - root.geometry("400x900") + + tabControl = ttk.Notebook(root) + tab1 = ttk.Frame(tabControl) + tab2 = ttk.Frame(tabControl) + tab3 = ttk.Frame(tabControl) + tabControl.add(tab1, text="Home") + tabControl.add(tab2, text="Dynamic Controls") + tabControl.add(tab3, text="Advanced") + tabControl.pack(expand=1, fill="both") # Configure dark theme style = ttk.Style() @@ -81,7 +89,7 @@ def run_gui(devices): device_checkboxes[dev.name] = checkbox_var # Device Control Buttons - device_control_frame = ttk.LabelFrame(root, text="Device Control", style="TLabelframe") + device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") device_control_frame.pack(fill="x", padx=10, pady=5) control_buttons = { "Bootloader": "bootloader", @@ -92,7 +100,7 @@ def run_gui(devices): ttk.Button(device_control_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) # Brightness Slider - brightness_frame = ttk.LabelFrame(root, text="Brightness", style="TLabelframe") + 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") @@ -100,7 +108,7 @@ def run_gui(devices): brightness_scale.pack(fill="x", padx=5, pady=5) # Animation Control - animation_frame = ttk.LabelFrame(root, text="Animation", style="TLabelframe") + animation_frame = ttk.LabelFrame(tab1, text="Animation", style="TLabelframe") animation_frame.pack(fill="x", padx=10, pady=5) animation_buttons = { "Start Animation": "start_animation", @@ -110,20 +118,20 @@ def run_gui(devices): 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(root, text="Pattern", style="TLabelframe") + 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") 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(root, text="Fill screen X% (could be volume indicator)", style="TLabelframe") + 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") percentage_scale.pack(fill="x", padx=5, pady=5) # Countdown Timer - countdown_frame = ttk.LabelFrame(root, text="Countdown Timer", style="TLabelframe") + 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, bg="#2b2b2b", fg="white", textvariable=tk.StringVar(value=10)) countdown_spinbox.pack(side="left", padx=5, pady=5) @@ -132,38 +140,38 @@ def run_gui(devices): 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(root, text="Black&White Images / Greyscale Images", style="TLabelframe") + 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(root, text="Display Current Time", style="TLabelframe") + 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(root, text="Custom Text", style="TLabelframe") + 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(root, text="Display Text with Symbols", style="TLabelframe") + 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) # PWM Frequency Combo Box - pwm_freq_frame = ttk.LabelFrame(root, text="PWM Frequency", style="TLabelframe") + 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") 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(root, text="Equalizer", style="TLabelframe") + 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) From c4b52f04501d71c24499385405371239abf1feb6 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 18 Nov 2024 19:19:13 +0800 Subject: [PATCH 20/43] python: Remove styling I don't like the styling. And if we do want to change the styling we should use the built-in styling/theming facility instead of building it from scratch. https://tkdocs.com/tutorial/styles.html Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 4399cf27..7465db7a 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -56,22 +56,6 @@ def run_gui(devices): tabControl.add(tab3, text="Advanced") tabControl.pack(expand=1, fill="both") - # Configure dark theme - style = ttk.Style() - root.configure(bg="#2b2b2b") - style.configure("TLabelframe", background="#2b2b2b", foreground="white") - style.configure("TLabelframe.Label", background="#2b2b2b", foreground="white") - style.configure("TCheckbutton", background="#2b2b2b", foreground="white") - if platform.system() == "Windows": # On Windows, I don't know why background always stays white even if I set it to black - style.configure("TButton", background="white", foreground="#2b2b2b") - else: - style.configure("TButton", background="#2b2b2b", foreground="white") - style.configure("TEntry", fieldbackground="#2b2b2b", foreground="white") - style.configure("TCombobox", fieldbackground="#2b2b2b", foreground="white") - style.configure("TScale", background="#2b2b2b", troughcolor="gray") - style.configure("TSpinbox", background="#2b2b2b", foreground="white") - style.map("TButton", background=[("active", "gray"), ("!active", "#2b2b2b")]) - # Device Checkboxes detected_devices_frame = ttk.LabelFrame(root, text="Detected Devices", style="TLabelframe") detected_devices_frame.pack(fill="x", padx=10, pady=5) @@ -103,7 +87,7 @@ def run_gui(devices): 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") + 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) @@ -120,20 +104,20 @@ def run_gui(devices): # 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") + 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), bg="#2b2b2b", fg="white", troughcolor="gray", highlightbackground="#2b2b2b") + 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) # 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, bg="#2b2b2b", fg="white", textvariable=tk.StringVar(value=10)) + 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) @@ -166,7 +150,7 @@ def run_gui(devices): # 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") + 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())) From 7f3e67c3f18c19d7fcd8551a427a9a550866ae7c Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 18 Nov 2024 19:36:19 +0800 Subject: [PATCH 21/43] python: Move device control to the bottom Users don't usually need it, move it away. Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 7465db7a..a5d1abcc 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -72,17 +72,6 @@ def run_gui(devices): checkbox.pack(anchor="w") device_checkboxes[dev.name] = checkbox_var - # Device Control Buttons - device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") - device_control_frame.pack(fill="x", padx=10, pady=5) - control_buttons = { - "Bootloader": "bootloader", - "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) - # Brightness Slider brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") brightness_frame.pack(fill="x", padx=10, pady=5) @@ -160,6 +149,18 @@ def run_gui(devices): 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(tab1, text="Device Control", style="TLabelframe") + device_control_frame.pack(fill="x", padx=10, pady=5) + control_buttons = { + "Bootloader": "bootloader", + "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): From 4333b1b3337cbf974bd582f5392b01e0ae54a282 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 18:35:14 +0800 Subject: [PATCH 22/43] python: Add ledris game Creates a window and draws on it Signed-off-by: Daniel Schaefer --- python/ledris.py | 220 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 python/ledris.py diff --git a/python/ledris.py b/python/ledris.py new file mode 100644 index 00000000..2d0ebadf --- /dev/null +++ b/python/ledris.py @@ -0,0 +1,220 @@ +# Run like +# python3 ledris.py + +import pygame +import random +import time + +# Initialize pygame +pygame.init() + +# 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) + +# Create the screen +screen = pygame.display.set_mode((width, height)) + +# Clock to control the speed of the game +clock = pygame.time.Clock() + +# 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 + +# Function to draw the game based on the board state +def draw_board(board, devices): + 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(screen, black, rect) + draw_grid() + pygame.display.update() + +# Function to draw a grid +def draw_grid(): + 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(screen, black, rect, 1) + +# 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 +] + +# Main game function +def gameLoop(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) + 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 += 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 + + clock.tick(30) + + # Flash the screen twice before waiting for restart + for _ in range(2): + screen.fill(black) + pygame.display.update() + time.sleep(0.3) + 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) + 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() + +gameLoop(devices) From 3d5fca92abf31f2d5f258dd095b4abd1561a13c7 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 18:35:41 +0800 Subject: [PATCH 23/43] python/ledris: Draw on led matrix Draws on all that are connected. Signed-off-by: Daniel Schaefer --- python/ledris.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/python/ledris.py b/python/ledris.py index 2d0ebadf..cc6472a3 100644 --- a/python/ledris.py +++ b/python/ledris.py @@ -5,6 +5,10 @@ import random import time +from inputmodule import cli +from inputmodule.gui.ledmatrix import show_string +from inputmodule.inputmodule import ledmatrix + # Initialize pygame pygame.init() @@ -49,8 +53,19 @@ def get_board_state(board, current_shape, current_pos): 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 draw the game based on the board state def draw_board(board, devices): + draw_ledmatrix(board, devices) screen.fill(white) for y in range(rows): for x in range(cols): @@ -189,9 +204,14 @@ def gameLoop(devices): # Flash the screen twice before waiting for restart for _ in range(2): + for dev in devices: + ledmatrix.percentage(dev, 0) screen.fill(black) pygame.display.update() time.sleep(0.3) + + for dev in devices: + ledmatrix.percentage(dev, 100) screen.fill(white) pygame.display.update() time.sleep(0.3) @@ -217,4 +237,8 @@ def gameLoop(devices): pygame.quit() quit() -gameLoop(devices) +if __name__ == "__main__": + devices = cli.find_devs() + for dev in devices: + show_string(dev, 'YAY') + gameLoop(devices) From f379c8575146967a418a2e4bb68cdfc18dc65601 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 20:38:27 +0800 Subject: [PATCH 24/43] python/ledris: Allow to be imported Signed-off-by: Daniel Schaefer --- python/ledris.py | 254 ++++++++++++++++++++++++----------------------- 1 file changed, 132 insertions(+), 122 deletions(-) diff --git a/python/ledris.py b/python/ledris.py index cc6472a3..469acfe9 100644 --- a/python/ledris.py +++ b/python/ledris.py @@ -9,9 +9,6 @@ from inputmodule.gui.ledmatrix import show_string from inputmodule.inputmodule import ledmatrix -# Initialize pygame -pygame.init() - # Set the screen width and height for a 34 x 9 block Ledris game block_width = 20 block_height = 20 @@ -25,12 +22,6 @@ black = (0, 0, 0) white = (255, 255, 255) -# Create the screen -screen = pygame.display.set_mode((width, height)) - -# Clock to control the speed of the game -clock = pygame.time.Clock() - # Ledrimino shapes shapes = [ [[1, 1, 1, 1]], # I shape @@ -63,25 +54,6 @@ def draw_ledmatrix(board, devices): #vals = [0 for _ in range(39)] #send_command(dev, CommandVals.Draw, vals) -# Function to draw the game based on the board state -def draw_board(board, devices): - draw_ledmatrix(board, devices) - 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(screen, black, rect) - draw_grid() - pygame.display.update() - -# Function to draw a grid -def draw_grid(): - 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(screen, black, rect, 1) - # Function to check if the position is valid def check_collision(board, shape, offset): off_x, off_y = offset @@ -139,106 +111,144 @@ def display_score(board, score): [[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9 ] -# Main game function -def gameLoop(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 + +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) - 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 += 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): + 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() - clock.tick(30) + pygame.quit() + quit() - # Flash the screen twice before waiting for restart - for _ in range(2): - for dev in devices: - ledmatrix.percentage(dev, 0) - screen.fill(black) - pygame.display.update() - time.sleep(0.3) + def __init__(self): + # Initialize pygame + pygame.init() - for dev in devices: - ledmatrix.percentage(dev, 100) - 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) - 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() + # Create the screen + self.screen = pygame.display.set_mode((width, height)) - pygame.quit() - quit() + # Clock to control the speed of the game + self.clock = pygame.time.Clock() -if __name__ == "__main__": +def main_devices(devices): + ledris = Ledris() + ledris.gameLoop(devices) + +def main(): devices = cli.find_devs() - for dev in devices: - show_string(dev, 'YAY') - gameLoop(devices) + + ledris = Ledris() + ledris.gameLoop(devices) + +if __name__ == "__main__": + main() From 4219c2ae23721a5648cb167325149f82de112da7 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 20:39:43 +0800 Subject: [PATCH 25/43] python: Add ledris in tkinter GUI Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 17 +++++++++++++++-- .../gui/{games.py => games/__init__.py} | 0 python/{ => inputmodule/gui/games}/ledris.py | 0 3 files changed, 15 insertions(+), 2 deletions(-) rename python/inputmodule/gui/{games.py => games/__init__.py} (100%) rename python/{ => inputmodule/gui/games}/ledris.py (100%) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index a5d1abcc..af8179c2 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -14,6 +14,7 @@ CommandVals, ) from inputmodule.gui.games import snake +from inputmodule.gui.games import ledris 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 ( @@ -49,9 +50,11 @@ def run_gui(devices): tabControl = ttk.Notebook(root) tab1 = ttk.Frame(tabControl) + tab_games = ttk.Frame(tabControl) tab2 = 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(tab3, text="Advanced") tabControl.pack(expand=1, fill="both") @@ -103,6 +106,11 @@ def run_gui(devices): 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="Games", style="TLabelframe") + games_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(games_frame, text="Ledris", command=lambda: perform_action(devices, 'game_ledris'), style="TButton").pack(side="left", padx=5, pady=5) + # Countdown Timer countdown_frame = ttk.LabelFrame(tab2, text="Countdown Timer", style="TLabelframe") countdown_frame.pack(fill="x", padx=10, pady=5) @@ -160,10 +168,15 @@ def run_gui(devices): 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): + action_map = { + "game_ledris": ledris.main_devices + } + if action in action_map: + threading.Thread(target=action_map[action], args=(devices,), daemon=True).start(), + action_map = { "bootloader": bootloader, "sleep": lambda dev: send_command(dev, CommandVals.Sleep, [True]), @@ -171,7 +184,7 @@ def perform_action(devices, action): "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() + "start_eq": lambda dev: threading.Thread(target=random_eq, args=(dev,), daemon=True).start(), } selected_devices = get_selected_devices(devices) for dev in selected_devices: diff --git a/python/inputmodule/gui/games.py b/python/inputmodule/gui/games/__init__.py similarity index 100% rename from python/inputmodule/gui/games.py rename to python/inputmodule/gui/games/__init__.py diff --git a/python/ledris.py b/python/inputmodule/gui/games/ledris.py similarity index 100% rename from python/ledris.py rename to python/inputmodule/gui/games/ledris.py From e24c3c9676e041962ffcb71e87793842645259c1 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 22:30:57 +0800 Subject: [PATCH 26/43] python: Migrate snake to pygame Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 4 +- python/inputmodule/gui/games/__init__.py | 115 ---------- python/inputmodule/gui/games/snake.py | 254 +++++++++++++++++++++++ 3 files changed, 257 insertions(+), 116 deletions(-) create mode 100644 python/inputmodule/gui/games/snake.py diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index af8179c2..365cad6b 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -109,6 +109,7 @@ def run_gui(devices): # Games tab games_frame = ttk.LabelFrame(tab_games, text="Games", 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) # Countdown Timer @@ -172,7 +173,8 @@ def run_gui(devices): def perform_action(devices, action): action_map = { - "game_ledris": ledris.main_devices + "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(), diff --git a/python/inputmodule/gui/games/__init__.py b/python/inputmodule/gui/games/__init__.py index 46df99dd..c3d01d5b 100644 --- a/python/inputmodule/gui/games/__init__.py +++ b/python/inputmodule/gui/games/__init__.py @@ -26,36 +26,6 @@ ARG_2LEFT = 5 ARG_2RIGHT = 6 -# Variables -direction = None -body = [] - - -def opposite_direction(direction): - if direction == keys.RIGHT: - return keys.LEFT - elif direction == keys.LEFT: - return keys.RIGHT - elif direction == keys.UP: - return keys.DOWN - elif direction == keys.DOWN: - return keys.UP - return direction - - -def snake_keyscan(): - global direction - global body - - while True: - current_dir = direction - key = getkey() - if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: - # Don't allow accidental suicide if we have a body - if key == opposite_direction(current_dir) and body: - continue - direction = key - def snake_embedded_keyscan(dev): while True: @@ -76,18 +46,6 @@ def snake_embedded_keyscan(dev): send_command(dev, CommandVals.GameControl, [key_arg]) -def game_over(dev): - global body - while True: - show_string(dev, "GAME ") - time.sleep(0.75) - show_string(dev, "OVER!") - time.sleep(0.75) - score = len(body) - show_string(dev, f"{score:>3} P") - time.sleep(0.75) - - def pong_embedded(dev): # Start game send_command(dev, CommandVals.StartGame, [Game.Pong]) @@ -124,79 +82,6 @@ def snake_embedded(dev): snake_embedded_keyscan(dev) -def snake(dev): - global direction - global body - head = (0, 0) - direction = keys.DOWN - food = (0, 0) - while food == head: - food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) - - # Setting - WRAP = False - - thread = threading.Thread(target=snake_keyscan, args=(), daemon=True) - thread.start() - - prev = datetime.now() - while True: - now = datetime.now() - delta = (now - prev) / timedelta(milliseconds=1) - - if delta > 200: - prev = now - else: - continue - - # Update position - (x, y) = head - oldhead = head - if direction == keys.RIGHT: - head = (x + 1, y) - elif direction == keys.LEFT: - head = (x - 1, y) - elif direction == keys.UP: - head = (x, y - 1) - elif direction == keys.DOWN: - head = (x, y + 1) - - # Detect edge condition - (x, y) = head - if head in body: - return game_over(dev) - elif x >= WIDTH or x < 0 or y >= HEIGHT or y < 0: - if WRAP: - if x >= WIDTH: - x = 0 - elif x < 0: - x = WIDTH - 1 - elif y >= HEIGHT: - y = 0 - elif y < 0: - y = HEIGHT - 1 - head = (x, y) - else: - return game_over(dev) - elif head == food: - body.insert(0, oldhead) - while food == head: - food = (random.randint(0, WIDTH - 1), - random.randint(0, HEIGHT - 1)) - elif body: - body.pop() - body.insert(0, oldhead) - - # Draw on screen - matrix = [[0 for _ in range(HEIGHT)] for _ in range(WIDTH)] - matrix[x][y] = 1 - matrix[food[0]][food[1]] = 1 - for bodypart in body: - (x, y) = bodypart - matrix[x][y] = 1 - render_matrix(dev, matrix) - - def wpm_demo(dev): """Capture keypresses and calculate the WPM of the last 10 seconds TODO: I'm not sure my calculation is right.""" diff --git a/python/inputmodule/gui/games/snake.py b/python/inputmodule/gui/games/snake.py new file mode 100644 index 00000000..6568639c --- /dev/null +++ b/python/inputmodule/gui/games/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() From 5ddd3820932c7fbd9aeb88953c2a9cb5855dd350 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Fri, 22 Nov 2024 22:43:11 +0800 Subject: [PATCH 27/43] python: Add game of life to tkinter gui Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 365cad6b..9439f0ac 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -12,6 +12,8 @@ get_brightness, bootloader, CommandVals, + Game, + GameControlVal ) from inputmodule.gui.games import snake from inputmodule.gui.games import ledris @@ -107,10 +109,23 @@ def run_gui(devices): percentage_scale.pack(fill="x", padx=5, pady=5) # Games tab - games_frame = ttk.LabelFrame(tab_games, text="Games", style="TLabelframe") + 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 Matrix": "gol_current", + "Pattern 1": "gol_pattern1", + "Blinker": "gol_blinker", + "Toad": "gol_toad", + "Beacon": "gol_beacon", + "Glider": "gol_glider", + "Stop": "game_stop", + } + for text, action in animation_buttons.items(): + ttk.Button(gol_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) # Countdown Timer countdown_frame = ttk.LabelFrame(tab2, text="Countdown Timer", style="TLabelframe") @@ -187,6 +202,13 @@ def perform_action(devices, action): "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: From b02823978b61e571d89c5217e8105e9d050b71d0 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 10:00:22 +0800 Subject: [PATCH 28/43] gh-actions: Update actions to v4 Signed-off-by: Daniel Schaefer --- .github/workflows/firmware.yml | 12 ++++++------ .github/workflows/software.yml | 18 +++++++++--------- .github/workflows/traditional-cargo.yml | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) 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..f98b11e8 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,7 +103,7 @@ jobs: name: Build GUI runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create Executable uses: Martin005/pyinstaller-action@main @@ -118,7 +118,7 @@ jobs: name: Package Python runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | cd python @@ -131,7 +131,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: | From 766b2351cb3866594d66c30ca3d00afe603105c3 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 10:00:39 +0800 Subject: [PATCH 29/43] python: Update windows bundle to Python 3.12 Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index f98b11e8..8015d01d 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -108,7 +108,7 @@ jobs: - name: Create Executable uses: Martin005/pyinstaller-action@main 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' From d97884dcf8cf9bcdd92a107773f3b27ff3cd459f Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 10:14:37 +0800 Subject: [PATCH 30/43] python: Move requirements.txt to subfolder Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 2 +- requirements.txt => python/requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename requirements.txt => python/requirements.txt (69%) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index 8015d01d..c1d67648 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -110,7 +110,7 @@ jobs: with: python_ver: '3.12' spec: python/inputmodule/cli.py #'src/build.spec' - requirements: 'requirements.txt' + requirements: 'python/requirements.txt' upload_exe_with_name: 'ledmatrixgui' options: --onefile, --windowed, --add-data 'res;res' diff --git a/requirements.txt b/python/requirements.txt similarity index 69% rename from requirements.txt rename to python/requirements.txt index 1972de45..7b53097c 100644 --- a/requirements.txt +++ b/python/requirements.txt @@ -1,4 +1,3 @@ get-key==1.60.0 Pillow==10.0.0 pyserial==3.5 -PySimpleGUI==4.60.5 From 7fb179fa4fd9827fa48fb2f7c9e7074fad060b9b Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 11:16:57 +0800 Subject: [PATCH 31/43] python: Implement firmware update Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 6 + python/inputmodule/cli.py | 5 +- python/inputmodule/firmware_update.py | 80 +++++ python/inputmodule/gui/__init__.py | 71 +++- python/inputmodule/inputmodule/__init__.py | 2 +- python/inputmodule/uf2conv.py | 397 +++++++++++++++++++++ 6 files changed, 552 insertions(+), 9 deletions(-) create mode 100644 python/inputmodule/firmware_update.py create mode 100644 python/inputmodule/uf2conv.py diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index c1d67648..327c38e3 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -105,6 +105,12 @@ jobs: steps: - 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 + - name: Create Executable uses: Martin005/pyinstaller-action@main with: diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index 5f97615d..3b9c0492 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -14,7 +14,7 @@ brightness, get_brightness, CommandVals, - bootloader, + bootloader_jump, GameOfLifeStartParam, GameControlVal, ) @@ -272,7 +272,7 @@ def main_cli(): 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 +394,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}") 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/__init__.py b/python/inputmodule/gui/__init__.py index 9439f0ac..9d65306c 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -5,12 +5,13 @@ 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 @@ -54,10 +55,12 @@ def run_gui(devices): 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") @@ -75,7 +78,7 @@ def run_gui(devices): 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 + device_checkboxes[dev.name] = (checkbox_var, checkbox) # Brightness Slider brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") @@ -160,6 +163,26 @@ def run_gui(devices): 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) @@ -177,7 +200,6 @@ def run_gui(devices): device_control_frame = ttk.LabelFrame(tab1, text="Device Control", style="TLabelframe") device_control_frame.pack(fill="x", padx=10, pady=5) control_buttons = { - "Bootloader": "bootloader", "Sleep": "sleep", "Wake": "wake" } @@ -194,8 +216,12 @@ def perform_action(devices, action): 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, + "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), @@ -263,7 +289,7 @@ def set_pwm_freq(devices, freq): 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].get()] + 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""" @@ -271,6 +297,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/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py index 616e7267..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]) 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() From 5440e259642e8172ccb6b68ead3787b1772d0da8 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 11:28:45 +0800 Subject: [PATCH 32/43] python: Rearrange game of life buttons in a grid Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 9d65306c..ea7d1309 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -119,7 +119,7 @@ def run_gui(devices): gol_frame = ttk.LabelFrame(tab_games, text="Game of Life", style="TLabelframe") gol_frame.pack(fill="x", padx=10, pady=5) animation_buttons = { - "Current Matrix": "gol_current", + "Current": "gol_current", "Pattern 1": "gol_pattern1", "Blinker": "gol_blinker", "Toad": "gol_toad", @@ -127,8 +127,15 @@ def run_gui(devices): "Glider": "gol_glider", "Stop": "game_stop", } - for text, action in animation_buttons.items(): - ttk.Button(gol_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + 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") From 1b17be570e4ca7ad94b7e4fce83bf856dd3bbd8e Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 11:32:07 +0800 Subject: [PATCH 33/43] python: Install pygame Signed-off-by: Daniel Schaefer --- python/pyproject.toml | 5 +++-- python/requirements.txt | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index 0111e850..d205db75 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", ] diff --git a/python/requirements.txt b/python/requirements.txt index 7b53097c..52366a88 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,3 +1,4 @@ get-key==1.60.0 Pillow==10.0.0 pyserial==3.5 +pygame==2.6.1 From 89186a511b379da21968d50f14e1970e1e13d62c Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 11:37:46 +0800 Subject: [PATCH 34/43] python: Fix pyinstaller - Include downloaded releases - Fix exe name Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index 327c38e3..bcf12000 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -111,14 +111,16 @@ jobs: 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" --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.12' spec: python/inputmodule/cli.py #'src/build.spec' requirements: 'python/requirements.txt' - upload_exe_with_name: 'ledmatrixgui' - options: --onefile, --windowed, --add-data 'res;res' + upload_exe_with_name: 'ledmatrixgui.exe' + options: --onefile, --name "ledmatrixgui", --windowed, --add-data "releases;releases" --add-data 'res;res' -p python/inputmodule package-python: name: Package Python From a6c0258525e70d96fde64c0eaed7cb8d538cb041 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 11:53:12 +0800 Subject: [PATCH 35/43] python: fix popup function changed when removing pysimplegui We removed the function earlier, and then introduced it back, but with different arguments. Signed-off-by: Daniel Schaefer --- python/inputmodule/cli.py | 6 +++--- python/inputmodule/gui/__init__.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index 3b9c0492..b65780e7 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -237,7 +237,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 +250,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,7 +268,7 @@ 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: diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index ea7d1309..3592dab9 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -44,8 +44,9 @@ def update_brightness_slider(devices): if average_brightness: brightness_scale.set(average_brightness) -def popup(message): - messagebox.showinfo("Framework Laptop 16 LED Matrix", message) +def popup(message, gui=True): + if gui: + messagebox.showinfo("Framework Laptop 16 LED Matrix", message) def run_gui(devices): root = tk.Tk() From 5d4128c689147f1edc0d30afb4f1239b8e2429d4 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 13:36:24 +0800 Subject: [PATCH 36/43] python: readd keyget snake Otherwise the cli snake is broken Signed-off-by: Daniel Schaefer --- python/inputmodule/cli.py | 2 +- python/inputmodule/games.py | 219 ++++++++++++++++++ python/inputmodule/gui/__init__.py | 3 +- python/inputmodule/gui/games/__init__.py | 103 -------- python/inputmodule/gui/pygames/__init__.py | 0 .../gui/{games => pygames}/ledris.py | 0 .../gui/{games => pygames}/snake.py | 0 7 files changed, 221 insertions(+), 106 deletions(-) create mode 100644 python/inputmodule/games.py delete mode 100644 python/inputmodule/gui/games/__init__.py create mode 100644 python/inputmodule/gui/pygames/__init__.py rename python/inputmodule/gui/{games => pygames}/ledris.py (100%) rename python/inputmodule/gui/{games => pygames}/snake.py (100%) diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index b65780e7..c12e5f4a 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -18,7 +18,7 @@ GameOfLifeStartParam, GameControlVal, ) -from inputmodule.gui.games import ( +from inputmodule.games import ( snake, snake_embedded, pong_embedded, diff --git a/python/inputmodule/games.py b/python/inputmodule/games.py new file mode 100644 index 00000000..f6bf1d9a --- /dev/null +++ b/python/inputmodule/games.py @@ -0,0 +1,219 @@ +from getkey import getkey, keys +import random +from datetime import datetime, timedelta +import time +import threading + +from inputmodule.inputmodule import ( + GameControlVal, + send_command, + CommandVals, + Game, +) +from inputmodule.inputmodule.ledmatrix import ( + show_string, + WIDTH, + HEIGHT, + render_matrix, +) + +# Constants +ARG_UP = 0 +ARG_DOWN = 1 +ARG_LEFT = 2 +ARG_RIGHT = 3 +ARG_QUIT = 4 +ARG_2LEFT = 5 +ARG_2RIGHT = 6 + +# Variables +direction = None +body = [] + + +def opposite_direction(direction): + if direction == keys.RIGHT: + return keys.LEFT + elif direction == keys.LEFT: + return keys.RIGHT + elif direction == keys.UP: + return keys.DOWN + elif direction == keys.DOWN: + return keys.UP + return direction + + + +def snake_keyscan(): + global direction + global body + + while True: + current_dir = direction + key = getkey() + if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: + # Don't allow accidental suicide if we have a body + if key == opposite_direction(current_dir) and body: + continue + direction = key + + +def snake_embedded_keyscan(dev): + while True: + key_arg = None + key = getkey() + if key == keys.UP: + key_arg = GameControlVal.Up + elif key == keys.DOWN: + key_arg = GameControlVal.Down + elif key == keys.LEFT: + key_arg = GameControlVal.Left + elif key == keys.RIGHT: + key_arg = GameControlVal.Right + elif key == "q": + # Quit + key_arg = GameControlVal.Quit + if key_arg is not None: + send_command(dev, CommandVals.GameControl, [key_arg]) + + +def game_over(dev): + global body + while True: + show_string(dev, "GAME ") + time.sleep(0.75) + show_string(dev, "OVER!") + time.sleep(0.75) + score = len(body) + show_string(dev, f"{score:>3} P") + time.sleep(0.75) + + +def pong_embedded(dev): + # Start game + send_command(dev, CommandVals.StartGame, [Game.Pong]) + + while True: + key_arg = None + key = getkey() + if key == keys.LEFT: + key_arg = ARG_LEFT + elif key == keys.RIGHT: + key_arg = ARG_RIGHT + elif key == "a": + key_arg = ARG_2LEFT + elif key == "d": + key_arg = ARG_2RIGHT + elif key == "q": + # Quit + key_arg = ARG_QUIT + if key_arg is not None: + send_command(dev, CommandVals.GameControl, [key_arg]) + + +def game_of_life_embedded(dev, arg): + # Start game + # TODO: Add a way to stop it + print("Game", int(arg)) + send_command(dev, CommandVals.StartGame, [Game.GameOfLife, int(arg)]) + + +def snake_embedded(dev): + # Start game + send_command(dev, CommandVals.StartGame, [Game.Snake]) + + snake_embedded_keyscan(dev) + + +def snake(dev): + global direction + global body + head = (0, 0) + direction = keys.DOWN + food = (0, 0) + while food == head: + food = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) + + # Setting + WRAP = False + + thread = threading.Thread(target=snake_keyscan, args=(), daemon=True) + thread.start() + + prev = datetime.now() + while True: + now = datetime.now() + delta = (now - prev) / timedelta(milliseconds=1) + + if delta > 200: + prev = now + else: + continue + + # Update position + (x, y) = head + oldhead = head + if direction == keys.RIGHT: + head = (x + 1, y) + elif direction == keys.LEFT: + head = (x - 1, y) + elif direction == keys.UP: + head = (x, y - 1) + elif direction == keys.DOWN: + head = (x, y + 1) + + # Detect edge condition + (x, y) = head + if head in body: + return game_over(dev) + elif x >= WIDTH or x < 0 or y >= HEIGHT or y < 0: + if WRAP: + if x >= WIDTH: + x = 0 + elif x < 0: + x = WIDTH - 1 + elif y >= HEIGHT: + y = 0 + elif y < 0: + y = HEIGHT - 1 + head = (x, y) + else: + return game_over(dev) + elif head == food: + body.insert(0, oldhead) + while food == head: + food = (random.randint(0, WIDTH - 1), + random.randint(0, HEIGHT - 1)) + elif body: + body.pop() + body.insert(0, oldhead) + + # Draw on screen + matrix = [[0 for _ in range(HEIGHT)] for _ in range(WIDTH)] + matrix[x][y] = 1 + matrix[food[0]][food[1]] = 1 + for bodypart in body: + (x, y) = bodypart + matrix[x][y] = 1 + render_matrix(dev, matrix) + + +def wpm_demo(dev): + """Capture keypresses and calculate the WPM of the last 10 seconds + TODO: I'm not sure my calculation is right.""" + start = datetime.now() + keypresses = [] + while True: + _ = getkey() + + now = datetime.now() + keypresses = [x for x in keypresses if (now - x).total_seconds() < 10] + keypresses.append(now) + # Word is five letters + wpm = (len(keypresses) / 5) * 6 + + total_time = (now - start).total_seconds() + if total_time < 10: + wpm = wpm / (total_time / 10) + + show_string(dev, " " + str(int(wpm))) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 3592dab9..0a495554 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -16,8 +16,7 @@ Game, GameControlVal ) -from inputmodule.gui.games import snake -from inputmodule.gui.games import ledris +from inputmodule.gui.pygames import snake, ledris 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 ( diff --git a/python/inputmodule/gui/games/__init__.py b/python/inputmodule/gui/games/__init__.py deleted file mode 100644 index c3d01d5b..00000000 --- a/python/inputmodule/gui/games/__init__.py +++ /dev/null @@ -1,103 +0,0 @@ -from getkey import getkey, keys -import random -from datetime import datetime, timedelta -import time -import threading - -from inputmodule.inputmodule import ( - GameControlVal, - send_command, - CommandVals, - Game, -) -from inputmodule.inputmodule.ledmatrix import ( - show_string, - WIDTH, - HEIGHT, - render_matrix, -) - -# Constants -ARG_UP = 0 -ARG_DOWN = 1 -ARG_LEFT = 2 -ARG_RIGHT = 3 -ARG_QUIT = 4 -ARG_2LEFT = 5 -ARG_2RIGHT = 6 - - -def snake_embedded_keyscan(dev): - while True: - key_arg = None - key = getkey() - if key == keys.UP: - key_arg = GameControlVal.Up - elif key == keys.DOWN: - key_arg = GameControlVal.Down - elif key == keys.LEFT: - key_arg = GameControlVal.Left - elif key == keys.RIGHT: - key_arg = GameControlVal.Right - elif key == "q": - # Quit - key_arg = GameControlVal.Quit - if key_arg is not None: - send_command(dev, CommandVals.GameControl, [key_arg]) - - -def pong_embedded(dev): - # Start game - send_command(dev, CommandVals.StartGame, [Game.Pong]) - - while True: - key_arg = None - key = getkey() - if key == keys.LEFT: - key_arg = ARG_LEFT - elif key == keys.RIGHT: - key_arg = ARG_RIGHT - elif key == "a": - key_arg = ARG_2LEFT - elif key == "d": - key_arg = ARG_2RIGHT - elif key == "q": - # Quit - key_arg = ARG_QUIT - if key_arg is not None: - send_command(dev, CommandVals.GameControl, [key_arg]) - - -def game_of_life_embedded(dev, arg): - # Start game - # TODO: Add a way to stop it - print("Game", int(arg)) - send_command(dev, CommandVals.StartGame, [Game.GameOfLife, int(arg)]) - - -def snake_embedded(dev): - # Start game - send_command(dev, CommandVals.StartGame, [Game.Snake]) - - snake_embedded_keyscan(dev) - - -def wpm_demo(dev): - """Capture keypresses and calculate the WPM of the last 10 seconds - TODO: I'm not sure my calculation is right.""" - start = datetime.now() - keypresses = [] - while True: - _ = getkey() - - now = datetime.now() - keypresses = [x for x in keypresses if (now - x).total_seconds() < 10] - keypresses.append(now) - # Word is five letters - wpm = (len(keypresses) / 5) * 6 - - total_time = (now - start).total_seconds() - if total_time < 10: - wpm = wpm / (total_time / 10) - - show_string(dev, " " + str(int(wpm))) diff --git a/python/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/games/ledris.py b/python/inputmodule/gui/pygames/ledris.py similarity index 100% rename from python/inputmodule/gui/games/ledris.py rename to python/inputmodule/gui/pygames/ledris.py diff --git a/python/inputmodule/gui/games/snake.py b/python/inputmodule/gui/pygames/snake.py similarity index 100% rename from python/inputmodule/gui/games/snake.py rename to python/inputmodule/gui/pygames/snake.py From ca29198056013a29c1a947065ba598200c9e9a8a Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 13:43:40 +0800 Subject: [PATCH 37/43] python: Add GUI screenshot Signed-off-by: Daniel Schaefer --- README.md | 6 +++++- res/ledmatrixgui-home.png | Bin 0 -> 63004 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 res/ledmatrixgui-home.png diff --git a/README.md b/README.md index ad2a53bd..a6c102b9 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/res/ledmatrixgui-home.png b/res/ledmatrixgui-home.png new file mode 100644 index 0000000000000000000000000000000000000000..c0a5ca49a3f79390705c0881cf053d9c8ba299e9 GIT binary patch literal 63004 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 Date: Mon, 25 Nov 2024 13:52:39 +0800 Subject: [PATCH 38/43] python: Add links to online info Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index 0a495554..bbb5b8ea 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -1,7 +1,9 @@ import os -import threading -import sys import platform +import sys +import threading +import webbrowser + import tkinter as tk from tkinter import ttk, messagebox @@ -80,6 +82,21 @@ def run_gui(devices): 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) From ca30109f9ec605007426d386611ff716b7cf3caf Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 14:00:08 +0800 Subject: [PATCH 39/43] python: Don't import pygame if not needed - Make pygame an optional dependency - Avoid the pygame hello print on startup ``` pygame 2.6.1 (SDL 2.28.4, Python 3.13.0) Hello from the pygame community. https://www.pygame.org/contribute.html ``` Signed-off-by: Daniel Schaefer --- python/inputmodule/cli.py | 4 ---- python/inputmodule/gui/__init__.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index c12e5f4a..8bc91b1f 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -62,10 +62,6 @@ RGB_COLORS, ) -# Optional dependencies: -# from PIL import Image -# import PySimpleGUI as sg - def main_cli(): parser = argparse.ArgumentParser() diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index bbb5b8ea..c3165794 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -18,7 +18,6 @@ Game, GameControlVal ) -from inputmodule.gui.pygames import snake, ledris 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 ( @@ -233,12 +232,14 @@ def run_gui(devices): root.mainloop() def perform_action(devices, action): - 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.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) From c3d735982d77c45464024b4cf2ba79259ddb875b Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 14:08:57 +0800 Subject: [PATCH 40/43] python: Move sleep/wake buttons to advanced tab They're not really useful to the average user. Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index c3165794..d47ddd08 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -220,7 +220,7 @@ def run_gui(devices): 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(tab1, text="Device Control", style="TLabelframe") + 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", From fd0b85cdaa2401566fb9f28ea8a810c6561b43a8 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 14:10:13 +0800 Subject: [PATCH 41/43] python: Launch GUI by default pyinstaller just runs the script, so we need to make the default, the gui. Signed-off-by: Daniel Schaefer --- python/inputmodule/cli.py | 2 +- python/pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py index 8bc91b1f..852641f2 100755 --- a/python/inputmodule/cli.py +++ b/python/inputmodule/cli.py @@ -404,4 +404,4 @@ def main_gui(): if __name__ == "__main__": - main_cli() + main_gui() diff --git a/python/pyproject.toml b/python/pyproject.toml index d205db75..32b41083 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -37,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" From 13d3d02ccf0e019609ada627e9babb925675e418 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 14:17:21 +0800 Subject: [PATCH 42/43] python: Add windows app icon Signed-off-by: Daniel Schaefer --- python/inputmodule/gui/__init__.py | 5 +++++ res/framework_startmenuicon.ico | Bin 0 -> 89962 bytes 2 files changed, 5 insertions(+) create mode 100644 res/framework_startmenuicon.ico diff --git a/python/inputmodule/gui/__init__.py b/python/inputmodule/gui/__init__.py index d47ddd08..f7e53a60 100644 --- a/python/inputmodule/gui/__init__.py +++ b/python/inputmodule/gui/__init__.py @@ -52,6 +52,11 @@ def run_gui(devices): 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) diff --git a/res/framework_startmenuicon.ico b/res/framework_startmenuicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8c6be29a6b1462e13005c3c113d81888f51b73ab GIT binary patch literal 89962 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 P!S%KZZ+yQv)_?v#QgVSl literal 0 HcmV?d00001 From b5b8739e74f44ece641ee3d06157edc8930ce994 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Mon, 25 Nov 2024 14:52:49 +0800 Subject: [PATCH 43/43] python: Add executable icon Signed-off-by: Daniel Schaefer --- .github/workflows/software.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml index bcf12000..105dde7b 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -112,7 +112,7 @@ jobs: 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" --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 + # 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: JohnAZoidberg/pyinstaller-action@dont-clean with: @@ -120,7 +120,7 @@ jobs: spec: python/inputmodule/cli.py #'src/build.spec' requirements: 'python/requirements.txt' upload_exe_with_name: 'ledmatrixgui.exe' - options: --onefile, --name "ledmatrixgui", --windowed, --add-data "releases;releases" --add-data 'res;res' -p python/inputmodule + 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