diff --git a/.cargo/config.toml b/.cargo/config.toml index 1caefdfb..06cfe1d7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,11 +1,8 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] # Choose a default "cargo run" tool: # - probe-run provides flashing and defmt via a hardware debugger -# - cargo embed offers flashing, rtt, defmt and a gdb server via a hardware debugger -# it is configured via the Embed.toml in the root of this project # - elf2uf2-rs loads firmware over USB when the rp2040 is in boot mode -#runner = "probe-run --chip RP2040" -# runner = "cargo embed" +# runner = "probe-run --chip RP2040" runner = "elf2uf2-rs -d" rustflags = [ diff --git a/.github/workflows/ci_checks.yml b/.github/workflows/ci_checks.yml deleted file mode 100644 index f8cd1215..00000000 --- a/.github/workflows/ci_checks.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: CI Checks - -# Currently disable -#on: [push, pull_request] - -env: - CARGO_TERM_COLOR: always - -jobs: - building: - name: Building - continue-on-error: ${{ matrix.experimental || false }} - strategy: - matrix: - # All generated code should be running on stable now - rust: [nightly, stable] - include: - # Nightly is only for reference and allowed to fail - - rust: nightly - experimental: true - os: - # Check compilation works on common OSes - # (i.e. no path issues) - - ubuntu-latest - - macOS-latest - - windows-latest - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.rust }} - override: true - - run: cargo install flip-link - - run: rustup target install --toolchain=${{ matrix.rust }} thumbv6m-none-eabi - - run: cargo build --all - - run: cargo build --all --release - linting: - name: Linting - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: clippy - - run: rustup target install thumbv6m-none-eabi - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features -- -D warnings - formatting: - name: Formatting - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: rustfmt - - run: rustup target install thumbv6m-none-eabi - - run: cargo fmt -- --check diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml new file mode 100644 index 00000000..cba0a1a5 --- /dev/null +++ b/.github/workflows/firmware.yml @@ -0,0 +1,143 @@ +name: Firmware + +on: + push: + branches: + - main + - dev-* + paths-ignore: + - '*.py' + - 'inputmodule-control/**' + pull_request: + branches: + - '*' + paths-ignore: + - '*.py' + - 'inputmodule-control/**' + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + building: + name: Building + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - run: cargo install flip-link + # Debug version + - run: cargo make --cwd b1display + - run: cargo make --cwd c1minimal + - run: cargo make --cwd ledmatrix + - run: cargo make --cwd qtpy + # Release version + - run: cargo make --cwd ledmatrix build-release + - run: cargo make --cwd ledmatrix build-release-10k + - run: cargo make --cwd ledmatrix build-release-evt + - run: cargo make --cwd b1display build-release + - run: cargo make --cwd c1minimal build-release + - run: cargo make --cwd qtpy build-release + + - name: Convert to UF2 format + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev + cargo make --cwd b1display uf2 + cargo make --cwd c1minimal uf2 + cargo make --cwd qtpy uf2 + cargo make --cwd ledmatrix build-release-10k-uf2 + cargo make --cwd ledmatrix build-release-evt-uf2 + cargo make --cwd ledmatrix uf2 + + - name: Convert to bin format + run: | + sudo apt-get update + sudo apt-get install -y llvm + cargo make --cwd b1display bin + cargo make --cwd qtpy bin + cargo make --cwd c1minimal bin + cargo make --cwd ledmatrix bin + + - name: Upload ledmatrix files + uses: actions/upload-artifact@v4 + with: + name: ledmatrix_fw_${{github.sha}} + path: | + # Main firmware + target/thumbv6m-none-eabi/release/ledmatrix.bin + target/thumbv6m-none-eabi/release/ledmatrix.uf2 + # EVT 10k resistor + target/thumbv6m-none-eabi/release/ledmatrix_10k.uf2 + # EVT (27k) resistor + target/thumbv6m-none-eabi/release/ledmatrix_evt.uf2 + + - name: Upload b1display files + uses: actions/upload-artifact@v4 + with: + name: b1display_fw_${{github.sha}} + path: | + target/thumbv6m-none-eabi/release/b1display.bin + target/thumbv6m-none-eabi/release/b1display.uf2 + + - name: Upload c1minimal files + uses: actions/upload-artifact@v4 + with: + name: c1minimal_fw_${{github.sha}} + path: | + target/thumbv6m-none-eabi/release/c1minimal.bin + target/thumbv6m-none-eabi/release/c1minimal.uf2 + + linting: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - run: | + sudo apt-get update + sudo apt-get install -y libudev-dev + cargo make clippy --cwd b1display + cargo make clippy --cwd qtpy + cargo make clippy --cwd c1minimal + cargo make clippy --cwd ledmatrix + + # fl16-inputmodules/src/serialnum.rs + # is currently used by all firmwares to show their firmware version. + # But it shows the version of the fl16-inputmodules package. + # So that needs to be the same as the firmware version. + - name: Check versions of all packages are the same + run: | + cargo pkgid -p fl16-inputmodules | cut -d "#" -f2 >> versions.tmp + cargo pkgid -p b1display | cut -d "#" -f2 >> versions.tmp + cargo pkgid -p qtpy | cut -d "#" -f2 >> versions.tmp + cargo pkgid -p c1minimal | cut -d "#" -f2 >> versions.tmp + cargo pkgid -p ledmatrix | cut -d "#" -f2 >> versions.tmp + uniq -c versions.tmp | [ $(wc -l) -eq 1 ] + + formatting: + name: Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: | + cargo fmt -p b1display -- --check + cargo fmt -p c1minimal -- --check + cargo fmt -p ledmatrix -- --check + cargo fmt -p qtpy -- --check + cargo fmt -p fl16-inputmodules -- --check diff --git a/.github/workflows/software.yml b/.github/workflows/software.yml new file mode 100644 index 00000000..105dde7b --- /dev/null +++ b/.github/workflows/software.yml @@ -0,0 +1,158 @@ +name: Software + +on: + push: + branches: + - main + - dev-* + paths-ignore: + - 'b1display/**' + - 'c1minimal/**' + - 'fl16-inputmodules/**' + - 'ledmatrix/**' + pull_request: + branches: + - '*' + paths-ignore: + - 'b1display/**' + - 'c1minimal/**' + - 'fl16-inputmodules/**' + - 'ledmatrix/**' + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + # Enable later + #freebsd-cross-build: + # name: Cross-Build for FreeBSD + # runs-on: 'ubuntu-22.04' + # steps: + # - uses: actions/checkout@v4 + + # - name: Setup Rust toolchain + # run: rustup show + + # - name: Install cross compilation tool + # run: cargo install cross + + # - name: Build FreeBSD tool + # run: cross build --target=x86_64-unknown-freebsd + + # - name: Upload FreeBSD App + # uses: actions/upload-artifact@v4 + # with: + # name: qmk_hid_freebsd + # path: target/x86_64-unknown-freebsd/debug/qmk_hid + + build: + name: Build Linux + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libasound2-dev + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - name: Build Linux tool + run: cargo make --cwd inputmodule-control build-release + + - name: Check if Linux tool can start + run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' + + - name: Upload Linux tool + uses: actions/upload-artifact@v4 + with: + name: inputmodule-control + path: target/x86_64-unknown-linux-gnu/release/inputmodule-control + + build-windows: + name: Build Windows + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - name: Build Windows tool + run: cargo make --cwd inputmodule-control build-release + + - name: Check if Windows tool can start + run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' + + - name: Upload Windows App + uses: actions/upload-artifact@v4 + with: + name: inputmodule-control.exe + path: target/x86_64-pc-windows-msvc/release/inputmodule-control.exe + + # Or manually with + # pyinstaller --onefile --windowed --add-data 'res;res' ledmatrix_control.py + build-gui: + name: Build GUI + runs-on: windows-latest + 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 + + # To run locally, need to make sure to include the pywin32 DLL + # pyinstaller --onefile, --name "python/inputmodule/cli.py", --windowed, --add-data "releases;releases" --icon=res\framework_startmenuicon.ico --path C:\users\skype\appdata\local\packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\localcache\local-packages\Python312\site-packages\pywin32_system32 --add-data 'res;res' -p python/inputmodule python/inputmodule/cli.py + - name: Create Executable + uses: 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.exe' + options: --onefile, --name "ledmatrixgui", --windowed, --add-data "releases;releases" --icon=res/framework_startmenuicon.ico --add-data 'res;res' -p python/inputmodule + + package-python: + name: Package Python + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - run: | + cd python + python3 -m pip install --upgrade build + python3 -m pip install --upgrade hatch + python3 -m pip install --upgrade twine + python3 -m build + + lints: + name: Lints + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libasound2-dev + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - name: Run cargo fmt + run: cargo fmt -p inputmodule-control -- --check + + - name: Run cargo clippy + run: cargo make clippy --cwd inputmodule-control diff --git a/.github/workflows/traditional-cargo.yml b/.github/workflows/traditional-cargo.yml new file mode 100644 index 00000000..231ead0d --- /dev/null +++ b/.github/workflows/traditional-cargo.yml @@ -0,0 +1,102 @@ +# Test builds without cargo-make +# Not the recommended path, but should make sure it still works +name: Traditional Cargo + +on: + push: + branches: + - main + - dev-* + paths-ignore: + - '*.py' + pull_request: + branches: + - '*' + paths-ignore: + - '*.py' + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + firmware: + name: Build firmware + runs-on: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install flip-link + + - run: cargo build -p ledmatrix + - run: cargo build -p ledmatrix --features 10k,evt + - run: cargo build -p ledmatrix --features evt + - run: cargo build -p b1display + - run: cargo build -p c1minimal + + linux-software: + name: Build Linux + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libasound2-dev + + - name: Setup Rust toolchain + run: rustup show + + - name: Build tool + run: cargo build --release --target x86_64-unknown-linux-gnu -p inputmodule-control + + - name: Check if tool can start + run: cargo run --release --target x86_64-unknown-linux-gnu -p inputmodule-control -- --help | grep 'RAW HID and VIA commandline' + + windows-software: + name: Build Windows + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust toolchain + run: rustup show + + - run: cargo install cargo-make + + - name: Build tool + run: cargo build --release --target x86_64-pc-windows-msvc -p inputmodule-control + + - name: Check if tool can start + run: cargo run --release --target x86_64-pc-windows-msvc -p inputmodule-control -- --help | grep 'RAW HID and VIA commandline' + + lint-format: + name: Lint and format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libasound2-dev + + - name: Setup Rust toolchain + run: rustup show + + - name: Firmware clippy + run: | + cargo clippy -p b1display -- --deny=warnings + cargo clippy -p c1minimal -- --deny=warnings + cargo clippy -p ledmatrix -- --deny=warnings + cargo clippy -p fl16-inputmodules -- --deny=warnings + + - name: Software clippy + run: cargo clippy --target x86_64-unknown-linux-gnu -p inputmodule-control -- -D warnings + + - name: All cargo fmt + run: cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore index d2db038f..579047c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ **/*.rs.bk .#* .gdb_history -Cargo.lock target/ +venv + # editor files .vscode/* !.vscode/*.md @@ -12,8 +13,18 @@ target/ !.vscode/tasks.json !.vscode/extensions.json !.vscode/settings.json - *.uf2 # Panic dump message.bin + +# Python +__pycache__ + +# Hatch +_version.py + +# pyinstaller +build/ +dist/ +ledmatrix_control.spec diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..afe9fa55 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2729 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach", +] + +[[package]] +name = "adafruit-qt-py-rp2040" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36e061b89c1354cab7a96173730928d67e9970ad509cd295af724abb30920b6" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "rp2040-boot2 0.2.1", + "rp2040-hal", +] + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" +dependencies = [ + "alsa-sys", + "bitflags 1.3.2", + "libc", + "nix 0.24.3", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "apodize" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca387cdc0a1f9c7a7c26556d584aa2d07fc529843082e4861003cde4ab914ed" + +[[package]] +name = "arrayvec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868f09ff8cea88b079da74ae569d9b8c62a23c68c746240b704ee6f7525c89c" + +[[package]] +name = "atomic-polyfill" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "b1display" +version = "0.2.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embedded-graphics", + "embedded-hal", + "fl16-inputmodules", + "fugit", + "heapless", + "rp2040-boot2 0.3.0", + "rp2040-hal", + "rp2040-panic-usb-boot", + "st7306", + "tinybmp", + "usb-device", + "usbd-hid", + "usbd-serial", +] + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version 0.2.3", +] + +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex 1.8.4", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "c1minimal" +version = "0.2.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embedded-hal", + "fl16-inputmodules", + "fugit", + "heapless", + "rp2040-boot2 0.3.0", + "rp2040-hal", + "rp2040-panic-usb-boot", + "smart-leds", + "usb-device", + "usbd-hid", + "usbd-serial", + "ws2812-pio", +] + +[[package]] +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell 1.18.0", +] + +[[package]] +name = "clap_builder" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab" +dependencies = [ + "anstream", + "anstyle", + "bitflags 1.3.2", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "color-backtrace" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c04463c99389fff045d2b90ce84f5131332712c7ffbede020f5e9ad1ed685" +dependencies = [ + "atty", + "backtrace", + "termcolor", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "coreaudio-rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys 0.6.2", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cortex-m" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" +dependencies = [ + "bare-metal", + "bitfield", + "embedded-hal", + "volatile-register", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee84e813d593101b1723e13ec38b6ab6abbdbaaa4546553f5395ed274079ddb1" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cpal" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.4", + "coreaudio-rs", + "dasp_sample", + "jni 0.19.0", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "once_cell 1.18.0", + "parking_lot 0.12.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.46.0", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-any" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774646b687f63643eb0f4bf13dc263cb581c8c9e57973b6ddf78bda3994d88df" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "defmt" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956673bd3cb347512bf988d1e8d89ac9a82b64f6eec54d3c01c3529dac019882" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4abc4821bd84d3d8f49945ddb24d029be9385ed9b77c99bf2f6296847a6a9f0" +dependencies = [ + "defmt-parser", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "defmt-parser" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269924c02afd7f94bc4cecbfa5c379f6ffcf9766b3408fe63d22c728654eccd0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "609923761264dd99ed9c7d209718cda4631c5fe84668e0f0960124cbb844c49f" +dependencies = [ + "critical-section", + "defmt", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "embedded-graphics" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2a8e0250a7e1212828166b01eed0219e488ebb2599f44624a29c9bd249f397" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +dependencies = [ + "az", + "byteorder", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex 1.8.4", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" +dependencies = [ + "backtrace", +] + +[[package]] +name = "ezconf" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a13b438ef62361572d5693bba58ad3b6304b4245ba5b6d3d7629e8eb92aa897" +dependencies = [ + "log", + "once_cell 0.1.8", + "toml", + "toml-query", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fl16-inputmodules" +version = "0.2.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "crc", + "defmt", + "defmt-rtt", + "embedded-graphics", + "embedded-hal", + "fugit", + "heapless", + "is31fl3741", + "num", + "num-derive", + "num-traits", + "rp2040-boot2 0.3.0", + "rp2040-hal", + "rp2040-panic-usb-boot", + "smart-leds", + "st7306", + "tinybmp", + "usb-device", + "usbd-hid", + "usbd-serial", + "ws2812-pio", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fugit" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7" +dependencies = [ + "gcd", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heapless" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.0", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.4", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.1.0", + "hashbrown", +] + +[[package]] +name = "inputmodule-control" +version = "0.2.0" +dependencies = [ + "chrono", + "clap", + "image", + "rand 0.8.5", + "serialport", + "static_vcruntime", + "vis-core", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys", +] + +[[package]] +name = "is-match" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5b386aef33a1c677be65237cb9d32c3f3ef56bd035949710c4bb13083eb053" + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "is31fl3741" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9abf02bbdd939fe7f46924002c004d8ab811f093392d8e354e8f583a40badf" +dependencies = [ + "embedded-graphics-core", + "embedded-hal", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "ledmatrix" +version = "0.2.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embedded-hal", + "fl16-inputmodules", + "fugit", + "heapless", + "is31fl3741", + "rp2040-boot2 0.3.0", + "rp2040-hal", + "rp2040-panic-usb-boot", + "usb-device", + "usbd-hid", + "usbd-serial", +] + +[[package]] +name = "libc" +version = "0.2.146" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +dependencies = [ + "scopeguard 0.3.3", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg 1.1.0", + "scopeguard 1.1.0", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "micromath" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39617bc909d64b068dcffd0e3e31679195b5576d0c83fadc52690268cc2b2b55" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg 1.1.0", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +dependencies = [ + "jni 0.20.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532c29a261168a45ce28948f9537ddd7a5dd272cc513b3017b1e82a88f962c37" +dependencies = [ + "parking_lot 0.7.1", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" +dependencies = [ + "lock_api 0.1.5", + "parking_lot_core 0.4.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api 0.4.10", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" +dependencies = [ + "libc", + "rand 0.6.5", + "rustc_version 0.2.3", + "smallvec 0.6.14", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec 1.10.0", + "windows-targets 0.48.0", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3" +dependencies = [ + "arrayvec", + "num_enum", + "paste", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primal-check" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell 1.18.0", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qtpy" +version = "0.2.0" +dependencies = [ + "adafruit-qt-py-rp2040", + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embedded-hal", + "fl16-inputmodules", + "fugit", + "heapless", + "rp2040-boot2 0.3.0", + "rp2040-hal", + "rp2040-panic-usb-boot", + "smart-leds", + "usb-device", + "usbd-hid", + "usbd-serial", + "ws2812-pio", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local", + "utf8-ranges", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rp2040-boot2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c773ec49b836077aa144b58dc7654a243e1eecdb6cf0d25361ae7c7600fabd8" +dependencies = [ + "crc-any", +] + +[[package]] +name = "rp2040-boot2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92f344f63f950ee36cf4080050e4dce850839b9175da38f9d2ffb69b4dbb21" +dependencies = [ + "crc-any", +] + +[[package]] +name = "rp2040-hal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1369bb84862d7f69391a96606b2f29a00bfce7f29a749e23d5f01fc3f607ada0" +dependencies = [ + "cortex-m", + "critical-section", + "embedded-dma", + "embedded-hal", + "fugit", + "itertools", + "nb 1.1.0", + "paste", + "pio", + "rand_core 0.6.4", + "rp2040-hal-macros", + "rp2040-pac", + "usb-device", + "vcell", + "void", +] + +[[package]] +name = "rp2040-hal-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86479063e497efe1ae81995ef9071f54fd1c7427e04d6c5b84cde545ff672a5e" +dependencies = [ + "cortex-m-rt", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rp2040-pac" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9192cafbb40d717c9e0ddf767aaf9c69fee1b4e48d22ed853b57b11f6d9f3d7e" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "vcell", +] + +[[package]] +name = "rp2040-panic-usb-boot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b311d2a72e5a63d5511345b61fe7dd02ce0990bd093ba3dc94b3e80b0a0e373" +dependencies = [ + "cortex-m", + "rp2040-hal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.17", +] + +[[package]] +name = "rustfft" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" + +[[package]] +name = "serialport" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353dc2cbfc67c9a14a89a1292a9d8e819bd51066b083e08c1974ba08e3f48c62" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags 2.0.2", + "cfg-if", + "libudev", + "mach2", + "nix 0.26.2", + "regex 1.8.4", + "scopeguard 1.1.0", + "winapi", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smart-leds" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd45fa275f70b4110eac5f5182611ad384f88bb22b68b9a9c3cafd7015290b" +dependencies = [ + "smart-leds-trait", +] + +[[package]] +name = "smart-leds-trait" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf6d833fa93f16a1c1874e62c2aebe8567e5bdd436d59bf543ed258b6f7a8e3" +dependencies = [ + "rgb", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api 0.4.10", +] + +[[package]] +name = "ssmarshal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" +dependencies = [ + "encode_unicode", + "serde", +] + +[[package]] +name = "st7306" +version = "0.8.2" +source = "git+https://github.com/FrameworkComputer/st7306-rs?branch=update-deps#8283fcdb91a559fe3a12cb01bc6523d737c28be8" +dependencies = [ + "embedded-graphics", + "embedded-hal", + "nb 1.1.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_vcruntime" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinybmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197cc000e382175ff15abd9c54c694ef80ef20cb07e7f956c71e3ea97fc8dc60" +dependencies = [ + "embedded-graphics", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml-query" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6854664bfc6df0360c695480836ee90e2d0c965f06db291d10be9344792d43e8" +dependencies = [ + "error-chain", + "is-match", + "lazy_static", + "regex 0.2.11", + "toml", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "transpose" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "triple_buffer" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88de9e10c067f441831d00cca341cf66fc69227ac6962292f944382dd61dacb9" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "usb-device" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6cc3adc849b5292b4075fc0d5fdcf2f24866e88e336dd27a8943090a520508" + +[[package]] +name = "usbd-hid" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975bd411f4a939986751ea09992a24fa47c4d25c6ed108d04b4c2999a4fd0132" +dependencies = [ + "serde", + "ssmarshal", + "usb-device", + "usbd-hid-macros", +] + +[[package]] +name = "usbd-hid-descriptors" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbee8c6735e90894fba04770bc41e11fd3c5256018856e15dc4dd1e6c8a3dd1" +dependencies = [ + "bitfield", +] + +[[package]] +name = "usbd-hid-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261079a9ada015fa1acac7cc73c98559f3a92585e15f508034beccf6a2ab75a2" +dependencies = [ + "byteorder", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "usbd-hid-descriptors", +] + +[[package]] +name = "usbd-serial" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db75519b86287f12dcf0d171c7cf4ecc839149fe9f3b720ac4cfce52959e1dfe" +dependencies = [ + "embedded-hal", + "nb 0.1.3", + "usb-device", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vis-core" +version = "0.1.0" +source = "git+https://github.com/Rahix/visualizer2.git?rev=1fe908012a9c156695921f3b6bb47178e1332b92#1fe908012a9c156695921f3b6bb47178e1332b92" +dependencies = [ + "apodize", + "color-backtrace", + "cpal", + "env_logger", + "ezconf", + "log", + "parking_lot 0.12.1", + "rustfft", + "triple_buffer", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee8f19f9d74293faf70901bc20ad067dc1ad390d2cbf1e3f75f721ffee908b6" +dependencies = [ + "vcell", +] + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell 1.18.0", + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + +[[package]] +name = "ws2812-pio" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d219e3b43c1e14305b36363060c0348d560314e235d999cf492bbbab1f38e8d" +dependencies = [ + "cortex-m", + "embedded-hal", + "fugit", + "nb 1.1.0", + "pio", + "rp2040-hal", + "smart-leds-trait", +] diff --git a/Cargo.toml b/Cargo.toml index 04bdc9e3..cf32392a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,81 +1,77 @@ -[package] -edition = "2021" -name = "led_matrix_fw" -version = "0.1.1" +# Not stable yet :( +# Trackign issue: https://github.com/rust-lang/cargo/issues/9406 +# cargo-features = ["per-package-target"] -[dependencies] -cortex-m = { version = "0.7", features = ["critical-section-single-core"]} +[workspace] +resolver = "2" +members = [ + "b1display", + "c1minimal", + "ledmatrix", + "fl16-inputmodules", + "inputmodule-control", + "qtpy", +] +# Don't build all of them by default. +# Because that'll lead to all features enabled in `fl16-inputmodules` and it +# doesn't currently support building with all features enabled at the same +# time. +# Can't add `inputmodule-control` because it must be built with the host system +# target. But we set the default target to thumbv6m-none-eabi +default-members = ["fl16-inputmodules"] + +[workspace.dependencies] +cortex-m = "0.7" cortex-m-rt = "0.7.3" embedded-hal = { version = "0.2.7", features = ["unproven"] } - +panic-probe = { version = "0.3", features = ["print-defmt"] } +rp2040-panic-usb-boot = "0.5.0" +rp2040-hal = { version = "0.8", features = ["rt", "critical-section-impl"] } +rp2040-boot2 = "0.3" defmt = "0.3" defmt-rtt = "0.4" - -#panic-probe = { version = "0.3", features = ["print-defmt"] } -rp2040-panic-usb-boot = { git = "https://github.com/rwalkr/rp2040-panic-usb-boot" } - -# Not using a BSP, we've got a LED Matrix BSP locally in this crate -rp2040-hal = { version="0.7", features=["rt"] } -rp2040-boot2 = "0.2" - # USB Serial -usb-device= "0.2.9" - -heapless = "0.7.9" +usb-device = "0.2.9" +heapless = "0.7.16" usbd-serial = "0.1.1" -usbd-hid = "0.5.1" -is31fl3741 = { git = "https://github.com/JohnAZoidberg/is31fl3741", branch = "all-at-once" } -fugit = "0.3.6" +usbd-hid = "0.6.1" +fugit = "0.3.7" +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" +tinybmp = "0.5.0" +# C1 Minimal +smart-leds = "0.3.0" +ws2812-pio = "0.6.0" -# cargo build/run -[profile.dev] +[profile.dev.package.ledmatrix] codegen-units = 1 -debug = 2 -debug-assertions = true -incremental = false +incremental = true # To allow single-stepping through code use 0. Will cause timing issues, though opt-level = 3 -overflow-checks = true -# cargo build/run --release -[profile.release] +[profile.dev.package.c1minimal] codegen-units = 1 -debug = 2 -debug-assertions = false -incremental = false -lto = 'fat' +incremental = true +# To allow single-stepping through code use 0. Will cause timing issues, though opt-level = 3 -overflow-checks = false - -# do not optimize proc-macro crates = faster builds from scratch -[profile.dev.build-override] -codegen-units = 8 -debug = false -debug-assertions = false -opt-level = 0 -overflow-checks = false -[profile.release.build-override] -codegen-units = 8 -debug = false -debug-assertions = false -opt-level = 0 -overflow-checks = false +[profile.dev.package.b1display] +codegen-units = 1 +incremental = true +# To allow single-stepping through code use 0. Will cause timing issues, though +opt-level = 3 -# cargo test -[profile.test] +[profile.dev.package.qtpy] codegen-units = 1 -debug = 2 -debug-assertions = true -incremental = false +incremental = true +# To allow single-stepping through code use 0. Will cause timing issues, though opt-level = 3 -overflow-checks = true -# cargo test --release -[profile.bench] +# Faster and smaller code but much slower to compile. +# Increase in rebuild time from <1s to 20s +[profile.release] codegen-units = 1 debug = 2 -debug-assertions = false -incremental = false lto = 'fat' -opt-level = 3 diff --git a/Embed.toml b/Embed.toml deleted file mode 100644 index 9c91b136..00000000 --- a/Embed.toml +++ /dev/null @@ -1,39 +0,0 @@ -[default.probe] -protocol = "Swd" -speed = 20000 -# If you only have one probe cargo embed will pick automatically -# Otherwise: add your probe's VID/PID/serial to filter - -## rust-dap -# usb_vid = "6666" -# usb_pid = "4444" -# serial = "test" - - -[default.flashing] -enabled = true - -[default.reset] -enabled = true -halt_afterwards = false - -[default.general] -chip = "RP2040" -log_level = "WARN" -# RP2040 does not support connect_under_reset -connect_under_reset = false - -[default.rtt] -enabled = true -up_mode = "NoBlockSkip" -channels = [ - { up = 0, down = 0, name = "name", up_mode = "NoBlockSkip", format = "Defmt" }, -] -timeout = 3000 -show_timestamps = true -log_enabled = false -log_path = "./logs" - -[default.gdb] -enabled = false -gdb_connection_string = "127.0.0.1:2345" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..06c9858d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Framework Computer Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 00000000..41c67187 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,22 @@ +[env] +TARGET_TRIPLE = "thumbv6m-none-eabi" +CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true +FEATURES = "" + +[tasks.build] +args = [ + "build", + "@@remove-empty(BUILD_TYPE)", + "--target", + "${TARGET_TRIPLE}", + "--features", + "${FEATURES}", +] + +[tasks.build-release] +clear = true +env.BUILD_TYPE = "--release" +run_task = "build" + +[tasks.test] +disabled = true diff --git a/README.md b/README.md index 4ad13925..a6c102b9 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,204 @@ -# Lotus LED Matrix Module +# Framework Laptop 16 - Input Module Firmware/Software -It's a 9x34 (306) LED matrix, controlled by RP2040 MCU and IS31FL3741A LED controller. +This repository contains both the firmware for the Framework Laptop 16 input modules, +as well as the tool to control them. -Connection to the host system is via USB 2.0 and currently there is a USB Serial API to control it without reflashing. +Rust firmware project setup based off of: https://github.com/rp-rs/rp2040-project-template -Rust project setup based off of: https://github.com/rp-rs/rp2040-project-template +## Modules -## Features +See pages of the individual modules for details about how they work and how +they're controlled. -- Reset into bootloader when firmware crashes/panics +- [LED Matrix](ledmatrix/README.md) +- [Minimal C1 Input Module](c1minimal/README.md) +- [2nd Display](b1display/README.md) +- [QT PY RP2040](qtpy/README.md) + +## Generic Features + +All modules are built with an RP2040 microcontroller +Features that all modules share + +- Firmware written in bare-metal Rust +- Reset into RP2040 bootloader when firmware crashes/panics +- Sleep Mode to save power - API over USB ACM Serial Port - Requires no Drivers on Windows and Linux - - Display various pre-programmed patterns - - Light up a percentage of the screen - - Change brightness - - Send a black/white image to the display - - Send a greyscale image to the display - Go to sleep - Reset into bootloader - - Scroll and loop the display content vertically - - A commandline script and graphical application to control it -- Sleep Mode - - Transition slowly turns off/on the LEDs - - Current hardware does not have the SLEEP# GPIO connected, can't sleep automatically + - Control and read module state (brightness, displayed image, ...) -Future features: +## Control from the host -- API - - Send a greyscale image to display - - Read current system state (brightness, sleeping, ...) +To build your own application see the: [API command documentation](commands.md) -## Control from the host +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). -Requirements: Python, [PySimpleGUI](https://www.pysimplegui.org) and optionally [pillow](https://pillow.readthedocs.io/en/stable/index.html) +For device specific commands, see their individual documentation pages. -Use `control.py`. Either the commandline, see `control.py --help` or the graphical version: `control.py --gui` +### GUI and Python +There are also a python library and GUI tool. See their [README](python/README.md). +![](res/ledmatrixgui-home.png) + +###### Permissions on Linux +To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: + +```sh +sudo cp release/50-framework-inputmodule.rules /etc/udev/rules.d/ +sudo udevadm control --reload && sudo udevadm trigger ``` -options: - -h, --help show this help message and exit - --bootloader Jump to the bootloader to flash new firmware - --sleep, --no-sleep Simulate the host going to sleep or waking up - --brightness BRIGHTNESS - Adjust the brightness. Value 0-255 - --animate, --no-animate - Start/stop vertical scrolling - --pattern {full,lotus,gradient,double-gradient,zigzag,panic,lotus2} - Display a pattern - --image IMAGE Display a PNG or GIF image in black and white only) - --image-grey IMAGE_GREY - Display a PNG or GIF image in greyscale - --percentage PERCENTAGE - Fill a percentage of the screen - --clock Display the current time - --string STRING Display a string or number, like FPS - --symbols SYMBOLS [SYMBOLS ...] - Show symbols (degF, degC, :), snow, cloud, ...) - --gui Launch the graphical version of the program - --blink Blink the current pattern - --breathing Breathing of the current pattern - --eq EQ [EQ ...] Equalizer - --random-eq Random Equalizer - --wpm WPM Demo - --snake Snake - --all-brightnesses Show every pixel in a different brightness - --serial-dev SERIAL_DEV - Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows + +##### Common commands: + +###### Listing available devices + +```sh +> inputmodule-control --list +/dev/ttyACM0 + VID 0x32AC + PID 0x0020 + SN FRAKDEAM0020110001 + Product LED_Matrix +/dev/ttyACM1 + VID 0x32AC + PID 0x0021 + SN FRAKDEAM0000000000 + Product B1_Display ``` -Examples +###### Apply command to single device + +By default a command will be sent to all devices that can be found, to apply it +to a single device, specify the COM port. +In this example the command is targeted at `b1-display`, so it will only apply +to this module type. ```sh -# Launch graphical application -./control.py --gui +# Example on Linux +> inputmodule-control --serial-dev /dev/ttyACM0 b1-display --pattern black + +# Example on Windows +> inputmodule-control.exe --serial-dev COM5 b1-display --pattern black +``` -# Show current time and keep updating it -./control.py --clock +###### Send command when device connects -# Draw PNG or GIF -./control.py --image stripe.gif -./control.py --image stripe.png +By default the app tries to connect with the device and aborts if it can't +connect. But you might want to start the app, have it wait until the device is +connected and then send the command. -# Change brightness (0-255) -./control.py --brightness 50 ``` +> inputmodule-control b1-display --pattern black +Failed to find serial device. Please manually specify with --serial-dev -## Building +# No failure, waits until the device is connected, sends command and exits +> inputmodule-control --wait-for-device b1-display --pattern black -Dependencies: Rust +# If the device is already connected, it does nothing, just wait 1s. +# This means you can run this command by a system service and restart it when +# it finishes. Then it will only ever do anything if the device reconnects. +> inputmodule-control --wait-for-device b1-display --pattern black +Device already present. No need to wait. Not executing command. +``` + +## Update the Firmware + +First, put the module into bootloader mode. -Prepare Rust toolchain: +This can be done either by pressing the bootsel button while plugging it in or +by using one of the following commands: + +```sh +inputmodule-control led-matrix --bootloader +inputmodule-control b1-display --bootloader +inputmodule-control c1-minimal --bootloader +``` + +Then the module will present itself in the same way as a USB thumb drive. +Copy the UF2 firmware file onto it and the device will flash and reset automatically. +Alternatively when building from source, run one of the following commands: + +```sh +cargo run -p ledmatrix +cargo run -p b1display +cargo run -p c1minimal +``` + +## Building the firmware + +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev + +Prepare Rust toolchain (once): ```sh rustup target install thumbv6m-none-eabi cargo install flip-link -cargo install elf2uf2-rs --locked +cargo install cargo-make +cargo install elf2uf2-rs ``` Build: ```sh -cargo build +cargo make --cwd ledmatrix +cargo make --cwd b1display +cargo make --cwd c1minimal ``` -Generate UF2 file: +Generate the UF2 update file into `target/thumbv6m-none-eabi/release/`: ```sh -elf2uf2-rs target/thumbv6m-none-eabi/debug/led_matrix_fw led_matrix.uf2 +cargo make --cwd ledmatrix uf2 +cargo make --cwd b1display uf2 +cargo make --cwd c1minimal uf2 ``` -## Flashing +## Building the Application -First, put the module into bootloader mode, which will expose a filesystem +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev -This can be done by pressing the bootsel button while plugging it in. +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 -cargo run +# Install cargo-make to help build it +cargo install cargo-make + +# Build it +> cargo make --cwd inputmodule-control + +# Build and run it, showing the tool version +> cargo make --cwd inputmodule-control run -- --version ``` -Or by copying the above generated UF2 file to the partition mounted when the -module is in the bootloder. +### Check the firmware version of the device + +###### In-band using commandline + +```sh +> inputmodule-control led-matrix --version +Device Version: 0.2.0 +``` + +###### By looking at the USB descriptor + +On Linux: + +```sh +> lsusb -d 32ac: -v 2> /dev/null | grep -P 'ID 32ac|bcdDevice' +Bus 003 Device 078: ID 32ac:0021 Framework Laptop 16 B1 Display + bcdDevice 0.10 +``` -## Panic +## Rust Panic -On panic the RP2040 resets itself into bootloader mode. +When the Rust code panics, the RP2040 resets itself into bootloader mode. This means a new firmware can be written to overwrite the old one. -Additionally the panic message is written to flash, which can be read as follows: +Additionally the panic message is written to XIP RAM, which can be read with [picotool](https://github.com/raspberrypi/picotool): ```sh sudo picotool save -r 0x15000000 0x15004000 message.bin diff --git a/b1display.png b/b1display.png new file mode 100644 index 00000000..a6cbf80c Binary files /dev/null and b/b1display.png differ diff --git a/b1display/Cargo.toml b/b1display/Cargo.toml new file mode 100644 index 00000000..49d878fc --- /dev/null +++ b/b1display/Cargo.toml @@ -0,0 +1,34 @@ +[package] +edition = "2021" +name = "b1display" +version = "0.2.0" + +[dependencies] +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true + +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true + +# Not using an external BSP, we've got the Framework Laptop 16 BSPs locally in this crate +rp2040-hal.workspace = true +rp2040-boot2.workspace = true + +# USB Serial +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true + +st7306.workspace = true +embedded-graphics.workspace = true +tinybmp.workspace = true + +[dependencies.fl16-inputmodules] +path = "../fl16-inputmodules" +features = [ "b1display" ] diff --git a/b1display/Makefile.toml b/b1display/Makefile.toml new file mode 100644 index 00000000..ea7d5ecc --- /dev/null +++ b/b1display/Makefile.toml @@ -0,0 +1,12 @@ +extend = "../Makefile.toml" + +[tasks.uf2] +command = "elf2uf2-rs" +args = ["../target/thumbv6m-none-eabi/release/b1display", "../target/thumbv6m-none-eabi/release/b1display.uf2"] +dependencies = ["build-release"] +install_crate = "elf2uf2-rs" + +[tasks.bin] +command = "llvm-objcopy" +args = ["-Obinary", "../target/thumbv6m-none-eabi/release/b1display", "../target/thumbv6m-none-eabi/release/b1display.bin"] +dependencies = ["build-release"] diff --git a/b1display/README.md b/b1display/README.md new file mode 100644 index 00000000..ac0cc05e --- /dev/null +++ b/b1display/README.md @@ -0,0 +1,113 @@ +# B1 Display + +A transmissive, mono-color (black/white) screen that's 300x400px in size. +It's 4.2 inches in size and mounted in portrait orientation. +Because it's optimized for power, the recommended framerate is 1 FPS. +But it can go up to 32 FPS. + +The current panel is susceptible to image retention, so the display will start +up with the screen saver. If you send a command to draw anything on the display, +the screensaver will exit. +Currently it does not re-appear after a timeout, it will only re-appear on the +next power-on or after waking from sleep. + +## Controlling + +### Display System Status + +For a similar type of display, there's an +[open-source software](https://github.com/mathoudebine/turing-smart-screen-python) +to get systems stats, render them into an image file and send it to the screen. + +For this display, we have [a fork](https://github.com/FrameworkComputer/lotus-smart-screen-python). +To run it, just install Python and the dependencies, then run `main.py`. +The configuration (`config.yaml`) is already adapted for this display - +it should be able to find the display by itself (Windows or Linux). + +###### Configuration + +Check out the [upstream documentation](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-%3A-themes) +for more information about editing themes. + +Currently we have two themes optimized for this display: `B1Terminal` and `B1Blank`. + +`B1Terminal` comes pre-configured with lots of system stats. + +`B1Blank` comes configured as rendering the text in `file1.txt` onto the screen. + +Both can be fully customized by changing the background image and the displayed statistics +in `res/themes/{B1Blank,B1Terminal}/background.png` and `res/themes/{B1Blank,B1Terminal}/theme.yaml` +respectively. + +### Commandline + +``` +> ./inputmodule-control b1-display +B1 Display + +Usage: ipc b1-display [OPTIONS] + +Options: + --sleeping [] + Set sleep status or get, if no value provided [possible values: true, false] + --bootloader + Jump to the bootloader + --panic + Crash the firmware (TESTING ONLY!) + -v, --version + Get the device version + --display-on [] + Turn display on/off [possible values: true, false] + --pattern + Display a simple pattern [possible values: white, black] + --invert-screen [] + Invert screen on/off [possible values: true, false] + --screen-saver [] + Screensaver on/off [possible values: true, false] + --fps [] + Set/get FPS [possible values: quarter, half, one, two, four, eight, sixteen, thirty-two] + --power-mode [] + Set/get power mode [possible values: low, high] + --animation-fps [] + Set/get animation FPS + --image + Display a black&white image (300x400px) + --animated-gif + Display an animated black&white GIF (300x400px) + --clear-ram + Clear display RAM + -h, --help + Print help +``` + +### Non-trivial Examples + +###### Display an Image + +Display an image (tested with PNG and GIF). It must be 300x400 pixels in size. +It doesn't have to be black/white. The program will calculate the brightness of +each pixel. But if the brightness doesn't vary enough, it won't look good. One +example image is included in the repository. + +```sh +# Should show the Framework Logo and a Lotus flower +inputmodule-control b1-display --image-bw b1display.gif +``` + +###### Invert the colors (dark-mode) + +Since the screen is just black and white, you can display black text on a +white/light background. This can be turned into dark mode by inverting the +colors, making it show light text on a black background. + +```sh +# Invert on +> inputmodule-control b1-display --invert-screen true + +# Invert off +> inputmodule-control b1-display --invert-screen false + +# Check if currently inverted +> inputmodule-control b1-display --invert-screen +Currently inverted: false +``` diff --git a/b1display/src/main.rs b/b1display/src/main.rs new file mode 100644 index 00000000..81ac88a2 --- /dev/null +++ b/b1display/src/main.rs @@ -0,0 +1,392 @@ +//! LED Matrix Module +#![no_std] +#![no_main] +#![allow(clippy::needless_range_loop)] + +use bsp::entry; +use cortex_m::delay::Delay; +//use defmt::*; +use defmt_rtt as _; +use embedded_hal::digital::v2::{InputPin, OutputPin}; + +use rp2040_hal::gpio::{Output, Pin, PushPull}; +//#[cfg(debug_assertions)] +//use panic_probe as _; +use rp2040_panic_usb_boot as _; + +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::*; +use embedded_graphics::primitives::*; +use embedded_hal::blocking::spi; +use st7306::{FpsConfig, HpmFps, LpmFps, PowerMode, ST7306}; + +// Provide an alias for our BSP so we can switch targets quickly. +// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. +use fl16_inputmodules::lcd_hal as bsp; +//use rp_pico as bsp; +// use sparkfun_pro_micro_rp2040 as bsp; + +use bsp::hal::{ + clocks::{init_clocks_and_plls, Clock}, + gpio, pac, + sio::Sio, + usb, + watchdog::Watchdog, + Timer, +}; +use fugit::RateExtU32; + +// USB Device support +use usb_device::{class_prelude::*, prelude::*}; + +// USB Communications Class Device support +use usbd_serial::{SerialPort, USB_CLASS_CDC}; + +// Used to demonstrate writing formatted strings +use core::fmt::Debug; +use core::fmt::Write; +use heapless::String; + +use fl16_inputmodules::control::*; +use fl16_inputmodules::graphics::*; +use fl16_inputmodules::serialnum::{device_release, get_serialnum}; + +// FRA - Framwork +// KDE - C1 LED Matrix +// AM - Atemitech +// 00 - Default Configuration +// 00000000 - Device Identifier +const DEFAULT_SERIAL: &str = "FRAKDEAM0000000000"; + +type B1ST7306 = ST7306< + rp2040_hal::Spi, + Pin>, + Pin>, + Pin>, + 25, + 200, +>; + +const DEBUG: bool = false; +const SCRNS_DELTA: i32 = 5; +const WIDTH: i32 = 300; +const HEIGHT: i32 = 400; +const SIZE: Size = Size::new(WIDTH as u32, HEIGHT as u32); + +#[entry] +fn main() -> ! { + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let sio = Sio::new(pac.SIO); + + let clocks = init_clocks_and_plls( + bsp::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = bsp::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Set up the USB driver + let usb_bus = UsbBusAllocator::new(usb::UsbBus::new( + pac.USBCTRL_REGS, + pac.USBCTRL_DPRAM, + clocks.usb_clock, + true, + &mut pac.RESETS, + )); + + // Set up the USB Communications Class Device driver + let mut serial = SerialPort::new(&usb_bus); + + let serialnum = if let Some(serialnum) = get_serialnum() { + serialnum.serialnum + } else { + DEFAULT_SERIAL + }; + + let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x32ac, 0x0021)) + .manufacturer("Framework Computer Inc") + .product("B1 Display") + .serial_number(serialnum) + .max_power(500) // TODO: Check how much + .device_release(device_release()) // TODO: Assign dynamically based on crate version + .device_class(USB_CLASS_CDC) + .build(); + + // Display SPI pins + let _spi_sclk = pins.scl.into_mode::(); + let _spi_mosi = pins.sda.into_mode::(); + let _spi_miso = pins.miso.into_mode::(); + let spi = bsp::hal::Spi::<_, _, 8>::new(pac.SPI0); + // Display control pins + let dc = pins.dc.into_push_pull_output(); + let mut cs = pins.cs.into_push_pull_output(); + cs.set_low().unwrap(); + let rst = pins.rstb.into_push_pull_output(); + + let spi = spi.init( + &mut pac.RESETS, + clocks.peripheral_clock.freq(), + 16_000_000u32.Hz(), + &embedded_hal::spi::MODE_0, + ); + + let mut state = B1DIsplayState { + sleeping: SimpleSleepState::Awake, + screen_inverted: false, + screen_on: true, + screensaver: Some(ScreenSaverState::default()), + power_mode: PowerMode::Lpm, + fps_config: FpsConfig { + hpm: HpmFps::ThirtyTwo, + lpm: LpmFps::Two, + }, + animation_period: 1_000_000, // 1000ms = 1Hz + }; + + const INVERTED: bool = false; + const AUTO_PWRDOWN: bool = true; + const TE_ENABLE: bool = true; + const COL_START: u16 = 0x12; + const ROW_START: u16 = 0x00; + let mut disp: B1ST7306 = ST7306::new( + spi, + dc, + cs, + rst, + INVERTED, + AUTO_PWRDOWN, + TE_ENABLE, + state.fps_config, + WIDTH as u16, + HEIGHT as u16, + COL_START, + ROW_START, + ); + disp.init(&mut delay).unwrap(); + + // Clear display, might have garbage in display memory + // TODO: Seems broken + //disp.clear(Rgb565::WHITE).unwrap(); + Rectangle::new(Point::new(0, 0), SIZE) + .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) + .draw(&mut disp) + .unwrap(); + + let logo_rect = draw_logo(&mut disp, Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y)).unwrap(); + if DEBUG { + Rectangle::new(Point::new(10, 10), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(20, 20), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(30, 30), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(40, 40), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + Rectangle::new(Point::new(50, 50), Size::new(10, 10)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK)) + .draw(&mut disp) + .unwrap(); + draw_text( + &mut disp, + "Framework", + Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), + ) + .unwrap(); + } + disp.flush().unwrap(); + + let sleep = pins.sleep.into_pull_down_input(); + + let timer = Timer::new(pac.TIMER, &mut pac.RESETS); + let mut prev_timer = timer.get_counter().ticks(); + let mut ticks = 0; + + let mut logo_pos = Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y); + + loop { + // Go to sleep if the host is sleeping + let host_sleeping = sleep.is_low().unwrap(); + handle_sleep(host_sleeping, &mut state, &mut delay, &mut disp); + + // Handle period display updates. Don't do it too often + if timer.get_counter().ticks() > prev_timer + state.animation_period { + prev_timer = timer.get_counter().ticks(); + + if let Some(ref mut screensaver) = state.screensaver { + let seconds = ticks / (1_000_000 / state.animation_period); + #[allow(clippy::modulo_one)] + let second_decimals = ticks % (1_000_000 / state.animation_period); + Rectangle::new(Point::new(0, 0), Size::new(300, 50)) + .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE)) + .draw(&mut disp) + .unwrap(); + let mut text: String<32> = String::new(); + write!( + &mut text, + "{:>4} Ticks ({:>4}.{} s)", + ticks, seconds, second_decimals + ) + .unwrap(); + // Uncomment to draw the ticks on the screen + //draw_text( + // &mut disp, + // &text, + // Point::new(0, 0), + //).unwrap(); + ticks += 1; + + logo_pos = { + let (x, y) = (logo_pos.x, logo_pos.y); + let w = logo_rect.size.width as i32; + let h = logo_rect.size.height as i32; + + // Bounce off the walls + if x <= 0 || x + w >= WIDTH { + screensaver.rightwards *= -1; + } + if y <= 0 || y + h >= HEIGHT { + screensaver.downwards *= -1; + } + + Point::new( + x + screensaver.rightwards * SCRNS_DELTA, + y + screensaver.downwards * SCRNS_DELTA, + ) + }; + // Draw a border around the new logo, to clear previously drawn adjacent logos + let style = PrimitiveStyleBuilder::new() + .stroke_color(Rgb565::WHITE) + .stroke_width(2 * SCRNS_DELTA as u32) + .build(); + Rectangle::new( + logo_pos - Point::new(SCRNS_DELTA, SCRNS_DELTA), + logo_rect.size + Size::new(2 * SCRNS_DELTA as u32, 2 * SCRNS_DELTA as u32), + ) + .into_styled(style) + .draw(&mut disp) + .unwrap(); + draw_logo(&mut disp, logo_pos).unwrap(); + disp.flush().unwrap(); + } + } + + // Check for new data + if usb_dev.poll(&mut [&mut serial]) { + let mut buf = [0u8; 64]; + match serial.read(&mut buf) { + Err(_e) => { + // Do nothing + } + Ok(0) => { + // Do nothing + } + Ok(count) => { + match (parse_command(count, &buf), &state.sleeping) { + (Some(Command::Sleep(go_sleeping)), _) => { + handle_sleep(go_sleeping, &mut state, &mut delay, &mut disp); + } + (Some(c @ Command::BootloaderReset), _) + | (Some(c @ Command::IsSleeping), _) => { + if let Some(response) = + handle_command(&c, &mut state, logo_rect, &mut disp, &mut delay) + { + let _ = serial.write(&response); + }; + } + (Some(command), SimpleSleepState::Awake) => { + // While sleeping no command is handled, except waking up + if let Some(response) = handle_command( + &command, &mut state, logo_rect, &mut disp, &mut delay, + ) { + let _ = serial.write(&response); + }; + // Must write AFTER writing response, otherwise the + // client interprets this debug message as the response + //let mut text: String<64> = String::new(); + //write!( + // &mut text, + // "Handled command {}:{}:{}:{}\r\n", + // buf[0], buf[1], buf[2], buf[3] + //) + //.unwrap(); + //let _ = serial.write(text.as_bytes()); + } + _ => {} + } + } + } + } + } +} + +fn handle_sleep( + go_sleeping: bool, + state: &mut B1DIsplayState, + delay: &mut Delay, + disp: &mut ST7306, +) where + SPI: spi::Write, + DC: OutputPin, + CS: OutputPin, + RST: OutputPin, + >::Error: Debug, +{ + match (state.sleeping.clone(), go_sleeping) { + (SimpleSleepState::Awake, false) => (), + (SimpleSleepState::Awake, true) => { + state.sleeping = SimpleSleepState::Sleeping; + + // Turn off display + //disp.on_off(false).unwrap(); + disp.sleep_in(delay).unwrap(); + + // TODO: Power Display controller down + + // TODO: Set up SLEEP# pin as interrupt and wfi + //cortex_m::asm::wfi(); + } + (SimpleSleepState::Sleeping, true) => (), + (SimpleSleepState::Sleeping, false) => { + // Restore back grid before sleeping + state.sleeping = SimpleSleepState::Awake; + + // Turn display back on + //disp.on_off(true).unwrap(); + disp.sleep_out(delay).unwrap(); + // Sleep-in has to go into HPM first, so we'll be in HPM after wake-up as well + if state.power_mode == PowerMode::Lpm { + disp.switch_mode(delay, PowerMode::Lpm).unwrap(); + } + + // Turn screensaver on when resuming from sleep + // TODO Subject to change, but currently I want to avoid burn-in by default + state.screensaver = Some(ScreenSaverState::default()); + + // TODO: Power display controller back on + } + } +} diff --git a/c1minimal/Cargo.toml b/c1minimal/Cargo.toml new file mode 100644 index 00000000..a932ccb3 --- /dev/null +++ b/c1minimal/Cargo.toml @@ -0,0 +1,34 @@ +[package] +edition = "2021" +name = "c1minimal" +version = "0.2.0" + +[dependencies] +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true + +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true + +# Not using an external BSP, we've got the Framework Laptop 16 BSPs locally in this crate +rp2040-hal.workspace = true +rp2040-boot2.workspace = true + +# USB Serial +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true + +# C1 Minimal +smart-leds.workspace = true +ws2812-pio.workspace = true + +[dependencies.fl16-inputmodules] +path = "../fl16-inputmodules" +features = [ "c1minimal" ] diff --git a/c1minimal/Makefile.toml b/c1minimal/Makefile.toml new file mode 100644 index 00000000..e31b7731 --- /dev/null +++ b/c1minimal/Makefile.toml @@ -0,0 +1,12 @@ +extend = "../Makefile.toml" + +[tasks.uf2] +command = "elf2uf2-rs" +args = ["../target/thumbv6m-none-eabi/release/c1minimal", "../target/thumbv6m-none-eabi/release/c1minimal.uf2"] +dependencies = ["build-release"] +install_crate = "elf2uf2-rs" + +[tasks.bin] +command = "llvm-objcopy" +args = ["-Obinary", "../target/thumbv6m-none-eabi/release/c1minimal", "../target/thumbv6m-none-eabi/release/c1minimal.bin"] +dependencies = ["build-release"] diff --git a/c1minimal/README.md b/c1minimal/README.md new file mode 100644 index 00000000..a64b58bf --- /dev/null +++ b/c1minimal/README.md @@ -0,0 +1,18 @@ +## C1 Minimal Input Module + +It's a very minimal input module. Many GPIO pins are exposed so that headers +can be soldered onto them. Additionally there are pads for a WS2812/Neopixel +compatible RGB LED. + +When booting up this LED is lit in green color. +Its color and brightness can be controlled via the commands: + +```sh +> ./ledmatrix_control.py --brightness 255 +> ./ledmatrix_control.py --get-brightness +Current brightness: 255 + +> ./ledmatrix_control.py --set-color yellow +> ./ledmatrix_control.py --get-color +Current color: RGB:(255, 255, 0) +``` diff --git a/c1minimal/src/main.rs b/c1minimal/src/main.rs new file mode 100644 index 00000000..9967774e --- /dev/null +++ b/c1minimal/src/main.rs @@ -0,0 +1,217 @@ +//! C1 Minimal Input Module +//! +//! Neopixel/WS2812 compatible RGB LED is connected to GPIO16. +//! This pin doesn't support SPI TX. +//! It does support UART TX, but that output would have to be inverted. +//! So instead we use PIO to drive the LED. +#![no_std] +#![no_main] +#![allow(clippy::needless_range_loop)] + +use bsp::entry; +use cortex_m::delay::Delay; +use defmt_rtt as _; +use embedded_hal::digital::v2::InputPin; + +use rp2040_hal::gpio::bank0::Gpio16; +use rp2040_hal::pio::PIOExt; +//#[cfg(debug_assertions)] +//use panic_probe as _; +use rp2040_panic_usb_boot as _; + +// Provide an alias for our BSP so we can switch targets quickly. +// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. +use fl16_inputmodules::minimal_hal as bsp; +//use rp_pico as bsp; + +use bsp::hal::{ + clocks::{init_clocks_and_plls, Clock}, + pac, + sio::Sio, + usb, + watchdog::Watchdog, + Timer, +}; + +// USB Device support +use usb_device::{class_prelude::*, prelude::*}; + +// USB Communications Class Device support +use usbd_serial::{SerialPort, USB_CLASS_CDC}; + +// Used to demonstrate writing formatted strings +// use core::fmt::Write; +// use heapless::String; + +// RGB LED +use smart_leds::{colors, SmartLedsWrite, RGB8}; +pub type Ws2812<'a> = ws2812_pio::Ws2812< + crate::pac::PIO0, + rp2040_hal::pio::SM0, + rp2040_hal::timer::CountDown<'a>, + Gpio16, +>; + +use fl16_inputmodules::control::*; +use fl16_inputmodules::serialnum::{device_release, get_serialnum}; + +// FRA - Framwork +// 000 - C1 Minimal Input Module (No assigned value) +// AM - Atemitech +// 00 - Default Configuration +// 00000000 - Device Identifier +const DEFAULT_SERIAL: &str = "FRA000AM0000000000"; + +#[entry] +fn main() -> ! { + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let sio = Sio::new(pac.SIO); + + let clocks = init_clocks_and_plls( + bsp::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = bsp::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Set up the USB driver + let usb_bus = UsbBusAllocator::new(usb::UsbBus::new( + pac.USBCTRL_REGS, + pac.USBCTRL_DPRAM, + clocks.usb_clock, + true, + &mut pac.RESETS, + )); + + // Set up the USB Communications Class Device driver + let mut serial = SerialPort::new(&usb_bus); + + let serialnum = if let Some(serialnum) = get_serialnum() { + serialnum.serialnum + } else { + DEFAULT_SERIAL + }; + + let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x32ac, 0x0022)) + .manufacturer("Framework Computer Inc") + .product("C1 Minimal Input Module") + .serial_number(serialnum) + .max_power(500) // TODO: Check how much + .device_release(device_release()) + .device_class(USB_CLASS_CDC) + .build(); + + let sleep = pins.sleep.into_pull_down_input(); + + let mut state = C1MinimalState { + sleeping: SimpleSleepState::Awake, + color: colors::GREEN, + brightness: 10, + }; + + let timer = Timer::new(pac.TIMER, &mut pac.RESETS); + let mut prev_timer = timer.get_counter().ticks(); + + let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); + let mut ws2812: Ws2812 = ws2812_pio::Ws2812::new( + pins.rgb_led.into_mode(), + &mut pio, + sm0, + clocks.peripheral_clock.freq(), + timer.count_down(), + ); + + ws2812 + .write(smart_leds::brightness( + [state.color].iter().cloned(), + state.brightness, + )) + .unwrap(); + + loop { + // Go to sleep if the host is sleeping + let host_sleeping = sleep.is_low().unwrap(); + handle_sleep(host_sleeping, &mut state, &mut delay, &mut ws2812); + + // Handle period LED updates. Don't do it too often or USB will get stuck + if timer.get_counter().ticks() > prev_timer + 20_000 { + // TODO: Can do animations here + prev_timer = timer.get_counter().ticks(); + } + + // Check for new data + if usb_dev.poll(&mut [&mut serial]) { + let mut buf = [0u8; 64]; + match serial.read(&mut buf) { + Err(_e) => { + // Do nothing + } + Ok(0) => { + // Do nothing + } + Ok(count) => { + if let Some(command) = parse_command(count, &buf) { + if let Command::Sleep(go_sleeping) = command { + handle_sleep(go_sleeping, &mut state, &mut delay, &mut ws2812); + } else if let SimpleSleepState::Awake = state.sleeping { + // While sleeping no command is handled, except waking up + if let Some(response) = + handle_command(&command, &mut state, &mut ws2812) + { + let _ = serial.write(&response); + }; + } + } + } + } + } + } +} + +fn handle_sleep( + go_sleeping: bool, + state: &mut C1MinimalState, + _delay: &mut Delay, + ws2812: &mut impl SmartLedsWrite, +) { + match (state.sleeping.clone(), go_sleeping) { + (SimpleSleepState::Awake, false) => (), + (SimpleSleepState::Awake, true) => { + state.sleeping = SimpleSleepState::Sleeping; + + // Turn off LED + ws2812.write([colors::BLACK].iter().cloned()).unwrap(); + + // TODO: Set up SLEEP# pin as interrupt and wfi + //cortex_m::asm::wfi(); + } + (SimpleSleepState::Sleeping, true) => (), + (SimpleSleepState::Sleeping, false) => { + state.sleeping = SimpleSleepState::Awake; + + // Turn LED back on + ws2812 + .write(smart_leds::brightness( + [state.color].iter().cloned(), + state.brightness, + )) + .unwrap(); + } + } +} diff --git a/commands.md b/commands.md new file mode 100644 index 00000000..46322edb --- /dev/null +++ b/commands.md @@ -0,0 +1,107 @@ +# Commands + +The input modules can be controlled by sending commands via the USB CDC-ACM +serial port. To send a command, write the two magic bytes, command ID and +parameters. Most commands don't return anything. + +Simple example in Python: + +```python +import serial + +def send_command(command_id, parameters, with_response=False): + with serial.Serial("/dev/ttyACM0", 115200) as s: + s.write([0x32, 0xAC, command_id] + parameters) + + if with_response: + res = s.read(32) + return res + +# 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])}") +``` + +Many commands support setting and writing a value, with the same command ID. +When no parameters are given, the current value is queried and returned. + +###### Modules: + +- L = LED Matrix +- D = B1 Display +- M = C1 Minimal Module + +## Command overview + +| Command | ID | Modules | Response | Parameters | Behavior | +| ------------ | ---- | ------- | -------- | ---------- | ------------------------ | +| Brightness | 0x00 | `L M` | | | Set LED brightness | +| Pattern | 0x01 | `L ` | | | Display a pattern | +| Bootloader | 0x02 | `LDM` | | | Jump to the bootloader | +| Sleep | 0x03 | `LDM` | | bool | Go to sleep or wake up | +| GetSleep | 0x03 | `LDM` | bool | | Check sleep state | +| Animate | 0x04 | `L ` | | bool | Scroll current pattern | +| GetAnimate | 0x04 | `L ` | bool | | Check whether animating | +| Panic | 0x05 | `LDM` | | | Cause a FW panic/crash | +| DrawBW | 0x06 | `L ` | | 39 Bytes | Draw a black/white image | +| StageCol | 0x07 | `L ` | | 1+34 Bytes | Send a greyscale column | +| FlushCols | 0x08 | `L ` | | | Flush/draw all columns | +| SetText | 0x09 | ` D ` | | | TODO: Remove | +| StartGame | 0x10 | `L ` | | 1B Game ID | Start an embeded game | +| GameCtrl | 0x11 | `L ` | | 1B Control | Send a game command | +| GameStatus | 0x12 | `L ` | WIP | | Check the game status | +| SetColor | 0x13 | ` M` | | 3B: RGB | Set the LED's color | +| DisplayOn | 0x14 | ` D ` | | bool | Turn the display on/off | +| 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 | `LDM` | 3 Bytes | | Get firmware version | + +#### Pattern (0x01) + +The following patterns are defined + +- 0x00 - Percentage (See below, needs another parameter) +- 0x01 - Gradient (Brightness gradient from top to bottom) +- 0x02 - DoubleGradient (Brightness gradient from the middle to top and bottom) +- 0x03 - DisplayLotusHorizontal (Display "LOTUS" 90 degree rotated) +- 0x04 - ZigZag (Display a zigzag pattern) +- 0x05 - FullBrightness (Turn every LED on and set the brightness to 100%) +- 0x06 - DisplayPanic (Display the string "PANIC") +- 0x07 - DisplayLotusVertical (Display the string "LOTUS") + +Pattern 0x00 is special. It needs another parameter to specify the percentage. +It will fill a percentage of the screen. It can serve as a progress indicator. + +#### DrawBW (0x06) +TODO + +#### StageCol (0x07) +TODO + +#### FlushCols (0x08) +TODO + +#### SetPxCol (0x16) +TODO + +#### FlushFB (0x17) +TODO + +#### Version (0x20) + +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/control.py b/control.py deleted file mode 100755 index fac54c75..00000000 --- a/control.py +++ /dev/null @@ -1,1093 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import sys -import threading -import time -from datetime import datetime, timedelta -import random -import math -import sys - -# Need to install -import serial - -# Optional dependencies: -# from PIL import Image -# import PySimpleGUI as sg - -FWK_MAGIC = [0x32, 0xAC] -PATTERNS = ['full', 'lotus', 'gradient', - 'double-gradient', 'zigzag', 'panic', 'lotus2'] -DRAW_PATTERNS = ['off', 'on', 'foo'] -GREYSCALE_DEPTH = 32 -WIDTH = 9 -HEIGHT = 34 - -SERIAL_DEV = None - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--bootloader", help="Jump to the bootloader to flash new firmware", - action="store_true") - parser.add_argument('--sleep', help='Simulate the host going to sleep or waking up', - action=argparse.BooleanOptionalAction) - parser.add_argument("--brightness", help="Adjust the brightness. Value 0-255", - type=int) - parser.add_argument('--animate', action=argparse.BooleanOptionalAction, - help='Start/stop vertical scrolling') - parser.add_argument("--pattern", help='Display a pattern', - type=str, choices=PATTERNS) - parser.add_argument("--image", help="Display a PNG or GIF image in black and white only)", - type=argparse.FileType('rb')) - parser.add_argument("--image-grey", help="Display a PNG or GIF image in greyscale", - type=argparse.FileType('rb')) - parser.add_argument("--percentage", help="Fill a percentage of the screen", - type=int) - parser.add_argument("--clock", help="Display the current time", - action="store_true") - parser.add_argument("--string", help="Display a string or number, like FPS", - type=str) - parser.add_argument("--symbols", help="Show symbols (degF, degC, :), snow, cloud, ...)", - nargs='+') - parser.add_argument("--gui", help="Launch the graphical version of the program", - action="store_true") - parser.add_argument("--panic", help="Crash the firmware (TESTING ONLY)", - action="store_true") - parser.add_argument("--blink", help="Blink the current pattern", - action="store_true") - parser.add_argument("--breathing", help="Breathing of the current pattern", - action="store_true") - parser.add_argument("--eq", help="Equalizer", nargs='+', type=int) - parser.add_argument( - "--random-eq", help="Random Equalizer", action="store_true") - parser.add_argument("--wpm", help="WPM Demo", action="store_true") - parser.add_argument("--snake", help="Snake", action="store_true") - parser.add_argument( - "--all-brightnesses", help="Show every pixel in a different brightness", action="store_true") - parser.add_argument("--serial-dev", help="Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows", - default='/dev/ttyACM0') - args = parser.parse_args() - - if args.serial_dev is not None: - global SERIAL_DEV - SERIAL_DEV = args.serial_dev - - if args.bootloader: - bootloader() - elif args.sleep is not None: - command = FWK_MAGIC + [0x03, args.sleep] - send_command(command) - elif args.brightness is not None: - if args.brightness > 255 or args.brightness < 0: - print("Brightness must be 0-255") - sys.exit(1) - brightness(args.brightness) - elif args.percentage is not None: - if args.percentage > 100 or args.percentage < 0: - print("Percentage must be 0-100") - sys.exit(1) - percentage(args.percentage) - elif args.pattern is not None: - pattern(args.pattern) - elif args.animate is not None: - animate(args.animate) - elif args.panic: - command = FWK_MAGIC + [0x05, 0x00] - send_command(command) - elif args.image is not None: - image_bl(args.image) - elif args.image_grey is not None: - image_greyscale(args.image_grey) - elif args.all_brightnesses: - all_brightnesses() - elif args.gui: - gui() - elif args.blink: - blinking() - elif args.breathing: - breathing() - elif args.wpm: - wpm_demo() - elif args.snake: - snake() - elif args.eq is not None: - eq(args.eq) - elif args.random_eq: - random_eq() - elif args.clock: - clock() - elif args.string is not None: - show_string(args.string) - elif args.symbols is not None: - show_symbols(args.symbols) - else: - print("Provide arg") - - -def bootloader(): - """Reboot into the bootloader to flash new firmware""" - command = FWK_MAGIC + [0x02, 0x00] - send_command(command) - - -def percentage(p): - """Fill a percentage of the screen. Bottom to top""" - command = FWK_MAGIC + [0x01, 0x00, p] - send_command(command) - - -def brightness(b: int): - """Adjust the brightness scaling of the entire screen. - """ - command = FWK_MAGIC + [0x00, b] - send_command(command) - - -def animate(b: bool): - """Tell the firmware to start/stop animation. - Scrolls the currently saved grid vertically down.""" - command = FWK_MAGIC + [0x04, b] - send_command(command) - - -def image_bl(image_file): - """Display an image in black and white - Confirmed working with PNG and GIF. - Must be 9x34 in size. - Sends everything in a single command - """ - vals = [0 for _ in range(39)] - - from PIL import Image - im = Image.open(image_file).convert("RGB") - width, height = im.size - assert (width == 9) - assert (height == 34) - pixel_values = list(im.getdata()) - for i, pixel in enumerate(pixel_values): - brightness = sum(pixel) / 3 - if brightness > 0xFF/2: - vals[int(i/8)] |= (1 << i % 8) - - command = FWK_MAGIC + [0x06] + vals - send_command(command) - - -def pixel_to_brightness(pixel): - """Calculate pixel brightness from an RGB triple""" - assert (len(pixel) == 3) - brightness = sum(pixel) / len(pixel) - - # Poor man's scaling to make the greyscale pop better. - # Should find a good function. - if brightness > 200: - brightness = brightness - elif brightness > 150: - brightness = brightness * 0.8 - elif brightness > 100: - brightness = brightness * 0.5 - elif brightness > 50: - brightness = brightness - else: - brightness = brightness * 2 - - return int(brightness) - - -def image_greyscale(image_file): - """Display an image in greyscale - Sends each 1x34 column and then commits => 10 commands - """ - with serial.Serial(SERIAL_DEV, 115200) as s: - from PIL import Image - im = Image.open(image_file).convert("RGB") - width, height = im.size - assert (width == 9) - assert (height == 34) - pixel_values = list(im.getdata()) - for x in range(0, WIDTH): - vals = [0 for _ in range(HEIGHT)] - - for y in range(HEIGHT): - vals[y] = pixel_to_brightness(pixel_values[x+y*WIDTH]) - - send_col(s, x, vals) - commit_cols(s) - - -def send_col(s, x, vals): - """Stage greyscale values for a single column. Must be committed with commit_cols()""" - command = FWK_MAGIC + [0x07, x] + vals - send_serial(s, command) - - -def commit_cols(s): - """Commit the changes from sending individual cols with send_col(), displaying the matrix. - This makes sure that the matrix isn't partially updated.""" - command = FWK_MAGIC + [0x08, 0x00] - send_serial(s, command) - - -def all_brightnesses(): - """Increase the brightness with each pixel. - Only 0-255 available, so it can't fill all 306 LEDs""" - with serial.Serial(SERIAL_DEV, 115200) as s: - for x in range(0, WIDTH): - vals = [0 for _ in range(HEIGHT)] - - for y in range(HEIGHT): - brightness = x + WIDTH * y - if brightness > 255: - vals[y] = 0 - else: - vals[y] = brightness - - send_col(s, x, vals) - commit_cols(s) - - -def countdown(seconds): - """ Run a countdown timer. Lighting more LEDs every 100th of a seconds. - Until the timer runs out and every LED is lit""" - start = datetime.now() - target = seconds * 1_000_000 - while True: - now = datetime.now() - passed_time = (now - start) / timedelta(microseconds=1) - - ratio = passed_time / target - if passed_time >= target: - break - - leds = int(306 * ratio) - light_leds(leds) - - time.sleep(0.01) - - light_leds(306) - # breathing() - blinking() - - -def blinking(): - """Blink brightness high/off every second. - Keeps currently displayed grid""" - while True: - brightness(0) - time.sleep(0.5) - brightness(200) - time.sleep(0.5) - - -def breathing(): - """Animate breathing brightness. - Keeps currently displayed grid""" - # Bright ranges appear similar, so we have to go through those faster - while True: - # Go quickly from 250 to 50 - for i in range(10): - time.sleep(0.03) - brightness(250 - i*20) - - # Go slowly from 50 to 0 - for i in range(10): - time.sleep(0.06) - brightness(50 - i*5) - - # Go slowly from 0 to 50 - for i in range(10): - time.sleep(0.06) - brightness(i*5) - - # Go quickly from 50 to 250 - for i in range(10): - time.sleep(0.03) - brightness(50 + i*20) - - -direction = None -body = [] - - -def opposite_direction(direction): - from getkey import keys - if direction == keys.RIGHT: - return keys.LEFT - elif direction == keys.LEFT: - return keys.RIGHT - elif direction == keys.UP: - return keys.DOWN - elif direction == keys.DOWN: - return keys.UP - return direction - - -def keyscan(): - from getkey import getkey, keys - global direction - global body - - while True: - current_dir = direction - key = getkey() - if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: - # Don't allow accidental suicide if we have a body - if key == opposite_direction(current_dir) and body: - continue - direction = key - - -def game_over(): - global body - while True: - show_string('GAME ') - time.sleep(0.75) - show_string('OVER!') - time.sleep(0.75) - score = len(body) - show_string(f'{score:>3} P') - time.sleep(0.75) - - -def snake(): - from getkey import keys - global direction - global body - head = (0, 0) - direction = keys.DOWN - food = (0, 0) - while food == head: - food = (random.randint(0, WIDTH-1), - random.randint(0, HEIGHT-1)) - - # Setting - WRAP = False - - thread = threading.Thread(target=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() - 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() - 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(matrix) - - -def wpm_demo(): - """Capture keypresses and calculate the WPM of the last 10 seconds - TODO: I'm not sure my calculation is right.""" - from getkey import getkey, keys - 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(' ' + str(int(wpm))) - - -def random_eq(): - """Display an equlizer looking animation with random values. - """ - while True: - # Lower values more likely, makes it look nicer - weights = [i*i for i in range(33, 0, -1)] - population = list(range(1, 34)) - vals = random.choices(population, weights=weights, k=9) - eq(vals) - time.sleep(0.2) - - -def eq(vals): - """Display 9 values in equalizer diagram starting from the middle, going up and down""" - matrix = [[0 for _ in range(34)] for _ in range(9)] - - for (col, val) in enumerate(vals[:9]): - row = int(34 / 2) - above = int(val / 2) - below = val - above - - for i in range(above): - matrix[col][row+i] = 0xFF - for i in range(below): - matrix[col][row-1-i] = 0xFF - - render_matrix(matrix) - - -def render_matrix(matrix): - """Show a black/white matrix - Send everything in a single command""" - vals = [0x00 for _ in range(39)] - - for x in range(9): - for y in range(34): - i = x + 9*y - if matrix[x][y]: - vals[int(i/8)] = vals[int(i/8)] | (1 << i % 8) - - command = FWK_MAGIC + [0x06] + vals - send_command(command) - - -def light_leds(leds): - """ Light a specific number of LEDs """ - vals = [0x00 for _ in range(39)] - for byte in range(int(leds / 8)): - vals[byte] = 0xFF - for i in range(leds % 8): - vals[int(leds / 8)] += 1 << i - command = FWK_MAGIC + [0x06] + vals - send_command(command) - - -def pattern(p): - """Display a pattern that's already programmed into the firmware""" - if p == 'full': - command = FWK_MAGIC + [0x01, 5] - send_command(command) - elif p == 'gradient': - command = FWK_MAGIC + [0x01, 1] - send_command(command) - elif p == 'double-gradient': - command = FWK_MAGIC + [0x01, 2] - send_command(command) - elif p == 'lotus': - command = FWK_MAGIC + [0x01, 3] - send_command(command) - elif p == 'zigzag': - command = FWK_MAGIC + [0x01, 4] - send_command(command) - elif p == 'panic': - command = FWK_MAGIC + [0x01, 6] - send_command(command) - elif p == 'lotus2': - command = FWK_MAGIC + [0x01, 7] - send_command(command) - else: - print("Invalid pattern") - - -def show_string(s): - """Render a string with up to five letters""" - show_font([convert_font(letter) for letter in str(s)[:5]]) - - -def show_font(font_items): - """Render up to five 5x6 pixel font items""" - vals = [0x00 for _ in range(39)] - - for digit_i, digit_pixels in enumerate(font_items): - offset = digit_i * 7 - for pixel_x in range(5): - for pixel_y in range(6): - pixel_value = digit_pixels[pixel_x + pixel_y*5] - i = (2+pixel_x) + (9*(pixel_y+offset)) - if pixel_value: - vals[int(i/8)] = vals[int(i/8)] | (1 << i % 8) - - command = FWK_MAGIC + [0x06] + vals - send_command(command) - - -def show_symbols(symbols): - """Render a list of up to five symbols - Can use letters/numbers or symbol names, like 'sun', ':)'""" - font_items = [] - for symbol in symbols: - s = convert_symbol(symbol) - if not s: - s = convert_font(symbol) - font_items.append(s) - - show_font(font_items) - - -def clock(): - """Render the current time and display. - Loops forever, updating every second""" - while True: - now = datetime.now() - current_time = now.strftime("%H:%M") - print("Current Time =", current_time) - - show_string(current_time) - time.sleep(1) - - -def send_command(command): - """Send a command to the device. - Opens new serial connection every time""" - # print(f"Sending command: {command}") - global SERIAL_DEV - with serial.Serial(SERIAL_DEV, 115200) as s: - s.write(command) - - -def send_serial(s, command): - """Send serial command by using existing serial connection""" - global SERIAL_DEV - s.write(command) - - -def gui(): - import PySimpleGUI as sg - - layout = [ - [sg.Text("Bootloader")], - [sg.Button("Bootloader")], - - [sg.Text("Brightness")], - [sg.Slider((0, 255), orientation='h', - k='-BRIGHTNESS-', enable_events=True)], - - [sg.Text("Animation")], - [sg.Button("Start Animation"), sg.Button("Stop Animation")], - - [sg.Text("Pattern")], - [sg.Combo(PATTERNS, k='-COMBO-', enable_events=True)], - - [sg.Text("Display Percentage")], - [sg.Slider((0, 100), orientation='h', - k='-PERCENTAGE-', enable_events=True)], - - [sg.Text("Countdown")], - [ - sg.Spin([i for i in range(1, 60)], - initial_value=10, k='-COUNTDOWN-'), - sg.Text("Seconds"), - sg.Button("Start", k='-START-COUNTDOWN-') - ], - - [sg.Text("Sleep")], - [sg.Button("Sleep"), sg.Button("Wake")], - # [sg.Button("Panic")] - - [sg.Button("Quit")] - ] - window = sg.Window("Lotus LED Matrix Control", layout) - while True: - event, values = window.read() - # print('Event', event) - # print('Values', values) - - if event == "Quit" or event == sg.WIN_CLOSED: - break - - if event == "Bootloader": - bootloader() - - if event == '-COMBO-': - pattern(values['-COMBO-']) - - if event == 'Start Animation': - animate(True) - - if event == 'Stop Animation': - animate(False) - - if event == '-BRIGHTNESS-': - brightness(int(values['-BRIGHTNESS-'])) - - if event == '-PERCENTAGE-': - percentage(int(values['-PERCENTAGE-'])) - - if event == '-START-COUNTDOWN-': - thread = threading.Thread(target=countdown, args=( - int(values['-COUNTDOWN-']),), daemon=True) - thread.start() - - if event == 'Sleep': - command = FWK_MAGIC + [0x03, True] - send_command(command) - - if event == 'Wake': - command = FWK_MAGIC + [0x03, False] - send_command(command) - - window.close() - -# 5x6 symbol font. Leaves 2 pixels on each side empty -# We can leave one row empty below and then the display fits 5 of these digits. - - -def convert_symbol(symbol): - symbols = { - 'degC': [ - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 0, 0, - ], - 'degF': [ - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 0, 0, - 1, 0, 0, 0, 0, - ], - 'snow': [ - 0, 0, 0, 0, 0, - 1, 0, 1, 0, 1, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 1, 0, 1, 0, 1, - ], - 'sun': [ - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - ], - 'cloud': [ - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'rain': [ - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 0, 0, 1, - 0, 0, 1, 0, 0, - 1, 0, 0, 1, 0, - ], - 'thunder': [ - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - 'batteryLow': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 0, - 1, 0, 0, 1, 1, - 1, 0, 0, 1, 1, - 1, 1, 1, 1, 0, - ], - '!!': [ - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - ], - 'heart': [ - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - 'heart0': [ - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'heart2': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 0, - ], - ':)': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - ':|': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - ], - ':(': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - ], - ';)': [ - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - } - if symbol in symbols: - return symbols[symbol] - else: - return None - - -def convert_font(num): - """ 5x6 font. Leaves 2 pixels on each side empty - We can leave one row empty below and then the display fits 5 of these digits.""" - font = { - '0': [ - 0, 1, 1, 0, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 0, 1, 1, 0, 0, - ], - - '1': [ - 0, 0, 1, 0, 0, - 0, 1, 1, 0, 0, - 1, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - ], - - '2': [ - 1, 1, 1, 1, 0, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - - '3': [ - 1, 1, 1, 1, 0, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - - '4': [ - 0, 0, 0, 1, 0, - 0, 0, 1, 1, 0, - 0, 1, 0, 1, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 0, - 0, 0, 0, 1, 0, - ], - - '5': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - - '6': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - '7': [ - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - - '8': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - '9': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 1, - 0, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - - ':': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - - ' ': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '?': [ - 0, 1, 1, 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - - '.': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - ',': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '!': [ - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, - ], - - '/': [ - 0, 0, 0, 0, 1, - 0, 0, 0, 1, 1, - 0, 0, 1, 1, 0, - 0, 1, 1, 0, 0, - 1, 1, 0, 0, 0, - 1, 0, 0, 0, 0, - ], - - '*': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '%': [ - 1, 1, 0, 0, 1, - 1, 1, 0, 1, 1, - 0, 0, 1, 1, 0, - 0, 1, 1, 0, 0, - 1, 1, 0, 1, 1, - 1, 0, 0, 1, 1, - ], - - '+': [ - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 0, 0, - ], - - '-': [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - - '=': [ - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ], - 'A': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - ], - 'D': [ - 1, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 1, 1, 1, 0, - ], - 'O': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - 'V': [ - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 1, - 0, 1, 0, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - ], - 'E': [ - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - ], - 'R': [ - 1, 1, 1, 1, 0, - 1, 0, 0, 1, 0, - 1, 1, 1, 1, 0, - 1, 1, 0, 0, 0, - 1, 0, 1, 0, 0, - 1, 0, 0, 1, 0, - ], - 'G': [ - 0, 1, 1, 1, 0, - 1, 0, 0, 0, 0, - 1, 0, 1, 1, 1, - 1, 0, 0, 0, 1, - 1, 0, 0, 0, 1, - 0, 1, 1, 1, 0, - ], - 'M': [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - 1, 0, 1, 0, 1, - ], - 'P': [ - 1, 1, 1, 0, 0, - 1, 0, 0, 1, 0, - 1, 0, 0, 1, 0, - 1, 1, 1, 0, 0, - 1, 0, 0, 0, 0, - 1, 0, 0, 0, 0, - ], - } - if num in font: - return font[num] - else: - return font['?'] - - -if __name__ == "__main__": - main() diff --git a/debug_probes.md b/debug_probes.md deleted file mode 100644 index dcc5d864..00000000 --- a/debug_probes.md +++ /dev/null @@ -1,39 +0,0 @@ -# Compatible CMSIS-DAP debug probes - -## Raspberry Pi Pico - - You can use a second Pico as your debugger. - - - Download this file: https://github.com/majbthrd/DapperMime/releases/download/20210225/raspberry_pi_pico-DapperMime.uf2 - - Boot the Pico in bootloader mode by holding the bootset button while plugging it in - - Open the drive RPI-RP2 when prompted - - Copy raspberry_pi_pico-DapperMime.uf2 from Downloads into RPI-RP2 - - Connect the debug pins of your CMSIS-DAP Pico to the target one - - Connect GP2 on the Probe to SWCLK on the Target - - Connect GP3 on the Probe to SWDIO on the Target - - Connect a ground line from the CMSIS-DAP Probe to the Target too - - If you have good wiring between your Pico's, you can instead use rust-dap for faster programming: - https://raw.githubusercontent.com/9names/binary-bits/main/rust-dap-pico-ramexec-setclock.uf2 - -## WeAct MiniF4 -https://therealprof.github.io/blog/usb-c-pill-part1/ - -## HS-Probe -https://github.com/probe-rs/hs-probe - -## ST-LINK v2 clone -It's getting harder to source these with stm32f103's as time goes on, so you might be better off choosing a stm32f103 dev board - -Firmware: https://github.com/devanlai/dap42 - -## LPC-Link2 -https://www.nxp.com/design/microcontrollers-developer-resources/lpc-link2:OM13054 - -## MCU-Link -https://www.nxp.com/part/MCU-LINK#/ - -## DAPLink -You can use DAPLink firmware with any of it's supported chips (LPC4322, LPC11U35, K20, K22, KL26). You'll need to use the 'develop' branch to use GCC to build it. You'll need to find a chip with the correct - -Firmware source: https://github.com/ARMmbed/DAPLink/tree/develop diff --git a/fl16-inputmodules/Cargo.toml b/fl16-inputmodules/Cargo.toml new file mode 100644 index 00000000..4af886d8 --- /dev/null +++ b/fl16-inputmodules/Cargo.toml @@ -0,0 +1,50 @@ +[package] +edition = "2021" +name = "fl16-inputmodules" +version = "0.2.0" + +[dependencies] +crc = "3.0" +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true + +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true + +# Not using an external BSP, we've got the Framework Laptop 16 BSPs locally in this crate +rp2040-hal.workspace = true +rp2040-boot2.workspace = true + +# USB Serial +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true + +num = { version = "0.4", default-features = false } +num-derive = "0.3" +num-traits = { version = "0.2", default-features = false } + +# LED Matrix +is31fl3741 = { workspace = true, optional = true } + +# B1 Display +st7306 = { workspace = true, optional = true } +embedded-graphics = { workspace = true, optional = true } +tinybmp = { workspace = true, optional = true } + +# C1 Minimal +smart-leds = { workspace = true, optional = true } +ws2812-pio = { workspace = true, optional = true } + +[features] +default = [] +ledmatrix = ["is31fl3741"] +b1display = ["st7306", "embedded-graphics", "tinybmp"] +c1minimal = ["smart-leds", "ws2812-pio"] +qtpy = ["c1minimal"] diff --git a/fl16-inputmodules/Makefile.toml b/fl16-inputmodules/Makefile.toml new file mode 100644 index 00000000..3751fcc5 --- /dev/null +++ b/fl16-inputmodules/Makefile.toml @@ -0,0 +1,21 @@ +extend = "../Makefile.toml" + +[tasks.build-all] +run_task = { name = [ + "build-ledmatrix", + "build-b1display", + "build-c1minimal", +], parallel = true } + + +[tasks.build-ledmatrix] +env.FEATURES = "ledmatrix" +run_task = "build" + +[tasks.build-b1display] +env.FEATURES = "b1display" +run_task = "build" + +[tasks.build-c1minimal] +env.FEATURES = "c1minimal" +run_task = "build" diff --git a/fl16-inputmodules/assets/logo.bmp b/fl16-inputmodules/assets/logo.bmp new file mode 100644 index 00000000..3002bcb6 Binary files /dev/null and b/fl16-inputmodules/assets/logo.bmp differ diff --git a/fl16-inputmodules/src/animations.rs b/fl16-inputmodules/src/animations.rs new file mode 100644 index 00000000..923e6e32 --- /dev/null +++ b/fl16-inputmodules/src/animations.rs @@ -0,0 +1,171 @@ +use crate::control::*; +use crate::games::game_of_life::*; +use crate::games::pong_animation::*; +use crate::games::snake_animation::*; +use crate::matrix::Grid; +use crate::matrix::*; +use crate::patterns::*; + +// TODO +// - [ ] Is there a cancellable Iterator? I think Java/Kotlin has one +// - [ ] Each one has a number of frames +// - [ ] Each one might have a different frame-rate + +#[allow(clippy::large_enum_variant)] +pub enum Animation { + ZigZag(ZigZagIterator), + Gof(GameOfLifeIterator), + Percentage(StartupPercentageIterator), + Breathing(BreathingIterator), + Snake(SnakeIterator), + Pong(PongIterator), +} +impl Iterator for Animation { + type Item = Grid; + + fn next(&mut self) -> Option { + match self { + Animation::ZigZag(x) => x.next(), + Animation::Gof(x) => x.next(), + Animation::Percentage(x) => x.next(), + Animation::Breathing(x) => x.next(), + Animation::Snake(x) => x.next(), + Animation::Pong(x) => x.next(), + } + } +} + +pub struct ZigZagIterator { + frames: usize, + current_frame: usize, +} + +impl ZigZagIterator { + pub fn new(frames: usize) -> Self { + Self { + frames, + current_frame: 0, + } + } +} + +impl Default for ZigZagIterator { + fn default() -> Self { + Self::new(34) + } +} + +impl Iterator for ZigZagIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.current_frame < self.frames { + let mut next = zigzag(); + next.rotate(self.current_frame); + self.current_frame += 1; + Some(next) + } else { + None + } + } +} + +pub struct StartupPercentageIterator { + frames: usize, + current_frame: usize, +} + +impl Default for StartupPercentageIterator { + fn default() -> Self { + Self { + frames: 34, + current_frame: 0, + } + } +} + +impl Iterator for StartupPercentageIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.current_frame < self.frames { + self.current_frame += 1; + Some(rows(self.current_frame)) + } else { + None + } + } +} + +pub struct GameOfLifeIterator { + state: GameOfLifeState, + frames_remaining: usize, +} + +impl GameOfLifeIterator { + pub fn new(start_param: GameOfLifeStartParam, frames: usize) -> Self { + Self { + // Could start with a custom grid + state: GameOfLifeState::new(start_param, &Grid::default()), + frames_remaining: frames, + } + } +} + +impl Iterator for GameOfLifeIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.frames_remaining > 0 { + self.frames_remaining -= 1; + // Only update every 8th frame, otherwise the animation is too fast + if self.frames_remaining % 8 == 0 { + self.state.tick(); + } + Some(self.state.draw_matrix()) + } else { + None + } + } +} + +pub struct BreathingIterator { + frames_remaining: usize, + current_brightness: u8, +} + +impl BreathingIterator { + pub fn new(frames: usize) -> Self { + Self { + frames_remaining: frames, + current_brightness: 0, + } + } +} +impl Default for BreathingIterator { + fn default() -> Self { + Self::new(64) + } +} + +impl Iterator for BreathingIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.frames_remaining > 0 { + let mut grid = Grid::default(); + let breath_step = 4; + // TODO: Make it cycle up and down + self.current_brightness = (self.current_brightness + breath_step) % 255; + for y in 0..HEIGHT { + for x in 0..WIDTH { + grid.0[x][y] = self.current_brightness; + } + } + self.frames_remaining -= 1; + Some(grid) + } else { + None + } + } +} diff --git a/fl16-inputmodules/src/control.rs b/fl16-inputmodules/src/control.rs new file mode 100644 index 00000000..ed1fbd32 --- /dev/null +++ b/fl16-inputmodules/src/control.rs @@ -0,0 +1,891 @@ +//! Firmware API - Commands +use num::FromPrimitive; +use rp2040_hal::rom_data::reset_to_usb_boot; + +use crate::serialnum::{device_release, is_pre_release}; + +#[cfg(feature = "b1display")] +use crate::graphics::*; +#[cfg(feature = "b1display")] +use core::fmt::{Debug, Write}; +#[cfg(feature = "b1display")] +use cortex_m::delay::Delay; +#[cfg(feature = "b1display")] +use embedded_graphics::Pixel; +#[cfg(feature = "b1display")] +use embedded_graphics::{ + pixelcolor::Rgb565, + prelude::{Point, RgbColor}, + primitives::Rectangle, +}; +#[cfg(feature = "b1display")] +use embedded_hal::blocking::spi; +#[cfg(feature = "b1display")] +use embedded_hal::digital::v2::OutputPin; +#[cfg(feature = "b1display")] +use heapless::String; +#[cfg(feature = "b1display")] +use st7306::{FpsConfig, PowerMode, ST7306}; + +#[cfg(feature = "ledmatrix")] +use crate::games::pong; +#[cfg(feature = "ledmatrix")] +use crate::games::snake; +#[cfg(feature = "ledmatrix")] +use crate::matrix::*; +#[cfg(feature = "ledmatrix")] +use crate::patterns::*; +#[cfg(feature = "ledmatrix")] +use is31fl3741::PwmFreq; + +#[cfg(feature = "c1minimal")] +use smart_leds::{SmartLedsWrite, RGB8}; + +#[repr(u8)] +#[derive(num_derive::FromPrimitive)] +/// All available commands +pub enum CommandVals { + Brightness = 0x00, + Pattern = 0x01, + BootloaderReset = 0x02, + Sleep = 0x03, + Animate = 0x04, + Panic = 0x05, + Draw = 0x06, + StageGreyCol = 0x07, + DrawGreyColBuffer = 0x08, + SetText = 0x09, + StartGame = 0x10, + GameControl = 0x11, + GameStatus = 0x12, + SetColor = 0x13, + DisplayOn = 0x14, + InvertScreen = 0x15, + SetPixelColumn = 0x16, + FlushFramebuffer = 0x17, + ClearRam = 0x18, + ScreenSaver = 0x19, + SetFps = 0x1A, + SetPowerMode = 0x1B, + AnimationPeriod = 0x1C, + PwmFreq = 0x1E, + DebugMode = 0x1F, + Version = 0x20, +} + +#[derive(num_derive::FromPrimitive)] +pub enum PatternVals { + Percentage = 0x00, + Gradient = 0x01, + DoubleGradient = 0x02, + DisplayLotus = 0x03, + ZigZag = 0x04, + FullBrightness = 0x05, + DisplayPanic = 0x06, + DisplayLotus2 = 0x07, +} + +pub enum Game { + Snake, + Pong, + Tetris, + GameOfLife(GameOfLifeStartParam), +} + +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum GameVal { + Snake = 0, + Pong = 1, + Tetris = 2, + GameOfLife = 3, +} + +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum GameControlArg { + Up = 0, + Down = 1, + Left = 2, + Right = 3, + Exit = 4, + SecondLeft = 5, + SecondRight = 6, +} + +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum GameOfLifeStartParam { + CurrentMatrix = 0x00, + Pattern1 = 0x01, + Blinker = 0x02, + Toad = 0x03, + Beacon = 0x04, + Glider = 0x05, + BeaconToadBlinker = 0x06, +} + +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum DisplayMode { + /// Low Power Mode + Lpm = 0x00, + /// High Power Mode + Hpm = 0x01, +} + +#[cfg(feature = "ledmatrix")] +#[derive(Copy, Clone, num_derive::FromPrimitive)] +pub enum PwmFreqArg { + /// 29kHz + P29k = 0x00, + /// 3.6kHz + P3k6 = 0x01, + /// 1.8kHz + P1k8 = 0x02, + /// 900Hz + P900 = 0x03, +} +#[cfg(feature = "ledmatrix")] +impl From for PwmFreq { + fn from(val: PwmFreqArg) -> Self { + match val { + PwmFreqArg::P29k => PwmFreq::P29k, + PwmFreqArg::P3k6 => PwmFreq::P3k6, + PwmFreqArg::P1k8 => PwmFreq::P1k8, + PwmFreqArg::P900 => PwmFreq::P900, + } + } +} + +// TODO: Reduce size for modules that don't require other commands +pub enum Command { + /// Get current brightness scaling + GetBrightness, + /// Set brightness scaling + SetBrightness(u8), + /// Display pre-programmed pattern + Pattern(PatternVals), + /// Reset into bootloader + BootloaderReset, + /// Light up a percentage of the screen + Percentage(u8), + /// Go to sleepe or wake up + Sleep(bool), + IsSleeping, + /// Start/stop animation (vertical scrolling) + SetAnimate(bool), + GetAnimate, + /// Panic. Just to test what happens + Panic, + /// Draw black/white on the grid + #[cfg(feature = "ledmatrix")] + Draw([u8; DRAW_BYTES]), + #[cfg(feature = "ledmatrix")] + StageGreyCol(u8, [u8; HEIGHT]), + DrawGreyColBuffer, + #[cfg(feature = "b1display")] + SetText(String<64>), + StartGame(Game), + GameControl(GameControlArg), + GameStatus, + Version, + GetColor, + #[cfg(feature = "c1minimal")] + SetColor(RGB8), + DisplayOn(bool), + GetDisplayOn, + InvertScreen(bool), + GetInvertScreen, + SetPixelColumn(usize, [u8; 50]), + FlushFramebuffer, + ClearRam, + ScreenSaver(bool), + GetScreenSaver, + SetFps(u8), + GetFps, + SetPowerMode(u8), + GetPowerMode, + /// Set the animation period in milliseconds + SetAnimationPeriod(u16), + /// Get the animation period in milliseconds + GetAnimationPeriod, + #[cfg(feature = "ledmatrix")] + SetPwmFreq(PwmFreqArg), + GetPwmFreq, + SetDebugMode(bool), + GetDebugMode, + _Unknown, +} + +#[cfg(any(feature = "c1minimal", feature = "b1display"))] +#[derive(Clone)] +pub enum SimpleSleepState { + Awake, + Sleeping, +} + +#[cfg(feature = "c1minimal")] +pub struct C1MinimalState { + pub sleeping: SimpleSleepState, + pub color: RGB8, + pub brightness: u8, +} + +#[derive(Copy, Clone)] +pub struct ScreenSaverState { + pub rightwards: i32, + pub downwards: i32, +} + +impl Default for ScreenSaverState { + fn default() -> Self { + Self { + rightwards: 1, + downwards: 1, + } + } +} + +#[cfg(feature = "b1display")] +pub struct B1DIsplayState { + pub sleeping: SimpleSleepState, + pub screen_inverted: bool, + pub screen_on: bool, + pub screensaver: Option, + pub power_mode: PowerMode, + pub fps_config: FpsConfig, + /// Animation period in microseconds + pub animation_period: u64, +} + +pub fn parse_command(count: usize, buf: &[u8]) -> Option { + if let Some(command) = parse_module_command(count, buf) { + return Some(command); + } + + // Parse the generic commands common to all modules + if count >= 3 && buf[0] == 0x32 && buf[1] == 0xAC { + let command = buf[2]; + let arg = if count <= 3 { None } else { Some(buf[3]) }; + + //let mut text: String<64> = String::new(); + //writeln!(&mut text, "Command: {command}, arg: {arg}").unwrap(); + //let _ = serial.write(text.as_bytes()); + match FromPrimitive::from_u8(command) { + Some(CommandVals::Sleep) => Some(if let Some(go_to_sleep) = arg { + Command::Sleep(go_to_sleep == 1) + } else { + Command::IsSleeping + }), + Some(CommandVals::BootloaderReset) => Some(Command::BootloaderReset), + Some(CommandVals::Panic) => Some(Command::Panic), + Some(CommandVals::Version) => Some(Command::Version), + _ => None, //Some(Command::Unknown), + } + } else { + None + } +} + +#[cfg(feature = "ledmatrix")] +pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { + if count >= 3 && buf[0] == 0x32 && buf[1] == 0xAC { + let command = buf[2]; + let arg = if count <= 3 { None } else { Some(buf[3]) }; + + match FromPrimitive::from_u8(command) { + Some(CommandVals::Brightness) => Some(if let Some(brightness) = arg { + Command::SetBrightness(brightness) + } else { + Command::GetBrightness + }), + Some(CommandVals::Pattern) => match arg.and_then(FromPrimitive::from_u8) { + // TODO: Convert arg to PatternVals + Some(PatternVals::Percentage) => { + if count >= 5 { + Some(Command::Percentage(buf[4])) + } else { + None + } + } + Some(PatternVals::Gradient) => Some(Command::Pattern(PatternVals::Gradient)), + Some(PatternVals::DoubleGradient) => { + Some(Command::Pattern(PatternVals::DoubleGradient)) + } + Some(PatternVals::DisplayLotus) => { + Some(Command::Pattern(PatternVals::DisplayLotus)) + } + Some(PatternVals::ZigZag) => Some(Command::Pattern(PatternVals::ZigZag)), + Some(PatternVals::FullBrightness) => { + Some(Command::Pattern(PatternVals::FullBrightness)) + } + Some(PatternVals::DisplayPanic) => { + Some(Command::Pattern(PatternVals::DisplayPanic)) + } + Some(PatternVals::DisplayLotus2) => { + Some(Command::Pattern(PatternVals::DisplayLotus2)) + } + None => None, + }, + Some(CommandVals::Animate) => Some(if let Some(run_animation) = arg { + Command::SetAnimate(run_animation == 1) + } else { + Command::GetAnimate + }), + Some(CommandVals::Draw) => { + if count >= 3 + DRAW_BYTES { + let mut bytes = [0; DRAW_BYTES]; + bytes.clone_from_slice(&buf[3..3 + DRAW_BYTES]); + Some(Command::Draw(bytes)) + } else { + None + } + } + Some(CommandVals::StageGreyCol) => { + if count >= 3 + 1 + HEIGHT { + let mut bytes = [0; HEIGHT]; + bytes.clone_from_slice(&buf[4..4 + HEIGHT]); + Some(Command::StageGreyCol(buf[3], bytes)) + } else { + None + } + } + Some(CommandVals::DrawGreyColBuffer) => Some(Command::DrawGreyColBuffer), + Some(CommandVals::StartGame) => match arg.and_then(FromPrimitive::from_u8) { + Some(GameVal::Snake) => Some(Command::StartGame(Game::Snake)), + Some(GameVal::Pong) => Some(Command::StartGame(Game::Pong)), + Some(GameVal::Tetris) => None, + Some(GameVal::GameOfLife) => { + if count >= 5 { + FromPrimitive::from_u8(buf[4]) + .map(|x| Command::StartGame(Game::GameOfLife(x))) + } else { + None + } + } + _ => None, + }, + Some(CommandVals::GameControl) => match arg.and_then(FromPrimitive::from_u8) { + Some(GameControlArg::Up) => Some(Command::GameControl(GameControlArg::Up)), + Some(GameControlArg::Down) => Some(Command::GameControl(GameControlArg::Down)), + Some(GameControlArg::Left) => Some(Command::GameControl(GameControlArg::Left)), + Some(GameControlArg::Right) => Some(Command::GameControl(GameControlArg::Right)), + Some(GameControlArg::Exit) => Some(Command::GameControl(GameControlArg::Exit)), + Some(GameControlArg::SecondLeft) => { + Some(Command::GameControl(GameControlArg::SecondLeft)) + } + Some(GameControlArg::SecondRight) => { + Some(Command::GameControl(GameControlArg::SecondRight)) + } + _ => None, + }, + Some(CommandVals::GameStatus) => Some(Command::GameStatus), + Some(CommandVals::AnimationPeriod) => { + if count == 3 + 2 { + let period = u16::from_le_bytes([buf[3], buf[4]]); + Some(Command::SetAnimationPeriod(period)) + } else { + Some(Command::GetAnimationPeriod) + } + } + Some(CommandVals::PwmFreq) => { + if let Some(freq) = arg { + FromPrimitive::from_u8(freq).map(Command::SetPwmFreq) + } else { + Some(Command::GetPwmFreq) + } + } + Some(CommandVals::DebugMode) => Some(if let Some(debug_mode) = arg { + Command::SetDebugMode(debug_mode == 1) + } else { + Command::GetDebugMode + }), + _ => None, + } + } else { + None + } +} + +#[cfg(feature = "b1display")] +pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { + if count >= 3 && buf[0] == 0x32 && buf[1] == 0xAC { + let command = buf[2]; + let arg = if count <= 3 { None } else { Some(buf[3]) }; + + match FromPrimitive::from_u8(command) { + Some(CommandVals::SetText) => { + if let Some(arg) = arg { + let available_len = count - 4; + let str_len = arg as usize; + assert!(str_len <= available_len); + + assert!(str_len < 32); + let mut bytes = [0; 32]; + bytes[..str_len].copy_from_slice(&buf[4..4 + str_len]); + + let text_str = core::str::from_utf8(&bytes[..str_len]).unwrap(); + let mut text: String<64> = String::new(); + writeln!(&mut text, "{}", text_str).unwrap(); + + Some(Command::SetText(text)) + } else { + None + } + } + Some(CommandVals::DisplayOn) => Some(if let Some(on) = arg { + Command::DisplayOn(on == 1) + } else { + Command::GetDisplayOn + }), + Some(CommandVals::InvertScreen) => Some(if let Some(invert) = arg { + Command::InvertScreen(invert == 1) + } else { + Command::GetInvertScreen + }), + Some(CommandVals::SetPixelColumn) => { + // 3B for magic and command + // 2B for column (u16) + // 50B for 400 pixels (400/8=50) + if count == 3 + 2 + 50 { + let column = u16::from_le_bytes([buf[3], buf[4]]); + //panic!("SetPixelColumn. Col: {}", column); + let mut pixels: [u8; 50] = [0; 50]; + pixels.clone_from_slice(&buf[5..55]); + Some(Command::SetPixelColumn(column as usize, pixels)) + } else { + None + } + } + Some(CommandVals::FlushFramebuffer) => Some(Command::FlushFramebuffer), + Some(CommandVals::ClearRam) => Some(Command::ClearRam), + Some(CommandVals::ScreenSaver) => Some(if let Some(on) = arg { + Command::ScreenSaver(on == 1) + } else { + Command::GetScreenSaver + }), + Some(CommandVals::SetFps) => Some(if let Some(fps) = arg { + Command::SetFps(fps) + } else { + Command::GetFps + }), + Some(CommandVals::SetPowerMode) => Some(if let Some(mode) = arg { + Command::SetPowerMode(mode) + } else { + Command::GetPowerMode + }), + Some(CommandVals::AnimationPeriod) => { + if count == 3 + 2 { + let period = u16::from_le_bytes([buf[3], buf[4]]); + Some(Command::SetAnimationPeriod(period)) + } else { + Some(Command::GetAnimationPeriod) + } + } + _ => None, + } + } else { + None + } +} + +#[cfg(not(any(feature = "ledmatrix", feature = "b1display", feature = "c1minimal")))] +pub fn parse_module_command(_count: usize, _buf: &[u8]) -> Option { + None +} + +pub fn handle_generic_command(command: &Command) -> Option<[u8; 32]> { + match command { + Command::BootloaderReset => { + //let _ = serial.write("Bootloader Reset".as_bytes()); + reset_to_usb_boot(0, 0); + None + } + Command::Panic => panic!("Ahhh"), + Command::Version => { + let mut response: [u8; 32] = [0; 32]; + let bcd_device = device_release().to_be_bytes(); + response[0] = bcd_device[0]; + response[1] = bcd_device[1]; + response[2] = is_pre_release() as u8; + Some(response) + } + _ => None, + } +} + +#[cfg(feature = "ledmatrix")] +pub fn handle_command( + command: &Command, + state: &mut LedmatrixState, + matrix: &mut Foo, + random: u8, +) -> Option<[u8; 32]> { + use crate::games::game_of_life; + + match command { + Command::GetBrightness => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.brightness; + Some(response) + } + Command::SetBrightness(br) => { + //let _ = serial.write("Brightness".as_bytes()); + set_brightness(state, *br, matrix); + None + } + Command::Percentage(p) => { + //let p = if count >= 5 { buf[4] } else { 100 }; + state.grid = percentage(*p as u16); + None + } + Command::Pattern(pattern) => { + //let _ = serial.write("Pattern".as_bytes()); + match pattern { + PatternVals::Gradient => state.grid = gradient(), + PatternVals::DoubleGradient => state.grid = double_gradient(), + PatternVals::DisplayLotus => state.grid = display_lotus(), + PatternVals::ZigZag => state.grid = zigzag(), + PatternVals::FullBrightness => { + state.grid = percentage(100); + set_brightness(state, BRIGHTNESS_LEVELS, matrix); + } + PatternVals::DisplayPanic => state.grid = display_panic(), + PatternVals::DisplayLotus2 => state.grid = display_lotus2(), + _ => {} + } + None + } + Command::SetAnimate(a) => { + state.animate = *a; + None + } + Command::GetAnimate => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.animate as u8; + Some(response) + } + Command::Draw(vals) => { + state.grid = draw(vals); + None + } + Command::StageGreyCol(col, vals) => { + draw_grey_col(&mut state.col_buffer, *col, vals); + None + } + Command::DrawGreyColBuffer => { + // Copy the staging buffer to the real grid and display it + state.grid = state.col_buffer.clone(); + // Zero the old staging buffer, just for good measure. + state.col_buffer = percentage(0); + None + } + // TODO: Move to handle_generic_command + Command::IsSleeping => { + let mut response: [u8; 32] = [0; 32]; + response[0] = match state.sleeping { + SleepState::Sleeping(_) => 1, + SleepState::Awake => 0, + }; + Some(response) + } + Command::StartGame(game) => { + match game { + Game::Snake => snake::start_game(state, random), + Game::Pong => pong::start_game(state, random), + Game::Tetris => {} + Game::GameOfLife(param) => game_of_life::start_game(state, random, *param), + } + None + } + Command::GameControl(arg) => { + match state.game { + Some(GameState::Snake(_)) => snake::handle_control(state, arg), + Some(GameState::Pong(_)) => pong::handle_control(state, arg), + Some(GameState::GameOfLife(_)) => game_of_life::handle_control(state, arg), + _ => {} + } + None + } + Command::GameStatus => None, + Command::SetAnimationPeriod(period) => { + state.animation_period = (*period as u64) * 1_000; + None + } + Command::GetAnimationPeriod => { + // TODO: Doesn't seem to work when the FPS is 16 or higher + let mut response: [u8; 32] = [0; 32]; + let period_ms = state.animation_period / 1_000; + response[0..2].copy_from_slice(&(period_ms as u16).to_le_bytes()); + Some(response) + } + Command::SetPwmFreq(arg) => { + state.pwm_freq = *arg; + matrix.device.set_pwm_freq(state.pwm_freq.into()).unwrap(); + None + } + Command::GetPwmFreq => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.pwm_freq as u8; + Some(response) + } + Command::SetDebugMode(arg) => { + state.debug_mode = *arg; + None + } + Command::GetDebugMode => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.debug_mode as u8; + Some(response) + } + _ => handle_generic_command(command), + } +} + +#[cfg(feature = "b1display")] +pub fn handle_command( + command: &Command, + state: &mut B1DIsplayState, + logo_rect: Rectangle, + disp: &mut ST7306, + delay: &mut Delay, +) -> Option<[u8; 32]> +where + SPI: spi::Write, + DC: OutputPin, + CS: OutputPin, + RST: OutputPin, + >::Error: Debug, +{ + match command { + // TODO: Move to handle_generic_command + Command::IsSleeping => { + let mut response: [u8; 32] = [0; 32]; + response[0] = match state.sleeping { + SimpleSleepState::Sleeping => 1, + SimpleSleepState::Awake => 0, + }; + Some(response) + } + Command::Panic => panic!("Ahhh"), + Command::SetText(text) => { + // Turn screensaver off, when drawing something + state.screensaver = None; + + clear_text( + disp, + Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), + Rgb565::WHITE, + ) + .unwrap(); + + draw_text( + disp, + text, + Point::new(LOGO_OFFSET_X, LOGO_OFFSET_Y + logo_rect.size.height as i32), + ) + .unwrap(); + disp.flush().unwrap(); + None + } + Command::DisplayOn(on) => { + state.screen_on = *on; + disp.on_off(*on).unwrap(); + None + } + Command::GetDisplayOn => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.screen_on as u8; + Some(response) + } + Command::InvertScreen(invert) => { + state.screen_inverted = *invert; + disp.invert_screen(state.screen_inverted).unwrap(); + None + } + Command::GetInvertScreen => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.screen_inverted as u8; + Some(response) + } + Command::SetPixelColumn(column, pixel_bytes) => { + // Turn screensaver off, when drawing something + state.screensaver = None; + + let mut pixels: [bool; 400] = [false; 400]; + for (i, byte) in pixel_bytes.iter().enumerate() { + pixels[8 * i] = byte & 0b00000001 != 0; + pixels[8 * i + 1] = byte & 0b00000010 != 0; + pixels[8 * i + 2] = byte & 0b00000100 != 0; + pixels[8 * i + 3] = byte & 0b00001000 != 0; + pixels[8 * i + 4] = byte & 0b00010000 != 0; + pixels[8 * i + 5] = byte & 0b00100000 != 0; + pixels[8 * i + 6] = byte & 0b01000000 != 0; + pixels[8 * i + 7] = byte & 0b10000000 != 0; + } + disp.draw_pixels( + pixels.iter().enumerate().map(|(y, black)| { + Pixel( + Point::new(*column as i32, y as i32), + if *black { Rgb565::BLACK } else { Rgb565::WHITE }, + ) + }), + false, + ) + .unwrap(); + None + } + Command::FlushFramebuffer => { + disp.flush().unwrap(); + None + } + Command::ClearRam => { + // Turn screensaver off, when drawing something + state.screensaver = None; + + disp.clear_ram().unwrap(); + None + } + Command::ScreenSaver(on) => { + state.screensaver = match (*on, state.screensaver) { + (true, Some(x)) => Some(x), + (true, None) => Some(ScreenSaverState::default()), + (false, Some(_)) => None, + (false, None) => None, + }; + None + } + Command::GetScreenSaver => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.screensaver.is_some() as u8; + Some(response) + } + Command::SetFps(fps) => { + if let Some(fps_config) = FpsConfig::from_u8(*fps) { + state.fps_config = fps_config; + disp.set_fps(state.fps_config).unwrap(); + // TODO: Need to reinit the display + } + None + } + Command::GetFps => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.fps_config.as_u8(); + Some(response) + } + Command::SetPowerMode(mode) => { + match mode { + 0 => { + state.power_mode = PowerMode::Lpm; + disp.switch_mode(delay, state.power_mode).unwrap(); + } + 1 => { + state.power_mode = PowerMode::Hpm; + disp.switch_mode(delay, state.power_mode).unwrap(); + } + _ => {} + } + None + } + Command::GetPowerMode => { + let mut response: [u8; 32] = [0; 32]; + response[0] = match state.power_mode { + PowerMode::Lpm => 0, + PowerMode::Hpm => 1, + }; + Some(response) + } + Command::SetAnimationPeriod(period) => { + state.animation_period = (*period as u64) * 1_000; + None + } + Command::GetAnimationPeriod => { + // TODO: Doesn't seem to work when the FPS is 16 or higher + let mut response: [u8; 32] = [0; 32]; + let period_ms = state.animation_period / 1_000; + response[0..2].copy_from_slice(&(period_ms as u16).to_le_bytes()); + Some(response) + } + _ => handle_generic_command(command), + } +} + +#[cfg(feature = "c1minimal")] +pub fn handle_command( + command: &Command, + state: &mut C1MinimalState, + ws2812: &mut impl SmartLedsWrite, +) -> Option<[u8; 32]> { + match command { + // TODO: Move to handle_generic_command + Command::IsSleeping => { + let mut response: [u8; 32] = [0; 32]; + response[0] = match state.sleeping { + SimpleSleepState::Sleeping => 1, + SimpleSleepState::Awake => 0, + }; + Some(response) + } + Command::GetBrightness => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.brightness; + Some(response) + } + Command::SetBrightness(br) => { + //let _ = serial.write("Brightness".as_bytes()); + state.brightness = *br; + ws2812 + .write(smart_leds::brightness( + [state.color].iter().cloned(), + state.brightness, + )) + .unwrap(); + None + } + Command::GetColor => { + let mut response: [u8; 32] = [0; 32]; + response[0] = state.color.r; + response[1] = state.color.g; + response[2] = state.color.b; + Some(response) + } + Command::SetColor(color) => { + state.color = *color; + ws2812 + .write(smart_leds::brightness( + [*color].iter().cloned(), + state.brightness, + )) + .unwrap(); + None + } + // TODO: Make it return something + _ => handle_generic_command(command), + } +} + +#[cfg(feature = "c1minimal")] +pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { + if count >= 3 && buf[0] == 0x32 && buf[1] == 0xAC { + let command = buf[2]; + let arg = if count <= 3 { None } else { Some(buf[3]) }; + + match FromPrimitive::from_u8(command) { + Some(CommandVals::Brightness) => Some(if let Some(brightness) = arg { + Command::SetBrightness(brightness) + } else { + Command::GetBrightness + }), + Some(CommandVals::SetColor) => { + if count >= 6 { + let (red, green, blue) = (buf[3], buf[4], buf[5]); + Some(Command::SetColor(RGB8::new(red, green, blue))) + } else if arg.is_none() { + Some(Command::GetColor) + } else { + None + } + } + _ => None, + } + } else { + None + } +} diff --git a/fl16-inputmodules/src/fl16.rs b/fl16-inputmodules/src/fl16.rs new file mode 100644 index 00000000..4a6bdd43 --- /dev/null +++ b/fl16-inputmodules/src/fl16.rs @@ -0,0 +1,317 @@ +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] = [ + (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 + (0x3c, 1), // x: 9, y: 1, sw: 9, cs: 1, 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 + (0x3d, 1), // x: 9, y: 2, sw: 9, cs: 2, 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 + (0x3e, 1), // x: 9, y: 3, sw: 9, cs: 3, 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 + (0x3f, 1), // x: 9, y: 4, sw: 9, cs: 4, 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 + (0x40, 1), // x: 9, y: 5, sw: 9, cs: 5, 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 + (0x41, 1), // x: 9, y: 6, sw: 9, cs: 6, 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 + (0x42, 1), // x: 9, y: 7, sw: 9, cs: 7, 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 + (0x43, 1), // x: 9, y: 8, sw: 9, cs: 8, 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 + (0x44, 1), // x: 9, y: 9, sw: 9, cs: 9, 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 + (0x45, 1), // x: 9, y:10, sw: 9, cs:10, 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 + (0x46, 1), // x: 9, y:11, sw: 9, cs:11, 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 + (0x47, 1), // x: 9, y:12, sw: 9, cs:12, 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 + (0x48, 1), // x: 9, y:13, sw: 9, cs:13, 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 + (0x49, 1), // x: 9, y:14, sw: 9, cs:14, 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 + (0x4a, 1), // x: 9, y:15, sw: 9, cs:15, 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 + (0x4b, 1), // x: 9, y:16, sw: 9, cs:16, 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 + (0x4c, 1), // x: 9, y:17, sw: 9, cs:17, 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 + (0x4d, 1), // x: 9, y:18, sw: 9, cs:18, 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 + (0x4e, 1), // x: 9, y:19, sw: 9, cs:19, 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 + (0x4f, 1), // x: 9, y:20, sw: 9, cs:20, 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 + (0x50, 1), // x: 9, y:21, sw: 9, cs:21, 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 + (0x51, 1), // x: 9, y:22, sw: 9, cs:22, 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 + (0x52, 1), // x: 9, y:23, sw: 9, cs:23, 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 + (0x53, 1), // x: 9, y:24, sw: 9, cs:24, 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 + (0x54, 1), // x: 9, y:25, sw: 9, cs:25, 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 + (0x55, 1), // x: 9, y:26, sw: 9, cs:26, 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 + (0x56, 1), // x: 9, y:27, sw: 9, cs:27, 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 + (0x57, 1), // x: 9, y:28, sw: 9, cs:28, 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 + (0x58, 1), // x: 9, y:29, sw: 9, cs:29, 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 + (0x59, 1), // x: 9, y:30, sw: 9, cs:30, 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 + (0xa2, 1), // x: 9, y:31, sw: 9, cs:31, 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 + (0xa3, 1), // x: 9, y:32, sw: 9, cs:32, 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 + (0xa4, 1), // x: 9, y:33, sw: 9, cs:33, 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 + (0xa5, 1), // x: 9, y:34, sw: 9, cs:34, id:306 + ]; + let index: usize = (x as usize) + (y as usize) * 9; + if index < lookup.len() { + lookup[index] + } else { + (0x00, 0) + } +}; diff --git a/fl16-inputmodules/src/games/game_of_life.rs b/fl16-inputmodules/src/games/game_of_life.rs new file mode 100644 index 00000000..1670bacf --- /dev/null +++ b/fl16-inputmodules/src/games/game_of_life.rs @@ -0,0 +1,229 @@ +use crate::control::{GameControlArg, GameOfLifeStartParam}; +use crate::matrix::{GameState, Grid, LedmatrixState, HEIGHT, WIDTH}; + +#[derive(Clone, Copy, num_derive::FromPrimitive, PartialEq, Eq)] +pub enum Cell { + Dead = 0, + Alive = 1, +} + +#[derive(Clone)] +pub struct GameOfLifeState { + cells: [[Cell; WIDTH]; HEIGHT], +} + +impl GameOfLifeState { + pub fn combine(&self, other: &Self) -> Self { + let mut state = self.clone(); + for x in 0..WIDTH { + for y in 0..HEIGHT { + if other.cells[y][x] == Cell::Alive { + state.cells[y][x] = Cell::Alive; + } + } + } + state + } +} + +pub fn start_game(state: &mut LedmatrixState, _random: u8, param: GameOfLifeStartParam) { + let gol = GameOfLifeState::new(param, &state.grid); + state.grid = gol.draw_matrix(); + state.game = Some(GameState::GameOfLife(gol)); +} +pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { + if let Some(GameState::GameOfLife(ref mut _gol_state)) = state.game { + if let GameControlArg::Exit = arg { + state.game = None + } + } +} +pub fn game_step(state: &mut LedmatrixState, _random: u8) { + if let Some(GameState::GameOfLife(ref mut gol_state)) = state.game { + gol_state.tick(); + state.grid = gol_state.draw_matrix(); + } else { + panic!("Game of Life not started!") + } +} + +impl GameOfLifeState { + // TODO: Integrate Grid into GameOfLifeStartParam because it's only used in one of the enum variants + pub fn new(param: GameOfLifeStartParam, grid: &Grid) -> Self { + match param { + GameOfLifeStartParam::Beacon => Self::beacon(), + GameOfLifeStartParam::CurrentMatrix => { + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + for row in 0..HEIGHT { + for col in 0..WIDTH { + cells[row][col] = if grid.0[col][row] == 0 { + Cell::Dead + } else { + Cell::Alive + }; + } + } + //cells: grid + // .0 + // .map(|col| col.map(|val| if val == 0 { Cell::Dead } else { Cell::Alive })), + GameOfLifeState { cells } + } + GameOfLifeStartParam::Pattern1 => Self::pattern1(), + GameOfLifeStartParam::Blinker => Self::blinker(), + GameOfLifeStartParam::Toad => Self::toad(), + GameOfLifeStartParam::Glider => Self::glider(), + GameOfLifeStartParam::BeaconToadBlinker => Self::beacon() + .combine(&Self::toad()) + .combine(&Self::blinker()), + } + } + fn pattern1() -> Self { + // Starts off with lots of alive cells, quickly reduced. + // Eventually reaches a stable pattern without changes. + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + for row in 0..HEIGHT { + for col in 0..WIDTH { + let i = col * HEIGHT + row; + if i % 2 == 0 || i % 7 == 0 { + cells[row][col] = Cell::Alive; + } + } + } + GameOfLifeState { cells } + } + fn blinker() -> Self { + // Oscillates between: + // XXX + // and + // X + // X + // X + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + cells[4][5] = Cell::Alive; + cells[4][6] = Cell::Alive; + cells[4][7] = Cell::Alive; + cells[8][5] = Cell::Alive; + cells[8][6] = Cell::Alive; + cells[8][7] = Cell::Alive; + GameOfLifeState { cells } + } + fn toad() -> Self { + // Oscillates between + // XXX + // XXX + // and + // X + // X X + // X X + // X + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + cells[17][4] = Cell::Alive; + cells[17][5] = Cell::Alive; + cells[17][6] = Cell::Alive; + cells[18][5] = Cell::Alive; + cells[18][6] = Cell::Alive; + cells[18][7] = Cell::Alive; + GameOfLifeState { cells } + } + fn beacon() -> Self { + // Oscillates between + // XX + // XX + // XX + // XX + // and + // XX + // X + // X + // XX + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + cells[26][4] = Cell::Alive; + cells[26][5] = Cell::Alive; + cells[27][4] = Cell::Alive; + cells[27][5] = Cell::Alive; + + cells[28][6] = Cell::Alive; + cells[28][7] = Cell::Alive; + cells[29][6] = Cell::Alive; + cells[29][7] = Cell::Alive; + GameOfLifeState { cells } + } + + fn glider() -> Self { + // X + // X + // XXX + let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; + cells[2][3] = Cell::Alive; + cells[3][4] = Cell::Alive; + cells[4][2] = Cell::Alive; + cells[4][3] = Cell::Alive; + cells[4][4] = Cell::Alive; + + cells[20][5] = Cell::Alive; + cells[21][6] = Cell::Alive; + cells[22][4] = Cell::Alive; + cells[22][5] = Cell::Alive; + cells[22][6] = Cell::Alive; + GameOfLifeState { cells } + } + + /// Count live neighbor cells + pub fn live_neighbor_count(&self, row: usize, col: usize) -> u8 { + let mut count = 0; + // Use HEIGHT-1 instead of -1 because usize can't go below 0 + for delta_row in [HEIGHT - 1, 0, 1] { + for delta_col in [WIDTH - 1, 0, 1] { + if delta_row == 0 && delta_col == 0 { + // The cell itself + continue; + } + + let neighbor_row = (row + delta_row) % HEIGHT; + let neighbor_col = (col + delta_col) % WIDTH; + + count += self.cells[neighbor_row][neighbor_col] as u8; + } + } + count + } + pub fn tick(&mut self) { + let mut next_generation = self.cells; + + for row in 0..HEIGHT { + for col in 0..WIDTH { + let cell = self.cells[row][col]; + let live_neighbors = self.live_neighbor_count(row, col); + + let child_cell = match (cell, live_neighbors) { + // Fewer than 2 neighbors causes it to die + (Cell::Alive, x) if x < 2 => Cell::Dead, + // 2 or three neighbors are good and it stays alive + (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive, + // More than 3 is too many and the cell dies + (Cell::Alive, x) if x > 3 => Cell::Dead, + // 3 neighbors when the cell is dead, revives it + (Cell::Dead, 3) => Cell::Alive, + // No change by default + (c, _) => c, + }; + + next_generation[row][col] = child_cell; + } + } + + self.cells = next_generation; + } + + pub fn draw_matrix(&self) -> Grid { + let mut grid = Grid::default(); + + for row in 0..HEIGHT { + for col in 0..WIDTH { + grid.0[col][row] = (self.cells[row][col] as u8) * 0xFF; + } + } + + grid + } +} diff --git a/fl16-inputmodules/src/games/mod.rs b/fl16-inputmodules/src/games/mod.rs new file mode 100644 index 00000000..6263c98d --- /dev/null +++ b/fl16-inputmodules/src/games/mod.rs @@ -0,0 +1,5 @@ +pub mod game_of_life; +pub mod pong; +pub mod pong_animation; +pub mod snake; +pub mod snake_animation; diff --git a/fl16-inputmodules/src/games/pong.rs b/fl16-inputmodules/src/games/pong.rs new file mode 100644 index 00000000..c063386c --- /dev/null +++ b/fl16-inputmodules/src/games/pong.rs @@ -0,0 +1,169 @@ +use crate::control::GameControlArg; +use crate::matrix::{GameState, Grid, LedmatrixState, HEIGHT, WIDTH}; + +const PADDLE_WIDTH: usize = 5; + +#[derive(Clone)] +struct Score { + _upper: u8, + _lower: u8, +} + +type Position = (usize, usize); +type Velocity = (i8, i8); + +#[derive(Clone)] +struct Ball { + pos: Position, + // Not a position, more like a directional vector + direction: Velocity, +} + +#[derive(Clone)] +pub struct PongState { + // TODO: Properly calculate score and display it + _score: Score, + ball: Ball, + paddles: (usize, usize), + pub speed: u64, +} + +impl Default for PongState { + fn default() -> Self { + PongState { + _score: Score { + _upper: 0, + _lower: 0, + }, + ball: Ball { + pos: (4, 20), + direction: (0, 1), + }, + paddles: (PADDLE_WIDTH / 2, PADDLE_WIDTH / 2), + speed: 0, + } + } +} + +impl PongState { + pub fn draw_matrix(&self) -> Grid { + let mut grid = Grid::default(); + + for x in self.paddles.0..self.paddles.0 + PADDLE_WIDTH { + grid.0[x][0] = 0xFF; + } + for x in self.paddles.1..self.paddles.1 + PADDLE_WIDTH { + grid.0[x][HEIGHT - 1] = 0xFF; + } + grid.0[self.ball.pos.0][self.ball.pos.1] = 0xFF; + + grid + } + + pub fn tick(&mut self) { + self.ball.pos = { + let (vx, vy) = self.ball.direction; + let (x, y) = add_velocity(self.ball.pos, self.ball.direction); + let x = if x > WIDTH - 1 { WIDTH - 1 } else { x }; + if x == 0 || x == WIDTH - 1 { + // Hit wall, bounce back + self.ball.direction = (-vx, vy); + } + + let y = if y > HEIGHT - 1 { HEIGHT - 1 } else { y }; + let (x, y) = if let Some(paddle_hit) = hit_paddle((x, y), self.paddles) { + // Hit paddle, bounce back + // TODO: Change vy direction slightly depending on where the paddle was hit + let (vx, vy) = self.ball.direction; + self.ball.direction = match paddle_hit { + 0 => (vx - 2, -vy), + 1 => (vx - 1, -vy), + 2 => (vx, -vy), + 3 => (vx + 1, -vy), + 4 => (vx + 2, -vy), + // Shouldn't occur + _ => (vx, -vy), + }; + // TODO: Not sure if I want the speed to change. Speed by angle change is already high enough + //self.speed += 1; + (x, y) + } else if y == 0 || y == HEIGHT - 1 { + self.speed = 0; + self.ball.direction = (1, 1); //random_v(random); + (WIDTH / 2, HEIGHT / 2) + } else { + (x, y) + }; + (x, y) + }; + } + pub fn handle_control(&mut self, arg: &GameControlArg) { + match arg { + GameControlArg::Left => { + if self.paddles.0 + PADDLE_WIDTH < WIDTH { + self.paddles.0 += 1; + } + } + GameControlArg::Right => { + if self.paddles.0 >= 1 { + self.paddles.0 -= 1; + } + } + GameControlArg::SecondLeft => { + if self.paddles.1 + PADDLE_WIDTH < WIDTH { + self.paddles.1 += 1; + } + } + GameControlArg::SecondRight => { + if self.paddles.1 >= 1 { + self.paddles.1 -= 1; + } + } + _ => {} + } + } +} + +pub fn start_game(state: &mut LedmatrixState, _random: u8) { + state.game = Some(GameState::Pong(PongState::default())) +} +pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { + if let Some(GameState::Pong(ref mut pong_state)) = state.game { + match arg { + GameControlArg::Exit => state.game = None, + _ => pong_state.handle_control(arg), + } + } +} + +// TODO: Randomize the velocity vector upon respawning +fn _random_v(random: u8) -> Velocity { + // TODO: while food == head: + let x = ((random & 0xF0) >> 4) % WIDTH as u8; + let y = (random & 0x0F) % HEIGHT as u8; + (x as i8, y as i8) +} + +fn add_velocity(pos: Position, v: Velocity) -> Position { + let (vx, vy) = v; + let (x, y) = pos; + (((x as i8) + vx) as usize, ((y as i8) + vy) as usize) +} + +fn hit_paddle(ball: Position, paddles: (usize, usize)) -> Option { + let (x, y) = ball; + if y == 1 && paddles.0 <= x && x <= paddles.0 + PADDLE_WIDTH { + Some(((paddles.0 as i32) - (x as i32)).unsigned_abs() as usize) + } else if y == HEIGHT - 2 && paddles.1 <= x && x <= paddles.1 + PADDLE_WIDTH { + Some(((paddles.1 as i32) - (x as i32)).unsigned_abs() as usize) + } else { + None + } +} + +pub fn game_step(state: &mut LedmatrixState, _random: u8) { + if let Some(GameState::Pong(ref mut pong_state)) = state.game { + pong_state.tick(); + state.grid = pong_state.draw_matrix(); + } +} diff --git a/fl16-inputmodules/src/games/pong_animation.rs b/fl16-inputmodules/src/games/pong_animation.rs new file mode 100644 index 00000000..67c2e99c --- /dev/null +++ b/fl16-inputmodules/src/games/pong_animation.rs @@ -0,0 +1,176 @@ +use crate::control::GameControlArg; +use crate::games::pong::PongState; +use crate::matrix::Grid; + +pub struct PongIterator { + state: PongState, + commands: [Option; 136], + current_command: usize, +} + +impl Default for PongIterator { + fn default() -> Self { + PongIterator { + state: PongState::default(), + commands: SAMPLE_GAME, + current_command: 0, + } + } +} + +impl Iterator for PongIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.current_command >= self.commands.len() { + return None; + } + + if let Some(command) = self.commands[self.current_command] { + self.state.handle_control(&command); + } + self.current_command += 1; + + self.state.tick(); + Some(self.state.draw_matrix()) + } +} + +const SAMPLE_GAME: [Option; 136] = [ + Some(GameControlArg::Left), // Middle + None, + Some(GameControlArg::Left), + None, + None, + None, + Some(GameControlArg::SecondRight), + Some(GameControlArg::SecondRight), + None, + None, + None, + Some(GameControlArg::SecondLeft), + None, // hit and bounce back + None, + None, + None, + Some(GameControlArg::Right), + None, + None, + None, + None, + None, + None, + Some(GameControlArg::Right), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(GameControlArg::SecondLeft), + None, + Some(GameControlArg::SecondLeft), + None, + Some(GameControlArg::SecondRight), + None, + None, + Some(GameControlArg::SecondLeft), + None, + None, + Some(GameControlArg::Right), + None, + None, + Some(GameControlArg::Left), + None, + Some(GameControlArg::Right), + None, + None, + Some(GameControlArg::Left), + None, + None, + None, + None, + None, + None, + None, + None, + Some(GameControlArg::Right), + Some(GameControlArg::Right), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, +]; diff --git a/fl16-inputmodules/src/games/snake.rs b/fl16-inputmodules/src/games/snake.rs new file mode 100644 index 00000000..57f52e2f --- /dev/null +++ b/fl16-inputmodules/src/games/snake.rs @@ -0,0 +1,148 @@ +use crate::control::GameControlArg; +use crate::matrix::{GameState, Grid, LedmatrixState, HEIGHT, LEDS, WIDTH}; + +use heapless::Vec; + +// Wrap around the edges +const WRAP_ENABLE: bool = false; + +#[derive(Clone, Debug, Copy)] +pub enum HeadDirection { + Up, + Down, + Left, + Right, +} + +type Position = (i8, i8); + +#[derive(Clone)] +pub struct SnakeState { + head: Position, + pub direction: HeadDirection, + // Unrealistic that the body will ever get this long + pub body: Vec, + pub game_over: bool, + food: Position, +} + +impl SnakeState { + pub fn new(random: u8) -> Self { + SnakeState { + head: (4, 0), + direction: HeadDirection::Down, + body: Vec::new(), + game_over: false, + food: place_food(random), + } + } + pub fn tick(&mut self, random: u8) { + if self.game_over { + return; + } + + let (x, y) = self.head; + let oldhead = self.head; + self.head = match self.direction { + // (0, 0) is at the top right corner + HeadDirection::Right => (x - 1, y), + HeadDirection::Left => (x + 1, y), + HeadDirection::Down => (x, y + 1), + HeadDirection::Up => (x, y - 1), + }; + let (x, y) = self.head; + let width = WIDTH as i8; + let height = HEIGHT as i8; + + if self.body.contains(&self.head) { + // Ran into itself + self.game_over = true + } else if x >= width || x < 0 || y >= height || y < 0 { + // Hit an edge + if WRAP_ENABLE { + self.head = if x >= width { + (0, y) + } else if x < 0 { + (width - 1, y) + } else if y >= height { + (x, 0) + } else if y < 0 { + (x, height - 1) + } else { + (x, y) + }; + } else { + self.game_over = true + } + } else if self.head == self.food { + // Eating food and growing + self.body.insert(0, oldhead).unwrap(); + self.food = place_food(random); + } else if !self.body.is_empty() { + // Move body along + self.body.pop(); + self.body.insert(0, oldhead).unwrap(); + } + } + + pub fn handle_control(&mut self, arg: &GameControlArg) { + match arg { + GameControlArg::Up => self.direction = HeadDirection::Up, + GameControlArg::Down => self.direction = HeadDirection::Down, + GameControlArg::Left => self.direction = HeadDirection::Left, + GameControlArg::Right => self.direction = HeadDirection::Right, + _ => {} + } + } + pub fn draw_matrix(&self) -> Grid { + let (x, y) = self.head; + let mut grid = Grid::default(); + + grid.0[x as usize][y as usize] = 0xFF; + grid.0[self.food.0 as usize][self.food.1 as usize] = 0xFF; + for bodypart in &self.body { + let (x, y) = bodypart; + grid.0[*x as usize][*y as usize] = 0xFF; + } + + grid + } +} + +fn place_food(random: u8) -> Position { + // TODO: while food == head: + let x = ((random & 0xF0) >> 4) % WIDTH as u8; + let y = (random & 0x0F) % HEIGHT as u8; + (x as i8, y as i8) +} + +pub fn start_game(state: &mut LedmatrixState, random: u8) { + state.game = Some(GameState::Snake(SnakeState::new(random))); +} + +pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { + if let Some(GameState::Snake(ref mut snake_state)) = state.game { + match arg { + GameControlArg::Exit => state.game = None, + _ => snake_state.handle_control(arg), + } + } +} + +pub fn game_step(state: &mut LedmatrixState, random: u8) -> (HeadDirection, bool, usize, Position) { + if let Some(GameState::Snake(ref mut snake_state)) = state.game { + snake_state.tick(random); + + if !snake_state.game_over { + state.grid = snake_state.draw_matrix(); + } + ( + snake_state.direction, + snake_state.game_over, + snake_state.body.len(), + snake_state.head, + ) + } else { + (HeadDirection::Down, true, 0, (0, 0)) + } +} diff --git a/fl16-inputmodules/src/games/snake_animation.rs b/fl16-inputmodules/src/games/snake_animation.rs new file mode 100644 index 00000000..533750e2 --- /dev/null +++ b/fl16-inputmodules/src/games/snake_animation.rs @@ -0,0 +1,118 @@ +use crate::control::GameControlArg; +use crate::games::snake::SnakeState; +use crate::matrix::Grid; + +pub struct SnakeIterator { + state: SnakeState, + commands: [(Option, u8); 64], + current_tick: usize, +} + +impl SnakeIterator { + pub fn new(random: u8) -> Self { + Self { + state: SnakeState::new(random), + commands: SAMPLE_GAME, + current_tick: 0, + } + } +} +impl Default for SnakeIterator { + fn default() -> Self { + Self::new(31) + } +} + +impl Iterator for SnakeIterator { + type Item = Grid; + + fn next(&mut self) -> Option { + if self.current_tick / 4 >= self.commands.len() { + return None; + } + + // Slow down animation by a factor of 4 + if self.current_tick % 4 == 0 { + let (maybe_cmd, random) = self.commands[self.current_tick / 4]; + if let Some(command) = maybe_cmd { + self.state.handle_control(&command); + } + self.state.tick(random); + } + self.current_tick += 1; + + if self.state.game_over { + None + } else { + Some(self.state.draw_matrix()) + } + } +} + +// TODO: Plan out a nice looking game +const SAMPLE_GAME: [(Option, u8); 64] = [ + (Some(GameControlArg::Down), 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (Some(GameControlArg::Left), 0), + (None, 0), + (None, 0), + (None, 0), + (Some(GameControlArg::Down), 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (Some(GameControlArg::Right), 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (Some(GameControlArg::Down), 0), + (None, 0), + (None, 10), + (None, 0), + (None, 0), + (Some(GameControlArg::Right), 0), + (Some(GameControlArg::Up), 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (Some(GameControlArg::Left), 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), + (None, 0), +]; diff --git a/fl16-inputmodules/src/graphics.rs b/fl16-inputmodules/src/graphics.rs new file mode 100644 index 00000000..69e9bb26 --- /dev/null +++ b/fl16-inputmodules/src/graphics.rs @@ -0,0 +1,54 @@ +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::prelude::*; +use embedded_graphics::{ + image::Image, + mono_font::{ascii::FONT_9X15, MonoTextStyle}, + primitives::{PrimitiveStyle, Rectangle}, + text::Text, +}; + +use tinybmp::Bmp; + +pub const LOGO_OFFSET_X: i32 = 100; +pub const LOGO_OFFSET_Y: i32 = 100; + +pub fn clear_text(target: &mut D, offset: Point, color: Rgb565) -> Result<(), D::Error> +where + D: DrawTarget, +{ + const TEXT_H: i32 = 20; + Rectangle::new( + Point::new(0, 30) + offset - Point::new(0, 15), + Size::new(130, TEXT_H as u32), + ) + .into_styled(PrimitiveStyle::with_fill(color)) + .draw(target)?; + + Ok(()) +} + +pub fn draw_text(target: &mut D, target_text: &str, offset: Point) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let text = Text::new( + target_text, + Point::new(30, 30) + offset, + MonoTextStyle::new(&FONT_9X15, Rgb565::BLACK), + ); + + text.draw(target)?; + + Ok(()) +} + +pub fn draw_logo(target: &mut D, offset: Point) -> Result +where + D: DrawTarget, +{ + let bmp: Bmp = Bmp::from_slice(include_bytes!("../assets/logo.bmp")).unwrap(); + let image = Image::new(&bmp, offset); + image.draw(target)?; + + Ok(image.bounding_box()) +} diff --git a/fl16-inputmodules/src/lcd_hal.rs b/fl16-inputmodules/src/lcd_hal.rs new file mode 100644 index 00000000..663f17ef --- /dev/null +++ b/fl16-inputmodules/src/lcd_hal.rs @@ -0,0 +1,57 @@ +// Taken from rp_pico hal and adjusted + +pub extern crate rp2040_hal as hal; + +extern crate cortex_m_rt; +pub use hal::entry; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +#[link_section = ".boot2"] +#[no_mangle] +#[used] +pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +pub use hal::pac; + +// Previously prototyping with ST7735 on Raspberry Pi +// LCD Module Input Module has a different pin mapping +// | FN | Pico | LCD Input Module | +// | SCL | GP14 | GP18 | +// | SDA | GP15 | GP19 | +// | RX | GP12 | GP16 | +// | DC/A0| GP13 | GP20 | +// | BL | GP18 | GP?? | +// | RSTB | GP16 | GP21 | +hal::bsp_pins!( + /// GPIO 0 is connected to the SLEEP# pin of the EC + Gpio0 { name: sleep }, + Gpio18 { + name: scl, + aliases: { + /// SPI Function alias for pin [crate::Pins::gpio14]. + FunctionSpi: Gp18Spi1Sck + } + }, + Gpio19 { + name: sda, + aliases: { + /// SPI Function alias for pin [crate::Pins::gpio15]. + FunctionSpi: Gp19Spi1Tx + } + }, + Gpio16 { + name: miso, + aliases: { + /// SPI Function alias for pin [crate::Pins::gpio12]. + FunctionSpi: Gp16Spi1Rx + } + }, + Gpio20 { name: dc }, + //Gpio18 { name: backlight }, + Gpio21 { name: rstb }, + Gpio17 { name: cs }, +); + +// External crystal frequency, same as Raspberry Pi Pico +pub const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; diff --git a/src/lotus_led_hal.rs b/fl16-inputmodules/src/led_hal.rs similarity index 94% rename from src/lotus_led_hal.rs rename to fl16-inputmodules/src/led_hal.rs index 5c3795e2..efe9e790 100644 --- a/src/lotus_led_hal.rs +++ b/fl16-inputmodules/src/led_hal.rs @@ -17,6 +17,8 @@ pub use hal::pac; hal::bsp_pins!( /// GPIO 0 is connected to the SLEEP# pin of the EC Gpio0 { name: sleep }, + /// GPIO 25 is connected to the DIP Switch #1 + Gpio25 { name: dip1 }, /// GPIO 26 is connected to I2C SDA of the LED controller Gpio26 { name: gpio26, diff --git a/fl16-inputmodules/src/lib.rs b/fl16-inputmodules/src/lib.rs new file mode 100644 index 00000000..8c445376 --- /dev/null +++ b/fl16-inputmodules/src/lib.rs @@ -0,0 +1,36 @@ +#![allow(clippy::needless_range_loop)] +#![no_std] + +#[cfg(any( + all(feature = "ledmatrix", feature = "b1display"), + all(feature = "ledmatrix", feature = "c1minimal"), + all(feature = "b1display", feature = "c1minimal"), +))] +compile_error!("Features \"ledmatrix\", \"b1display\", and \"c1minimal\" are mutually exclusive"); + +#[cfg(feature = "ledmatrix")] +pub mod fl16; +#[cfg(feature = "ledmatrix")] +pub mod games; +#[cfg(feature = "ledmatrix")] +pub mod led_hal; +#[cfg(feature = "ledmatrix")] +#[rustfmt::skip] +pub mod mapping; +#[cfg(feature = "ledmatrix")] +pub mod animations; +#[cfg(feature = "ledmatrix")] +pub mod matrix; +#[cfg(feature = "ledmatrix")] +pub mod patterns; + +#[cfg(feature = "b1display")] +pub mod graphics; +#[cfg(feature = "b1display")] +pub mod lcd_hal; + +#[cfg(all(feature = "c1minimal", not(feature = "qtpy")))] +pub mod minimal_hal; + +pub mod control; +pub mod serialnum; diff --git a/fl16-inputmodules/src/mapping.rs b/fl16-inputmodules/src/mapping.rs new file mode 100644 index 00000000..de17157b --- /dev/null +++ b/fl16-inputmodules/src/mapping.rs @@ -0,0 +1,340 @@ +// Taken from https://github.com/phip1611/max-7219-led-matrix-util/blob/main/src/mappings.rs + +/// We have 8 rows and 8 bits per row. +pub type SingleDisplayData = [u8; 8]; + +/// Capital letter A +pub const CAP_A: SingleDisplayData = [ + 0b00010000, + 0b00101000, + 0b00101000, + 0b01000100, + 0b01111100, + 0b01000100, + 0b01000100, + 0b01000100, +]; +/// Capital letter B +pub const CAP_B: SingleDisplayData = [ + 0b01111000, + 0b01000100, + 0b01000100, + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, +]; +/// Capital letter C +pub const CAP_C: SingleDisplayData = [ + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01111100, +]; +/// Capital letter D +pub const CAP_D: SingleDisplayData = [ + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, +]; +/// Capital letter E +pub const CAP_E: SingleDisplayData = [ + 0b01111100, + 0b01000000, + 0b01000000, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01111100, +]; +/// Capital letter F +pub const CAP_F: SingleDisplayData = [ + 0b01111100, + 0b01000000, + 0b01000000, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, +]; +/// Capital letter G +pub const CAP_G: SingleDisplayData = [ + 0b01111000, + 0b11000100, + 0b10000100, + 0b10000000, + 0b10011100, + 0b10000100, + 0b11000100, + 0b01111100, +]; +/// Capital letter H +pub const CAP_H: SingleDisplayData = [ + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, +]; +/// Capital letter I +pub const CAP_I: SingleDisplayData = [ + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, +]; +/// Capital letter J +pub const CAP_J: SingleDisplayData = [ + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b10010000, + 0b01100000, +]; +/// Capital letter K +pub const CAP_K: SingleDisplayData = [ + 0b01000100, + 0b01001000, + 0b01010000, + 0b01100000, + 0b01010000, + 0b01001000, + 0b01000100, + 0b01000010, +]; +/// Capital letter L +/// I shifted it one left +pub const CAP_L: SingleDisplayData = [ + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111000, +]; +/// Capital letter M +pub const CAP_M: SingleDisplayData = [ + 0b10000010, + 0b11000110, + 0b10101010, + 0b10111010, + 0b10010010, + 0b10000010, + 0b10000010, + 0b10000010, +]; +/// Capital letter N +pub const CAP_N: SingleDisplayData = [ + 0b01000100, + 0b01100100, + 0b01110100, + 0b01010100, + 0b01011100, + 0b01001100, + 0b01001100, + 0b01000100, +]; +/// Capital letter O +pub const CAP_O: SingleDisplayData = [ + 0b00011000, + 0b00100100, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b00100100, + 0b00011000, +]; +/// Capital letter P +pub const CAP_P: SingleDisplayData = [ + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, + 0b01000000, + 0b01000000, + 0b01000000, +]; +/// Capital letter Q +pub const CAP_Q: SingleDisplayData = [ + 0b00011000, + 0b00100100, + 0b01000010, + 0b01000010, + 0b01001010, + 0b01000110, + 0b00100110, + 0b00011001, +]; +/// Capital letter R +pub const CAP_R: SingleDisplayData = [ + 0b01111000, + 0b01000100, + 0b01000100, + 0b01111000, + 0b01100000, + 0b01010000, + 0b01001000, + 0b01000100, +]; +/// Capital letter S +/// I shifted it one to the right +pub const CAP_S: SingleDisplayData = [ + 0b00000111, + 0b00001000, + 0b00010000, + 0b00001100, + 0b00000010, + 0b00000001, + 0b00000001, + 0b00011110, +]; +/// Capital letter T +pub const CAP_T: SingleDisplayData = [ + 0b11111110, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, +]; +/// Capital letter U +pub const CAP_U: SingleDisplayData = [ + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b00111100, +]; +/// Capital letter V +pub const CAP_V: SingleDisplayData = [ + 0b10000001, + 0b10000001, + 0b10000001, + 0b10000001, + 0b10000010, + 0b01000100, + 0b00101000, + 0b00010000, +]; +/// Capital letter W +pub const CAP_W: SingleDisplayData = [ + 0b10000010, + 0b10010010, + 0b11010110, + 0b01010100, + 0b01111100, + 0b00110000, + 0b00010000, + 0b00000000, +]; +/// Capital letter X +pub const CAP_X: SingleDisplayData = [ + 0b00000000, + 0b10000010, + 0b01000100, + 0b00101000, + 0b00010000, + 0b00101000, + 0b01000100, + 0b10000010, +]; +/// Capital letter Y +pub const CAP_Y: SingleDisplayData = [ + 0b01000100, + 0b01000100, + 0b00101000, + 0b00101000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, +]; +/// Capital letter Z +pub const CAP_Z: SingleDisplayData = [ + 0b01111110, + 0b00000010, + 0b00000100, + 0b00001000, + 0b00010000, + 0b00100000, + 0b01000000, + 0b01111110, +]; +/// Number 0 +pub const ZERO: SingleDisplayData = [ + 0b00111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b00111000, +]; +/// Number 1 +pub const ONE: SingleDisplayData = [ + 0b00000100, + 0b00011100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, +]; +/// " " character +pub const SPACE: SingleDisplayData = [0; 8]; +/// "." character +pub const DOT: SingleDisplayData = [0, 0, 0, 0, 0, 0, 0, 0b00010000]; +/// "!" character +pub const EXCLAMATION_MARK: SingleDisplayData = [ + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00000000, + 0b00010000, +]; +pub const HASH: SingleDisplayData = [ + 0b00100100, + 0b00100111, + 0b00111100, + 0b11100100, + 0b00100111, + 0b00111100, + 0b11100100, + 0b00100100, +]; diff --git a/fl16-inputmodules/src/matrix.rs b/fl16-inputmodules/src/matrix.rs new file mode 100644 index 00000000..71a625fd --- /dev/null +++ b/fl16-inputmodules/src/matrix.rs @@ -0,0 +1,77 @@ +use crate::animations::*; +use crate::control::PwmFreqArg; +use crate::games::game_of_life::GameOfLifeState; +use crate::games::pong::PongState; +use crate::games::snake::SnakeState; + +pub const WIDTH: usize = 9; +pub const HEIGHT: usize = 34; +pub const LEDS: usize = WIDTH * HEIGHT; + +#[derive(Clone)] +pub struct Grid(pub [[u8; HEIGHT]; WIDTH]); +impl Default for Grid { + fn default() -> Self { + Grid([[0; HEIGHT]; WIDTH]) + } +} + +impl Grid { + pub fn rotate(&mut self, rotations: usize) { + for x in 0..WIDTH { + self.0[x].rotate_right(rotations); + } + } +} + +pub struct LedmatrixState { + /// Currently displayed grid + pub grid: Grid, + /// Temporary buffer for building a new grid + pub col_buffer: Grid, + /// Whether the grid is currently being animated + pub animate: bool, + /// LED brightness out of 255 + pub brightness: u8, + /// Current sleep state + pub sleeping: SleepState, + /// State of the current game, if any + pub game: Option, + /// Animation period in microseconds + pub animation_period: u64, + /// Current LED PWM frequency + pub pwm_freq: PwmFreqArg, + /// Whether debug mode is active + /// + /// In debug mode: + /// - Startup is instant, no animation + /// - Sleep/wake transition is instant, no animation/fading + /// - No automatic sleeping + pub debug_mode: bool, + pub upcoming_frames: Option, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +/// Whether asleep or not, if asleep contains data to restore previous LED grid +pub enum SleepState { + Awake, + Sleeping((Grid, u8)), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SleepReason { + Command, + SleepPin, + Timeout, + UsbSuspend, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +/// State that's used for each game +pub enum GameState { + Snake(SnakeState), + Pong(PongState), + GameOfLife(GameOfLifeState), +} diff --git a/fl16-inputmodules/src/minimal_hal.rs b/fl16-inputmodules/src/minimal_hal.rs new file mode 100644 index 00000000..bab6d363 --- /dev/null +++ b/fl16-inputmodules/src/minimal_hal.rs @@ -0,0 +1,578 @@ +// Taken from rp_pico hal and adjusted + +pub extern crate rp2040_hal as hal; + +extern crate cortex_m_rt; +pub use hal::entry; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +#[link_section = ".boot2"] +#[no_mangle] +#[used] +pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; + +pub use hal::pac; + +hal::bsp_pins!( + // Pins not connected: + // - Gpio5 + // - Gpio14 + // - Gpio15 + // - Gpio17 + // - Gpio21 + // - Gpio22 + // - Gpio23 + + /// GPIO 0 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 RX` | [crate::Gp0Spi0Rx] | + /// | `UART0 TX` | [crate::Gp0Uart0Tx] | + /// | `I2C0 SDA` | [crate::Gp0I2C0Sda] | + /// | `PWM0 A` | [crate::Gp0Pwm0A] | + /// | `PIO0` | [crate::Gp0Pio0] | + /// | `PIO1` | [crate::Gp0Pio1] | + Gpio0 { + name: tx, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio0]. + FunctionUart: Gp0Uart0Tx, + /// SPI Function alias for pin [crate::Pins::gpio0]. + FunctionSpi: Gp0Spi0Rx, + /// I2C Function alias for pin [crate::Pins::gpio0]. + FunctionI2C: Gp0I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio0]. + FunctionPwm: Gp0Pwm0A, + /// PIO0 Function alias for pin [crate::Pins::gpio0]. + FunctionPio0: Gp0Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio0]. + FunctionPio1: Gp0Pio1 + } + }, + + /// GPIO 1 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 CSn` | [crate::Gp1Spi0Csn] | + /// | `UART0 RX` | [crate::Gp1Uart0Rx] | + /// | `I2C0 SCL` | [crate::Gp1I2C0Scl] | + /// | `PWM0 B` | [crate::Gp1Pwm0B] | + /// | `PIO0` | [crate::Gp1Pio0] | + /// | `PIO1` | [crate::Gp1Pio1] | + Gpio1 { + name: rx, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio1]. + FunctionUart: Gp1Uart0Rx, + /// SPI Function alias for pin [crate::Pins::gpio1]. + FunctionSpi: Gp1Spi0Csn, + /// I2C Function alias for pin [crate::Pins::gpio1]. + FunctionI2C: Gp1I2C0Scl, + /// PWM Function alias for pin [crate::Pins::gpio1]. + FunctionPwm: Gp1Pwm0B, + /// PIO0 Function alias for pin [crate::Pins::gpio1]. + FunctionPio0: Gp1Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio1]. + FunctionPio1: Gp1Pio1 + } + }, + + /// GPIO 2 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 SCK` | [crate::Gp2Spi0Sck] | + /// | `UART0 CTS` | [crate::Gp2Uart0Cts] | + /// | `I2C1 SDA` | [crate::Gp2I2C1Sda] | + /// | `PWM1 A` | [crate::Gp2Pwm1A] | + /// | `PIO0` | [crate::Gp2Pio0] | + /// | `PIO1` | [crate::Gp2Pio1] | + Gpio2 { + name: sda, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio2]. + FunctionUart: Gp2Uart0Cts, + /// SPI Function alias for pin [crate::Pins::gpio2]. + FunctionSpi: Gp2Spi0Sck, + /// I2C Function alias for pin [crate::Pins::gpio2]. + FunctionI2C: Gp2I2C1Sda, + /// PWM Function alias for pin [crate::Pins::gpio2]. + FunctionPwm: Gp2Pwm1A, + /// PIO0 Function alias for pin [crate::Pins::gpio2]. + FunctionPio0: Gp2Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio2]. + FunctionPio1: Gp2Pio1 + } + }, + + /// GPIO 3 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 TX` | [crate::Gp3Spi0Tx] | + /// | `UART0 RTS` | [crate::Gp3Uart0Rts] | + /// | `I2C1 SCL` | [crate::Gp3I2C1Scl] | + /// | `PWM1 B` | [crate::Gp3Pwm1B] | + /// | `PIO0` | [crate::Gp3Pio0] | + /// | `PIO1` | [crate::Gp3Pio1] | + Gpio3 { + name: scl, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio3]. + FunctionUart: Gp3Uart0Rts, + /// SPI Function alias for pin [crate::Pins::gpio3]. + FunctionSpi: Gp3Spi0Tx, + /// I2C Function alias for pin [crate::Pins::gpio3]. + FunctionI2C: Gp3I2C1Scl, + /// PWM Function alias for pin [crate::Pins::gpio3]. + FunctionPwm: Gp3Pwm1B, + /// PIO0 Function alias for pin [crate::Pins::gpio3]. + FunctionPio0: Gp3Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio3]. + FunctionPio1: Gp3Pio1 + } + }, + + /// GPIO 4 is connected to the sleep pin. Low when host is asleep + Gpio4 { + name: sleep, + }, + + /// GPIO 6 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 SCK` | [crate::Gp6Spi0Sck] | + /// | `UART1 CTS` | [crate::Gp6Uart1Cts] | + /// | `I2C1 SDA` | [crate::Gp6I2C1Sda] | + /// | `PWM3 A` | [crate::Gp6Pwm3A] | + /// | `PIO0` | [crate::Gp6Pio0] | + /// | `PIO1` | [crate::Gp6Pio1] | + Gpio6 { + name: d4, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio6]. + FunctionUart: Gp6Uart1Cts, + /// SPI Function alias for pin [crate::Pins::gpio6]. + FunctionSpi: Gp6Spi0Sck, + /// I2C Function alias for pin [crate::Pins::gpio6]. + FunctionI2C: Gp6I2C1Sda, + /// PWM Function alias for pin [crate::Pins::gpio6]. + FunctionPwm: Gp6Pwm3A, + /// PIO0 Function alias for pin [crate::Pins::gpio6]. + FunctionPio0: Gp6Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio6]. + FunctionPio1: Gp6Pio1 + } + }, + + /// GPIO 7 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 TX` | [crate::Gp7Spi0Tx] | + /// | `UART1 RTS` | [crate::Gp7Uart1Rts] | + /// | `I2C1 SCL` | [crate::Gp7I2C1Scl] | + /// | `PWM3 B` | [crate::Gp7Pwm3B] | + /// | `PIO0` | [crate::Gp7Pio0] | + /// | `PIO1` | [crate::Gp7Pio1] | + Gpio7 { + name: d5, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio7]. + FunctionUart: Gp7Uart1Rts, + /// SPI Function alias for pin [crate::Pins::gpio7]. + FunctionSpi: Gp7Spi0Tx, + /// I2C Function alias for pin [crate::Pins::gpio7]. + FunctionI2C: Gp7I2C1Scl, + /// PWM Function alias for pin [crate::Pins::gpio7]. + FunctionPwm: Gp7Pwm3B, + /// PIO0 Function alias for pin [crate::Pins::gpio7]. + FunctionPio0: Gp7Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio7]. + FunctionPio1: Gp7Pio1 + } + }, + + /// GPIO 8 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 RX` | [crate::Gp8Spi1Rx] | + /// | `UART1 TX` | [crate::Gp8Uart1Tx] | + /// | `I2C0 SDA` | [crate::Gp8I2C0Sda] | + /// | `PWM4 A` | [crate::Gp8Pwm4A] | + /// | `PIO0` | [crate::Gp8Pio0] | + /// | `PIO1` | [crate::Gp8Pio1] | + Gpio8 { + name: d6, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio8]. + FunctionUart: Gp8Uart1Tx, + /// SPI Function alias for pin [crate::Pins::gpio8]. + FunctionSpi: Gp8Spi1Rx, + /// I2C Function alias for pin [crate::Pins::gpio8]. + FunctionI2C: Gp8I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio8]. + FunctionPwm: Gp8Pwm4A, + /// PIO0 Function alias for pin [crate::Pins::gpio8]. + FunctionPio0: Gp8Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio8]. + FunctionPio1: Gp8Pio1 + } + }, + + /// GPIO 9 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 CSn` | [crate::Gp9Spi1Csn] | + /// | `UART1 RX` | [crate::Gp9Uart1Rx] | + /// | `I2C0 SCL` | [crate::Gp9I2C0Scl] | + /// | `PWM4 B` | [crate::Gp9Pwm4B] | + /// | `PIO0` | [crate::Gp9Pio0] | + /// | `PIO1` | [crate::Gp9Pio1] | + Gpio9 { + name: d9, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio9]. + FunctionUart: Gp9Uart1Rx, + /// SPI Function alias for pin [crate::Pins::gpio9]. + FunctionSpi: Gp9Spi1Csn, + /// I2C Function alias for pin [crate::Pins::gpio9]. + FunctionI2C: Gp9I2C0Scl, + /// PWM Function alias for pin [crate::Pins::gpio9]. + FunctionPwm: Gp9Pwm4B, + /// PIO0 Function alias for pin [crate::Pins::gpio9]. + FunctionPio0: Gp9Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio9]. + FunctionPio1: Gp9Pio1 + } + }, + + /// GPIO 10 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 SCK` | [crate::Gp10Spi1Sck] | + /// | `UART1 CTS` | [crate::Gp10Uart1Cts] | + /// | `I2C1 SDA` | [crate::Gp10I2C1Sda] | + /// | `PWM5 A` | [crate::Gp10Pwm5A] | + /// | `PIO0` | [crate::Gp10Pio0] | + /// | `PIO1` | [crate::Gp10Pio1] | + Gpio10 { + name: d10, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio10]. + FunctionUart: Gp10Uart1Cts, + /// SPI Function alias for pin [crate::Pins::gpio10]. + FunctionSpi: Gp10Spi1Sck, + /// I2C Function alias for pin [crate::Pins::gpio10]. + FunctionI2C: Gp10I2C1Sda, + /// PWM Function alias for pin [crate::Pins::gpio10]. + FunctionPwm: Gp10Pwm5A, + /// PIO0 Function alias for pin [crate::Pins::gpio10]. + FunctionPio0: Gp10Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio10]. + FunctionPio1: Gp10Pio1 + } + }, + + /// GPIO 11 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 TX` | [crate::Gp11Spi1Tx] | + /// | `UART1 RTS` | [crate::Gp11Uart1Rts] | + /// | `I2C1 SCL` | [crate::Gp11I2C1Scl] | + /// | `PWM5 B` | [crate::Gp11Pwm5B] | + /// | `PIO0` | [crate::Gp11Pio0] | + /// | `PIO1` | [crate::Gp11Pio1] | + Gpio11 { + name: d11, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio11]. + FunctionUart: Gp11Uart1Rts, + /// SPI Function alias for pin [crate::Pins::gpio11]. + FunctionSpi: Gp11Spi1Tx, + /// I2C Function alias for pin [crate::Pins::gpio11]. + FunctionI2C: Gp11I2C1Scl, + /// PWM Function alias for pin [crate::Pins::gpio11]. + FunctionPwm: Gp11Pwm5B, + /// PIO0 Function alias for pin [crate::Pins::gpio11]. + FunctionPio0: Gp11Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio11]. + FunctionPio1: Gp11Pio1 + } + }, + + /// GPIO 12 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 RX` | [crate::Gp12Spi1Rx] | + /// | `UART0 TX` | [crate::Gp12Uart0Tx] | + /// | `I2C0 SDA` | [crate::Gp12I2C0Sda] | + /// | `PWM6 A` | [crate::Gp12Pwm6A] | + /// | `PIO0` | [crate::Gp12Pio0] | + /// | `PIO1` | [crate::Gp12Pio1] | + Gpio12 { + name: d12, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio12]. + FunctionUart: Gp12Uart0Tx, + /// SPI Function alias for pin [crate::Pins::gpio12]. + FunctionSpi: Gp12Spi1Rx, + /// I2C Function alias for pin [crate::Pins::gpio12]. + FunctionI2C: Gp12I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio12]. + FunctionPwm: Gp12Pwm6A, + /// PIO0 Function alias for pin [crate::Pins::gpio12]. + FunctionPio0: Gp12Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio12]. + FunctionPio1: Gp12Pio1 + } + }, + + /// GPIO 13 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 CSn` | [crate::Gp13Spi1Csn] | + /// | `UART0 RX` | [crate::Gp13Uart0Rx] | + /// | `I2C0 SCL` | [crate::Gp13I2C0Scl] | + /// | `PWM6 B` | [crate::Gp13Pwm6B] | + /// | `PIO0` | [crate::Gp13Pio0] | + /// | `PIO1` | [crate::Gp13Pio1] | + Gpio13 { + name: d13, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio13]. + FunctionUart: Gp13Uart0Rx, + /// SPI Function alias for pin [crate::Pins::gpio13]. + FunctionSpi: Gp13Spi1Csn, + /// I2C Function alias for pin [crate::Pins::gpio13]. + FunctionI2C: Gp13I2C0Scl, + /// PWM Function alias for pin [crate::Pins::gpio13]. + FunctionPwm: Gp13Pwm6B, + /// PIO0 Function alias for pin [crate::Pins::gpio13]. + FunctionPio0: Gp13Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio13]. + FunctionPio1: Gp13Pio1 + } + }, + + /// GPIO 16 is connected to RGB led + Gpio16 { + name: rgb_led, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio16]. + FunctionUart: Gp16Uart0Tx, + /// SPI Function alias for pin [crate::Pins::gpio16]. + FunctionSpi: Gp16Spi0Rx, + /// I2C Function alias for pin [crate::Pins::gpio16]. + FunctionI2C: Gp16I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio16]. + FunctionPwm: Gp16Pwm0A, + /// PIO0 Function alias for pin [crate::Pins::gpio16]. + FunctionPio0: Gp16Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio16]. + FunctionPio1: Gp16Pio1 + } + }, + + /// GPIO 18 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 SCK` | [crate::Gp18Spi0Sck] | + /// | `UART0 CTS` | [crate::Gp18Uart0Cts] | + /// | `I2C1 SDA` | [crate::Gp18I2C1Sda] | + /// | `PWM1 A` | [crate::Gp18Pwm1A] | + /// | `PIO0` | [crate::Gp18Pio0] | + /// | `PIO1` | [crate::Gp18Pio1] | + Gpio18 { + name: sck, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio18]. + FunctionUart: Gp18Uart0Cts, + /// SPI Function alias for pin [crate::Pins::gpio18]. + FunctionSpi: Gp18Spi0Sck, + /// I2C Function alias for pin [crate::Pins::gpio18]. + FunctionI2C: Gp18I2C1Sda, + /// PWM Function alias for pin [crate::Pins::gpio18]. + FunctionPwm: Gp18Pwm1A, + /// PIO0 Function alias for pin [crate::Pins::gpio18]. + FunctionPio0: Gp18Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio18]. + FunctionPio1: Gp18Pio1 + } + }, + + /// GPIO 19 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 TX` | [crate::Gp19Spi0Tx] | + /// | `UART0 RTS` | [crate::Gp19Uart0Rts] | + /// | `I2C1 SCL` | [crate::Gp19I2C1Scl] | + /// | `PWM1 B` | [crate::Gp19Pwm1B] | + /// | `PIO0` | [crate::Gp19Pio0] | + /// | `PIO1` | [crate::Gp19Pio1] | + Gpio19 { + name: mosi, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio19]. + FunctionUart: Gp19Uart0Rts, + /// SPI Function alias for pin [crate::Pins::gpio19]. + FunctionSpi: Gp19Spi0Tx, + /// I2C Function alias for pin [crate::Pins::gpio19]. + FunctionI2C: Gp19I2C1Scl, + /// PWM Function alias for pin [crate::Pins::gpio19]. + FunctionPwm: Gp19Pwm1B, + /// PIO0 Function alias for pin [crate::Pins::gpio19]. + FunctionPio0: Gp19Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio19]. + FunctionPio1: Gp19Pio1 + } + }, + + /// GPIO 20 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI0 RX` | [crate::Gp20Spi0Rx] | + /// | `UART1 TX` | [crate::Gp20Uart1Tx] | + /// | `I2C0 SDA` | [crate::Gp20I2C0Sda] | + /// | `PWM2 A` | [crate::Gp20Pwm2A] | + /// | `PIO0` | [crate::Gp20Pio0] | + /// | `PIO1` | [crate::Gp20Pio1] | + Gpio20 { + name: miso, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio20]. + FunctionUart: Gp20Uart1Tx, + /// SPI Function alias for pin [crate::Pins::gpio20]. + FunctionSpi: Gp20Spi0Rx, + /// I2C Function alias for pin [crate::Pins::gpio20]. + FunctionI2C: Gp20I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio20]. + FunctionPwm: Gp20Pwm2A, + /// PIO0 Function alias for pin [crate::Pins::gpio20]. + FunctionPio0: Gp20Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio20]. + FunctionPio1: Gp20Pio1 + } + }, + + /// GPIO 24 + Gpio24 { + name: d24, + // TODO: Add aliases + }, + + /// GPIO 25 + Gpio25 { + name: d25, + // TODO: Add aliases + }, + + /// GPIO 26 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 SCK` | [crate::Gp26Spi1Sck] | + /// | `UART1 CTS` | [crate::Gp26Uart1Cts] | + /// | `I2C1 SDA` | [crate::Gp26I2C1Sda] | + /// | `PWM5 A` | [crate::Gp26Pwm5A] | + /// | `PIO0` | [crate::Gp26Pio0] | + /// | `PIO1` | [crate::Gp26Pio1] | + Gpio26 { + name: a0, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio26]. + FunctionUart: Gp26Uart1Cts, + /// SPI Function alias for pin [crate::Pins::gpio26]. + FunctionSpi: Gp26Spi1Sck, + /// I2C Function alias for pin [crate::Pins::gpio26]. + FunctionI2C: Gp26I2C1Sda, + /// PWM Function alias for pin [crate::Pins::gpio26]. + FunctionPwm: Gp26Pwm5A, + /// PIO0 Function alias for pin [crate::Pins::gpio26]. + FunctionPio0: Gp26Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio26]. + FunctionPio1: Gp26Pio1 + } + }, + + /// GPIO 27 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 TX` | [crate::Gp27Spi1Tx] | + /// | `UART1 RTS` | [crate::Gp27Uart1Rts] | + /// | `I2C1 SCL` | [crate::Gp27I2C1Scl] | + /// | `PWM5 B` | [crate::Gp27Pwm5B] | + /// | `PIO0` | [crate::Gp27Pio0] | + /// | `PIO1` | [crate::Gp27Pio1] | + Gpio27 { + name: a1, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio27]. + FunctionUart: Gp27Uart1Rts, + /// SPI Function alias for pin [crate::Pins::gpio27]. + FunctionSpi: Gp27Spi1Tx, + /// I2C Function alias for pin [crate::Pins::gpio27]. + FunctionI2C: Gp27I2C1Scl, + /// PWM Function alias for pin [crate::Pins::gpio27]. + FunctionPwm: Gp27Pwm5B, + /// PIO0 Function alias for pin [crate::Pins::gpio27]. + FunctionPio0: Gp27Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio27]. + FunctionPio1: Gp27Pio1 + } + }, + + /// GPIO 28 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + /// | `SPI1 RX` | [crate::Gp28Spi1Rx] | + /// | `UART0 TX` | [crate::Gp28Uart0Tx] | + /// | `I2C0 SDA` | [crate::Gp28I2C0Sda] | + /// | `PWM6 A` | [crate::Gp28Pwm6A] | + /// | `PIO0` | [crate::Gp28Pio0] | + /// | `PIO1` | [crate::Gp28Pio1] | + Gpio28 { + name: a2, + aliases: { + /// UART Function alias for pin [crate::Pins::gpio28]. + FunctionUart: Gp28Uart0Tx, + /// SPI Function alias for pin [crate::Pins::gpio28]. + FunctionSpi: Gp28Spi1Rx, + /// I2C Function alias for pin [crate::Pins::gpio28]. + FunctionI2C: Gp28I2C0Sda, + /// PWM Function alias for pin [crate::Pins::gpio28]. + FunctionPwm: Gp28Pwm6A, + /// PIO0 Function alias for pin [crate::Pins::gpio28]. + FunctionPio0: Gp28Pio0, + /// PIO1 Function alias for pin [crate::Pins::gpio28]. + FunctionPio1: Gp28Pio1 + } + }, + + /// GPIO 29 supports following functions: + /// + /// | Function | Alias with applied function | + /// |--------------|-----------------------------| + Gpio29 { + name: a3, + // TODO: Add aliases + }, +); + +// External crystal frequency, same as Raspberry Pi Pico +pub const XOSC_CRYSTAL_FREQ: u32 = 12_000_000; diff --git a/src/patterns.rs b/fl16-inputmodules/src/patterns.rs similarity index 83% rename from src/patterns.rs rename to fl16-inputmodules/src/patterns.rs index e65f3839..5ed6fc9b 100644 --- a/src/patterns.rs +++ b/fl16-inputmodules/src/patterns.rs @@ -3,18 +3,19 @@ use rp2040_hal::{ pac::I2C1, }; -use crate::lotus_led_hal as bsp; +use crate::led_hal as bsp; use crate::mapping::*; -use crate::{lotus::LotusLedMatrix, Grid}; - -pub const WIDTH: usize = 9; -pub const HEIGHT: usize = 34; +use crate::matrix::*; +use is31fl3741::devices::LedMatrix; /// Bytes needed to represent all LEDs with a single bit /// math.ceil(WIDTH * HEIGHT / 8) pub const DRAW_BYTES: usize = 39; -pub type Foo = LotusLedMatrix< +/// Maximum number of brightneses levels +pub const BRIGHTNESS_LEVELS: u8 = 255; + +pub type Foo = LedMatrix< bsp::hal::I2C< I2C1, ( @@ -45,9 +46,39 @@ pub fn draw(bytes: &[u8; DRAW_BYTES]) -> Grid { } pub fn draw_grey_col(grid: &mut Grid, col: u8, levels: &[u8; HEIGHT]) { - for y in 0..HEIGHT { - grid.0[8 - col as usize][y as usize] = levels[y]; - } + // TODO: I don't think I need the [..HEIGHT] slicing + grid.0[8 - col as usize][..HEIGHT].copy_from_slice(&levels[..HEIGHT]); +} + +pub fn display_sleep_reason(sleep_reason: SleepReason) -> Grid { + let mut grid = Grid::default(); + + match sleep_reason { + SleepReason::Command => { + display_letter(20, &mut grid, CAP_C); + display_letter(10, &mut grid, CAP_M); + display_letter(0, &mut grid, CAP_D); + } + SleepReason::SleepPin => { + display_letter(23, &mut grid, CAP_S); + display_letter(13, &mut grid, CAP_L); + display_letter(7, &mut grid, CAP_P); + display_letter(0, &mut grid, HASH); + } + SleepReason::Timeout => { + display_letter(24, &mut grid, CAP_T); + display_letter(16, &mut grid, CAP_I); + display_letter(8, &mut grid, CAP_M); + display_letter(0, &mut grid, CAP_E); + } + SleepReason::UsbSuspend => { + display_letter(17, &mut grid, CAP_U); + display_letter(10, &mut grid, CAP_S); + display_letter(0, &mut grid, CAP_B); + } + }; + + grid } pub fn display_sleep() -> Grid { @@ -246,6 +277,17 @@ pub fn percentage(percentage: u16) -> Grid { grid } +/// Same as percentage but exactly n rows +pub fn rows(n: usize) -> Grid { + let mut grid = Grid::default(); + for y in (HEIGHT - n)..HEIGHT { + for x in 0..WIDTH { + grid.0[x][y] = 0xFF; + } + } + grid +} + /// Double sided gradient, bright in the middle, dim top and bottom pub fn double_gradient() -> Grid { let gradient_drop = 1; // Brightness drop between rows @@ -273,14 +315,21 @@ pub fn _fill_grid(grid: &Grid, matrix: &mut Foo) { } } +pub fn set_brightness(state: &mut LedmatrixState, brightness: u8, matrix: &mut Foo) { + state.brightness = brightness; + fill_grid_pixels(state, matrix); +} + /// Just sends two I2C commands for the entire grid -pub fn fill_grid_pixels(grid: &Grid, matrix: &mut Foo) { - // B4 LEDs on the first page, 0xAB on the second page +pub fn fill_grid_pixels(state: &LedmatrixState, matrix: &mut Foo) { + // 0xB4 LEDs on the first page, 0xAB on the second page let mut brightnesses = [0x00; 0xB4 + 0xAB]; for y in 0..HEIGHT { for x in 0..WIDTH { let (register, page) = (matrix.device.calc_pixel)(x as u8, y as u8); - brightnesses[(page as usize) * 0xB4 + (register as usize)] = grid.0[x][y]; + brightnesses[(page as usize) * 0xB4 + (register as usize)] = + ((state.grid.0[x][y] as u64) * (state.brightness as u64) + / (BRIGHTNESS_LEVELS as u64)) as u8; } } matrix.device.fill_matrix(&brightnesses).unwrap(); @@ -321,3 +370,17 @@ pub fn zigzag() -> Grid { grid } + +pub fn every_nth_col(n: usize) -> Grid { + let mut grid = Grid::default(); + + for y in 0..HEIGHT { + for x in 0..WIDTH { + if x % n == 0 { + grid.0[x][y] = 0xFF; + } + } + } + + grid +} diff --git a/fl16-inputmodules/src/serialnum.rs b/fl16-inputmodules/src/serialnum.rs new file mode 100644 index 00000000..545054ac --- /dev/null +++ b/fl16-inputmodules/src/serialnum.rs @@ -0,0 +1,55 @@ +// Get serial number from last 4K block of the first 1M +const FLASH_OFFSET: usize = 0x10000000; +const LAST_4K_BLOCK: usize = 0xff000; +const SERIALNUM_LEN: usize = 18; + +#[repr(packed)] +pub struct SerialnumStructRaw { + sn_rev: u8, + serialnum: [u8; SERIALNUM_LEN], + crc32: [u8; 4], +} + +pub struct SerialnumStruct { + pub serialnum: &'static str, +} + +pub fn get_serialnum() -> Option { + // Flash is mapped into memory, just read it from there + let ptr: *const u8 = (FLASH_OFFSET + LAST_4K_BLOCK) as *const u8; + let sn_raw_ptr = ptr as *const SerialnumStructRaw; + let sn_raw = unsafe { sn_raw_ptr.as_ref()? }; + + // Only rev 1 supported + if sn_raw.sn_rev != 1 { + return None; + } + + let crc: crc::Crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + let mut digest = crc.digest(); + digest.update(&[sn_raw.sn_rev]); + digest.update(&sn_raw.serialnum); + let calc_checksum = digest.finalize(); + + let actual_checksum = u32::from_le_bytes(sn_raw.crc32); + // Checksum invalid, serial fall back to default serial number + if calc_checksum != actual_checksum { + return None; + } + + Some(SerialnumStruct { + serialnum: core::str::from_utf8(&sn_raw.serialnum).ok()?, + }) +} + +/// Get the firmware version in a format for USB Device Release +/// The value is in binary coded decimal with a format of 0xJJMN where JJ is the major version number, M is the minor version number and N is the sub minor version number. e.g. USB 2.0 is reported as 0x0200, USB 1.1 as 0x0110 and USB 1.0 as 0x0100. +pub fn device_release() -> u16 { + (env!("CARGO_PKG_VERSION_MAJOR").parse::().unwrap() << 8) + + (env!("CARGO_PKG_VERSION_MINOR").parse::().unwrap() << 4) + + env!("CARGO_PKG_VERSION_PATCH").parse::().unwrap() +} + +pub fn is_pre_release() -> bool { + !env!("CARGO_PKG_VERSION_PRE").is_empty() +} diff --git a/flash_layout.md b/flash_layout.md new file mode 100644 index 00000000..ec4e1c78 --- /dev/null +++ b/flash_layout.md @@ -0,0 +1,38 @@ +# Flash Layout + +The flash is 1MB large and consists of 256 4K blocks. +The last block is used to store the serial number. + +###### LED Matrix + +| Start | End | Size | Name | +|----------|----------|---------------|--------------------| +| 0x000000 | Dynamic | Roughly 40K | Firmware | +| TBD | 0x0FF000 | TBD | Persistent Storage | +| 0x0FF000 | 0x100000 | 0x1000 (4K) | Serial Number | + +###### QMK Keyboards + +| Start | End | Size | Name | +|----------|----------|---------------|--------------------| +| 0x000000 | Dynamic | Roughly 60K | Firmware | +| 0xef000 | 0x0FF000 | 0x10000 (16K) | Persistent Storage | +| 0x0FF000 | 0x100000 | 0x01000 (4K) | Serial Number | + +## Serial Number + +- 1 byte serial number revision (== 1) +- 18 bytes serial number +- 1 byte hardware revision +- 4 byte CRC checksum over serial number (CRC32B, same as Python's `zlib.crc32()`) + +Hardware Revisions: + +- B1 Display + - 1 First Prototype, very early prototype +- LED Matrix + - 1 First Prototype (ATC) + - 2 Second Prototype (BizLink) + - 3 Third Prototype, 27k Resistor +- Keyboard, Numpad, Macropad + - 1 First Prototype diff --git a/inputmodule-control/Cargo.toml b/inputmodule-control/Cargo.toml new file mode 100644 index 00000000..27be7699 --- /dev/null +++ b/inputmodule-control/Cargo.toml @@ -0,0 +1,25 @@ +[package] +edition = "2021" +name = "inputmodule-control" +version = "0.2.0" + +[dependencies] +clap = { version = "4.3", features = ["derive"] } +serialport = "4.2.1" + +# For ledmatrix +chrono = "0.4.26" +image = { version = "0.24.6", default-features = false, features = [ + "ico", + "gif", +] } +rand = "0.8.5" + +# For audio visualizations +# Depending on an experimental crate, therefore optional dependency +vis-core = { git = 'https://github.com/Rahix/visualizer2.git', rev = '1fe908012a9c156695921f3b6bb47178e1332b92', optional = true } +[features] +audio-visualizations = ["vis-core"] + +[build-dependencies] +static_vcruntime = "2.0" diff --git a/inputmodule-control/Makefile.toml b/inputmodule-control/Makefile.toml new file mode 100644 index 00000000..4cd6f690 --- /dev/null +++ b/inputmodule-control/Makefile.toml @@ -0,0 +1,18 @@ +extend = "../Makefile.toml" + +# Since it's a tool, build it for the platform we're running on +[env] +TARGET_TRIPLE = "${CARGO_MAKE_RUST_TARGET_TRIPLE}" + +# Seems clippy doesn't respect TARGET_TRIPLE +[tasks.clippy] +args = ["clippy", "--target", "${CARGO_MAKE_RUST_TARGET_TRIPLE}", "--", "-Dwarnings"] + +[tasks.run] +command = "cargo" +args = [ + "run", + "--target", + "${CARGO_MAKE_RUST_TARGET_TRIPLE}", + "${@}", +] diff --git a/inputmodule-control/build.rs b/inputmodule-control/build.rs new file mode 100644 index 00000000..20e1c8e9 --- /dev/null +++ b/inputmodule-control/build.rs @@ -0,0 +1,3 @@ +fn main() { + static_vcruntime::metabuild(); +} diff --git a/inputmodule-control/src/b1display.rs b/inputmodule-control/src/b1display.rs new file mode 100644 index 00000000..f800e396 --- /dev/null +++ b/inputmodule-control/src/b1display.rs @@ -0,0 +1,90 @@ +use clap::Parser; + +#[derive(Copy, Clone, Debug, PartialEq, clap::ValueEnum)] +pub enum B1Pattern { + White, + Black, + //Checkerboard, +} + +#[derive(Copy, Clone, Debug, PartialEq, clap::ValueEnum)] +pub enum Fps { + Quarter, + Half, + One, + Two, + Four, + Eight, + Sixteen, + ThirtyTwo, +} + +#[derive(Copy, Clone, Debug, PartialEq, clap::ValueEnum)] +pub enum PowerMode { + Low, + High, +} + +/// B1 Display +#[derive(Parser, Debug)] +#[command(arg_required_else_help = true)] +pub struct B1DisplaySubcommand { + /// Set sleep status or get, if no value provided + #[arg(long)] + pub sleeping: Option>, + + /// Jump to the bootloader + #[arg(long)] + pub bootloader: bool, + + /// Crash the firmware (TESTING ONLY!) + #[arg(long)] + pub panic: bool, + + /// Get the device version + #[arg(short, long)] + pub version: bool, + + /// Turn display on/off + // TODO: Allow getting current state + #[arg(long)] + pub display_on: Option>, + + /// Display a simple pattern + #[arg(long)] + #[clap(value_enum)] + pub pattern: Option, + + /// Invert screen on/off + #[arg(long)] + pub invert_screen: Option>, + + /// Screensaver on/off + #[arg(long)] + pub screen_saver: Option>, + + /// Set/get FPS + #[arg(long)] + #[clap(value_enum)] + pub fps: Option>, + + /// Set/get power mode + #[arg(long)] + pub power_mode: Option>, + + /// Set/get animation FPS + #[arg(long)] + pub animation_fps: Option>, + + /// Display a black&white image (300x400px) + #[arg(long)] + pub image: Option, + + /// Display an animated black&white GIF (300x400px) + #[arg(long)] + pub animated_gif: Option, + + /// Clear display RAM + #[arg(long)] + pub clear_ram: bool, +} diff --git a/inputmodule-control/src/c1minimal.rs b/inputmodule-control/src/c1minimal.rs new file mode 100644 index 00000000..a050bb37 --- /dev/null +++ b/inputmodule-control/src/c1minimal.rs @@ -0,0 +1,39 @@ +use clap::Parser; + +#[derive(Clone, Copy, Debug, PartialEq, clap::ValueEnum)] +pub enum Color { + White, + Black, + Red, + Green, + Blue, + Yellow, + Cyan, + Purple, +} + +#[derive(Parser, Debug)] +#[command(arg_required_else_help = true)] +pub struct C1MinimalSubcommand { + /// Set sleep status or get, if no value provided + #[arg(long)] + pub sleeping: Option>, + + /// Jump to the bootloader + #[arg(long)] + pub bootloader: bool, + + /// Crash the firmware (TESTING ONLY!) + #[arg(long)] + pub panic: bool, + + /// Get the device version + #[arg(short, long)] + pub version: bool, + + /// Set color + // TODO: Allow getting current state + #[arg(long)] + #[clap(value_enum)] + pub set_color: Option, +} diff --git a/inputmodule-control/src/font.rs b/inputmodule-control/src/font.rs new file mode 100644 index 00000000..49c9db8d --- /dev/null +++ b/inputmodule-control/src/font.rs @@ -0,0 +1,555 @@ +/// 5x6 symbol font. Leaves 2 pixels on each side empty +/// We can leave one row empty below and then the display fits 5 of these digits. +#[rustfmt::skip] +pub fn convert_symbol(symbol: &str) -> Vec { + match symbol { + "degC" => vec![ + 1, 1, 0, 0, 0, + 1, 1, 0, 0, 0, + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + "degF" => vec![ + 1, 1, 0, 0, 0, + 1, 1, 0, 0, 0, + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 0, + ], + "snow" => vec![ + 0, 0, 0, 0, 0, + 1, 0, 1, 0, 1, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 1, 0, 1, 0, 1, + ], + "sun" => vec![ + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + ], + "cloud" => vec![ + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + "rain" => vec![ + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, + 1, 0, 0, 1, 0, + ], + "thunder" => vec![ + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + ], + "batteryLow" => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 0, + 1, 0, 0, 1, 1, + 1, 0, 0, 1, 1, + 1, 1, 1, 1, 0, + ], + "!!" => vec![ + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + ], + "heart" => vec![ + 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + ], + "heart0" => vec![ + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + "heart2" => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 0, + ], + ":)" => vec![ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + ":|" => vec![ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + ], + ":(" => vec![ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + ], + ";)" => vec![ + 0, 0, 0, 0, 0, + 1, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + _ => { + if symbol.len() == 1 { + convert_font(symbol.chars().next().unwrap()) + } else { + convert_font('?') + } + }, + } +} + +/// 5x6 font. Leaves 2 pixels on each side empty +/// We can leave one row empty below and then the display fits 5 of these digits. +#[rustfmt::skip] +pub fn convert_font(c: char) -> Vec { + match c { + '0' => vec![ + 0, 1, 1, 0, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 0, 1, 1, 0, 0, + ], + + '1' => vec![ + 0, 0, 1, 0, 0, + 0, 1, 1, 0, 0, + 1, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + ], + + '2' => vec![ + 1, 1, 1, 1, 0, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + ], + + '3' => vec![ + 1, 1, 1, 1, 0, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0, + ], + + '4' => vec![ + 0, 0, 0, 1, 0, + 0, 0, 1, 1, 0, + 0, 1, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, + 0, 0, 0, 1, 0, + ], + + '5' => vec![ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0, + ], + + '6' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + + '7' => vec![ + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + ], + + '8' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + + '9' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + + ':' => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + ], + + ' ' => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + + '?' => vec![ + 0, 1, 1, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + ], + + '.' => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + + ',' => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + + '!' => vec![ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + ], + + '/' => vec![ + 0, 0, 0, 0, 1, + 0, 0, 0, 1, 1, + 0, 0, 1, 1, 0, + 0, 1, 1, 0, 0, + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + ], + + '*' => vec![ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + + '%' => vec![ + 1, 1, 0, 0, 1, + 1, 1, 0, 1, 1, + 0, 0, 1, 1, 0, + 0, 1, 1, 0, 0, + 1, 1, 0, 1, 1, + 1, 0, 0, 1, 1, + ], + + '+' => vec![ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, + ], + + '-' => vec![ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + '=' => vec![ + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + 'A' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + ], + 'B' => vec![ + 1, 1, 1, 0, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 0, 0, + ], + 'C' => vec![ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0, + ], + 'D' => vec![ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 0, + ], + 'E' => vec![ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + ], + 'F' => vec![ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + ], + 'G' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 0, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + 'H' => vec![ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + ], + 'I' => vec![ + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + ], + 'J' => vec![ + 0, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + 'K' => vec![ + 1, 0, 0, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 0, 0, 0, + 1, 1, 0, 0, 0, + 1, 0, 1, 0, 0, + 1, 0, 0, 1, 0, + ], + 'L' => vec![ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + ], + 'M' => vec![ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + ], + 'N' => vec![ + 1, 0, 0, 0, 1, + 1, 1, 0, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 0, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + ], + 'O' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + 'P' => vec![ + 1, 1, 1, 0, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + ], + 'Q' => vec![ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 0, 1, 0, + 0, 1, 1, 0, 1, + ], + 'R' => vec![ + 1, 1, 1, 1, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 1, 0, + 1, 1, 0, 0, 0, + 1, 0, 1, 0, 0, + 1, 0, 0, 1, 0, + ], + 'S' => vec![ + 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0 + ], + 'T' => vec![ + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + ], + 'U' => vec![ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0, + ], + 'V' => vec![ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + ], + 'W' => vec![ + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0 + ], + 'X' => vec![ + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 0, 0, 1, + ], + 'Y' => vec![ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + ], + 'Z' => vec![ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + ], + _ => convert_font('?'), + } +} diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs new file mode 100644 index 00000000..d2d4e83c --- /dev/null +++ b/inputmodule-control/src/inputmodule.rs @@ -0,0 +1,1184 @@ +use std::thread; +use std::time::Duration; + +use chrono::Local; +use image::codecs::gif::GifDecoder; +use image::{io::Reader as ImageReader, Luma}; +use image::{AnimationDecoder, DynamicImage, ImageBuffer}; +use rand::prelude::*; +use serialport::{SerialPort, SerialPortInfo, SerialPortType}; + +use crate::b1display::{B1Pattern, Fps, PowerMode}; +use crate::c1minimal::Color; +use crate::font::{convert_font, convert_symbol}; +use crate::ledmatrix::{Game, GameOfLifeStartParam, Pattern}; + +const FWK_MAGIC: &[u8] = &[0x32, 0xAC]; +pub const FRAMEWORK_VID: u16 = 0x32AC; +pub const LED_MATRIX_PID: u16 = 0x0020; +pub const B1_LCD_PID: u16 = 0x0021; + +type Brightness = u8; + +// TODO: Use a shared enum with the firmware code +#[derive(Clone, Copy)] +#[repr(u8)] +enum Command { + Brightness = 0x00, + Pattern = 0x01, + Bootloader = 0x02, + Sleeping = 0x03, + Animate = 0x04, + Panic = 0x05, + DisplayBwImage = 0x06, + SendCol = 0x07, + CommitCols = 0x08, + _B1Reserved = 0x09, + StartGame = 0x10, + GameControl = 0x11, + _GameStatus = 0x12, + SetColor = 0x13, + DisplayOn = 0x14, + InvertScreen = 0x15, + SetPixelColumn = 0x16, + FlushFramebuffer = 0x17, + ClearRam = 0x18, + ScreenSaver = 0x19, + Fps = 0x1A, + PowerMode = 0x1B, + AnimationPeriod = 0x1C, + PwmFreq = 0x1E, + DebugMode = 0x1F, + Version = 0x20, +} + +enum GameControlArg { + _Up = 0, + _Down = 1, + _Left = 2, + _Right = 3, + Exit = 4, + _SecondLeft = 5, + _SecondRight = 6, +} + +const WIDTH: usize = 9; +const HEIGHT: usize = 34; + +const SERIAL_TIMEOUT: Duration = Duration::from_millis(20); + +fn match_serialdevs( + ports: &[SerialPortInfo], + requested: &Option, + pid: Option, +) -> Vec { + if let Some(requested) = requested { + for p in ports { + if requested == &p.port_name { + return vec![p.port_name.clone()]; + } + } + vec![] + } else { + let mut compatible_devs = vec![]; + let pids = if let Some(pid) = pid { + vec![pid] + } else { + // By default accept any type + vec![LED_MATRIX_PID, B1_LCD_PID, 0x22, 0xFF] + }; + // 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()); + } + } + } + compatible_devs + } +} + +pub fn find_serialdevs(args: &crate::ClapCli, wait_for_device: bool) -> (Vec, bool) { + let mut serialdevs: Vec; + let mut waited = false; + loop { + let ports = serialport::available_ports().expect("No ports found!"); + if args.list || args.verbose { + for p in &ports { + match &p.port_type { + SerialPortType::UsbPort(usbinfo) => { + println!("{}", p.port_name); + println!(" VID {:#06X}", usbinfo.vid); + println!(" PID {:#06X}", usbinfo.pid); + if let Some(sn) = &usbinfo.serial_number { + println!(" SN {}", sn); + } + if let Some(product) = &usbinfo.product { + // TODO: Seems to replace the spaces with underscore, not sure why + println!(" Product {}", product); + } + } + _ => { + //println!("{}", p.port_name); + //println!(" Unknown (PCI Port)"); + } + } + } + } + serialdevs = match_serialdevs( + &ports, + &args.serial_dev, + args.command.as_ref().map(|x| x.to_pid()), + ); + if serialdevs.is_empty() { + if wait_for_device { + // Waited at least once, that means the device was not present + // when the program started + waited = true; + + // Try again after short wait + thread::sleep(Duration::from_millis(100)); + continue; + } else { + return (vec![], waited); + } + } else { + break; + } + } + (serialdevs, waited) +} + +/// Commands that interact with serial devices +pub fn serial_commands(args: &crate::ClapCli) { + let (serialdevs, waited): (Vec, bool) = find_serialdevs(args, args.wait_for_device); + if serialdevs.is_empty() { + 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"); + thread::sleep(Duration::from_millis(1000)); + return; + } + + match &args.command { + // TODO: Handle generic commands without code deduplication + Some(crate::Commands::LedMatrix(ledmatrix_args)) => { + for serialdev in &serialdevs { + if args.verbose { + println!("Selected serialdev: {:?}", serialdev); + } + + if ledmatrix_args.bootloader { + bootloader_cmd(serialdev); + } + if let Some(sleeping_arg) = ledmatrix_args.sleeping { + sleeping_cmd(serialdev, sleeping_arg); + } + if let Some(brightness_arg) = ledmatrix_args.brightness { + brightness_cmd(serialdev, brightness_arg); + } + if let Some(percentage) = ledmatrix_args.percentage { + assert!(percentage <= 100); + percentage_cmd(serialdev, percentage); + } + if let Some(animate_arg) = ledmatrix_args.animate { + animate_cmd(serialdev, animate_arg); + } + if let Some(pattern) = ledmatrix_args.pattern { + pattern_cmd(serialdev, pattern); + } + if ledmatrix_args.all_brightnesses { + all_brightnesses_cmd(serialdev); + } + if ledmatrix_args.panic { + simple_cmd(serialdev, Command::Panic, &[0x00]); + } + if let Some(image_path) = &ledmatrix_args.image_bw { + display_bw_image_cmd(serialdev, image_path); + } + + if let Some(image_path) = &ledmatrix_args.image_gray { + display_gray_image_cmd(serialdev, image_path); + } + + if let Some(values) = &ledmatrix_args.eq { + eq_cmd(serialdev, values); + } + + if let Some(s) = &ledmatrix_args.string { + show_string(serialdev, s); + } + + if let Some(symbols) = &ledmatrix_args.symbols { + show_symbols(serialdev, symbols); + } + + if let Some(game) = ledmatrix_args.start_game { + start_game_cmd(serialdev, game, ledmatrix_args.game_param); + } + + if let Some(fps) = ledmatrix_args.animation_fps { + animation_fps_cmd(serialdev, fps); + } + + if let Some(freq) = ledmatrix_args.pwm_freq { + pwm_freq_cmd(serialdev, freq); + } + if let Some(debug_mode) = ledmatrix_args.debug_mode { + debug_mode_cmd(serialdev, debug_mode); + } + + if ledmatrix_args.stop_game { + simple_cmd( + serialdev, + Command::GameControl, + &[GameControlArg::Exit as u8], + ); + } + if ledmatrix_args.version { + get_device_version(serialdev); + } + } + // Commands that block and need manual looping + if ledmatrix_args.blinking { + blinking_cmd(&serialdevs); + } + if ledmatrix_args.breathing { + breathing_cmd(&serialdevs); + } + + if ledmatrix_args.random_eq { + random_eq_cmd(&serialdevs); + } + + #[cfg(feature = "audio-visualizations")] + if ledmatrix_args.input_eq { + input_eq_cmd(&serialdevs); + } + + if ledmatrix_args.clock { + clock_cmd(&serialdevs); + } + } + Some(crate::Commands::B1Display(b1display_args)) => { + for serialdev in &serialdevs { + if args.verbose { + println!("Selected serialdev: {:?}", serialdev); + } + + if b1display_args.bootloader { + bootloader_cmd(serialdev); + } + if let Some(sleeping_arg) = b1display_args.sleeping { + sleeping_cmd(serialdev, sleeping_arg); + } + if b1display_args.panic { + simple_cmd(serialdev, Command::Panic, &[0x00]); + } + if b1display_args.version { + get_device_version(serialdev); + } + if let Some(display_on) = b1display_args.display_on { + display_on_cmd(serialdev, display_on); + } + if let Some(invert_screen) = b1display_args.invert_screen { + invert_screen_cmd(serialdev, invert_screen); + } + if let Some(screensaver_on) = b1display_args.screen_saver { + screensaver_cmd(serialdev, screensaver_on); + } + if let Some(fps) = b1display_args.fps { + fps_cmd(serialdev, fps); + } + if let Some(power_mode) = b1display_args.power_mode { + power_mode_cmd(serialdev, power_mode); + } + if let Some(fps) = b1display_args.animation_fps { + animation_fps_cmd(serialdev, fps); + } + if let Some(image_path) = &b1display_args.image { + b1display_bw_image_cmd(serialdev, image_path); + } + if let Some(image_path) = &b1display_args.animated_gif { + gif_cmd(serialdev, image_path); + } + if b1display_args.clear_ram { + simple_cmd(serialdev, Command::ClearRam, &[0x00]); + } + if let Some(pattern) = b1display_args.pattern { + b1_display_pattern(serialdev, pattern); + } + } + } + Some(crate::Commands::C1Minimal(c1minimal_args)) => { + for serialdev in &serialdevs { + if args.verbose { + println!("Selected serialdev: {:?}", serialdev); + } + + if c1minimal_args.bootloader { + bootloader_cmd(serialdev); + } + if let Some(sleeping_arg) = c1minimal_args.sleeping { + sleeping_cmd(serialdev, sleeping_arg); + } + if c1minimal_args.panic { + simple_cmd(serialdev, Command::Panic, &[0x00]); + } + if c1minimal_args.version { + get_device_version(serialdev); + } + if let Some(color) = c1minimal_args.set_color { + set_color_cmd(serialdev, color); + } + } + } + _ => {} + } +} + +fn get_device_version(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::Version, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let major = response[0]; + let minor = (response[1] & 0xF0) >> 4; + let patch = response[1] & 0x0F; + let pre_release = response[2] == 1; + print!("Device Version: {major}.{minor}.{patch}"); + if pre_release { + print!(" (Pre-Release)"); + } + println!(); +} + +fn bootloader_cmd(serialdev: &str) { + simple_cmd(serialdev, Command::Bootloader, &[0x00]); +} + +fn percentage_cmd(serialdev: &str, arg: u8) { + simple_cmd( + serialdev, + Command::Pattern, + &[Pattern::Percentage as u8, arg], + ); +} + +fn pattern_cmd(serialdev: &str, arg: Pattern) { + simple_cmd(serialdev, Command::Pattern, &[arg as u8]); +} + +fn start_game_cmd(serialdev: &str, game: Game, param: Option) { + match (game, param) { + (Game::GameOfLife, Some(param)) => { + simple_cmd(serialdev, Command::StartGame, &[game as u8, param as u8]) + } + (Game::GameOfLife, None) => { + println!("To start Game of Life, provide a --game-param"); + } + (_, _) => simple_cmd(serialdev, Command::StartGame, &[game as u8]), + } +} + +fn simple_cmd_multiple(serialdevs: &Vec, command: Command, args: &[u8]) { + for serialdev in serialdevs { + simple_cmd(serialdev, command, args); + } +} + +fn simple_cmd(serialdev: &str, command: Command, args: &[u8]) { + let port_result = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open(); + + match port_result { + Ok(mut port) => simple_cmd_port(&mut port, command, args), + Err(error) => match error.kind { + serialport::ErrorKind::Io(std::io::ErrorKind::PermissionDenied) => panic!("Permission denied, couldn't access inputmodule serialport. Ensure that you have permission, for example using a udev rule or sudo."), + other_error => panic!("Couldn't open port: {:?}", other_error) + } + }; +} + +fn open_serialport(serialdev: &str) -> Box { + serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port") +} + +fn simple_open_cmd(serialport: &mut Box, command: Command, args: &[u8]) { + simple_cmd_port(serialport, command, args); +} + +fn simple_cmd_port(port: &mut Box, command: Command, args: &[u8]) { + let mut buffer: [u8; 64] = [0; 64]; + buffer[..2].copy_from_slice(FWK_MAGIC); + buffer[2] = command as u8; + buffer[3..3 + args.len()].copy_from_slice(args); + port.write_all(&buffer[..3 + args.len()]) + .expect("Write failed!"); +} + +fn sleeping_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(goto_sleep) = arg { + simple_cmd_port(&mut port, Command::Sleeping, &[u8::from(goto_sleep)]); + } else { + simple_cmd_port(&mut port, Command::Sleeping, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let sleeping: bool = response[0] == 1; + println!("Currently sleeping: {sleeping}"); + } +} + +fn debug_mode_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(enable_debug) = arg { + simple_cmd_port(&mut port, Command::DebugMode, &[u8::from(enable_debug)]); + } else { + simple_cmd_port(&mut port, Command::DebugMode, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let debug_mode: bool = response[0] == 1; + println!("Debug Mode enabled: {debug_mode}"); + } +} + +fn brightness_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(brightness) = arg { + simple_cmd_port(&mut port, Command::Brightness, &[brightness]); + } else { + simple_cmd_port(&mut port, Command::Brightness, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let brightness: u8 = response[0]; + println!("Current brightness: {brightness}"); + } +} + +fn animate_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(animate) = arg { + simple_cmd_port(&mut port, Command::Animate, &[animate as u8]); + } else { + simple_cmd_port(&mut port, Command::Animate, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let animating = response[0] == 1; + println!("Currently animating: {animating}"); + } +} + +/// Stage greyscale values for a single column. Must be committed with commit_cols() +fn send_col(port: &mut Box, x: u8, vals: &[u8]) { + let mut buffer: [u8; 64] = [0; 64]; + buffer[0] = x; + buffer[1..vals.len() + 1].copy_from_slice(vals); + simple_cmd_port(port, Command::SendCol, &buffer[0..vals.len() + 1]); +} + +/// Commit the changes from sending individual cols with send_col(), displaying the matrix. +/// This makes sure that the matrix isn't partially updated. +fn commit_cols(port: &mut Box) { + simple_cmd_port(port, Command::CommitCols, &[]); +} + +///Increase the brightness with each pixel. +///Only 0-255 available, so it can't fill all 306 LEDs +fn all_brightnesses_cmd(serialdev: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + for x in 0..WIDTH { + let mut vals: [u8; HEIGHT] = [0; HEIGHT]; + + for y in 0..HEIGHT { + let brightness = x + WIDTH * y; + vals[y] = if brightness > 255 { 0 } else { brightness } as u8; + } + + send_col(&mut port, x as u8, &vals); + } + commit_cols(&mut port); +} + +fn blinking_cmd(serialdevs: &Vec) { + let duration = Duration::from_millis(500); + loop { + simple_cmd_multiple(serialdevs, Command::Brightness, &[0]); + thread::sleep(duration); + simple_cmd_multiple(serialdevs, Command::Brightness, &[200]); + thread::sleep(duration); + } +} + +fn breathing_cmd(serialdevs: &Vec) { + loop { + // Go quickly from 250 to 50 + for i in 0..40 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[250 - i * 5]); + thread::sleep(Duration::from_millis(25)); + } + + // Go slowly from 50 to 0 + for i in 0..50 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[50 - i]); + thread::sleep(Duration::from_millis(10)); + } + + // Go slowly from 0 to 50 + for i in 0..50 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[i]); + thread::sleep(Duration::from_millis(10)); + } + + // Go quickly from 50 to 250 + for i in 0..40 { + simple_cmd_multiple(serialdevs, Command::Brightness, &[50 + i * 5]); + thread::sleep(Duration::from_millis(25)); + } + } +} + +/// Display an image in black and white +/// Confirmed working with PNG and GIF. +/// Must be 9x34 in size. +/// Sends everything in a single command +fn display_bw_image_cmd(serialdev: &str, image_path: &str) { + let mut vals: [u8; 39] = [0; 39]; + + let img = ImageReader::open(image_path) + .unwrap() + .decode() + .unwrap() + .to_luma8(); + let width = img.width(); + let height = img.height(); + assert!(width == 9); + assert!(height == 34); + for (x, y, pixel) in img.enumerate_pixels() { + let brightness = pixel.0[0]; + if brightness > 0xFF / 2 { + let i = (x as usize) + (y as usize) * WIDTH; + vals[i / 8] |= 1 << (i % 8); + } + } + + simple_cmd(serialdev, Command::DisplayBwImage, &vals); +} + +// Calculate pixel brightness from an RGB triple +fn pixel_to_brightness(pixel: &Luma) -> u8 { + let brightness = pixel.0[0]; + // Poor man's scaling to make the greyscale pop better. + // Should find a good function. + if brightness > 200 { + brightness + } else if brightness > 150 { + ((brightness as u32) * 10 / 8) as u8 + } else if brightness > 100 { + brightness / 2 + } else if brightness > 50 { + brightness + } else { + brightness * 2 + } +} + +/// Display an image in greyscale +/// Sends each 1x34 column and then commits => 10 commands +fn display_gray_image_cmd(serialdev: &str, image_path: &str) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + let img = ImageReader::open(image_path) + .unwrap() + .decode() + .unwrap() + .to_luma8(); + let width = img.width(); + let height = img.height(); + assert!(width == 9); + assert!(height == 34); + for x in 0..WIDTH { + let mut vals: [u8; HEIGHT] = [0; HEIGHT]; + + for y in 0..HEIGHT { + let pixel = img.get_pixel(x as u32, y as u32); + vals[y] = pixel_to_brightness(pixel); + } + + send_col(&mut port, x as u8, &vals) + } + commit_cols(&mut port); +} + +/// Display an equlizer looking animation with random values. +fn random_eq_cmd(serialdevs: &Vec) { + loop { + // Lower values more likely, makes it look nicer + //weights = [i*i for i in range(33, 0, -1)] + let population: Vec = (1..34).collect(); + let mut rng = thread_rng(); + let vals = population + .choose_multiple_weighted(&mut rng, 9, |item| (34 - item) ^ 2) + .unwrap() + .copied() + .collect::>(); + for serialdev in serialdevs { + eq_cmd(serialdev, vals.as_slice()); + } + thread::sleep(Duration::from_millis(200)); + } +} + +#[cfg(feature = "audio-visualizations")] +/// The data-type for storing analyzer results +#[derive(Debug, Clone)] +pub struct AnalyzerResult { + spectrum: vis_core::analyzer::Spectrum>, + volume: f32, + beat: f32, +} + +#[cfg(feature = "audio-visualizations")] +// Equalizer-like animation that expands as volume goes up and retracts as it goes down +fn input_eq_cmd(serialdevs: &Vec) { + // Example from https://github.com/Rahix/visualizer2/blob/canon/README.md + + // Initialize the logger. Take a look at the sources if you want to customize + // the logger. + vis_core::default_log(); + + // Load the default config source. More about config later on. You can also + // do this manually if you have special requirements. + vis_core::default_config(); + + // Initialize some analyzer-tools. These will be moved into the analyzer closure + // later on. + let mut analyzer = vis_core::analyzer::FourierBuilder::new() + .length(512) + .window(vis_core::analyzer::window::nuttall) + .plan(); + + let spectrum = vis_core::analyzer::Spectrum::new(vec![0.0; analyzer.buckets()], 0.0, 1.0); + + let mut frames = vis_core::Visualizer::new( + AnalyzerResult { + spectrum, + volume: 0.0, + beat: 0.0, + }, + // This closure is the "analyzer". It will be executed in a loop to always + // have the latest data available. + move |info, samples| { + analyzer.analyze(samples); + + info.spectrum.fill_from(&analyzer.average()); + info.volume = samples.volume(0.3) * 400.0; + info.beat = info.spectrum.slice(50.0, 100.0).max() * 0.01; + info + }, + ) + // Build the frame iterator which is the base of your loop later on + .frames(); + + for frame in frames.iter() { + // This is just a primitive example, your vis core belongs here + + frame.info(|info| { + let sampled_volume = info.volume; + let limited_volume = sampled_volume.min(34.0); + + let display_max_widths = [10.0, 14.0, 20.0, 28.0, 34.0, 28.0, 20.0, 14.0, 10.0]; + + let volumes_to_display = display_max_widths + .iter() + .map(|x| { + let computed_width = (limited_volume / 34.0) * x; + let next_lowest_odd = computed_width - (computed_width % 2.0) - 1.0; + next_lowest_odd as u8 + }) + .collect::>(); + + for serialdev in serialdevs { + eq_cmd(serialdev, volumes_to_display.as_slice()) + } + }); + thread::sleep(Duration::from_millis(30)); + } +} + +/// Display 9 values in equalizer diagram starting from the middle, going up and down +/// TODO: Implement a commandline parameter for this +fn eq_cmd(serialdev: &str, vals: &[u8]) { + assert!(vals.len() <= WIDTH); + let mut matrix: [[Brightness; 34]; 9] = [[0; 34]; 9]; + + for (col, val) in vals[..9].iter().enumerate() { + let row: usize = 34 / 2; + let above: usize = (*val as usize) / 2; + let below = (*val as usize) - above; + + for i in 0..above { + matrix[col][row + i] = 0xFF; // Set this LED to full brightness + } + for i in 0..below { + matrix[col][row - 1 - i] = 0xFF; // Set this LED to full brightness + } + } + + render_matrix(serialdev, &matrix); +} + +/// Show a black/white matrix +/// Send everything in a single command +fn render_matrix(serialdev: &str, matrix: &[[u8; 34]; 9]) { + // One bit for each LED, on or off + // 39 = ceil(34 * 9 / 8) + let mut vals: [u8; 39] = [0x00; 39]; + + for x in 0..9 { + for y in 0..34 { + let i = x + 9 * y; + if matrix[x][y] == 0xFF { + vals[i / 8] |= 1 << (i % 8); + } + } + } + + simple_cmd(serialdev, Command::DisplayBwImage, &vals); +} + +/// Render the current time and display. +/// Loops forever, updating every second +fn clock_cmd(serialdevs: &Vec) { + loop { + let date = Local::now(); + let current_time = date.format("%H:%M").to_string(); + println!("Current Time = {current_time}"); + + for serialdev in serialdevs { + show_string(serialdev, ¤t_time); + } + thread::sleep(Duration::from_millis(1000)); + } +} + +/// Render a string with up to five letters +fn show_string(serialdev: &str, s: &str) { + let items: Vec> = s.chars().take(5).map(convert_font).collect(); + show_font(serialdev, &items); +} + +/// Render up to five 5x6 pixel font items +fn show_font(serialdev: &str, font_items: &[Vec]) { + let mut vals: [u8; 39] = [0x00; 39]; + + for (digit_i, digit_pixels) in font_items.iter().enumerate() { + let offset = digit_i * 7; + for pixel_x in 0..5 { + for pixel_y in 0..6 { + let pixel_value = digit_pixels[pixel_x + pixel_y * 5]; + let i = (2 + pixel_x) + (9 * (pixel_y + offset)); + if pixel_value == 1 { + vals[i / 8] |= 1 << (i % 8); + } + } + } + } + + simple_cmd(serialdev, Command::DisplayBwImage, &vals); +} + +/// Render a list of up to five symbols +/// Can use letters/numbers or symbol names, like 'sun', ':)' +fn show_symbols(serialdev: &str, symbols: &Vec) { + println!("Symbols: {symbols:?}"); + let font_items: Vec> = symbols.iter().map(|x| convert_symbol(x)).collect(); + show_font(serialdev, &font_items); +} + +fn display_on_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(display_on) = arg { + simple_cmd_port(&mut port, Command::DisplayOn, &[display_on as u8]); + } else { + simple_cmd_port(&mut port, Command::DisplayOn, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let on = response[0] == 1; + println!("Currently on: {on}"); + } +} + +fn invert_screen_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(invert_on) = arg { + simple_cmd_port(&mut port, Command::InvertScreen, &[invert_on as u8]); + } else { + simple_cmd_port(&mut port, Command::InvertScreen, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let inverted = response[0] == 1; + println!("Currently inverted: {inverted}"); + } +} + +fn screensaver_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(display_on) = arg { + simple_cmd_port(&mut port, Command::ScreenSaver, &[display_on as u8]); + } else { + simple_cmd_port(&mut port, Command::ScreenSaver, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let on = response[0] == 1; + println!("Currently on: {on}"); + } +} + +fn fps_cmd(serialdev: &str, arg: Option) { + const HIGH_FPS_MASK: u8 = 0b00010000; + const LOW_FPS_MASK: u8 = 0b00000111; + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + simple_cmd_port(&mut port, Command::Fps, &[]); + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + let current_fps = response[0]; + + if let Some(fps) = arg { + let power_mode = match fps { + Fps::Sixteen | Fps::ThirtyTwo => PowerMode::High, + _ => PowerMode::Low, + }; + let fps_bits = match fps { + Fps::Quarter => current_fps & !LOW_FPS_MASK, + Fps::Half => (current_fps & !LOW_FPS_MASK) | 0b001, + Fps::One => (current_fps & !LOW_FPS_MASK) | 0b010, + Fps::Two => (current_fps & !LOW_FPS_MASK) | 0b011, + Fps::Four => (current_fps & !LOW_FPS_MASK) | 0b100, + Fps::Eight => (current_fps & !LOW_FPS_MASK) | 0b101, + Fps::Sixteen => current_fps & !HIGH_FPS_MASK, + Fps::ThirtyTwo => (current_fps & !HIGH_FPS_MASK) | 0b00010000, + }; + set_power_mode(&mut port, power_mode); + simple_cmd_port(&mut port, Command::Fps, &[fps_bits]); + } else { + simple_cmd_port(&mut port, Command::PowerMode, &[]); + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + let high = response[0] == 1; + + let fps = if high { + if current_fps & HIGH_FPS_MASK == 0 { + 16.0 + } else { + 32.0 + } + } else { + let current_fps = current_fps & LOW_FPS_MASK; + if current_fps == 0 { + 0.25 + } else if current_fps == 1 { + 0.5 + } else { + (1 << (current_fps - 2)) as f32 + } + }; + + println!("Current FPS: {fps}"); + } +} + +fn power_mode_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(mode) = arg { + set_power_mode(&mut port, mode); + } else { + simple_cmd_port(&mut port, Command::PowerMode, &[]); + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + let high = response[0] == 1; + + if high { + println!("Current Power Mode: High"); + } else { + println!("Current Power Mode: Low"); + } + } +} + +fn set_power_mode(port: &mut Box, mode: PowerMode) { + match mode { + PowerMode::Low => simple_cmd_port(port, Command::PowerMode, &[0]), + PowerMode::High => simple_cmd_port(port, Command::PowerMode, &[1]), + } +} + +fn animation_fps_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(fps) = arg { + 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, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let period = u16::from_le_bytes([response[0], response[1]]); + println!("Animation Frequency: {}ms / {}Hz", period, 1_000 / period); + } +} + +fn pwm_freq_cmd(serialdev: &str, arg: Option) { + let mut port = serialport::new(serialdev, 115_200) + .timeout(SERIAL_TIMEOUT) + .open() + .expect("Failed to open port"); + + if let Some(freq) = arg { + let hz = match freq { + 29000 => 0, + 3600 => 1, + 1800 => 2, + 900 => 3, + _ => panic!("Invalid frequency"), + }; + simple_cmd_port(&mut port, Command::PwmFreq, &[hz]); + } else { + simple_cmd_port(&mut port, Command::PwmFreq, &[]); + + let mut response: Vec = vec![0; 32]; + port.read_exact(response.as_mut_slice()) + .expect("Found no data!"); + + let hz = match response[0] { + 0 => 29000, + 1 => 3600, + 2 => 1800, + 3 => 900, + _ => panic!("Invalid frequency"), + }; + println!("Animation Frequency: {}Hz", hz); + } +} + +fn set_color_cmd(serialdev: &str, color: Color) { + let args = match color { + Color::White => &[0xFF, 0xFF, 0xFF], + Color::Black => &[0x00, 0x00, 0x00], + Color::Red => &[0xFF, 0x00, 0x00], + Color::Green => &[0x00, 0xFF, 0x00], + Color::Blue => &[0x00, 0x00, 0xFF], + Color::Yellow => &[0xFF, 0xFF, 0x00], + Color::Cyan => &[0x00, 0xFF, 0xFF], + Color::Purple => &[0xFF, 0x00, 0xFF], + }; + simple_cmd(serialdev, Command::SetColor, args); +} + +fn gif_cmd(serialdev: &str, image_path: &str) { + let mut serialport = open_serialport(serialdev); + + loop { + let img = std::fs::File::open(image_path).unwrap(); + let gif = GifDecoder::new(img).unwrap(); + let frames = gif.into_frames(); + for (_i, frame) in frames.enumerate() { + //println!("Frame {i}"); + let frame = frame.unwrap(); + //let delay = frame.delay(); + //println!(" Delay: {:?}", Duration::from(delay)); + let frame_img = frame.into_buffer(); + let frame_img = DynamicImage::from(frame_img); + let frame_img = frame_img.resize(300, 400, image::imageops::FilterType::Gaussian); + let frame_img = frame_img.into_luma8(); + display_img(&mut serialport, &frame_img); + // Not delaying any further. Current transmission delay is big enough + //thread::sleep(delay.into()); + } + } +} + +/// Display an image in black and white +/// Confirmed working with PNG and GIF. +/// Must be 300x400 in size. +/// Sends one 400px column in a single commands and a flush at the end +fn generic_img_cmd(serialdev: &str, image_path: &str) { + let mut serialport = open_serialport(serialdev); + let img = ImageReader::open(image_path) + .unwrap() + .decode() + .unwrap() + .to_luma8(); + display_img(&mut serialport, &img); +} + +fn b1display_bw_image_cmd(serialdev: &str, image_path: &str) { + generic_img_cmd(serialdev, image_path); +} + +fn display_img(serialport: &mut Box, img: &ImageBuffer, Vec>) { + let width = img.width(); + let height = img.height(); + assert!(width == 300); + assert!(height == 400); + + let (brightest, darkest) = img + .pixels() + .fold((0xFF, 0x00), |(brightest, darkest), pixel| { + let br = pixel.0[0]; + let brightest = if br > brightest { br } else { brightest }; + let darkest = if br < darkest { br } else { darkest }; + (brightest, darkest) + }); + let bright_diff = brightest - darkest; + // Anything brighter than 90% between darkest and brightest counts as white + // Just a heuristic. Don't use greyscale images! Use black and white instead + let threshold = darkest + (bright_diff / 10) * 9; + + for x in 0..300 { + let mut vals: [u8; 2 + 50] = [0; 2 + 50]; + let column = (x as u16).to_le_bytes(); + vals[0] = column[0]; + vals[1] = column[1]; + + let mut byte: u8 = 0; + for y in 0..400usize { + let pixel = img.get_pixel(x, y as u32); + let brightness = pixel.0[0]; + let black = brightness < threshold; + + let bit = y % 8; + if bit == 0 { + byte = 0; + } + if black { + byte |= 1 << bit; + } + if bit == 7 { + vals[2 + y / 8] = byte; + } + } + + simple_open_cmd(serialport, Command::SetPixelColumn, &vals); + } + + simple_open_cmd(serialport, Command::FlushFramebuffer, &[]); +} + +fn b1_display_color(serialdev: &str, black: bool) { + let mut serialport = open_serialport(serialdev); + for x in 0..300 { + let byte = if black { 0xFF } else { 0x00 }; + let mut vals: [u8; 2 + 50] = [byte; 2 + 50]; + let column = (x as u16).to_le_bytes(); + vals[0] = column[0]; + vals[1] = column[1]; + simple_open_cmd(&mut serialport, Command::SetPixelColumn, &vals); + } + simple_open_cmd(&mut serialport, Command::FlushFramebuffer, &[]); +} + +fn b1_display_pattern(serialdev: &str, pattern: B1Pattern) { + match pattern { + B1Pattern::Black => b1_display_color(serialdev, true), + B1Pattern::White => b1_display_color(serialdev, false), + } +} diff --git a/inputmodule-control/src/ledmatrix.rs b/inputmodule-control/src/ledmatrix.rs new file mode 100644 index 00000000..30ae5750 --- /dev/null +++ b/inputmodule-control/src/ledmatrix.rs @@ -0,0 +1,146 @@ +use clap::Parser; + +#[derive(Clone, Copy, Debug, PartialEq, clap::ValueEnum)] +#[repr(u8)] +pub enum Pattern { + Percentage = 0, + Gradient = 1, + DoubleGradient = 2, + LotusSideways = 3, + Zigzag = 4, + AllOn = 5, + Panic = 6, + LotusTopDown = 7, + //AllBrightnesses +} + +#[derive(Clone, Copy, Debug, PartialEq, clap::ValueEnum)] +#[repr(u8)] +pub enum Game { + Snake = 0, + Pong = 1, + Tetris = 2, + GameOfLife = 3, +} + +#[derive(Copy, Clone, Debug, PartialEq, clap::ValueEnum)] +pub enum GameOfLifeStartParam { + CurrentMatrix = 0x00, + Pattern1 = 0x01, + Blinker = 0x02, + Toad = 0x03, + Beacon = 0x04, + Glider = 0x05, + BeaconToadBlinker = 0x06, +} + +/// LED Matrix +#[derive(Parser, Debug)] +#[command(arg_required_else_help = true)] +pub struct LedMatrixSubcommand { + /// Set LED max brightness percentage or get, if no value provided + #[arg(long)] + pub brightness: Option>, + + /// Set sleep status or get, if no value provided + #[arg(long)] + pub sleeping: Option>, + + /// Jump to the bootloader + #[arg(long)] + pub bootloader: bool, + + /// Display a percentage (0-100) + #[arg(long)] + pub percentage: Option, + + /// Start/stop animation + #[arg(long)] + pub animate: Option>, + + /// Display a pattern + #[arg(long)] + #[clap(value_enum)] + pub pattern: Option, + + /// Show every brightness, one per pixel + #[arg(long)] + pub all_brightnesses: bool, + + /// Blink the current pattern once a second + #[arg(long)] + pub blinking: bool, + + /// Breathing brightness of the current pattern + #[arg(long)] + pub breathing: bool, + + /// Display black&white image (9x34px) + #[arg(long)] + pub image_bw: Option, + + /// Display grayscale image + #[arg(long)] + pub image_gray: Option, + + /// Random EQ + #[arg(long)] + pub random_eq: bool, + + /// Display EQ of microphone input + #[cfg(feature = "audio-visualizations")] + #[arg(long)] + pub input_eq: bool, + + /// EQ with custom values + #[arg(long, num_args(9))] + pub eq: Option>, + + /// Clock + #[arg(long)] + pub clock: bool, + + /// Display a string (max 5 chars) + #[arg(long)] + pub string: Option, + + /// Display a string (max 5 symbols) + #[arg(long, num_args(0..6))] + pub symbols: Option>, + + /// Start a game + #[arg(long)] + #[clap(value_enum)] + pub start_game: Option, + + /// Paramater for starting the game. Required for some games + #[arg(long)] + #[clap(value_enum)] + pub game_param: Option, + + /// Stop the currently running game + #[arg(long)] + #[clap(value_enum)] + pub stop_game: bool, + + /// Set/get animation FPS + #[arg(long)] + pub animation_fps: Option>, + + /// Set/get PWM Frequency in Hz + #[arg(long)] + #[clap(value_enum)] + pub pwm_freq: Option>, + + /// Set debug mode or get current mode, if no value provided + #[arg(long)] + pub debug_mode: Option>, + + /// Crash the firmware (TESTING ONLY!) + #[arg(long)] + pub panic: bool, + + /// Get the device version + #[arg(short, long)] + pub version: bool, +} diff --git a/inputmodule-control/src/main.rs b/inputmodule-control/src/main.rs new file mode 100644 index 00000000..8348ad74 --- /dev/null +++ b/inputmodule-control/src/main.rs @@ -0,0 +1,70 @@ +#![allow(clippy::needless_range_loop)] +#![allow(clippy::single_match)] +mod b1display; +mod c1minimal; +mod font; +mod inputmodule; +mod ledmatrix; + +use clap::{Parser, Subcommand}; +use inputmodule::find_serialdevs; + +use crate::b1display::B1DisplaySubcommand; +use crate::c1minimal::C1MinimalSubcommand; +use crate::inputmodule::{serial_commands, B1_LCD_PID, LED_MATRIX_PID}; +use crate::ledmatrix::LedMatrixSubcommand; + +#[derive(Subcommand, Debug)] +enum Commands { + LedMatrix(LedMatrixSubcommand), + B1Display(B1DisplaySubcommand), + C1Minimal(C1MinimalSubcommand), +} + +impl Commands { + pub fn to_pid(&self) -> u16 { + match self { + Self::LedMatrix(_) => LED_MATRIX_PID, + Self::B1Display(_) => B1_LCD_PID, + Self::C1Minimal(_) => 0x22, + } + } +} + +/// RAW HID and VIA commandline for QMK devices +#[derive(Parser, Debug)] +#[command(version, arg_required_else_help = true)] +pub struct ClapCli { + #[command(subcommand)] + command: Option, + + /// List connected HID devices + #[arg(short, long)] + list: bool, + + /// Verbose outputs to the console + #[arg(short, long)] + verbose: bool, + + /// Serial device, like /dev/ttyACM0 or COM0 + #[arg(long)] + pub serial_dev: Option, + + /// Retry connecting to the device until it works + #[arg(long)] + wait_for_device: bool, +} + +fn main() { + let args: Vec = std::env::args().collect(); + let args = ClapCli::parse_from(args); + + match args.command { + Some(_) => serial_commands(&args), + None => { + if args.list { + find_serialdevs(&args, false); + } + } + } +} diff --git a/lotus-led-matrix.py b/led-matrix.py similarity index 68% rename from lotus-led-matrix.py rename to led-matrix.py index a53c9337..39bb616c 100755 --- a/lotus-led-matrix.py +++ b/led-matrix.py @@ -7,6 +7,7 @@ # (0x1e, 0), // x:2, y:1, sw:2, cs:1, id:2 # [...] +import math from dataclasses import dataclass WIDTH = 9 @@ -36,16 +37,22 @@ def led_register(self): register = 0x5A + self.cs - 31 + (self.sw-1) * 9 return (register, page) + def __lt__(self, other): + if self.y == other.y: + return self.x < other.x + else: + return self.y < other.y + def get_leds(): leds = [] - # Generate LED mapping as how they are mapped in the Lotus LED Matrix Module + # Generate LED mapping as how they are mapped in the Framework Laptop 16 LED Matrix Module # First down and then right # CS1 through CS4 for cs in range(1, 5): - for sw in range(1, WIDTH+1): + for sw in range(1, WIDTH): leds.append(Led(id=WIDTH * (cs-1) + sw, x=sw, y=cs, sw=sw, cs=cs)) # First right and then down @@ -53,43 +60,70 @@ def get_leds(): base_cs = 4 base_id = WIDTH * base_cs for cs in range(1, 5): - for sw in range(1, WIDTH+1): + for sw in range(1, WIDTH): leds.append(Led(id=base_id + 4 * (sw-1) + cs, x=sw, y=cs+base_cs, sw=sw, cs=cs+base_cs)) + base_id+=5 # First right and then down # CS9 through CS16 base_cs = 8 base_id = WIDTH * base_cs for cs in range(1, 9): - for sw in range(1, WIDTH+1): + for sw in range(1, WIDTH): leds.append(Led(id=base_id + 8 * (sw-1) + cs, x=sw, y=cs+base_cs, sw=sw, cs=cs+base_cs)) + base_id+=9 # First right and then down # CS17 through CS32 base_cs = 16 base_id = WIDTH * base_cs for cs in range(1, 17): - for sw in range(1, WIDTH+1): + for sw in range(1, WIDTH): leds.append(Led(id=base_id + 16 * (sw-1) + cs, x=sw, y=cs+base_cs, sw=sw, cs=cs+base_cs)) + base_id+=17 # First down and then right # CS33 through CS34 base_cs = 32 base_id = WIDTH * base_cs for cs in range(1, 3): - for sw in range(1, WIDTH+1): + for sw in range(1, WIDTH): leds.append(Led(id=base_id + 9 * (cs-1) + sw, x=sw, y=cs+base_cs, sw=sw, cs=cs+base_cs)) + base_id+=3 + + # DVT2 Last column + five_cycle=[36, 37, 38, 39, 35] + four_cycle=[36, 37, 38, 35] + for y in range(1, HEIGHT+1): + ledid = WIDTH*y + if y >= 5: + ledid = 69 + y%5 + if y >= 9: + ledid = 137 + y%9 + if y >= 17: + ledid = 273 + y%17 + if y >= 33: + ledid = WIDTH*y + + if y <= 10: + leds.append(Led(id=ledid, x=WIDTH, y=y, sw=math.ceil(y/5), cs=five_cycle[(y-1)%5])) + else: + sw = 2 + math.ceil((y-10)/4) + cs = four_cycle[(y-10-1)%4] + leds.append(Led(id=ledid, x=WIDTH, y=y, sw=sw, cs=cs)) return leds def main(): - # Assumes that the index in the LEDs list is: index = x + y * 9 leds = get_leds() + # Needs to be sorted according to x and y + leds.sort() + debug = False @@ -98,7 +132,7 @@ def main(): if debug: print(led, "(0x{:02x}, {})".format(register, page)) else: - print("(0x{:02x}, {}), // x:{}, y:{}, sw:{}, cs:{}, id:{}".format( + print("(0x{:02x}, {}), // x:{:2d}, y:{:2d}, sw:{:2d}, cs:{:2d}, id:{:3d}".format( register, page, led.x, led.y, led.sw, led.cs, led.id)) # print_led(leds, 0, 30) diff --git a/ledmatrix/Cargo.toml b/ledmatrix/Cargo.toml new file mode 100644 index 00000000..563971c6 --- /dev/null +++ b/ledmatrix/Cargo.toml @@ -0,0 +1,36 @@ +[package] +edition = "2021" +name = "ledmatrix" +version = "0.2.0" + +[features] +10k = [] +evt = [] + +[dependencies] +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true + +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true + +# Not using an external BSP, we've got the Framework Laptop 16 BSPs locally in this crate +rp2040-hal.workspace = true +rp2040-boot2.workspace = true + +# USB Serial +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true + +is31fl3741.workspace = true + +[dependencies.fl16-inputmodules] +path = "../fl16-inputmodules" +features = ["ledmatrix"] diff --git a/ledmatrix/Makefile.toml b/ledmatrix/Makefile.toml new file mode 100644 index 00000000..49d34cd9 --- /dev/null +++ b/ledmatrix/Makefile.toml @@ -0,0 +1,57 @@ +extend = "../Makefile.toml" + +[tasks.build-release-evt] +command = "cargo" +args = [ + "build", + "--target=thumbv6m-none-eabi", + "--release", + "--features", + "evt", +] + +[tasks.build-release-evt-uf2] +command = "elf2uf2-rs" +args = [ + "../target/thumbv6m-none-eabi/release/ledmatrix", + "../target/thumbv6m-none-eabi/release/ledmatrix_evt.uf2", +] +dependencies = ["build-release-evt"] +install_crate = "elf2uf2-rs" + +[tasks.build-release-10k] +command = "cargo" +args = [ + "build", + "--target=thumbv6m-none-eabi", + "--release", + "--features", + "10k,evt", +] + +[tasks.build-release-10k-uf2] +command = "elf2uf2-rs" +args = [ + "../target/thumbv6m-none-eabi/release/ledmatrix", + "../target/thumbv6m-none-eabi/release/ledmatrix_10k.uf2", +] +dependencies = ["build-release-10k"] +install_crate = "elf2uf2-rs" + +[tasks.uf2] +command = "elf2uf2-rs" +args = [ + "../target/thumbv6m-none-eabi/release/ledmatrix", + "../target/thumbv6m-none-eabi/release/ledmatrix.uf2", +] +dependencies = ["build-release"] +install_crate = "elf2uf2-rs" + +[tasks.bin] +command = "llvm-objcopy" +args = [ + "-Obinary", + "../target/thumbv6m-none-eabi/release/ledmatrix", + "../target/thumbv6m-none-eabi/release/ledmatrix.bin", +] +dependencies = ["build-release"] diff --git a/ledmatrix/README.md b/ledmatrix/README.md new file mode 100644 index 00000000..d2f6cc25 --- /dev/null +++ b/ledmatrix/README.md @@ -0,0 +1,249 @@ +# LED Matrix + +It's a 9x34 (306) LED matrix, controlled by RP2040 MCU and IS31FL3741A LED controller. + +Connection to the host system is via USB 2.0 and currently there is a USB Serial API to control it without reflashing. + +- Commands + - Display various pre-programmed patterns + - Light up a percentage of the screen + - Change brightness + - Send a black/white image to the display + - Send a greyscale image to the display + - Scroll and loop the display content vertically + - A commandline script and graphical application to control it +- Sleep Mode + - Transition slowly turns off/on the LEDs + +## Controlling + +### Commandline + +``` +> inputmodule-control led-matrix +LED Matrix + +Usage: ipc led-matrix [OPTIONS] + +Options: + --brightness [] + Set LED max brightness percentage or get, if no value provided + --sleeping [] + Set sleep status or get, if no value provided [possible values: true, false] + --bootloader + Jump to the bootloader + --percentage + Display a percentage (0-100) + --animate [] + Start/stop animation [possible values: true, false] + --pattern + Display a pattern [possible values: percentage, gradient, double-gradient, lotus-sideways, zigzag, all-on, panic, lotus-top-down] + --all-brightnesses + Show every brightness, one per pixel + --blinking + Blink the current pattern once a second + --breathing + Breathing brightness of the current pattern + --image-bw + Display black&white image (9x34px) + --image-gray + Display grayscale image + --random-eq + Random EQ + --eq + EQ with custom values + --clock + Show the current time + --string + Display a string (max 5 chars) + --symbols [...] + Display a string (max 5 symbols) + --start-game + Start a game [possible values: snake, pong, tetris, game-of-life] + --game-param + Paramater for starting the game. Required for some games [possible values: current-matrix, pattern1, blinker, toad, beacon, glider] + --stop-game + Stop the currently running game + --animation-fps [] + Set/get animation FPS + --panic + Crash the firmware (TESTING ONLY!) + -v, --version + Get the device version + -h, --help + Print help +``` + +### Non-trivial Examples + +Most commandline arguments should be self-explanatory. +If not, please open an issue. +Those that require an argument or setup have examples here: + +###### Percentage + +Light up a percentage of the module. From bottom to top. +This could be used to show volume level, progress of something, or similar. + +```sh +inputmodule-control led-matrix --percentage 30 +``` + +###### Display an Image + +Display an image (tested with PNG and GIF). It must be 9x34 pixels in size. It +doesn't have to be black/white or grayscale. The program will calculate the +brightness of each pixel. But if the brightness doesn't vary enough, it won't +look good. +Two example images are included in the repository. + +```sh +# Convert image to black/white and display +inputmodule-control led-matrix --image-bw stripe.gif + +# Convert image to grayscale and display +inputmodule-control led-matrix --image-gray grayscale.gif +``` + +###### Random equalizer +To show off the equalizer use-case, this command generates a +random but authentic looking equalizer pattern until the command is terminated. + +Alternatively you can provide 9 EQ values yourself. A script might capture +audio input and feed it into this command. + +```sh +inputmodule-control led-matrix --random-eq +inputmodule-control led-matrix --eq 1 2 3 4 5 4 3 2 1 +``` + +###### Input equalizer + +This command generates an equalizer-like visualization of the current audio input (microphone). +It supports most platforms - for details, see [documentation of the cpal crate](https://github.com/RustAudio/cpal). + +You must compile the `inputmodule-control` binary with the `audio-visualization` feature on: +`cargo build --features audio-visualizations --target x86_64-unknown-linux-gnu -p inputmodule-control` + +Once compiled, you can use the `--input-eq` arg to try the visualizer: +```sh +inputmodule-control led-matrix --input-eq +``` + +###### Custom string + +Display a custom string of up to 5 characters. +Currently only uppercase A-Z, 0-9 and some punctuation is implemented. + +```sh +inputmodule-control led-matrix --string "LOTUS" +``` + +The symbols parameter is much more powerful, it can also show extra symbols. +The full list of symbols is defined [here](https://github.com/FrameworkComputer/led_matrix_fw/blob/main/inputmodule-control/src/font.rs). + +```sh +# Show 0 °C, a snow icon and a smiley +inputmodule-control led-matrix --symbols 0 degC ' ' snow ':)' +``` + +###### Games + +While the game commands are implemented, the controls don't take easy keyboard +input. +Instead try out the [Python script](../python.md): + +```sh +# Snake +./ledmatrix_control.py --snake + +# Pong (Seems broken at the moment) +./ledmatrix_control.py --pong-embedded +``` + +###### Game of Life + +[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) +needs a parameter to start. Choose either one of the preprogrammed starting patterns. +Or display whatever you like using the other commands and have the game start based on that. +Font patterns generally look pretty good and survive for a while or even stay alive forever. + +The game board wraps around the edges to make gliders possible that move continuously. + +```sh +# Start from the currently displayed pattern +inputmodule-control led-matrix --start-game game-of-life --game-param current-matrix + +# Show two gliders that move forever +inputmodule-control led-matrix --start-game game-of-life --game-param glider +``` + +If you want to display something else, either reset the module (unplugging) or +run the stop command. + +```sh +inputmodule-control led-matrix --stop-game +``` + +## Sleep Behavior + +Currently sleeping means all LEDs and the LED controller are turned off. +Transitions of sleep state slowly fade the LEDs on or off. + +Optionally the firmware can be configured, at build-time, to turn the LEDs +on/off immediately. Or display "SLEEP" instead of turning the LEDs off, which +is useful for debugging whether the device is sleeping or not powered. + + +###### Changing Sleep State + +What can change the sleep state + +- Hardware/OS triggers + - `SLEEP#` pin + - USB Suspend +- Software/Firmware Triggers + - Sleep/Wake or other command via USB Serial + - Idle timer + +Both of the hardware/OS triggers change the sleep state if they transition from one state to another. +For example, if USB suspends, the LED matrix turns off. If it resumes, the LEDs come back on. +Same for the `SLEEP#` pin. +If either of them indicates sleep, even if they didn'td change state, the module goes to sleep. +If they're active, they don't influence module state. That way sleep state can be controlled by commands and isn't overridden immediately. + +The sleep/wake command always changes the state. But it can't be received when USB is suspended. +Any other command will also wake up the device. + +The idle timer will send the device to sleep after a configured timeout (default 60 seconds). +The idle timer is reset once the device wakes up or once it receives a command. + +## DIP Switch + +LED Matrix hardware since DVT2 (September 2023) has a DIP switch with two +switches, let's call them DIP1 and DIP2. + +###### DIP2 (Bootloader) + +DIP2 is the bootloader switch. To enter bootloader mode follow these steps: + +1. Unplug module and flip the switch to ON +2. Plug module back in, it will appear as a flash drive with the name `RPI-RP2` +3. Copy the firmware `.uf2` file onto that drive, it will automatically flash and reappear as a flash drive +4. To exit bootloader mode, unplug the module to flip the switch back, and plug it back in +5. Now the new firmware should be running + +As a side effect of being in bootloader mode, the LEDs all stay off. + +###### DIP1 (General Purpose) + +DIP1 could serve many purposes. Currently it is configured to enable the debug mode. +When debug mode is enabled and the module goes to sleep, it will not turn the LEDs off to save power. +Instead it will display the reason why it went to sleep. This is useful for debugging module and host system behavior. +Debug mode will start up to a fully lit matrix and never goes to sleep based on a timeout. + +Sleep Reasons can be: + +- `SLEEP#` pin: `SLP#` +- USB Suspend: `USB` +- Command: `CMD` diff --git a/ledmatrix/src/main.rs b/ledmatrix/src/main.rs new file mode 100644 index 00000000..c4082c72 --- /dev/null +++ b/ledmatrix/src/main.rs @@ -0,0 +1,680 @@ +//! LED Matrix Module +#![no_std] +#![no_main] +#![allow(clippy::needless_range_loop)] + +use cortex_m::delay::Delay; +//use defmt::*; +use defmt_rtt as _; +use embedded_hal::digital::v2::{InputPin, OutputPin}; + +use rp2040_hal::{ + gpio::bank0::Gpio29, + rosc::{Enabled, RingOscillator}, +}; +//#[cfg(debug_assertions)] +//use panic_probe as _; +use rp2040_panic_usb_boot as _; + +#[derive(PartialEq, Eq)] +#[allow(dead_code)] +enum SleepMode { + /// Instantly go to sleep ant + Instant, + /// Fade brightness out and in slowly when sleeping/waking-up + Fading, + // Display "SLEEP" when sleeping, instead of turning LEDs off + Debug, +} + +/// Static configuration whether sleep shohld instantly turn all LEDs on/off or +/// slowly fade themm on/off +const SLEEP_MODE: SleepMode = SleepMode::Fading; + +const STARTUP_ANIMATION: bool = true; + +/// Go to sleep after 60s awake +const SLEEP_TIMEOUT: u64 = 60_000_000; + +/// List maximum current as 500mA in the USB descriptor +const MAX_CURRENT: usize = 500; + +/// Maximum brightness out of 255 +/// On HW Rev 1 from BizLink set to 94 to have just below 500mA current draw. +/// +/// BizLink HW Rev 2 has a larger current limiting resistor. +/// 100/255 results in 250mA current draw which is plenty bright. +/// 50/255 results in 160mA current draw which is plenty bright. +#[cfg(feature = "10k")] +const MAX_BRIGHTNESS: u8 = 94; +#[cfg(not(feature = "10k"))] +const MAX_BRIGHTNESS: u8 = 50; + +// TODO: Doesn't work yet, unless I panic right at the beginning of main +//#[cfg(not(debug_assertions))] +//use core::panic::PanicInfo; +//#[cfg(not(debug_assertions))] +//#[panic_handler] +//fn panic(_info: &PanicInfo) -> ! { +// let mut pac = pac::Peripherals::take().unwrap(); +// let core = pac::CorePeripherals::take().unwrap(); +// let mut watchdog = Watchdog::new(pac.WATCHDOG); +// let sio = Sio::new(pac.SIO); +// +// let clocks = init_clocks_and_plls( +// bsp::XOSC_CRYSTAL_FREQ, +// pac.XOSC, +// pac.CLOCKS, +// pac.PLL_SYS, +// pac.PLL_USB, +// &mut pac.RESETS, +// &mut watchdog, +// ) +// .ok() +// .unwrap(); +// +// let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); +// +// let pins = bsp::Pins::new( +// pac.IO_BANK0, +// pac.PADS_BANK0, +// sio.gpio_bank0, +// &mut pac.RESETS, +// ); +// +// let mut led_enable = pins.sdb.into_push_pull_output(); +// led_enable.set_high().unwrap(); +// +// let i2c = bsp::hal::I2C::i2c1( +// pac.I2C1, +// pins.gpio26.into_mode::(), +// pins.gpio27.into_mode::(), +// 1000.kHz(), +// &mut pac.RESETS, +// &clocks.peripheral_clock, +// ); +// +// let mut matrix = LedMatrix::configure(i2c); +// matrix +// .setup(&mut delay) +// .expect("failed to setup rgb controller"); +// +// set_brightness(state, 255, &mut matrix); +// let grid = display_panic(); +// fill_grid_pixels(state, &mut matrix); +// +// loop {} +//} + +// Provide an alias for our BSP so we can switch targets quickly. +// 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(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; + +use bsp::hal::{ + clocks::{init_clocks_and_plls, Clock}, + gpio, pac, + sio::Sio, + usb, + watchdog::Watchdog, + Timer, +}; +use fugit::RateExtU32; + +// USB Device support +use usb_device::{class_prelude::*, prelude::*}; + +// USB Communications Class Device support +use usbd_serial::{SerialPort, USB_CLASS_CDC}; + +// Used to demonstrate writing formatted strings +use core::fmt::Write; +use heapless::String; + +use fl16_inputmodules::control::*; +use fl16_inputmodules::games::{pong, snake}; +use fl16_inputmodules::matrix::*; +use fl16_inputmodules::patterns::*; +use fl16_inputmodules::serialnum::{device_release, get_serialnum}; + +// FRA - Framwork +// KDE - C1 LED Matrix +// BZ - BizLink +// 01 - SKU, Default Configuration +// 00000000 - Device Identifier +const DEFAULT_SERIAL: &str = "FRAKDEBZ0100000000"; + +#[entry] +fn main() -> ! { + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let sio = Sio::new(pac.SIO); + + let clocks = init_clocks_and_plls( + bsp::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + //rp2040_pac::rosc::RANDOMBIT::read(&self) + let rosc = rp2040_hal::rosc::RingOscillator::new(pac.ROSC); + let rosc = rosc.initialize(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = bsp::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Set up the USB driver + let usb_bus = UsbBusAllocator::new(usb::UsbBus::new( + pac.USBCTRL_REGS, + pac.USBCTRL_DPRAM, + clocks.usb_clock, + true, + &mut pac.RESETS, + )); + + // Set up the USB Communications Class Device driver + let mut serial = SerialPort::new(&usb_bus); + + let serialnum = if let Some(serialnum) = get_serialnum() { + serialnum.serialnum + } else { + DEFAULT_SERIAL + }; + + let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x32ac, 0x0020)) + .manufacturer("Framework Computer Inc") + .product("LED Matrix Input Module") + .serial_number(serialnum) + .max_power(MAX_CURRENT) + .device_release(device_release()) + .device_class(USB_CLASS_CDC) + .build(); + + // Enable LED controller + // SDB + let mut led_enable = pins.sdb.into_push_pull_output(); + led_enable.set_high().unwrap(); + // INTB. Currently ignoring + pins.intb.into_floating_input(); + + let i2c = bsp::hal::I2C::i2c1( + pac.I2C1, + pins.gpio26.into_mode::(), + pins.gpio27.into_mode::(), + 1000.kHz(), + &mut pac.RESETS, + &clocks.peripheral_clock, + ); + + let dip1 = pins.dip1.into_pull_up_input(); + + let mut state = LedmatrixState { + grid: percentage(0), + col_buffer: Grid::default(), + animate: false, + brightness: 51, // Default to 51/255 = 20% brightness + sleeping: SleepState::Awake, + game: None, + animation_period: 31_250, // 31,250 us = 32 FPS + pwm_freq: PwmFreqArg::P29k, + debug_mode: false, + upcoming_frames: None, + }; + state.debug_mode = dip1.is_low().unwrap(); + if show_startup_animation(&state) { + state.upcoming_frames = Some(match get_random_byte(&rosc) % 8 { + 0 => Animation::Percentage(StartupPercentageIterator::default()), + 1 => Animation::ZigZag(ZigZagIterator::default()), + 2 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Pattern1, 200)), + 3 => Animation::Gof(GameOfLifeIterator::new( + GameOfLifeStartParam::BeaconToadBlinker, + 128, + )), + 4 => Animation::Gof(GameOfLifeIterator::new(GameOfLifeStartParam::Glider, 128)), + 5 => Animation::Breathing(BreathingIterator::default()), + 6 => Animation::Pong(PongIterator::default()), + 7 => Animation::Snake(SnakeIterator::default()), + _ => unreachable!(), + }); + } else { + // If no startup animation, keep display always on + state.grid = percentage(100); + }; + + #[cfg(feature = "evt")] + let mut matrix = LedMatrix::new(i2c, EVT_CALC_PIXEL); + #[cfg(not(feature = "evt"))] + let mut matrix = LedMatrix::new(i2c, CALC_PIXEL); + matrix + .setup(&mut delay) + .expect("failed to setup RGB controller"); + + // EVT + #[cfg(feature = "evt")] + matrix + .device + .sw_enablement(is31fl3741::SwSetting::Sw1Sw9) + .unwrap(); + // DVT + #[cfg(not(feature = "evt"))] + matrix + .device + .sw_enablement(is31fl3741::SwSetting::Sw1Sw8) + .unwrap(); + + matrix + .set_scaling(MAX_BRIGHTNESS) + .expect("failed to set scaling"); + + matrix.device.set_pwm_freq(state.pwm_freq.into()).unwrap(); + + fill_grid_pixels(&state, &mut matrix); + + let timer = Timer::new(pac.TIMER, &mut pac.RESETS); + let mut animation_timer = timer.get_counter().ticks(); + let mut game_timer = timer.get_counter().ticks(); + let mut sleep_timer = timer.get_counter().ticks(); + + // Detect whether the sleep pin is connected + // Early revisions of the hardware didn't have it wired up, if that is the + // case we have to ignore its state. + let mut sleep_present = false; + let sleep = pins.sleep.into_pull_up_input(); + if sleep.is_low().unwrap() { + sleep_present = true; + } + let sleep = sleep.into_pull_down_input(); + if sleep.is_high().unwrap() { + sleep_present = true; + } + + let mut usb_initialized = false; + let mut usb_suspended = false; + let mut last_usb_suspended = usb_suspended; + let mut sleep_reason: Option = None; + let mut last_sleep_reason: Option; + let mut last_host_sleep = sleep.is_low().unwrap(); + + loop { + last_sleep_reason = sleep_reason; + + state.debug_mode = dip1.is_low().unwrap(); + if sleep_present { + // Go to sleep if the host is sleeping + let host_sleeping = sleep.is_low().unwrap(); + let host_sleep_changed = host_sleeping != last_host_sleep; + // Change sleep state either if SLEEP# has changed + // Or if it currently sleeping. Don't change if not sleeping + // because then sleep is controlled by timing or by API. + if host_sleep_changed || host_sleeping { + sleep_reason = assign_sleep_reason( + last_sleep_reason, + sleep_reason, + host_sleeping, + host_sleep_changed, + SleepReason::SleepPin, + ); + } + last_host_sleep = host_sleeping; + } + + // Change sleep state either if SLEEP# has changed + // Or if it currently sleeping. Don't change if not sleeping + // because then sleep is controlled by timing or by API. + let usb_suspended_changed = usb_suspended != last_usb_suspended; + // Only if USB was previously initialized, + // since the OS puts the device into suspend before it's fully + // initialized for the first time. But we don't want to show the + // sleep animation during startup. + if usb_initialized && (usb_suspended_changed || usb_suspended) { + sleep_reason = assign_sleep_reason( + last_sleep_reason, + sleep_reason, + usb_suspended, + usb_suspended_changed, + SleepReason::UsbSuspend, + ); + } + last_usb_suspended = usb_suspended; + + // Go to sleep after the timer has run out + if timer.get_counter().ticks() > sleep_timer + SLEEP_TIMEOUT && !state.debug_mode { + sleep_reason = assign_sleep_reason( + last_sleep_reason, + sleep_reason, + true, + true, + SleepReason::Timeout, + ); + } + // Constantly resetting timer during sleep is same as reset it once on waking up. + // This means the timer ends up counting the time spent awake. + if sleep_reason.is_some() { + sleep_timer = timer.get_counter().ticks(); + } + + handle_sleep( + sleep_reason, + &mut state, + &mut matrix, + &mut delay, + &mut led_enable, + ); + + // Handle period display updates. Don't do it too often + let render_again = timer.get_counter().ticks() > animation_timer + state.animation_period; + if matches!(state.sleeping, SleepState::Awake) && render_again { + if let Some(ref mut upcoming) = state.upcoming_frames { + if let Some(next_frame) = upcoming.next() { + state.grid = next_frame; + } else { + // Animation is over. Clear screen + state.grid = Grid::default(); + } + } + + fill_grid_pixels(&state, &mut matrix); + if state.animate { + for x in 0..WIDTH { + state.grid.0[x].rotate_right(1); + } + } + animation_timer = timer.get_counter().ticks(); + } + + // Check for new data + if usb_dev.poll(&mut [&mut serial]) { + match usb_dev.state() { + // Default: Device has just been created or reset + // Addressed: Device has received an address for the host + UsbDeviceState::Default | UsbDeviceState::Addressed => { + usb_initialized = false; + usb_suspended = false; + // Must not display anything or windows cannot enumerate properly + } + // Configured and is fully operational + UsbDeviceState::Configured => { + usb_initialized = true; + usb_suspended = false; + } + // Never occurs here. Only if poll() returns false + UsbDeviceState::Suspend => { + panic!("Never occurs here. Only if poll() returns false") + } + } + let mut buf = [0u8; 64]; + match serial.read(&mut buf) { + Err(_e) => { + // Do nothing + } + Ok(0) => { + // Do nothing + } + Ok(count) => { + let random = get_random_byte(&rosc); + match (parse_command(count, &buf), &state.sleeping) { + // Handle bootloader command without any delay + // No need, it'll reset the device anyways + (Some(c @ Command::BootloaderReset), _) => { + handle_command(&c, &mut state, &mut matrix, random); + } + (Some(command), _) => { + if let Command::Sleep(go_sleeping) = command { + sleep_reason = assign_sleep_reason( + last_sleep_reason, + sleep_reason, + go_sleeping, + true, + SleepReason::Command, + ); + } else { + // If already sleeping, wake up. + // This means every command will wake the device up. + // Much more convenient than having to send the wakeup commmand. + sleep_reason = None; + } + // Make sure sleep animation only goes up to newly set brightness, + // if setting the brightness causes wakeup + if let SleepState::Sleeping((ref grid, _)) = state.sleeping { + if let Command::SetBrightness(new_brightness) = command { + state.sleeping = + SleepState::Sleeping((grid.clone(), new_brightness)); + } + } + handle_sleep( + sleep_reason, + &mut state, + &mut matrix, + &mut delay, + &mut led_enable, + ); + + // If there's a very early command, cancel the startup animation + state.upcoming_frames = None; + + // Reset sleep timer when interacting with the device + // Very easy way to keep the device from going to sleep + sleep_timer = timer.get_counter().ticks(); + + if let Some(response) = + handle_command(&command, &mut state, &mut matrix, random) + { + let _ = serial.write(&response); + }; + // Must write AFTER writing response, otherwise the + // client interprets this debug message as the response + let mut text: String<64> = String::new(); + write!( + &mut text, + "Handled command {}:{}:{}:{}\r\n", + buf[0], buf[1], buf[2], buf[3] + ) + .unwrap(); + // let _ = serial.write(text.as_bytes()); + + fill_grid_pixels(&state, &mut matrix); + } + (None, _) => {} + } + } + } + } else { + match usb_dev.state() { + // No new data + UsbDeviceState::Default | UsbDeviceState::Addressed => { + usb_initialized = false; + usb_suspended = false; + } + UsbDeviceState::Configured => { + usb_initialized = true; + usb_suspended = false; + } + UsbDeviceState::Suspend => { + usb_suspended = true; + } + } + } + + // Handle game state + let game_step_diff = match state.game { + Some(GameState::Pong(ref pong_state)) => 100_000 - 5_000 * pong_state.speed, + Some(GameState::Snake(_)) => 500_000, + Some(GameState::GameOfLife(_)) => 500_000, + _ => 500_000, + }; + if timer.get_counter().ticks() > game_timer + game_step_diff { + let random = get_random_byte(&rosc); + match state.game { + Some(GameState::GameOfLife(_)) => { + let _ = serial.write(b"GOL Game step\r\n"); + game_of_life::game_step(&mut state, random); + } + Some(GameState::Pong(_)) => { + let _ = serial.write(b"Pong Game step\r\n"); + pong::game_step(&mut state, random); + } + Some(GameState::Snake(_)) => { + let _ = serial.write(b"Snake Game step\r\n"); + let (direction, game_over, points, (x, y)) = + snake::game_step(&mut state, random); + + if game_over { + // TODO: Show score + } else { + let mut text: String<64> = String::new(); + write!( + &mut text, + "Dir: {:?} Status: {}, Points: {}, Head: ({},{})\r\n", + direction, game_over, points, x, y + ) + .unwrap(); + let _ = serial.write(text.as_bytes()); + } + } + None => {} + } + game_timer = timer.get_counter().ticks(); + } + } +} + +fn get_random_byte(rosc: &RingOscillator) -> u8 { + let mut byte = 0; + for i in 0..8 { + byte += (rosc.get_random_bit() as u8) << i; + } + byte +} + +fn dyn_sleep_mode(state: &LedmatrixState) -> SleepMode { + if state.debug_mode { + SleepMode::Debug + } else { + SLEEP_MODE + } +} + +fn debug_mode(state: &LedmatrixState) -> bool { + dyn_sleep_mode(state) == SleepMode::Debug +} + +fn show_startup_animation(state: &LedmatrixState) -> bool { + // Show startup animation + STARTUP_ANIMATION && !debug_mode(state) +} + +fn assign_sleep_reason( + previous: Option, + current: Option, + need_sleep: bool, + // Whether the signal has actually changed in between firing + signal_changed: bool, + new: SleepReason, +) -> Option { + if !need_sleep { + None + } else if current.is_some() && (Some(new) == previous || !signal_changed) { + current + } else { + Some(new) + } +} + +// Will do nothing if already in the right state +fn handle_sleep( + sleep_reason: Option, + state: &mut LedmatrixState, + matrix: &mut Foo, + delay: &mut Delay, + led_enable: &mut gpio::Pin>, +) { + match (state.sleeping.clone(), sleep_reason) { + // Awake and staying awake + (SleepState::Awake, None) => (), + (SleepState::Awake, Some(sleep_reason)) => { + state.sleeping = SleepState::Sleeping((state.grid.clone(), state.brightness)); + // Slowly decrease brightness + if dyn_sleep_mode(state) == SleepMode::Fading { + let mut brightness = state.brightness; + loop { + delay.delay_ms(100); + brightness = if brightness <= 5 { 0 } else { brightness - 5 }; + set_brightness(state, brightness, matrix); + if brightness == 0 { + break; + } + } + } + + if debug_mode(state) { + state.grid = display_sleep_reason(sleep_reason); + fill_grid_pixels(state, matrix); + } else { + // Turn LED controller off to save power + led_enable.set_low().unwrap(); + } + + // TODO: Set up SLEEP# pin as interrupt and wfi + //cortex_m::asm::wfi(); + } + // Already sleeping and new sleep reason => just keep sleeping + (SleepState::Sleeping(_), Some(sleep_reason)) => { + // If debug mode is enabled, then make sure the latest sleep reason is displayed + if debug_mode(state) { + state.grid = display_sleep_reason(sleep_reason); + fill_grid_pixels(state, matrix); + } + } + // Sleeping and need to wake up + (SleepState::Sleeping((old_grid, old_brightness)), None) => { + // Restore back grid before sleeping + state.sleeping = SleepState::Awake; + state.grid = old_grid; + fill_grid_pixels(state, matrix); + + // Power LED controller back on + if !debug_mode(state) { + led_enable.set_high().unwrap(); + } + + // Slowly increase brightness + if dyn_sleep_mode(state) == SleepMode::Fading { + let mut brightness = 0; + loop { + delay.delay_ms(100); + brightness = if brightness >= old_brightness - 5 { + old_brightness + } else { + brightness + 5 + }; + set_brightness(state, brightness, matrix); + if brightness == old_brightness { + break; + } + } + } + } + } +} diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..921e9ccb --- /dev/null +++ b/python/README.md @@ -0,0 +1,202 @@ +# Framework Laptop 16 - Input Module Software + +This repository contains a python library and scripts to control the +(non-keyboard) input modules, which is currently just the LED Matrix. + +## Installing + +Pre-requisites: Python with pip + +```sh +python3 -m pip install inputmodule +``` + +## Control from the host + +To build your own application see the: [API command documentation](https://github.com/FrameworkComputer/inputmodule-rs/tree/main/commands.md) + +###### Permissions on Linux +To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload: + +``` +sudo cp release/50-framework-inputmodule.rules /etc/udev/rules.d/ +sudo udevadm control --reload && sudo udevadm trigger +``` + +##### Common commands: + +###### Listing available devices + +```sh +> ledmatrixctl +More than 1 compatible device found. Please choose with --serial-dev ... +Example on Windows: --serial-dev COM3 +Example on Linux: --serial-dev /dev/ttyACM0 +/dev/ttyACM1 + VID: 0x32AC + PID: 0x0020 + SN: FRAKDEBZ0100000000 + Product: LED Matrix Input Module +/dev/ttyACM0 + VID: 0x32AC + PID: 0x0020 + SN: FRAKDEBZ0100000000 + Product: LED Matrix Input Module +``` + +###### Apply command to single device + +When there are multiple devices you need to select which one to control. + +``` +# Example on Linux +> ledmatrixctl --serial-dev /dev/ttyACM0 --percentage 33 + +# Example on Windows +> ledmatrixctl --serial-dev COM5 --percentage 33 +``` + +### Graphical Application + +Launch the graphical application + +```sh +# Either via the commandline +ledmatrixctl --gui + +# Or using the standanlone application +ledmatrixgui +``` + +### Other example commands + +```sh + +# Show current time and keep updating it +ledmatrixctl --clock + +# Draw PNG or GIF +ledmatrixctl --image stripe.gif +ledmatrixctl --image stripe.png + +# Change brightness (0-255) +ledmatrixctl --brightness 50 +``` + +### All commandline options + +``` +> ledmatrixctl --help +options: + -h, --help show this help message and exit + -l, --list List all compatible devices + --bootloader Jump to the bootloader to flash new firmware + --sleep, --no-sleep Simulate the host going to sleep or waking up + --is-sleeping Check current sleep state + --brightness BRIGHTNESS + Adjust the brightness. Value 0-255 + --get-brightness Get current brightness + --animate, --no-animate + Start/stop vertical scrolling + --get-animate Check if currently animating + --pwm {29000,3600,1800,900} + Adjust the PWM frequency. Value 0-255 + --get-pwm Get current PWM Frequency + --pattern {...} Display a pattern + --image IMAGE Display a PNG or GIF image in black and white only) + --image-grey IMAGE_GREY + Display a PNG or GIF image in greyscale + --camera Stream from the webcam + --video VIDEO Play a video + --percentage PERCENTAGE + Fill a percentage of the screen + --clock Display the current time + --string STRING Display a string or number, like FPS + --symbols SYMBOLS [SYMBOLS ...] + Show symbols (degF, degC, :), snow, cloud, ...) + --gui Launch the graphical version of the program + --panic Crash the firmware (TESTING ONLY) + --blink Blink the current pattern + --breathing Breathing of the current pattern + --eq EQ [EQ ...] Equalizer + --random-eq Random Equalizer + --wpm WPM Demo + --snake Snake + --snake-embedded Snake on the module + --pong-embedded Pong on the module + --game-of-life-embedded {currentmatrix,pattern1,blinker,toad,beacon,glider} + Game of Life + --quit-embedded-game Quit the current game + --all-brightnesses Show every pixel in a different brightness + -v, --version Get device version + --serial-dev SERIAL_DEV + Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows +``` + +## Update the Firmware + +First, put the module into bootloader mode. + +This can be done either by flipping DIP switch #2 or +by using one of the following commands: + +```sh +> ledmatrixctl --bootloader +``` + +Then the module will present itself in the same way as a USB thumb drive. +Copy the UF2 firmware file onto it and the device will flash and reset automatically. +``` + +### Check the firmware version of the device + +```sh +> ledmatrixctl --version +Device Version: 0.1.7 +``` + +###### By looking at the USB descriptor + +On Linux: + +```sh +> lsusb -d 32ac: -v 2> /dev/null | grep -P 'ID 32ac|bcdDevice' +Bus 003 Device 078: ID 32ac:0020 Framework Computer Inc LED Matrix Input Module + bcdDevice 0.17 +``` + +## Developing + +One time setup + +``` +# Install dependencies on Ubuntu +sudo apt install python3 python3-tk + +# Install dependencies on Fedora +sudo dnf install python3 python3-tkinter + +# Create local venv and enter it +python3 -m venv venv +source venv/bin/activate + +# Install package into local env +python3 -m pip install -e . +``` + +Developing + +``` +# In every new shell, source the virtual environment +source venv/bin/activate + +# Launch GUI or commandline +ledmatrixgui +ledmatrixctl + +# Launch Python REPL and import the library +# As example, launch the GUI +> python3 +>>> from inputmodule import cli +>>> cli.main_gui() +``` diff --git a/python/inputmodule/__init__.py b/python/inputmodule/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/inputmodule/cli.py b/python/inputmodule/cli.py new file mode 100755 index 00000000..852641f2 --- /dev/null +++ b/python/inputmodule/cli.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +import argparse +import sys + +# Need to install +from serial.tools import list_ports + +# Local dependencies +from inputmodule import gui +from inputmodule.inputmodule import ( + INPUTMODULE_PIDS, + send_command, + get_version, + brightness, + get_brightness, + CommandVals, + bootloader_jump, + GameOfLifeStartParam, + GameControlVal, +) +from inputmodule.games import ( + snake, + snake_embedded, + pong_embedded, + game_of_life_embedded, + wpm_demo, +) +from inputmodule.gui.ledmatrix import random_eq, clock, blinking +from inputmodule.inputmodule.ledmatrix import ( + eq, + breathing, + camera, + video, + all_brightnesses, + percentage, + pattern, + animate, + get_animate, + pwm_freq, + get_pwm_freq, + show_string, + show_symbols, + PATTERNS, + image_bl, + image_greyscale, +) +from inputmodule.inputmodule.b1display import ( + b1image_bl, + invert_screen_cmd, + screen_saver_cmd, + set_fps_cmd, + set_power_mode_cmd, + get_power_mode_cmd, + get_fps_cmd, + SCREEN_FPS, + display_on_cmd, + display_string, +) +from inputmodule.inputmodule.c1minimal import ( + set_color, + get_color, + RGB_COLORS, +) + + +def main_cli(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", "--list", help="List all compatible devices", action="store_true" + ) + parser.add_argument( + "--bootloader", + help="Jump to the bootloader to flash new firmware", + action="store_true", + ) + parser.add_argument( + "--sleep", + help="Simulate the host going to sleep or waking up", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--is-sleeping", help="Check current sleep state", action="store_true" + ) + parser.add_argument( + "--brightness", help="Adjust the brightness. Value 0-255", type=int + ) + parser.add_argument( + "--get-brightness", help="Get current brightness", action="store_true" + ) + parser.add_argument( + "--animate", + action=argparse.BooleanOptionalAction, + help="Start/stop vertical scrolling", + ) + parser.add_argument( + "--get-animate", action="store_true", help="Check if currently animating" + ) + parser.add_argument( + "--pwm", + help="Adjust the PWM frequency. Value 0-255", + type=int, + choices=[29000, 3600, 1800, 900], + ) + parser.add_argument( + "--get-pwm", help="Get current PWM Frequency", action="store_true" + ) + parser.add_argument( + "--pattern", help="Display a pattern", type=str, choices=PATTERNS + ) + parser.add_argument( + "--image", + help="Display a PNG or GIF image in black and white only)", + type=argparse.FileType("rb"), + ) + parser.add_argument( + "--image-grey", + help="Display a PNG or GIF image in greyscale", + type=argparse.FileType("rb"), + ) + parser.add_argument( + "--camera", help="Stream from the webcam", action="store_true") + parser.add_argument("--video", help="Play a video", type=str) + parser.add_argument( + "--percentage", help="Fill a percentage of the screen", type=int + ) + parser.add_argument( + "--clock", help="Display the current time", action="store_true") + parser.add_argument( + "--string", help="Display a string or number, like FPS", type=str + ) + parser.add_argument( + "--symbols", help="Show symbols (degF, degC, :), snow, cloud, ...)", nargs="+" + ) + parser.add_argument( + "--gui", help="Launch the graphical version of the program", action="store_true" + ) + parser.add_argument( + "--panic", help="Crash the firmware (TESTING ONLY)", action="store_true" + ) + parser.add_argument( + "--blink", help="Blink the current pattern", action="store_true" + ) + parser.add_argument( + "--breathing", help="Breathing of the current pattern", action="store_true" + ) + parser.add_argument("--eq", help="Equalizer", nargs="+", type=int) + parser.add_argument( + "--random-eq", help="Random Equalizer", action="store_true") + parser.add_argument("--wpm", help="WPM Demo", action="store_true") + parser.add_argument("--snake", help="Snake", action="store_true") + parser.add_argument( + "--snake-embedded", help="Snake on the module", action="store_true" + ) + parser.add_argument( + "--pong-embedded", help="Pong on the module", action="store_true" + ) + parser.add_argument( + "--game-of-life-embedded", + help="Game of Life", + type=GameOfLifeStartParam.argparse, + choices=list(GameOfLifeStartParam), + ) + parser.add_argument( + "--quit-embedded-game", help="Quit the current game", action="store_true" + ) + parser.add_argument( + "--all-brightnesses", + help="Show every pixel in a different brightness", + action="store_true", + ) + parser.add_argument( + "--set-color", + help="Set RGB color (C1 Minimal Input Module)", + choices=RGB_COLORS, + ) + parser.add_argument( + "--get-color", + help="Get RGB color (C1 Minimal Input Module)", + action="store_true", + ) + parser.add_argument( + "-v", "--version", help="Get device version", action="store_true" + ) + parser.add_argument( + "--serial-dev", + help="Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows", + ) + + parser.add_argument( + "--disp-str", help="Display a string on the LCD Display", type=str + ) + parser.add_argument( + "--display-on", + help="Control display power", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument( + "--invert-screen", help="Invert display", action=argparse.BooleanOptionalAction + ) + parser.add_argument( + "--screen-saver", + help="Turn on/off screensaver", + action=argparse.BooleanOptionalAction, + ) + parser.add_argument("--set-fps", help="Set screen FPS", choices=SCREEN_FPS) + parser.add_argument( + "--set-power-mode", help="Set screen power mode", choices=["high", "low"] + ) + parser.add_argument("--get-fps", help="Set screen FPS", + action="store_true") + parser.add_argument( + "--get-power-mode", help="Set screen power mode", action="store_true" + ) + parser.add_argument( + "--b1image", + help="On the B1 display, show a PNG or GIF image in black and white only)", + type=argparse.FileType("rb"), + ) + + args = parser.parse_args() + + # Selected device + dev = None + ports = find_devs() + + if args.list: + print_devs(ports) + sys.exit(0) + + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Force GUI in pyinstaller bundled app + args.gui = True + + if not ports: + print("No device found") + gui.popup("No device found", gui=args.gui) + sys.exit(1) + elif args.serial_dev is not None: + filtered_devs = [ + port for port in ports if port.name in args.serial_dev] + if not filtered_devs: + print("Failed to find requested device") + sys.exit(1) + dev = filtered_devs[0] + elif len(ports) == 1: + dev = ports[0] + elif len(ports) >= 1 and not args.gui: + gui.popup( + "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 ..." + ) + print("Example on Windows: --serial-dev COM3") + print("Example on Linux: --serial-dev /dev/ttyACM0") + print_devs(ports) + sys.exit(1) + elif args.gui: + # TODO: Allow selection in GUI + print("Select in GUI") + + if not args.gui and dev is None: + print("No device selected") + gui.popup("No device selected", gui=args.gui) + sys.exit(1) + + if args.bootloader: + bootloader_jump(dev) + elif args.sleep is not None: + send_command(dev, CommandVals.Sleep, [args.sleep]) + elif args.is_sleeping: + res = send_command(dev, CommandVals.Sleep, with_response=True) + sleeping = bool(res[0]) + print(f"Currently sleeping: {sleeping}") + elif args.brightness is not None: + if args.brightness > 255 or args.brightness < 0: + print("Brightness must be 0-255") + sys.exit(1) + brightness(dev, args.brightness) + elif args.get_brightness: + br = get_brightness(dev) + print(f"Current brightness: {br}") + elif args.pwm is not None: + if args.pwm == 29000: + pwm_freq(dev, "29kHz") + elif args.pwm == 3600: + pwm_freq(dev, "3.6kHz") + elif args.pwm == 1800: + pwm_freq(dev, "1.8kHz") + elif args.pwm == 900: + pwm_freq(dev, "900Hz") + elif args.get_pwm: + p = get_pwm_freq(dev) + print(f"Current PWM Frequency: {p} Hz") + elif args.percentage is not None: + if args.percentage > 100 or args.percentage < 0: + print("Percentage must be 0-100") + sys.exit(1) + percentage(dev, args.percentage) + elif args.pattern is not None: + pattern(dev, args.pattern) + elif args.animate is not None: + animate(dev, args.animate) + elif args.get_animate: + animating = get_animate(dev) + print(f"Currently animating: {animating}") + elif args.panic: + send_command(dev, CommandVals.Panic, [0x00]) + elif args.image is not None: + image_bl(dev, args.image) + elif args.image_grey is not None: + image_greyscale(dev, args.image_grey) + elif args.camera: + camera(dev) + elif args.video is not None: + video(dev, args.video) + elif args.all_brightnesses: + all_brightnesses(dev) + elif args.set_color: + set_color(dev, args.set_color) + elif args.get_color: + (red, green, blue) = get_color(dev) + print(f"Current color: RGB:({red}, {green}, {blue})") + elif args.gui: + devices = find_devs() # show=False, verbose=False) + print("Found {} devices".format(len(devices))) + gui.run_gui(devices) + elif args.blink: + blinking(dev) + elif args.breathing: + breathing(dev) + elif args.wpm: + wpm_demo(dev) + elif args.snake: + snake(dev) + elif args.snake_embedded: + snake_embedded(dev) + elif args.game_of_life_embedded is not None: + game_of_life_embedded(dev, args.game_of_life_embedded) + elif args.quit_embedded_game: + send_command(dev, CommandVals.GameControl, [GameControlVal.Quit]) + elif args.pong_embedded: + pong_embedded(dev) + elif args.eq is not None: + eq(dev, args.eq) + elif args.random_eq: + random_eq(dev) + elif args.clock: + clock(dev) + elif args.string is not None: + show_string(dev, args.string) + elif args.symbols is not None: + show_symbols(dev, args.symbols) + elif args.disp_str is not None: + display_string(dev, args.disp_str) + elif args.display_on is not None: + display_on_cmd(dev, args.display_on) + elif args.invert_screen is not None: + invert_screen_cmd(dev, args.invert_screen) + elif args.screen_saver is not None: + screen_saver_cmd(dev, args.screen_saver) + elif args.set_fps is not None: + set_fps_cmd(dev, args.set_fps) + elif args.set_power_mode is not None: + set_power_mode_cmd(dev, args.set_power_mode) + elif args.get_fps: + get_fps_cmd(dev) + elif args.get_power_mode: + get_power_mode_cmd(dev) + elif args.b1image is not None: + b1image_bl(dev, args.b1image) + elif args.version: + version = get_version(dev) + print(f"Device version: {version}") + else: + parser.print_help(sys.stderr) + sys.exit(1) + + +def find_devs(): + ports = list_ports.comports() + return [ + port for port in ports if port.vid == 0x32AC and port.pid in INPUTMODULE_PIDS + ] + + +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}") + print(f" Product: {port.product}") + + +def main_gui(): + devices = find_devs() # show=False, verbose=False) + print("Found {} devices".format(len(devices))) + gui.run_gui(devices) + + +if __name__ == "__main__": + main_gui() diff --git a/python/inputmodule/firmware_update.py b/python/inputmodule/firmware_update.py new file mode 100644 index 00000000..7d2e752e --- /dev/null +++ b/python/inputmodule/firmware_update.py @@ -0,0 +1,80 @@ +import os +import time + +from inputmodule.inputmodule import bootloader_jump +from inputmodule import uf2conv + +def dev_to_str(dev): + return dev.name + +def flash_firmware(dev, fw_path): + print(f"Flashing {fw_path} onto {dev_to_str(dev)}") + + # First jump to bootloader + drives = uf2conv.list_drives() + if not drives: + print("Jump to bootloader") + bootloader_jump(dev) + + timeout = 10 # 5s + while not drives: + if timeout == 0: + print("Failed to find device in bootloader") + # TODO: Handle return value + return False + # Wait for it to appear + time.sleep(0.5) + timeout -= 1 + drives = uf2conv.get_drives() + + + if len(drives) == 0: + print("No drive to deploy.") + return False + + # Firmware is pretty small, can just fit it all into memory + with open(fw_path, 'rb') as f: + fw_buf = f.read() + + for d in drives: + print("Flashing {} ({})".format(d, uf2conv.board_id(d))) + uf2conv.write_file(d + "/NEW.UF2", fw_buf) + + print("Flashing finished") + +# Example return value +# { +# '0.1.7': { +# 'ansi': 'framework_ansi_default_v0.1.7.uf2', +# 'gridpad': 'framework_gridpad_default_v0.1.7.uf2' +# }, +# '0.1.8': { +# 'ansi': 'framework_ansi_default.uf2', +# 'gridpad': 'framework_gridpad_default.uf2', +# } +# } +def find_releases(res_path, filename_format): + from os import listdir + from os.path import isfile, join + import re + + releases = {} + try: + versions = listdir(os.path.join(res_path, "releases")) + except FileNotFoundError: + return releases + + for version in versions: + path = join(res_path, "releases", version) + releases[version] = {} + for filename in listdir(path): + if not isfile(join(path, filename)): + continue + type_search = re.search(filename_format, filename) + if not type_search: + print(f"Filename '{filename}' not matching patten!") + sys.exit(1) + continue + fw_type = type_search.group(1) + releases[version][fw_type] = os.path.join(res_path, "releases", version, filename) + return releases diff --git a/python/inputmodule/font.py b/python/inputmodule/font.py new file mode 100644 index 00000000..3f349a5d --- /dev/null +++ b/python/inputmodule/font.py @@ -0,0 +1,2164 @@ +def convert_symbol(symbol): + """5x6 symbol font. Leaves 2 pixels on each side empty + We can leave one row empty below and then the display fits 5 of these digits.""" + symbols = { + "degC": [ + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + ], + "degF": [ + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + ], + "snow": [ + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + ], + "sun": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "cloud": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "rain": [ + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + ], + "thunder": [ + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "batteryLow": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "!!": [ + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + ], + "heart": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "heart0": [ + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "heart2": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + ], + ":)": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + ":|": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + ], + ":(": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + ";)": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + } + if symbol in symbols: + return symbols[symbol] + else: + return None + + +def convert_font(num): + """5x6 font. Leaves 2 pixels on each side empty + We can leave one row empty below and then the display fits 5 of these digits.""" + font = { + "0": [ + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + ], + "1": [ + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "2": [ + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "3": [ + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "4": [ + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + ], + "5": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "6": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "7": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "8": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "9": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + ":": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + " ": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "?": [ + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + ".": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + ",": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "!": [ + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "/": [ + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "*": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "%": [ + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + ], + "+": [ + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "-": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "=": [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + "A": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "B": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "C": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "D": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "E": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "F": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "G": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "H": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "I": [ + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + ], + "J": [ + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + ], + "K": [ + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + "L": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "M": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + ], + "N": [ + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + ], + "O": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "P": [ + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + ], + "Q": [ + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + ], + "R": [ + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + ], + "S": [ + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + ], + "T": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "U": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + ], + "V": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "W": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + ], + "X": [ + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + ], + "Y": [ + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + "Z": [ + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + ], + "Ä": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + ], + "Ö": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + ], + "Ü": [ + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + ], + } + if num in font: + return font[num] + else: + return font["?"] diff --git a/python/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 new file mode 100644 index 00000000..f7e53a60 --- /dev/null +++ b/python/inputmodule/gui/__init__.py @@ -0,0 +1,365 @@ +import os +import platform +import sys +import threading +import webbrowser + +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_jump, + CommandVals, + Game, + GameControlVal +) +from inputmodule.gui.ledmatrix import countdown, random_eq, clock +from inputmodule.gui.gui_threading import stop_thread, is_dev_disconnected +from inputmodule.inputmodule.ledmatrix import ( + percentage, + pattern, + animate, + PATTERNS, + PWM_FREQUENCIES, + show_symbols, + show_string, + pwm_freq, + image_bl, + image_greyscale, +) + +def update_brightness_slider(devices): + average_brightness = None + for dev in devices: + if not average_brightness: + average_brightness = 0 + + br = get_brightness(dev) + average_brightness += br + if average_brightness: + brightness_scale.set(average_brightness) + +def popup(message, gui=True): + if gui: + messagebox.showinfo("Framework Laptop 16 LED Matrix", message) + +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) + tab2 = ttk.Frame(tabControl) + tab_fw = ttk.Frame(tabControl) + tab3 = ttk.Frame(tabControl) + tabControl.add(tab1, text="Home") + tabControl.add(tab_games, text="Games") + tabControl.add(tab2, text="Dynamic Controls") + tabControl.add(tab_fw, text="Firmware Update") + tabControl.add(tab3, text="Advanced") + tabControl.pack(expand=1, fill="both") + + # Device Checkboxes + detected_devices_frame = ttk.LabelFrame(root, text="Detected Devices", style="TLabelframe") + detected_devices_frame.pack(fill="x", padx=10, pady=5) + + global device_checkboxes + device_checkboxes = {} + for dev in devices: + version = get_version(dev) + device_info = ( + f"{dev.name}\nSerial No: {dev.serial_number}\nFW Version:{version}" + ) + checkbox_var = tk.BooleanVar(value=True) + checkbox = ttk.Checkbutton(detected_devices_frame, text=device_info, variable=checkbox_var, style="TCheckbutton") + checkbox.pack(anchor="w") + device_checkboxes[dev.name] = (checkbox_var, checkbox) + + # Online Info + info_frame = ttk.LabelFrame(tab1, text="Online Info", style="TLabelframe") + info_frame.pack(fill="x", padx=10, pady=5) + infos = { + "Web Interface": "https://ledmatrix.frame.work", + "Latest Releases": "https://github.com/FrameworkComputer/inputmodule-rs/releases", + "Hardware Info": "https://github.com/FrameworkComputer/InputModules", + } + for (i, (text, url)) in enumerate(infos.items()): + # Organize in columns of three + row = int(i / 3) + column = i % 3 + btn = ttk.Button(info_frame, text=text, command=lambda url=url: webbrowser.open(url), style="TButton") + btn.grid(row=row, column=column) + + # Brightness Slider + brightness_frame = ttk.LabelFrame(tab1, text="Brightness", style="TLabelframe") + brightness_frame.pack(fill="x", padx=10, pady=5) + global brightness_scale + brightness_scale = tk.Scale(brightness_frame, from_=0, to=255, orient='horizontal', command=lambda value: set_brightness(devices, value)) + brightness_scale.set(120) # Default value + brightness_scale.pack(fill="x", padx=5, pady=5) + + # Animation Control + animation_frame = ttk.LabelFrame(tab1, text="Animation", style="TLabelframe") + animation_frame.pack(fill="x", padx=10, pady=5) + animation_buttons = { + "Start Animation": "start_animation", + "Stop Animation": "stop_animation" + } + for text, action in animation_buttons.items(): + ttk.Button(animation_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + + # Pattern Combo Box + pattern_frame = ttk.LabelFrame(tab1, text="Pattern", style="TLabelframe") + pattern_frame.pack(fill="x", padx=10, pady=5) + pattern_combo = ttk.Combobox(pattern_frame, values=PATTERNS, style="TCombobox", state="readonly") + pattern_combo.pack(fill="x", padx=5, pady=5) + pattern_combo.bind("<>", lambda event: set_pattern(devices, pattern_combo.get())) + + # Percentage Slider + percentage_frame = ttk.LabelFrame(tab1, text="Fill screen X% (could be volume indicator)", style="TLabelframe") + percentage_frame.pack(fill="x", padx=10, pady=5) + percentage_scale = tk.Scale(percentage_frame, from_=0, to=100, orient='horizontal', command=lambda value: set_percentage(devices, value)) + percentage_scale.pack(fill="x", padx=5, pady=5) + + # Games tab + games_frame = ttk.LabelFrame(tab_games, text="Interactive", style="TLabelframe") + games_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(games_frame, text="Snake", command=lambda: perform_action(devices, 'game_snake'), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(games_frame, text="Ledris", command=lambda: perform_action(devices, 'game_ledris'), style="TButton").pack(side="left", padx=5, pady=5) + gol_frame = ttk.LabelFrame(tab_games, text="Game of Life", style="TLabelframe") + gol_frame.pack(fill="x", padx=10, pady=5) + animation_buttons = { + "Current": "gol_current", + "Pattern 1": "gol_pattern1", + "Blinker": "gol_blinker", + "Toad": "gol_toad", + "Beacon": "gol_beacon", + "Glider": "gol_glider", + "Stop": "game_stop", + } + for (i, (text, action)) in enumerate(animation_buttons.items()): + # Organize in columns of three + row = int(i / 3) + column = i % 3 + if action == "game_stop": + column = 0 + row += 1 + btn = ttk.Button(gol_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton") + btn.grid(row=row, column=column) + + # Countdown Timer + countdown_frame = ttk.LabelFrame(tab2, text="Countdown Timer", style="TLabelframe") + countdown_frame.pack(fill="x", padx=10, pady=5) + countdown_spinbox = tk.Spinbox(countdown_frame, from_=1, to=60, width=5, textvariable=tk.StringVar(value=10)) + countdown_spinbox.pack(side="left", padx=5, pady=5) + ttk.Label(countdown_frame, text="Seconds", style="TLabel").pack(side="left") + ttk.Button(countdown_frame, text="Start", command=lambda: start_countdown(devices, countdown_spinbox.get()), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(countdown_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Black & White and Greyscale Images in same row + image_frame = ttk.LabelFrame(tab1, text="Black&White Images / Greyscale Images", style="TLabelframe") + image_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(image_frame, text="Send stripe.gif", command=lambda: send_image(devices, "stripe.gif", image_bl), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(image_frame, text="Send greyscale.gif", command=lambda: send_image(devices, "greyscale.gif", image_greyscale), style="TButton").pack(side="left", padx=5, pady=5) + + # Display Current Time + time_frame = ttk.LabelFrame(tab2, text="Display Current Time", style="TLabelframe") + time_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(time_frame, text="Start", command=lambda: perform_action(devices, "start_time"), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(time_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Custom Text + custom_text_frame = ttk.LabelFrame(tab1, text="Custom Text", style="TLabelframe") + custom_text_frame.pack(fill="x", padx=10, pady=5) + custom_text_entry = ttk.Entry(custom_text_frame, width=20, style="TEntry") + custom_text_entry.pack(side="left", padx=5, pady=5) + ttk.Button(custom_text_frame, text="Show", command=lambda: show_custom_text(devices, custom_text_entry.get()), style="TButton").pack(side="left", padx=5, pady=5) + + # Display Text with Symbols + symbols_frame = ttk.LabelFrame(tab1, text="Display Text with Symbols", style="TLabelframe") + symbols_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(symbols_frame, text="Send '2 5 degC thunder'", command=lambda: send_symbols(devices), style="TButton").pack(side="left", padx=5, pady=5) + + # Firmware Update + bootloader_frame = ttk.LabelFrame(tab_fw, text="Bootloader", style="TLabelframe") + bootloader_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(bootloader_frame, text="Enter Bootloader", command=lambda: perform_action(devices, "bootloader"), style="TButton").pack(side="left", padx=5, pady=5) + + bundled_fw_frame = ttk.LabelFrame(tab_fw, text="Bundled Updates", style="TLabelframe") + bundled_fw_frame.pack(fill="x", padx=10, pady=5) + releases = firmware_update.find_releases(resource_path(), r'(ledmatrix).uf2') + if not releases: + tk.Label(bundled_fw_frame, text="Cannot find firmware updates").pack(side="top", padx=5, pady=5) + else: + versions = sorted(list(releases.keys()), reverse=True) + + #tk.Label(fw_update_frame, text="Ignore user configured keymap").pack(side="top", padx=5, pady=5) + fw_ver_combo = ttk.Combobox(bundled_fw_frame, values=versions, style="TCombobox", state="readonly") + fw_ver_combo.pack(side=tk.LEFT, padx=5, pady=5) + fw_ver_combo.current(0) + flash_btn = ttk.Button(bundled_fw_frame, text="Update", command=lambda: tk_flash_firmware(devices, releases, fw_ver_combo.get(), 'ledmatrix'), style="TButton") + flash_btn.pack(side="left", padx=5, pady=5) + + # PWM Frequency Combo Box + pwm_freq_frame = ttk.LabelFrame(tab3, text="PWM Frequency", style="TLabelframe") + pwm_freq_frame.pack(fill="x", padx=10, pady=5) + pwm_freq_combo = ttk.Combobox(pwm_freq_frame, values=PWM_FREQUENCIES, style="TCombobox", state="readonly") + pwm_freq_combo.pack(fill="x", padx=5, pady=5) + pwm_freq_combo.bind("<>", lambda: set_pwm_freq(devices, pwm_freq_combo.get())) + + # Equalizer + equalizer_frame = ttk.LabelFrame(tab2, text="Equalizer", style="TLabelframe") + equalizer_frame.pack(fill="x", padx=10, pady=5) + ttk.Button(equalizer_frame, text="Start random equalizer", command=lambda: perform_action(devices, "start_eq"), style="TButton").pack(side="left", padx=5, pady=5) + ttk.Button(equalizer_frame, text="Stop", command=stop_thread, style="TButton").pack(side="left", padx=5, pady=5) + + # Device Control Buttons + device_control_frame = ttk.LabelFrame(tab3, text="Device Control", style="TLabelframe") + device_control_frame.pack(fill="x", padx=10, pady=5) + control_buttons = { + "Sleep": "sleep", + "Wake": "wake" + } + for text, action in control_buttons.items(): + ttk.Button(device_control_frame, text=text, command=lambda a=action: perform_action(devices, a), style="TButton").pack(side="left", padx=5, pady=5) + + root.mainloop() + +def perform_action(devices, action): + if action.startswith("game_"): + from inputmodule.gui.pygames import snake, ledris + action_map = { + "game_snake": snake.main_devices, + "game_ledris": ledris.main_devices, + } + if action in action_map: + threading.Thread(target=action_map[action], args=(devices,), daemon=True).start(), + + if action == "bootloader": + disable_devices(devices) + restart_hint() + + action_map = { + "bootloader": bootloader_jump, + "sleep": lambda dev: send_command(dev, CommandVals.Sleep, [True]), + "wake": lambda dev: send_command(dev, CommandVals.Sleep, [False]), + "start_animation": lambda dev: animate(dev, True), + "stop_animation": lambda dev: animate(dev, False), + "start_time": lambda dev: threading.Thread(target=clock, args=(dev,), daemon=True).start(), + "start_eq": lambda dev: threading.Thread(target=random_eq, args=(dev,), daemon=True).start(), + "gol_current": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 0]), + "gol_pattern1": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 1]), + "gol_blinker": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 2]), + "gol_toad": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 3]), + "gol_beacon": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 4]), + "gol_glider": lambda dev: send_command(dev, CommandVals.StartGame, [Game.GameOfLife, 5]), + "game_stop": lambda dev: send_command(dev, CommandVals.GameControl, [GameControlVal.Quit]), + } + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + if action in action_map: + action_map[action](dev) + +def set_brightness(devices, value): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + brightness(dev, int(value)) + +def set_pattern(devices, pattern_name): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + pattern(dev, pattern_name) + +def set_percentage(devices, value): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + percentage(dev, int(value)) + +def show_custom_text(devices, text): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + show_string(dev, text.upper()) + +def send_image(devices, image_name, image_function): + selected_devices = get_selected_devices(devices) + path = os.path.join(resource_path(), "res", image_name) + if not os.path.exists(path): + popup(f"Image file {image_name} not found.") + return + for dev in selected_devices: + image_function(dev, path) + +def send_symbols(devices): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + show_symbols(dev, ["2", "5", "degC", " ", "thunder"]) + +def start_countdown(devices, countdown_time): + selected_devices = get_selected_devices(devices) + if len(selected_devices) == 1: + dev = selected_devices[0] + threading.Thread(target=countdown, args=(dev, int(countdown_time)), daemon=True).start() + else: + popup("Select exactly 1 device for this action") + +def set_pwm_freq(devices, freq): + selected_devices = get_selected_devices(devices) + for dev in selected_devices: + pwm_freq(dev, freq) + +def get_selected_devices(devices): + return [dev for dev in devices if dev.name in device_checkboxes and device_checkboxes[dev.name][0].get()] + +def resource_path(): + """Get absolute path to resource, works for dev and for PyInstaller""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return base_path + +def info_popup(msg): + parent = tk.Tk() + parent.title("Info") + message = tk.Message(parent, text=msg, width=800) + message.pack(padx=20, pady=20) + parent.mainloop() + +def tk_flash_firmware(devices, releases, version, fw_type): + selected_devices = get_selected_devices(devices) + if len(selected_devices) != 1: + info_popup('To flash select exactly 1 device.') + return + dev = selected_devices[0] + firmware_update.flash_firmware(dev, releases[version][fw_type]) + # Disable device that we just flashed + disable_devices(devices) + restart_hint() + +def restart_hint(): + parent = tk.Tk() + parent.title("Restart Application") + message = tk.Message(parent, text="After updating a device,\n restart the application to reload the connections.", width=800) + message.pack(padx=20, pady=20) + parent.mainloop() + +def disable_devices(devices): + # Disable checkbox of selected devices + for dev in devices: + for name, (checkbox_var, checkbox) in device_checkboxes.items(): + if name == dev.name: + checkbox_var.set(False) + checkbox.config(state=tk.DISABLED) diff --git a/python/inputmodule/gui/gui_threading.py b/python/inputmodule/gui/gui_threading.py new file mode 100644 index 00000000..0f7fec20 --- /dev/null +++ b/python/inputmodule/gui/gui_threading.py @@ -0,0 +1,36 @@ +# Global GUI variables +DISCONNECTED_DEVS = [] +STATUS = '' + +def set_status(status): + global STATUS + STATUS = status + +def get_status(): + global STATUS + return STATUS + +def stop_thread(): + global STATUS + STATUS = 'STOP_THREAD' + + +def reset_thread(): + global STATUS + if STATUS == 'STOP_THREAD': + STATUS = '' + + +def is_thread_stopped(): + global STATUS + return STATUS == 'STOP_THREAD' + + +def is_dev_disconnected(dev): + global DISCONNECTED_DEVS + return dev in DISCONNECTED_DEVS + + +def disconnect_dev(device): + global DISCONNECTED_DEVS + DISCONNECTED_DEVS.append(device) diff --git a/python/inputmodule/gui/ledmatrix.py b/python/inputmodule/gui/ledmatrix.py new file mode 100644 index 00000000..dddd1659 --- /dev/null +++ b/python/inputmodule/gui/ledmatrix.py @@ -0,0 +1,94 @@ +from datetime import datetime, timedelta +import time +import random + +from inputmodule.gui.gui_threading import ( + 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 get_status() == 'countdown': + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + now = datetime.now() + passed_time = (now - start) / timedelta(microseconds=1) + + ratio = passed_time / target + if passed_time >= target: + break + + leds = int(306 * ratio) + light_leds(dev, leds) + + time.sleep(0.01) + + 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""" + set_status('blinking') + while get_status() == 'blinking': + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + brightness(dev, 0) + time.sleep(0.5) + brightness(dev, 200) + time.sleep(0.5) + + +def random_eq(dev): + """Display an equlizer looking animation with random values.""" + 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 + # Lower values more likely, makes it look nicer + weights = [i * i for i in range(33, 0, -1)] + population = list(range(1, 34)) + vals = random.choices(population, weights=weights, k=9) + eq(dev, vals) + time.sleep(0.2) + + +def clock(dev): + """Render the current time and display. + Loops forever, updating every second""" + animate(dev, False) + set_status('clock') + while get_status() == 'clock': + if is_thread_stopped() or is_dev_disconnected(dev.device): + reset_thread() + return + now = datetime.now() + current_time = now.strftime("%H:%M") + print("Current Time =", current_time) + + show_string(dev, current_time) + time.sleep(1) diff --git a/python/inputmodule/gui/pygames/__init__.py b/python/inputmodule/gui/pygames/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/inputmodule/gui/pygames/ledris.py b/python/inputmodule/gui/pygames/ledris.py new file mode 100644 index 00000000..469acfe9 --- /dev/null +++ b/python/inputmodule/gui/pygames/ledris.py @@ -0,0 +1,254 @@ +# Run like +# python3 ledris.py + +import pygame +import random +import time + +from inputmodule import cli +from inputmodule.gui.ledmatrix import show_string +from inputmodule.inputmodule import ledmatrix + +# Set the screen width and height for a 34 x 9 block Ledris game +block_width = 20 +block_height = 20 +cols = 9 +rows = 34 + +width = cols * block_width +height = rows * block_height + +# Colors +black = (0, 0, 0) +white = (255, 255, 255) + +# Ledrimino shapes +shapes = [ + [[1, 1, 1, 1]], # I shape + [[1, 1], [1, 1]], # O shape + [[0, 1, 0], [1, 1, 1]], # T shape + [[1, 1, 0], [0, 1, 1]], # S shape + [[0, 1, 1], [1, 1, 0]], # Z shape + [[1, 1, 1], [1, 0, 0]], # L shape + [[1, 1, 1], [0, 0, 1]] # J shape +] + +# Function to get the current board state +def get_board_state(board, current_shape, current_pos): + temp_board = [row[:] for row in board] + off_x, off_y = current_pos + for y, row in enumerate(current_shape): + for x, cell in enumerate(row): + if cell: + if 0 <= off_y + y < rows and 0 <= off_x + x < cols: + temp_board[off_y + y][off_x + x] = 1 + return temp_board + +def draw_ledmatrix(board, devices): + for dev in devices: + matrix = [[0 for _ in range(34)] for _ in range(9)] + for y in range(rows): + for x in range(cols): + matrix[x][y] = board[y][x] + ledmatrix.render_matrix(dev, matrix) + #vals = [0 for _ in range(39)] + #send_command(dev, CommandVals.Draw, vals) + +# Function to check if the position is valid +def check_collision(board, shape, offset): + off_x, off_y = offset + for y, row in enumerate(shape): + for x, cell in enumerate(row): + if cell: + if x + off_x < 0 or x + off_x >= cols or y + off_y >= rows: + return True + if y + off_y >= 0 and board[y + off_y][x + off_x]: + return True + return False + +# Function to merge the shape into the board +def merge_shape(board, shape, offset): + off_x, off_y = offset + for y, row in enumerate(shape): + for x, cell in enumerate(row): + if cell: + if 0 <= off_y + y < rows and 0 <= off_x + x < cols: + board[off_y + y][off_x + x] = 1 + +# Function to clear complete rows +def clear_rows(board): + new_board = [row for row in board if any(cell == 0 for cell in row)] + cleared_rows = rows - len(new_board) + while len(new_board) < rows: + new_board.insert(0, [0 for _ in range(cols)]) + return new_board, cleared_rows + +# Function to display the score using blocks +def display_score(board, score): + score_str = str(score) + start_x = cols - len(score_str) * 4 + for i, digit in enumerate(score_str): + if digit.isdigit(): + digit = int(digit) + for y in range(5): + for x in range(3): + if digit_blocks[digit][y][x]: + if y < rows and start_x + i * 4 + x < cols: + board[y][start_x + i * 4 + x] = 1 + +# Digit blocks for representing score +# Each number is represented in a 5x3 block matrix +digit_blocks = [ + [[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0 + [[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3 + [[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6 + [[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9 +] + + +class Ledris: + # Function to draw a grid + def draw_grid(self): + for y in range(rows): + for x in range(cols): + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect, 1) + + # Function to draw the game based on the board state + def draw_board(self, board, devices): + draw_ledmatrix(board, devices) + self.screen.fill(white) + for y in range(rows): + for x in range(cols): + if board[y][x]: + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect) + self.draw_grid() + pygame.display.update() + + # Main game function + def gameLoop(self, devices): + board = [[0 for _ in range(cols)] for _ in range(rows)] + current_shape = random.choice(shapes) + current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display + game_over = False + fall_time = 0 + fall_speed = 500 # Falling speed in milliseconds + score = 0 + + while not game_over: + # Adjust falling speed based on score + fall_speed = max(100, 500 - (score * 10)) + + # Draw the current board state + board_state = get_board_state(board, current_shape, current_pos) + display_score(board_state, score) + self.draw_board(board_state, devices) + + # Event handling + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game_over = True + + if event.type == pygame.KEYDOWN: + if event.key in [pygame.K_LEFT, pygame.K_h]: + new_pos = [current_pos[0] - 1, current_pos[1]] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_RIGHT, pygame.K_l]: + new_pos = [current_pos[0] + 1, current_pos[1]] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_DOWN, pygame.K_j]: + new_pos = [current_pos[0], current_pos[1] + 1] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + elif event.key in [pygame.K_UP, pygame.K_k]: + rotated_shape = list(zip(*current_shape[::-1])) + if not check_collision(board, rotated_shape, current_pos): + current_shape = rotated_shape + elif event.key == pygame.K_SPACE: # Hard drop + while not check_collision(board, current_shape, [current_pos[0], current_pos[1] + 1]): + current_pos[1] += 1 + + # Automatic falling + fall_time += self.clock.get_time() + if fall_time >= fall_speed: + fall_time = 0 + new_pos = [current_pos[0], current_pos[1] + 1] + if not check_collision(board, current_shape, new_pos): + current_pos = new_pos + else: + merge_shape(board, current_shape, current_pos) + board, cleared_rows = clear_rows(board) + score += cleared_rows # Increase score by one for each row cleared + current_shape = random.choice(shapes) + current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display + if check_collision(board, current_shape, current_pos): + game_over = True + + self.clock.tick(30) + + # Flash the screen twice before waiting for restart + for _ in range(2): + for dev in devices: + ledmatrix.percentage(dev, 0) + self.screen.fill(black) + pygame.display.update() + time.sleep(0.3) + + for dev in devices: + ledmatrix.percentage(dev, 100) + self.screen.fill(white) + pygame.display.update() + time.sleep(0.3) + + # Display final score and wait for restart without clearing the screen + board_state = get_board_state(board, current_shape, current_pos) + display_score(board_state, score) + self.draw_board(board_state, devices) + + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + waiting = False + game_over = True + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + waiting = False + if event.key == pygame.K_r: + board = [[0 for _ in range(cols)] for _ in range(rows)] + gameLoop() + + pygame.quit() + quit() + + def __init__(self): + # Initialize pygame + pygame.init() + + # Create the screen + self.screen = pygame.display.set_mode((width, height)) + + # Clock to control the speed of the game + self.clock = pygame.time.Clock() + +def main_devices(devices): + ledris = Ledris() + ledris.gameLoop(devices) + +def main(): + devices = cli.find_devs() + + ledris = Ledris() + ledris.gameLoop(devices) + +if __name__ == "__main__": + main() diff --git a/python/inputmodule/gui/pygames/snake.py b/python/inputmodule/gui/pygames/snake.py new file mode 100644 index 00000000..6568639c --- /dev/null +++ b/python/inputmodule/gui/pygames/snake.py @@ -0,0 +1,254 @@ +import pygame +import random +import time + +from inputmodule import cli +from inputmodule.inputmodule import ledmatrix + +# Set the screen width and height for a 34 x 9 block game +block_width = 20 +block_height = 20 +COLS = 9 +ROWS = 34 + +WIDTH = COLS * block_width +HEIGHT = ROWS * block_height + +# Colors +black = (0, 0, 0) +white = (255, 255, 255) + +def opposite_direction(direction): + if direction == pygame.K_RIGHT: + return pygame.K_LEFT + elif direction == pygame.K_LEFT: + return pygame.K_RIGHT + elif direction == pygame.K_UP: + return pygame.K_DOWN + elif direction == pygame.K_DOWN: + return pygame.K_UP + return direction + +# Function to get the current board state +def get_board_state(board): + temp_board = [row[:] for row in board] + #off_x, off_y = current_pos + #for y, row in enumerate(current_shape): + # for x, cell in enumerate(row): + # if cell: + # if 0 <= off_y + y < ROWS and 0 <= off_x + x < COLS: + # temp_board[off_y + y][off_x + x] = 1 + return temp_board + +def draw_ledmatrix(board, devices): + for dev in devices: + matrix = [[0 for _ in range(34)] for _ in range(9)] + for y in range(ROWS): + for x in range(COLS): + matrix[x][y] = board[y][x] + ledmatrix.render_matrix(dev, matrix) + #vals = [0 for _ in range(39)] + #send_command(dev, CommandVals.Draw, vals) + +# Function to display the score using blocks +def display_score(board, score): + return + score_str = str(score) + start_x = COLS - len(score_str) * 4 + for i, digit in enumerate(score_str): + if digit.isdigit(): + digit = int(digit) + for y in range(5): + for x in range(3): + if digit_blocks[digit][y][x]: + if y < ROWS and start_x + i * 4 + x < COLS: + board[y][start_x + i * 4 + x] = 1 + +# Digit blocks for representing score +# Each number is represented in a 5x3 block matrix +digit_blocks = [ + [[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0 + [[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2 + [[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3 + [[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5 + [[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6 + [[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8 + [[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9 +] + + +class Snake: + # Function to draw a grid + def draw_grid(self): + for y in range(ROWS): + for x in range(COLS): + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect, 1) + + # Function to draw the game based on the board state + def draw_board(self, board, devices): + draw_ledmatrix(board, devices) + self.screen.fill(white) + for y in range(ROWS): + for x in range(COLS): + if board[y][x]: + rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height) + pygame.draw.rect(self.screen, black, rect) + self.draw_grid() + pygame.display.update() + + # Main game function + def gameLoop(self, devices): + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + + game_over = False + body = [] + score = 0 + head = (0, 0) + direction = pygame.K_DOWN + food = (0, 0) + while food == head: + food = (random.randint(0, COLS - 1), random.randint(0, ROWS - 1)) + move_time = 0 + + # Setting + # Wrap and let the snake come out the other side + WRAP = False + MOVE_PERIOD = 200 + + while not game_over: + # Draw the current board state + board_state = get_board_state(board) + display_score(board_state, score) + self.draw_board(board_state, devices) + + # Event handling + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game_over = True + + if event.type == pygame.KEYDOWN: + if event.key == opposite_direction(direction) and body: + continue + if event.key in [pygame.K_LEFT, pygame.K_h]: + direction = pygame.K_LEFT + elif event.key in [pygame.K_RIGHT, pygame.K_l]: + direction = pygame.K_RIGHT + elif event.key in [pygame.K_DOWN, pygame.K_j]: + direction = pygame.K_DOWN + elif event.key in [pygame.K_UP, pygame.K_k]: + direction = pygame.K_UP + + move_time += self.clock.get_time() + if move_time >= MOVE_PERIOD: + move_time = 0 + + # Update position + (x, y) = head + oldhead = head + if direction == pygame.K_LEFT: + head = (x - 1, y) + elif direction == pygame.K_RIGHT: + head = (x + 1, y) + elif direction == pygame.K_DOWN: + head = (x, y + 1) + elif direction == pygame.K_UP: + head = (x, y - 1) + + # Detect edge condition + (x, y) = head + if head in body: + game_over = True + elif x >= COLS or x < 0 or y >= ROWS or y < 0: + if WRAP: + if x >= COLS: + x = 0 + elif x < 0: + x = COLS - 1 + elif y >= ROWS: + y = 0 + elif y < 0: + y = ROWS - 1 + head = (x, y) + else: + game_over = True + elif head == food: + body.insert(0, oldhead) + while food == head: + food = (random.randint(0, COLS - 1), + random.randint(0, ROWS - 1)) + elif body: + body.pop() + body.insert(0, oldhead) + + # Draw on screen + if not game_over: + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + board[y][x] = 1 + board[food[1]][food[0]] = 1 + for bodypart in body: + (x, y) = bodypart + board[y][x] = 1 + + self.clock.tick(30) + + # Flash the screen twice before waiting for restart + for _ in range(2): + for dev in devices: + ledmatrix.percentage(dev, 0) + self.screen.fill(black) + pygame.display.update() + time.sleep(0.3) + + for dev in devices: + ledmatrix.percentage(dev, 100) + self.screen.fill(white) + pygame.display.update() + time.sleep(0.3) + + # Display final score and wait for restart without clearing the screen + board_state = get_board_state(board) + display_score(board_state, score) + self.draw_board(board_state, devices) + + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + waiting = False + game_over = True + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + waiting = False + if event.key == pygame.K_r: + board = [[0 for _ in range(COLS)] for _ in range(ROWS)] + gameLoop() + + pygame.quit() + quit() + + def __init__(self): + # Initialize pygame + pygame.init() + + # Create the screen + self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) + + # Clock to control the speed of the game + self.clock = pygame.time.Clock() + +def main_devices(devices): + snake = Snake() + snake.gameLoop(devices) + +def main(): + devices = cli.find_devs() + + snake = Snake() + snake.gameLoop(devices) + +if __name__ == "__main__": + main() diff --git a/python/inputmodule/inputmodule/__init__.py b/python/inputmodule/inputmodule/__init__.py new file mode 100644 index 00000000..7fb67f8b --- /dev/null +++ b/python/inputmodule/inputmodule/__init__.py @@ -0,0 +1,152 @@ +from enum import IntEnum +import serial + +# TODO: Make independent from GUI +from inputmodule.gui.gui_threading import disconnect_dev + +FWK_MAGIC = [0x32, 0xAC] +FWK_VID = 0x32AC +LED_MATRIX_PID = 0x20 +QTPY_PID = 0x001F +INPUTMODULE_PIDS = [LED_MATRIX_PID, QTPY_PID] + + +class CommandVals(IntEnum): + Brightness = 0x00 + Pattern = 0x01 + BootloaderReset = 0x02 + Sleep = 0x03 + Animate = 0x04 + Panic = 0x05 + Draw = 0x06 + StageGreyCol = 0x07 + DrawGreyColBuffer = 0x08 + SetText = 0x09 + StartGame = 0x10 + GameControl = 0x11 + GameStatus = 0x12 + SetColor = 0x13 + DisplayOn = 0x14 + InvertScreen = 0x15 + SetPixelColumn = 0x16 + FlushFramebuffer = 0x17 + ClearRam = 0x18 + ScreenSaver = 0x19 + SetFps = 0x1A + SetPowerMode = 0x1B + PwmFreq = 0x1E + DebugMode = 0x1F + Version = 0x20 + + +class Game(IntEnum): + Snake = 0x00 + Pong = 0x01 + Tetris = 0x02 + GameOfLife = 0x03 + + +class PatternVals(IntEnum): + Percentage = 0x00 + Gradient = 0x01 + DoubleGradient = 0x02 + DisplayLotus = 0x03 + ZigZag = 0x04 + FullBrightness = 0x05 + DisplayPanic = 0x06 + DisplayLotus2 = 0x07 + + +class GameOfLifeStartParam(IntEnum): + Currentmatrix = 0x00 + Pattern1 = 0x01 + Blinker = 0x02 + Toad = 0x03 + Beacon = 0x04 + Glider = 0x05 + + def __str__(self): + return self.name.lower() + + def __repr__(self): + return str(self) + + @staticmethod + def argparse(s): + try: + return GameOfLifeStartParam[s.lower().capitalize()] + except KeyError: + return s + + +class GameControlVal(IntEnum): + Up = 0 + Down = 1 + Left = 2 + Right = 3 + Quit = 4 + + +RESPONSE_SIZE = 32 + + +def bootloader_jump(dev): + """Reboot into the bootloader to flash new firmware""" + send_command(dev, CommandVals.BootloaderReset, [0x00]) + + +def brightness(dev, b: int): + """Adjust the brightness scaling of the entire screen.""" + send_command(dev, CommandVals.Brightness, [b]) + + +def get_brightness(dev): + """Adjust the brightness scaling of the entire screen.""" + res = send_command(dev, CommandVals.Brightness, with_response=True) + return int(res[0]) + + +def get_version(dev): + """Get the device's firmware version""" + res = send_command(dev, CommandVals.Version, with_response=True) + if not res: + return 'Unknown' + major = res[0] + minor = (res[1] & 0xF0) >> 4 + patch = res[1] & 0xF + pre_release = res[2] + + version = f"{major}.{minor}.{patch}" + if pre_release: + version += " (Pre-release)" + return version + + +def send_command(dev, command, parameters=[], with_response=False): + return send_command_raw(dev, FWK_MAGIC + [command] + parameters, with_response) + + +def send_command_raw(dev, command, with_response=False): + """Send a command to the device. + Opens new serial connection every time""" + # print(f"Sending command: {command}") + try: + with serial.Serial(dev.device, 115200) as s: + s.write(command) + + if with_response: + res = s.read(RESPONSE_SIZE) + # print(f"Received: {res}") + return res + except (IOError, OSError) as _ex: + disconnect_dev(dev.device) + # print("Error: ", ex) + + +def send_serial(dev, s, command): + """Send serial command by using existing serial connection""" + try: + s.write(command) + except (IOError, OSError) as _ex: + disconnect_dev(dev.device) + # print("Error: ", ex) diff --git a/python/inputmodule/inputmodule/b1display.py b/python/inputmodule/inputmodule/b1display.py new file mode 100644 index 00000000..f471edf4 --- /dev/null +++ b/python/inputmodule/inputmodule/b1display.py @@ -0,0 +1,160 @@ +import sys + +from inputmodule.inputmodule import send_command, CommandVals, FWK_MAGIC + +B1_WIDTH = 300 +B1_HEIGHT = 400 +GREYSCALE_DEPTH = 32 + +SCREEN_FPS = ["quarter", "half", "one", "two", + "four", "eight", "sixteen", "thirtytwo"] +HIGH_FPS_MASK = 0b00010000 +LOW_FPS_MASK = 0b00000111 + + +def b1image_bl(dev, image_file): + """Display an image in black and white + Confirmed working with PNG and GIF. + Must be 300x400 in size. + Sends one 400px column in a single commands and a flush at the end + """ + + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == B1_WIDTH + assert height == B1_HEIGHT + pixel_values = list(im.getdata()) + + for x in range(B1_WIDTH): + vals = [0 for _ in range(50)] + + byte = None + for y in range(B1_HEIGHT): + pixel = pixel_values[y * B1_WIDTH + x] + brightness = sum(pixel) / 3 + black = brightness < 0xFF / 2 + + bit = y % 8 + + if bit == 0: + byte = 0 + if black: + byte |= 1 << bit + + if bit == 7: + vals[int(y / 8)] = byte + + column_le = list((x).to_bytes(2, "little")) + command = FWK_MAGIC + [0x16] + column_le + vals + send_command(dev, command) + + # Flush + command = FWK_MAGIC + [0x17] + send_command(dev, command) + + +def display_string(dev, disp_str): + b = [ord(x) for x in disp_str] + send_command(dev, CommandVals.SetText, [len(disp_str)] + b) + + +def display_on_cmd(dev, on): + send_command(dev, CommandVals.DisplayOn, [on]) + + +def invert_screen_cmd(dev, invert): + send_command(dev, CommandVals.InvertScreen, [invert]) + + +def screen_saver_cmd(dev, on): + send_command(dev, CommandVals.ScreenSaver, [on]) + + +def set_fps_cmd(dev, mode): + res = send_command(dev, CommandVals.SetFps, with_response=True) + current_fps = res[0] + + if mode == "quarter": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "half": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b001 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "one": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b010 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "two": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b011 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "four": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b100 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "eight": + fps = current_fps & ~LOW_FPS_MASK + fps |= 0b101 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("low") + elif mode == "sixteen": + fps = current_fps & ~HIGH_FPS_MASK + fps |= 0b00000000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("high") + elif mode == "thirtytwo": + fps = current_fps & ~HIGH_FPS_MASK + fps |= 0b00010000 + send_command(dev, CommandVals.SetFps, [fps]) + set_power_mode_cmd("high") + + +def set_power_mode_cmd(dev, mode): + if mode == "low": + send_command(dev, CommandVals.SetPowerMode, [0]) + elif mode == "high": + send_command(dev, CommandVals.SetPowerMode, [1]) + else: + print("Unsupported power mode") + sys.exit(1) + + +def get_power_mode_cmd(dev): + res = send_command(dev, CommandVals.SetPowerMode, with_response=True) + current_mode = int(res[0]) + if current_mode == 0: + print("Current Power Mode: Low Power") + elif current_mode == 1: + print("Current Power Mode: High Power") + + +def get_fps_cmd(dev): + res = send_command(dev, CommandVals.SetFps, with_response=True) + current_fps = res[0] + res = send_command(dev, CommandVals.SetPowerMode, with_response=True) + current_mode = int(res[0]) + + if current_mode == 0: + current_fps &= LOW_FPS_MASK + if current_fps == 0: + fps = 0.25 + elif current_fps == 1: + fps = 0.5 + else: + fps = 2 ** (current_fps - 2) + elif current_mode == 1: + if current_fps & HIGH_FPS_MASK: + fps = 32 + else: + fps = 16 + + print(f"Current FPS: {fps}") diff --git a/python/inputmodule/inputmodule/c1minimal.py b/python/inputmodule/inputmodule/c1minimal.py new file mode 100644 index 00000000..7b7fbf49 --- /dev/null +++ b/python/inputmodule/inputmodule/c1minimal.py @@ -0,0 +1,35 @@ +from inputmodule.inputmodule import send_command, CommandVals + +RGB_COLORS = ["white", "black", "red", "green", + "blue", "cyan", "yellow", "purple"] + + +def get_color(dev): + res = send_command(dev, CommandVals.SetColor, with_response=True) + return (int(res[0]), int(res[1]), int(res[2])) + + +def set_color(dev, color): + rgb = None + if color == "white": + rgb = [0xFF, 0xFF, 0xFF] + elif color == "black": + rgb = [0x00, 0x00, 0x00] + elif color == "red": + rgb = [0xFF, 0x00, 0x00] + elif color == "green": + rgb = [0x00, 0xFF, 0x00] + elif color == "blue": + rgb = [0x00, 0x00, 0xFF] + elif color == "yellow": + rgb = [0xFF, 0xFF, 0x00] + elif color == "cyan": + rgb = [0x00, 0xFF, 0xFF] + elif color == "purple": + rgb = [0xFF, 0x00, 0xFF] + else: + print(f"Unknown color: {color}") + return + + if rgb: + send_command(dev, CommandVals.SetColor, rgb) diff --git a/python/inputmodule/inputmodule/ledmatrix.py b/python/inputmodule/inputmodule/ledmatrix.py new file mode 100644 index 00000000..7b351bdc --- /dev/null +++ b/python/inputmodule/inputmodule/ledmatrix.py @@ -0,0 +1,458 @@ +import time + +import serial + +from inputmodule import font +from inputmodule.inputmodule import ( + send_command, + CommandVals, + PatternVals, + FWK_MAGIC, + send_serial, + brightness, +) +from inputmodule.gui.gui_threading import get_status, set_status + +WIDTH = 9 +HEIGHT = 34 +PATTERNS = [ + "All LEDs on", + '"LOTUS" sideways', + "Gradient (0-13% Brightness)", + "Double Gradient (0-7-0% Brightness)", + "Zigzag", + '"PANIC"', + '"LOTUS" Top Down', + "All brightness levels (1 LED each)", + "Every Second Row", + "Every Third Row", + "Every Fourth Row", + "Every Fifth Row", + "Every Sixth Row", + "Every Second Col", + "Every Third Col", + "Every Fourth Col", + "Every Fifth Col", + "Checkerboard", + "Double Checkerboard", + "Triple Checkerboard", + "Quad Checkerboard", +] +PWM_FREQUENCIES = [ + "29kHz", + "3.6kHz", + "1.8kHz", + "900Hz", +] + + +def get_pwm_freq(dev): + """Adjust the brightness scaling of the entire screen.""" + res = send_command(dev, CommandVals.PwmFreq, with_response=True) + freq = int(res[0]) + if freq == 0: + return 29000 + elif freq == 1: + return 3600 + elif freq == 2: + return 1800 + elif freq == 3: + return 900 + else: + return None + + +def percentage(dev, p): + """Fill a percentage of the screen. Bottom to top""" + send_command(dev, CommandVals.Pattern, [PatternVals.Percentage, p]) + + +def animate(dev, b: bool): + """Tell the firmware to start/stop animation. + Scrolls the currently saved grid vertically down.""" + if b: + set_status('animate') + send_command(dev, CommandVals.Animate, [b]) + + +def get_animate(dev): + """Tell the firmware to start/stop animation. + Scrolls the currently saved grid vertically down.""" + res = send_command(dev, CommandVals.Animate, with_response=True) + return bool(res[0]) + + +def image_bl(dev, image_file): + """Display an image in black and white + Confirmed working with PNG and GIF. + Must be 9x34 in size. + Sends everything in a single command + """ + vals = [0 for _ in range(39)] + + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == 9 + assert height == 34 + pixel_values = list(im.getdata()) + for i, pixel in enumerate(pixel_values): + brightness = sum(pixel) / 3 + if brightness > 0xFF / 2: + vals[int(i / 8)] |= 1 << i % 8 + + send_command(dev, CommandVals.Draw, vals) + + +def camera(dev): + """Play a live view from the webcam, for fun""" + set_status('camera') + with serial.Serial(dev.device, 115200) as s: + import cv2 + + capture = cv2.VideoCapture(0) + ret, frame = capture.read() + + scale_y = HEIGHT / frame.shape[0] + + # Scale the video to 34 pixels height + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) + # Find the starting position to crop the width to be centered + # For very narrow videos, make sure to stay in bounds + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) + end_x = min(dim[1], start_x + WIDTH) + + # Pre-process the video into resized, cropped, grayscale frames + while get_status() == 'camera': + ret, frame = capture.read() + if not ret: + print("Failed to capture video frames") + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + resized = cv2.resize(gray, (dim[1], dim[0])) + cropped = resized[0:HEIGHT, start_x:end_x] + + for x in range(0, cropped.shape[1]): + vals = [0 for _ in range(HEIGHT)] + + for y in range(0, HEIGHT): + vals[y] = cropped[y, x] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def video(dev, video_file): + set_status('video') + """Resize and play back a video""" + with serial.Serial(dev.device, 115200) as s: + import cv2 + + capture = cv2.VideoCapture(video_file) + ret, frame = capture.read() + + scale_y = HEIGHT / frame.shape[0] + + # Scale the video to 34 pixels height + dim = (HEIGHT, int(round(frame.shape[1] * scale_y))) + # Find the starting position to crop the width to be centered + # For very narrow videos, make sure to stay in bounds + start_x = max(0, int(round(dim[1] / 2 - WIDTH / 2))) + end_x = min(dim[1], start_x + WIDTH) + + processed = [] + + # Pre-process the video into resized, cropped, grayscale frames + while get_status() == 'video': + ret, frame = capture.read() + if not ret: + print("Failed to read video frames") + break + + gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + + resized = cv2.resize(gray, (dim[1], dim[0])) + cropped = resized[0:HEIGHT, start_x:end_x] + + processed.append(cropped) + + # Write it out to the module one frame at a time + # TODO: actually control for framerate + for frame in processed: + for x in range(0, cropped.shape[1]): + vals = [0 for _ in range(HEIGHT)] + + for y in range(0, HEIGHT): + vals[y] = frame[y, x] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def pixel_to_brightness(pixel): + """Calculate pixel brightness from an RGB triple""" + assert len(pixel) == 3 + brightness = sum(pixel) / len(pixel) + + # Poor man's scaling to make the greyscale pop better. + # Should find a good function. + if brightness > 200: + brightness = brightness + elif brightness > 150: + brightness = brightness * 0.8 + elif brightness > 100: + brightness = brightness * 0.5 + elif brightness > 50: + brightness = brightness + else: + brightness = brightness * 2 + + return int(brightness) + + +def image_greyscale(dev, image_file): + """Display an image in greyscale + Sends each 1x34 column and then commits => 10 commands + """ + with serial.Serial(dev.device, 115200) as s: + from PIL import Image + + im = Image.open(image_file).convert("RGB") + width, height = im.size + assert width == 9 + assert height == 34 + pixel_values = list(im.getdata()) + for x in range(0, WIDTH): + vals = [0 for _ in range(HEIGHT)] + + for y in range(HEIGHT): + vals[y] = pixel_to_brightness(pixel_values[x + y * WIDTH]) + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def send_col(dev, s, x, vals): + """Stage greyscale values for a single column. Must be committed with commit_cols()""" + command = FWK_MAGIC + [CommandVals.StageGreyCol, x] + vals + send_serial(dev, s, command) + + +def commit_cols(dev, s): + """Commit the changes from sending individual cols with send_col(), displaying the matrix. + This makes sure that the matrix isn't partially updated.""" + command = FWK_MAGIC + [CommandVals.DrawGreyColBuffer, 0x00] + send_serial(dev, s, command) + + +def checkerboard(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = (([0xFF] * n) + ([0x00] * n)) * int(HEIGHT / 2) + if x % (n * 2) < n: + # Rotate once + vals = vals[n:] + vals[:n] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def every_nth_col(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [(0xFF if x % n == 0 else 0) for _ in range(HEIGHT)] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def every_nth_row(dev, n): + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [(0xFF if y % n == 0 else 0) for y in range(HEIGHT)] + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def all_brightnesses(dev): + """Increase the brightness with each pixel. + Only 0-255 available, so it can't fill all 306 LEDs""" + with serial.Serial(dev.device, 115200) as s: + for x in range(0, WIDTH): + vals = [0 for _ in range(HEIGHT)] + + for y in range(HEIGHT): + brightness = x + WIDTH * y + if brightness > 255: + vals[y] = 0 + else: + vals[y] = brightness + + send_col(dev, s, x, vals) + commit_cols(dev, s) + + +def breathing(dev): + """Animate breathing brightness. + Keeps currently displayed grid""" + set_status('breathing') + # Bright ranges appear similar, so we have to go through those faster + while get_status() == 'breathing': + # Go quickly from 250 to 50 + for i in range(10): + time.sleep(0.03) + brightness(dev, 250 - i * 20) + + # Go slowly from 50 to 0 + for i in range(10): + time.sleep(0.06) + brightness(dev, 50 - i * 5) + + # Go slowly from 0 to 50 + for i in range(10): + time.sleep(0.06) + brightness(dev, i * 5) + + # Go quickly from 50 to 250 + for i in range(10): + time.sleep(0.03) + brightness(dev, 50 + i * 20) + + +def eq(dev, vals): + """Display 9 values in equalizer diagram starting from the middle, going up and down""" + matrix = [[0 for _ in range(34)] for _ in range(9)] + + for col, val in enumerate(vals[:9]): + row = int(34 / 2) + above = int(val / 2) + below = val - above + + for i in range(above): + matrix[col][row + i] = 0xFF + for i in range(below): + matrix[col][row - 1 - i] = 0xFF + + render_matrix(dev, matrix) + + +def render_matrix(dev, matrix): + """Show a black/white matrix + Send everything in a single command""" + vals = [0x00 for _ in range(39)] + + for x in range(9): + for y in range(34): + i = x + 9 * y + if matrix[x][y]: + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) + + send_command(dev, CommandVals.Draw, vals) + + +def light_leds(dev, leds): + """Light a specific number of LEDs""" + vals = [0x00 for _ in range(39)] + for byte in range(int(leds / 8)): + vals[byte] = 0xFF + for i in range(leds % 8): + vals[int(leds / 8)] += 1 << i + send_command(dev, CommandVals.Draw, vals) + + +def pwm_freq(dev, freq): + """Display a pattern that's already programmed into the firmware""" + if freq == "29kHz": + send_command(dev, CommandVals.PwmFreq, [0]) + elif freq == "3.6kHz": + send_command(dev, CommandVals.PwmFreq, [1]) + elif freq == "1.8kHz": + send_command(dev, CommandVals.PwmFreq, [2]) + elif freq == "900Hz": + send_command(dev, CommandVals.PwmFreq, [3]) + + +def pattern(dev, p): + """Display a pattern that's already programmed into the firmware""" + if p == "All LEDs on": + send_command(dev, CommandVals.Pattern, [PatternVals.FullBrightness]) + elif p == "Gradient (0-13% Brightness)": + send_command(dev, CommandVals.Pattern, [PatternVals.Gradient]) + elif p == "Double Gradient (0-7-0% Brightness)": + send_command(dev, CommandVals.Pattern, [PatternVals.DoubleGradient]) + elif p == '"LOTUS" sideways': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus]) + elif p == "Zigzag": + send_command(dev, CommandVals.Pattern, [PatternVals.ZigZag]) + elif p == '"PANIC"': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayPanic]) + elif p == '"LOTUS" Top Down': + send_command(dev, CommandVals.Pattern, [PatternVals.DisplayLotus2]) + elif p == "All brightness levels (1 LED each)": + all_brightnesses(dev) + elif p == "Every Second Row": + every_nth_row(dev, 2) + elif p == "Every Third Row": + every_nth_row(dev, 3) + elif p == "Every Fourth Row": + every_nth_row(dev, 4) + elif p == "Every Fifth Row": + every_nth_row(dev, 5) + elif p == "Every Sixth Row": + every_nth_row(dev, 6) + elif p == "Every Second Col": + every_nth_col(dev, 2) + elif p == "Every Third Col": + every_nth_col(dev, 3) + elif p == "Every Fourth Col": + every_nth_col(dev, 4) + elif p == "Every Fifth Col": + every_nth_col(dev, 4) + elif p == "Checkerboard": + checkerboard(dev, 1) + elif p == "Double Checkerboard": + checkerboard(dev, 2) + elif p == "Triple Checkerboard": + checkerboard(dev, 3) + elif p == "Quad Checkerboard": + checkerboard(dev, 4) + else: + print("Invalid pattern") + + +def show_string(dev, s): + """Render a string with up to five letters""" + show_font(dev, [font.convert_font(letter) for letter in str(s)[:5]]) + + +def show_font(dev, font_items): + """Render up to five 5x6 pixel font items""" + vals = [0x00 for _ in range(39)] + + for digit_i, digit_pixels in enumerate(font_items): + offset = digit_i * 7 + for pixel_x in range(5): + for pixel_y in range(6): + pixel_value = digit_pixels[pixel_x + pixel_y * 5] + i = (2 + pixel_x) + (9 * (pixel_y + offset)) + if pixel_value: + vals[int(i / 8)] = vals[int(i / 8)] | (1 << i % 8) + + send_command(dev, CommandVals.Draw, vals) + + +def show_symbols(dev, symbols): + """Render a list of up to five symbols + Can use letters/numbers or symbol names, like 'sun', ':)'""" + font_items = [] + for symbol in symbols: + s = font.convert_symbol(symbol) + if not s: + s = font.convert_font(symbol) + font_items.append(s) + + show_font(dev, font_items) diff --git a/python/inputmodule/uf2conv.py b/python/inputmodule/uf2conv.py new file mode 100644 index 00000000..e545b69c --- /dev/null +++ b/python/inputmodule/uf2conv.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: MIT +# Copyright (c) Microsoft Corporation and others +# Taken from: https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py +# And modified, some changes already upstreamed + +# yapf: disable +import sys +import struct +import subprocess +import re +import os +import os.path +import argparse +import json + +# Don't even need -b. hex has this embedded +# > ./util/uf2conv.py .build/framework_ansi_default.hex -o ansi.uf2 -b 0x10000000 -f rp2040 --convert --blocks-reserved 1 +# Converted to 222 blocks +# Converted to uf2, output size: 113664, start address: 0x10000000 +# Wrote 113664 bytes to ansi.uf2 +# # 113664 / 512 = 222 +# +# > ./util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert --blocks-offset 222 +# Converted to 1 blocks +# Converted to uf2, output size: 512, start address: 0x100ff000 +# Wrote 512 bytes to serial.uf2 + + + +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +INFO_FILE = "/INFO_UF2.TXT" + +appstartaddr = 0x2000 +familyid = 0x0 + + +def is_uf2(buf): + w = struct.unpack(" 476: + assert False, "Invalid UF2 data size at " + ptr + newaddr = hd[3] + if (hd[2] & 0x2000) and (currfamilyid == None): + currfamilyid = hd[7] + if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid): + currfamilyid = hd[7] + curraddr = newaddr + if familyid == 0x0 or familyid == hd[7]: + appstartaddr = newaddr + print(f" flags: 0x{hd[2]:02x}") + print(f" addr: 0x{hd[3]:02x}") + print(f" len: {hd[4]}") + print(f" block no: {hd[5]}") + print(f" blocks: {hd[6]}") + print(f" size/famid: {hd[7]}") + print() + padding = newaddr - curraddr + if padding < 0: + assert False, "Block out of order at " + ptr + if padding > 10*1024*1024: + assert False, "More than 10M of padding needed at " + ptr + if padding % 4 != 0: + assert False, "Non-word padding size at " + ptr + while padding > 0: + padding -= 4 + outp.append(b"\x00\x00\x00\x00") + if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]): + outp.append(block[32 : 32 + datalen]) + curraddr = newaddr + datalen + if hd[2] & 0x2000: + if hd[7] in families_found.keys(): + if families_found[hd[7]] > newaddr: + families_found[hd[7]] = newaddr + else: + families_found[hd[7]] = newaddr + if prev_flag == None: + prev_flag = hd[2] + if prev_flag != hd[2]: + all_flags_same = False + if blockno == (numblocks - 1): + print("--- UF2 File Header Info ---") + families = load_families() + for family_hex in families_found.keys(): + family_short_name = "" + for name, value in families.items(): + if value == family_hex: + family_short_name = name + print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex)) + print("Target Address is 0x{:08x}".format(families_found[family_hex])) + if all_flags_same: + print("All block flag values consistent, 0x{:04x}".format(hd[2])) + else: + print("Flags were not all the same") + print("----------------------------") + if len(families_found) > 1 and familyid == 0x0: + outp = [] + appstartaddr = 0x0 + return b"".join(outp) + +def convert_to_carray(file_content): + outp = "const unsigned long bindata_len = %d;\n" % len(file_content) + outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {" + for i in range(len(file_content)): + if i % 16 == 0: + outp += "\n" + outp += "0x%02x, " % file_content[i] + outp += "\n};\n" + return bytes(outp, "utf-8") + +def convert_to_uf2(file_content, blocks_reserved=0, blocks_offset=0): + global familyid + datapadding = b"" + while len(datapadding) < 512 - 256 - 32 - 4: + datapadding += b"\x00\x00\x00\x00" + numblocks = (len(file_content) + 255) // 256 + outp = [] + for blockno in range(numblocks): + ptr = 256 * blockno + chunk = file_content[ptr:ptr + 256] + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(b"= 3 and words[1] == "2" and words[2] == "FAT": + drives.append(words[0]) + else: + rootpath = "/media" + if sys.platform == "darwin": + rootpath = "/Volumes" + elif sys.platform == "linux": + tmp = rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + tmp = "/run" + rootpath + "/" + os.environ["USER"] + if os.path.isdir(tmp): + rootpath = tmp + for d in os.listdir(rootpath): + drives.append(os.path.join(rootpath, d)) + + + def has_info(d): + try: + return os.path.isfile(d + INFO_FILE) + except: + return False + + return list(filter(has_info, drives)) + + +def board_id(path): + with open(path + INFO_FILE, mode='r') as file: + file_content = file.read() + return re.search("Board-ID: ([^\r\n]*)", file_content).group(1) + + +def list_drives(): + for d in get_drives(): + print(d, board_id(d)) + + +def write_file(name, buf): + with open(name, "wb") as f: + f.write(buf) + print("Wrote %d bytes to %s" % (len(buf), name)) + + +def load_families(): + # The expectation is that the `uf2families.json` file is in the same + # directory as this script. Make a path that works using `__file__` + # which contains the full path to this script. + filename = "uf2families.json" + pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + with open(pathname) as f: + raw_families = json.load(f) + + families = {} + for family in raw_families: + families[family["short_name"]] = int(family["id"], 0) + + return families + + +def main(): + global appstartaddr, familyid + def error(msg): + print(msg, file=sys.stderr) + sys.exit(1) + parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.') + parser.add_argument('input', metavar='INPUT', type=str, nargs='?', + help='input file (HEX, BIN or UF2)') + parser.add_argument('-b' , '--base', dest='base', type=str, + default="0x2000", + help='set base address of application for BIN format (default: 0x2000)') + parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str, + help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible') + parser.add_argument('-d' , '--device', dest="device_path", + help='select a device path to flash') + parser.add_argument('-l' , '--list', action='store_true', + help='list connected devices') + parser.add_argument('-c' , '--convert', action='store_true', + help='do not flash, just convert') + parser.add_argument('-D' , '--deploy', action='store_true', + help='just flash, do not convert') + parser.add_argument('-f' , '--family', dest='family', type=str, + default="0x0", + help='specify familyID - number or name (default: 0x0)') + parser.add_argument('--blocks-offset', dest='blocks_offset', type=str, + default="0x0", + help='TODO') + parser.add_argument('--blocks-reserved', dest='blocks_reserved', type=str, + default="0x0", + help='TODO') + parser.add_argument('-C' , '--carray', action='store_true', + help='convert binary file to a C array, not UF2') + parser.add_argument('-i', '--info', action='store_true', + help='display header information from UF2, do not convert') + args = parser.parse_args() + appstartaddr = int(args.base, 0) + blocks_offset = int(args.blocks_offset, 0) + blocks_reserved = int(args.blocks_reserved, 0) + + families = load_families() + + if args.family.upper() in families: + familyid = families[args.family.upper()] + else: + try: + familyid = int(args.family, 0) + except ValueError: + error("Family ID needs to be a number or one of: " + ", ".join(families.keys())) + + if args.list: + list_drives() + else: + if not args.input: + error("Need input file") + with open(args.input, mode='rb') as f: + inpbuf = f.read() + from_uf2 = is_uf2(inpbuf) + ext = "uf2" + if args.deploy: + outbuf = inpbuf + elif from_uf2 and not args.info: + outbuf = convert_from_uf2(inpbuf) + ext = "bin" + elif from_uf2 and args.info: + outbuf = "" + convert_from_uf2(inpbuf) + + elif is_hex(inpbuf): + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), blocks_reserved, blocks_offset) + elif args.carray: + outbuf = convert_to_carray(inpbuf) + ext = "h" + else: + outbuf = convert_to_uf2(inpbuf, blocks_reserved, blocks_offset) + if not args.deploy and not args.info: + print("Converted to %s, output size: %d, start address: 0x%x" % + (ext, len(outbuf), appstartaddr)) + if args.convert or ext != "uf2": + drives = [] + if args.output == None: + args.output = "flash." + ext + else: + drives = get_drives() + + if args.output: + write_file(args.output, outbuf) + else: + if len(drives) == 0: + error("No drive to deploy.") + if outbuf: + for d in drives: + print("Flashing %s (%s)" % (d, board_id(d))) + write_file(d + "/NEW.UF2", outbuf) + + +if __name__ == "__main__": + main() diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..32b41083 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "inputmodule" +# TODO: Dynamic version from git (requires tags) +#dynamic = ["version"] +version = "0.1.1" +description = 'A library to control input modules on the Framework 16 Laptop' +# TODO: Custom README for python project +readme = "README.md" +requires-python = ">=3.7" +license = { text = "MIT" } +keywords = [ + "hatch", +] +authors = [ + { name = "Daniel Schaefer", email = "dhs@frame.work" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +dependencies = [ + "pyserial", + # Optional for CLI + "getkey", + # Optional for GUI + "pygame", + # Optional for image operations + "Pillow", +] + +[project.urls] +Issues = "https://github.com/FrameworkComputer/inputmodule-rs/issues" +Source = "https://github.com/FrameworkComputer/inputmodule-rs" + +[project.scripts] +ledmatrixctl = "inputmodule.cli:main_cli" + +[project.gui-scripts] +ledmatrixgui = "inputmodule.cli:main_gui" + +#[tool.hatch.version] +#source = "vcs" +# +#[tool.hatch.build.hooks.vcs] +#version-file = "inputmodule/_version.py" + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github", +] + +# TODO: Maybe typing with mypy +# [tool.hatch.build.targets.wheel.hooks.mypyc] +# enable-by-default = false +# dependencies = ["hatch-mypyc>=0.14.1"] +# require-runtime-dependencies = true +# mypy-args = [ +# "--no-warn-unused-ignores", +# ] +# +# [tool.mypy] +# disallow_untyped_defs = false +# follow_imports = "normal" +# ignore_missing_imports = true +# pretty = true +# show_column_numbers = true +# warn_no_return = false +# warn_unused_ignores = true diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 00000000..52366a88 --- /dev/null +++ b/python/requirements.txt @@ -0,0 +1,4 @@ +get-key==1.60.0 +Pillow==10.0.0 +pyserial==3.5 +pygame==2.6.1 diff --git a/qtpy/Cargo.toml b/qtpy/Cargo.toml new file mode 100644 index 00000000..39e128f9 --- /dev/null +++ b/qtpy/Cargo.toml @@ -0,0 +1,38 @@ +[package] +edition = "2021" +name = "qtpy" +version = "0.2.0" + +[dependencies] +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true + +defmt.workspace = true +defmt-rtt.workspace = true + +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true + +# Not using an external BSP, we've got the Framework Laptop 16 BSPs locally in this crate +rp2040-hal.workspace = true +rp2040-boot2.workspace = true + +# USB Serial +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true + +# C1 Minimal +smart-leds.workspace = true +ws2812-pio.workspace = true + +# QT Py +adafruit-qt-py-rp2040 = "0.6.0" + +[dependencies.fl16-inputmodules] +path = "../fl16-inputmodules" +# Same feature as c1minimal +features = [ "c1minimal", "qtpy" ] diff --git a/qtpy/Makefile.toml b/qtpy/Makefile.toml new file mode 100644 index 00000000..54d98f28 --- /dev/null +++ b/qtpy/Makefile.toml @@ -0,0 +1,12 @@ +extend = "../Makefile.toml" + +[tasks.uf2] +command = "elf2uf2-rs" +args = ["../target/thumbv6m-none-eabi/release/qtpy", "../target/thumbv6m-none-eabi/release/qtpy.uf2"] +dependencies = ["build-release"] +install_crate = "elf2uf2-rs" + +[tasks.bin] +command = "llvm-objcopy" +args = ["-Obinary", "../target/thumbv6m-none-eabi/release/qtpy", "../target/thumbv6m-none-eabi/release/qtpy.bin"] +dependencies = ["build-release"] diff --git a/qtpy/README.md b/qtpy/README.md new file mode 100644 index 00000000..526807c6 --- /dev/null +++ b/qtpy/README.md @@ -0,0 +1,18 @@ +## QT PY RP2040 + +**NOT** an official Framework module. +Just reference firmware that's easy to get started with, without having a +Framework module. Has GPIO and WS2812/Neopixel compatible RGB LED. + +When booting up this LED is lit in green color. +Its color and brightness can be controlled via the commands: + +```sh +> ./ledmatrix_control.py --brightness 255 +> ./ledmatrix_control.py --get-brightness +Current brightness: 255 + +> ./ledmatrix_control.py --set-color yellow +> ./ledmatrix_control.py --get-color +Current color: RGB:(255, 255, 0) +``` diff --git a/qtpy/src/main.rs b/qtpy/src/main.rs new file mode 100644 index 00000000..1ba7cd1a --- /dev/null +++ b/qtpy/src/main.rs @@ -0,0 +1,199 @@ +//! QT PY RP2040 with Framework 16 Input Module Firmware +//! +//! Neopixel/WS2812 compatible RGB LED is connected to GPIO12. +//! This pin doesn't support SPI TX. +//! It does support UART TX, but that output would have to be inverted. +//! So instead we use PIO to drive the LED. +#![no_std] +#![no_main] +#![allow(clippy::needless_range_loop)] + +use bsp::entry; +use cortex_m::delay::Delay; +use defmt_rtt as _; + +use rp2040_hal::gpio::bank0::Gpio12; +use rp2040_hal::pio::PIOExt; +//#[cfg(debug_assertions)] +//use panic_probe as _; +use rp2040_panic_usb_boot as _; + +// Provide an alias for our BSP so we can switch targets quickly. +// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. +use adafruit_qt_py_rp2040 as bsp; +//use rp_pico as bsp; + +use bsp::hal::{ + clocks::{init_clocks_and_plls, Clock}, + gpio::PinState, + pac, + sio::Sio, + usb, + watchdog::Watchdog, + Timer, +}; + +// USB Device support +use usb_device::{class_prelude::*, prelude::*}; + +// USB Communications Class Device support +use usbd_serial::{SerialPort, USB_CLASS_CDC}; + +// Used to demonstrate writing formatted strings +// use core::fmt::Write; +// use heapless::String; + +// RGB LED +use smart_leds::{colors, SmartLedsWrite, RGB8}; +pub type Ws2812<'a> = ws2812_pio::Ws2812< + crate::pac::PIO0, + rp2040_hal::pio::SM0, + rp2040_hal::timer::CountDown<'a>, + Gpio12, +>; + +use fl16_inputmodules::control::*; +use fl16_inputmodules::serialnum::device_release; + +const FRAMEWORK_VID: u16 = 0x32AC; +const COMMUNITY_PID: u16 = 0x001F; + +#[entry] +fn main() -> ! { + let mut pac = pac::Peripherals::take().unwrap(); + let core = pac::CorePeripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + let sio = Sio::new(pac.SIO); + + let clocks = init_clocks_and_plls( + bsp::XOSC_CRYSTAL_FREQ, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); + + let pins = bsp::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Set up the USB driver + let usb_bus = UsbBusAllocator::new(usb::UsbBus::new( + pac.USBCTRL_REGS, + pac.USBCTRL_DPRAM, + clocks.usb_clock, + true, + &mut pac.RESETS, + )); + + // Set up the USB Communications Class Device driver + let mut serial = SerialPort::new(&usb_bus); + + let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(FRAMEWORK_VID, COMMUNITY_PID)) + .manufacturer("Adafruit") + .product("QT PY - Framework 16 Inputmodule FW") + .max_power(500) + .device_release(device_release()) + .device_class(USB_CLASS_CDC) + .build(); + + let mut state = C1MinimalState { + sleeping: SimpleSleepState::Awake, + color: colors::GREEN, + brightness: 10, + }; + + let timer = Timer::new(pac.TIMER, &mut pac.RESETS); + let mut prev_timer = timer.get_counter().ticks(); + + pins.neopixel_power + .into_push_pull_output_in_state(PinState::High); + let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS); + let mut ws2812: Ws2812 = ws2812_pio::Ws2812::new( + pins.neopixel_data.into_mode(), + &mut pio, + sm0, + clocks.peripheral_clock.freq(), + timer.count_down(), + ); + + ws2812 + .write(smart_leds::brightness( + [state.color].iter().cloned(), + state.brightness, + )) + .unwrap(); + + loop { + // Handle period LED updates. Don't do it too often or USB will get stuck + if timer.get_counter().ticks() > prev_timer + 20_000 { + // TODO: Can do animations here + prev_timer = timer.get_counter().ticks(); + } + + // Check for new data + if usb_dev.poll(&mut [&mut serial]) { + let mut buf = [0u8; 64]; + match serial.read(&mut buf) { + Err(_e) => { + // Do nothing + } + Ok(0) => { + // Do nothing + } + Ok(count) => { + if let Some(command) = parse_command(count, &buf) { + if let Command::Sleep(go_sleeping) = command { + handle_sleep(go_sleeping, &mut state, &mut delay, &mut ws2812); + } else if let SimpleSleepState::Awake = state.sleeping { + // While sleeping no command is handled, except waking up + if let Some(response) = + handle_command(&command, &mut state, &mut ws2812) + { + let _ = serial.write(&response); + }; + } + } + } + } + } + } +} + +fn handle_sleep( + go_sleeping: bool, + state: &mut C1MinimalState, + _delay: &mut Delay, + ws2812: &mut impl SmartLedsWrite, +) { + match (state.sleeping.clone(), go_sleeping) { + (SimpleSleepState::Awake, false) => (), + (SimpleSleepState::Awake, true) => { + state.sleeping = SimpleSleepState::Sleeping; + + // Turn off LED + ws2812.write([colors::BLACK].iter().cloned()).unwrap(); + } + (SimpleSleepState::Sleeping, true) => (), + (SimpleSleepState::Sleeping, false) => { + state.sleeping = SimpleSleepState::Awake; + + // Turn LED back on + ws2812 + .write(smart_leds::brightness( + [state.color].iter().cloned(), + state.brightness, + )) + .unwrap(); + } + } +} diff --git a/release/50-framework-inputmodule.rules b/release/50-framework-inputmodule.rules new file mode 100644 index 00000000..08d4f983 --- /dev/null +++ b/release/50-framework-inputmodule.rules @@ -0,0 +1,8 @@ +# Framework Laptop 16 - LED Matrix +SUBSYSTEMS=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0020", MODE="0660", TAG+="uaccess" + +# B1 Display (Experimental prototype, not a product) +SUBSYSTEMS=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0021", MODE="0660", TAG+="uaccess" + +# C1 Minimal Microcontroller Module (Template for DIY Module) +SUBSYSTEMS=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0022", MODE="0660", TAG+="uaccess" diff --git a/res/framework_startmenuicon.ico b/res/framework_startmenuicon.ico new file mode 100644 index 00000000..8c6be29a Binary files /dev/null and b/res/framework_startmenuicon.ico differ diff --git a/greyscale.gif b/res/greyscale.gif similarity index 100% rename from greyscale.gif rename to res/greyscale.gif diff --git a/res/ledmatrixgui-home.png b/res/ledmatrixgui-home.png new file mode 100644 index 00000000..c0a5ca49 Binary files /dev/null and b/res/ledmatrixgui-home.png differ diff --git a/stripe.gif b/res/stripe.gif similarity index 100% rename from stripe.gif rename to res/stripe.gif diff --git a/stripe.png b/res/stripe.png similarity index 100% rename from stripe.png rename to res/stripe.png diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..460b3c4b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.74.0" +targets = ["thumbv6m-none-eabi"] +components = ["clippy", "rustfmt"] diff --git a/scripts/serial.py b/scripts/serial.py new file mode 100755 index 00000000..aca3396d --- /dev/null +++ b/scripts/serial.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import zlib + +ledmatrix_1 = b'FRAKDEAM1100000000' # POC 1 +ledmatrix_2 = b'FRAKDEBZ4100000000' # EVT 1, config 1 +ledmatrix_3 = b'FRAKDEBZ4200000000' # EVT 1, config 2 (27k resistor) +ansi_keyboard = b'FRAKDWEN4100000000' # EVT 1, config 1 (US ANSI) +rgb_keyboard = b'FRAKDKEN4100000000' # EVT 1, config 1 (US ANSI) +iso_keyboard = b'FRAKDWEN4200000000' # EVT 1, config 2 (UK ISO) +jis_keyboard = b'FRAKDWEN4J00000000' # EVT 1, config J (JIS) +numpad = b'FRAKDMEN4100000000' # EVT 1, config 1 +macropad = b'FRAKDNEN4100000000' # EVT 1, config 1 + +# This section is for modifying +selected = ledmatrix_2 +year = b'3' # 2023 +week = b'01' +day = b'1' +part_sn = b'0001' + +config = selected[8:10] +serial_rev = b'\x01' +snum = selected +print(serial_rev + snum) +snum = snum[0:8] + config + year + week + day + part_sn + +checksum = zlib.crc32(serial_rev + snum) +print(serial_rev + snum) + +print('Checksum:', hex(zlib.crc32(snum))) +print('Digest: ', hex(checksum)) +with open('serial.bin', 'wb') as f: + f.write(serial_rev) + f.write(snum) + f.write(checksum.to_bytes(4, 'little')) diff --git a/scripts/serial.sh b/scripts/serial.sh new file mode 100755 index 00000000..fb5c10a2 --- /dev/null +++ b/scripts/serial.sh @@ -0,0 +1,3 @@ +#!/bin/sh +./serial.py +../qmk_firmware/util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert diff --git a/src/control.rs b/src/control.rs deleted file mode 100644 index 5de1495d..00000000 --- a/src/control.rs +++ /dev/null @@ -1,161 +0,0 @@ -use rp2040_hal::rom_data::reset_to_usb_boot; - -use crate::{patterns::*, State}; - -pub enum _CommandVals { - _Brightness = 0x00, - _Pattern = 0x01, - _BootloaderReset = 0x02, - _Sleep = 0x03, - _Animate = 0x04, - _Panic = 0x05, - _Draw = 0x06, - _StageGreyCol = 0x07, - _DrawGreyColBuffer = 0x08, -} - -pub enum PatternVals { - _Percentage = 0x00, - Gradient, - DoubleGradient, - DisplayLotus, - ZigZag, - FullBrightness, - DisplayPanic, - DisplayLotus2, -} - -pub enum Command { - /// Set brightness scaling - Brightness(u8), - /// Display pre-programmed pattern - Pattern(PatternVals), - /// Reset into bootloader - BootloaderReset, - /// Light up a percentage of the screen - Percentage(u8), - /// Go to sleepe or wake up - Sleep(bool), - /// Start/stop animation (vertical scrolling) - Animate(bool), - /// Panic. Just to test what happens - Panic, - /// Draw black/white on the grid - Draw([u8; DRAW_BYTES]), - StageGreyCol(u8, [u8; HEIGHT]), - DrawGreyColBuffer, - _Unknown, -} - -pub fn parse_command(count: usize, buf: &[u8]) -> Option { - if count >= 4 && buf[0] == 0x32 && buf[1] == 0xAC { - let command = buf[2]; - let arg = buf[3]; - - //let mut text: String<64> = String::new(); - //writeln!(&mut text, "Command: {command}, arg: {arg}").unwrap(); - //let _ = serial.write(text.as_bytes()); - - match command { - 0x00 => Some(Command::Brightness(arg)), - 0x01 => match arg { - 0x00 => { - if count >= 5 { - Some(Command::Percentage(buf[4])) - } else { - None - } - } - 0x01 => Some(Command::Pattern(PatternVals::Gradient)), - 0x02 => Some(Command::Pattern(PatternVals::DoubleGradient)), - 0x03 => Some(Command::Pattern(PatternVals::DisplayLotus)), - 0x04 => Some(Command::Pattern(PatternVals::ZigZag)), - 0x05 => Some(Command::Pattern(PatternVals::FullBrightness)), - 0x06 => Some(Command::Pattern(PatternVals::DisplayPanic)), - 0x07 => Some(Command::Pattern(PatternVals::DisplayLotus2)), - _ => None, - }, - 0x02 => Some(Command::BootloaderReset), - 0x03 => Some(Command::Sleep(arg == 1)), - 0x04 => Some(Command::Animate(arg == 1)), - 0x05 => Some(Command::Panic), - 0x06 => { - if count >= 3 + DRAW_BYTES { - let mut bytes = [0; DRAW_BYTES]; - bytes.clone_from_slice(&buf[3..3 + DRAW_BYTES]); - Some(Command::Draw(bytes)) - } else { - None - } - } - 0x07 => { - if count >= 3 + 1 + HEIGHT { - let mut bytes = [0; HEIGHT]; - bytes.clone_from_slice(&buf[4..4 + HEIGHT]); - Some(Command::StageGreyCol(buf[3], bytes)) - } else { - None - } - } - 0x08 => Some(Command::DrawGreyColBuffer), - _ => None, //Some(Command::Unknown), - } - } else { - None - } -} - -pub fn handle_command(command: &Command, state: &mut State, matrix: &mut Foo) { - match command { - Command::Brightness(br) => { - //let _ = serial.write("Brightness".as_bytes()); - state.brightness = *br; - matrix - .set_scaling(state.brightness) - .expect("failed to set scaling"); - } - Command::Percentage(p) => { - //let p = if count >= 5 { buf[4] } else { 100 }; - state.grid = percentage(*p as u16); - } - Command::Pattern(pattern) => { - //let _ = serial.write("Pattern".as_bytes()); - match pattern { - PatternVals::Gradient => state.grid = gradient(), - PatternVals::DoubleGradient => state.grid = double_gradient(), - PatternVals::DisplayLotus => state.grid = display_lotus(), - PatternVals::ZigZag => state.grid = zigzag(), - PatternVals::FullBrightness => { - state.grid = percentage(100); - state.brightness = 0xFF; - matrix - .set_scaling(state.brightness) - .expect("failed to set scaling"); - } - PatternVals::DisplayPanic => state.grid = display_panic(), - PatternVals::DisplayLotus2 => state.grid = display_lotus2(), - _ => {} - } - } - Command::BootloaderReset => { - //let _ = serial.write("Bootloader Reset".as_bytes()); - reset_to_usb_boot(0, 0); - } - Command::Sleep(_go_sleeping) => { - // Handled elsewhere - } - Command::Animate(a) => state.animate = *a, - Command::Panic => panic!("Ahhh"), - Command::Draw(vals) => state.grid = draw(vals), - Command::StageGreyCol(col, vals) => { - draw_grey_col(&mut state.col_buffer, *col, vals); - } - Command::DrawGreyColBuffer => { - // Copy the staging buffer to the real grid and display it - state.grid = state.col_buffer.clone(); - // Zero the old staging buffer, just for good measure. - state.col_buffer = percentage(0); - } - _ => {} - } -} diff --git a/src/lotus.rs b/src/lotus.rs deleted file mode 100644 index 4a79bc2c..00000000 --- a/src/lotus.rs +++ /dev/null @@ -1,367 +0,0 @@ -// #[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; -#[allow(unused_imports)] -use embedded_hal::blocking::i2c::Write; -#[allow(unused_imports)] -use is31fl3741::{Error, IS31FL3741}; - -pub struct LotusLedMatrix { - pub device: IS31FL3741, -} - -impl LotusLedMatrix -where - I2C: Write, -{ - pub fn unwrap(self) -> I2C { - self.device.i2c - } - - pub fn set_scaling(&mut self, scale: u8) -> Result<(), I2cError> { - self.device.set_scaling(scale) - } - - pub fn configure(i2c: I2C) -> LotusLedMatrix { - LotusLedMatrix { - device: IS31FL3741 { - i2c, - address: 0x30, - width: 9, - height: 34, - calc_pixel: |x: u8, y: u8| -> (u8, u8) { - // Generated by lotus-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 - (0x3c, 1), // x:9, y:1, sw:9, cs:1, 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 - (0x3d, 1), // x:9, y:2, sw:9, cs:2, 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 - (0x3e, 1), // x:9, y:3, sw:9, cs:3, 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 - (0x3f, 1), // x:9, y:4, sw:9, cs:4, 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 - (0x40, 1), // x:9, y:5, sw:9, cs:5, 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 - (0x41, 1), // x:9, y:6, sw:9, cs:6, 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 - (0x42, 1), // x:9, y:7, sw:9, cs:7, 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 - (0x43, 1), // x:9, y:8, sw:9, cs:8, 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 - (0x44, 1), // x:9, y:9, sw:9, cs:9, 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 - (0x45, 1), // x:9, y:10, sw:9, cs:10, 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 - (0x46, 1), // x:9, y:11, sw:9, cs:11, 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 - (0x47, 1), // x:9, y:12, sw:9, cs:12, 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 - (0x48, 1), // x:9, y:13, sw:9, cs:13, 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 - (0x49, 1), // x:9, y:14, sw:9, cs:14, 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 - (0x4a, 1), // x:9, y:15, sw:9, cs:15, 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 - (0x4b, 1), // x:9, y:16, sw:9, cs:16, 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 - (0x4c, 1), // x:9, y:17, sw:9, cs:17, 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 - (0x4d, 1), // x:9, y:18, sw:9, cs:18, 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 - (0x4e, 1), // x:9, y:19, sw:9, cs:19, 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 - (0x4f, 1), // x:9, y:20, sw:9, cs:20, 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 - (0x50, 1), // x:9, y:21, sw:9, cs:21, 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 - (0x51, 1), // x:9, y:22, sw:9, cs:22, 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 - (0x52, 1), // x:9, y:23, sw:9, cs:23, 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 - (0x53, 1), // x:9, y:24, sw:9, cs:24, 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 - (0x54, 1), // x:9, y:25, sw:9, cs:25, 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 - (0x55, 1), // x:9, y:26, sw:9, cs:26, 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 - (0x56, 1), // x:9, y:27, sw:9, cs:27, 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 - (0x57, 1), // x:9, y:28, sw:9, cs:28, 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 - (0x58, 1), // x:9, y:29, sw:9, cs:29, 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 - (0x59, 1), // x:9, y:30, sw:9, cs:30, 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 - (0xa2, 1), // x:9, y:31, sw:9, cs:31, 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 - (0xa3, 1), // x:9, y:32, sw:9, cs:32, 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 - (0xa4, 1), // x:9, y:33, sw:9, cs:33, 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 - (0xa5, 1), // x:9, y:34, sw:9, cs:34, id:306 - ]; - let index: usize = (x as usize) + (y as usize) * 9; - if index < lookup.len() { - lookup[index] - } else { - (0x00, 0) - } - }, - }, - } - } - - pub fn setup>(&mut self, delay: &mut DEL) -> Result<(), Error> { - self.device.setup(delay) - } - - 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/src/main.rs b/src/main.rs deleted file mode 100644 index 22999d4e..00000000 --- a/src/main.rs +++ /dev/null @@ -1,383 +0,0 @@ -//! Lotus LED Matrix Module -#![no_std] -#![no_main] -#![allow(clippy::needless_range_loop)] - -use bsp::entry; -use cortex_m::delay::Delay; -//use defmt::*; -use defmt_rtt as _; -use embedded_hal::digital::v2::{InputPin, OutputPin}; - -use rp2040_hal::gpio::bank0::Gpio29; -//#[cfg(debug_assertions)] -//use panic_probe as _; -use rp2040_panic_usb_boot as _; - -// TODO: Doesn't work yet, unless I panic right at the beginning of main -//#[cfg(not(debug_assertions))] -//use core::panic::PanicInfo; -//#[cfg(not(debug_assertions))] -//#[panic_handler] -//fn panic(_info: &PanicInfo) -> ! { -// let mut pac = pac::Peripherals::take().unwrap(); -// let core = pac::CorePeripherals::take().unwrap(); -// let mut watchdog = Watchdog::new(pac.WATCHDOG); -// let sio = Sio::new(pac.SIO); -// -// let clocks = init_clocks_and_plls( -// bsp::XOSC_CRYSTAL_FREQ, -// pac.XOSC, -// pac.CLOCKS, -// pac.PLL_SYS, -// pac.PLL_USB, -// &mut pac.RESETS, -// &mut watchdog, -// ) -// .ok() -// .unwrap(); -// -// let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); -// -// let pins = bsp::Pins::new( -// pac.IO_BANK0, -// pac.PADS_BANK0, -// sio.gpio_bank0, -// &mut pac.RESETS, -// ); -// -// let mut led_enable = pins.sdb.into_push_pull_output(); -// led_enable.set_high().unwrap(); -// -// let i2c = bsp::hal::I2C::i2c1( -// pac.I2C1, -// pins.gpio26.into_mode::(), -// pins.gpio27.into_mode::(), -// 1000.kHz(), -// &mut pac.RESETS, -// &clocks.peripheral_clock, -// ); -// -// let mut matrix = LotusLedMatrix::configure(i2c); -// matrix -// .setup(&mut delay) -// .expect("failed to setup rgb controller"); -// -// matrix.set_scaling(100).expect("failed to set scaling"); -// let grid = display_panic(); -// fill_grid_pixels(grid, &mut matrix); -// -// loop {} -//} - -// Provide an alias for our BSP so we can switch targets quickly. -// Uncomment the BSP you included in Cargo.toml, the rest of the code does not need to change. -mod lotus_led_hal; -use lotus_led_hal as bsp; -//use rp_pico as bsp; -// use sparkfun_pro_micro_rp2040 as bsp; - -use bsp::hal::{ - clocks::{init_clocks_and_plls, Clock}, - gpio, pac, - sio::Sio, - usb, - watchdog::Watchdog, - Timer, -}; -use fugit::RateExtU32; - -// USB Device support -use usb_device::{class_prelude::*, prelude::*}; - -// USB Communications Class Device support -use usbd_serial::{SerialPort, USB_CLASS_CDC}; - -// Used to demonstrate writing formatted strings -use core::fmt::Write; -use heapless::String; - -pub mod lotus; -use lotus::LotusLedMatrix; - -pub mod mapping; - -pub mod patterns; -use patterns::*; - -mod control; -use control::*; - -// FRA - Framwork -// KDE - Lotus C2 LED Matrix -// AM - Atemitech -// 00 - Default Configuration -// 00000000 - Device Identifier -const DEFAULT_SERIAL: &str = "FRAKDEAM0000000000"; -// Get serial number from last 4K block of the first 1M -const FLASH_OFFSET: usize = 0x10000000; -const LAST_4K_BLOCK: usize = 0xff000; -const SERIALNUM_LEN: usize = 18; - -fn get_serialnum() -> Option<&'static str> { - // Flash is mapped into memory, just read it from there - let ptr: *const u8 = (FLASH_OFFSET + LAST_4K_BLOCK) as *const u8; - unsafe { - let slice: &[u8] = core::slice::from_raw_parts(ptr, SERIALNUM_LEN); - if slice[0] == 0xFF || slice[0] == 0x00 { - return None; - } - core::str::from_utf8(slice).ok() - } -} - -#[derive(Clone)] -pub struct Grid([[u8; HEIGHT]; WIDTH]); -impl Default for Grid { - fn default() -> Self { - Grid([[0; HEIGHT]; WIDTH]) - } -} - -#[allow(clippy::large_enum_variant)] -#[derive(Clone)] -enum SleepState { - Awake, - Sleeping(Grid), -} - -pub struct State { - grid: Grid, - col_buffer: Grid, - animate: bool, - brightness: u8, - sleeping: SleepState, -} - -#[entry] -fn main() -> ! { - let mut pac = pac::Peripherals::take().unwrap(); - let core = pac::CorePeripherals::take().unwrap(); - let mut watchdog = Watchdog::new(pac.WATCHDOG); - let sio = Sio::new(pac.SIO); - - let clocks = init_clocks_and_plls( - bsp::XOSC_CRYSTAL_FREQ, - pac.XOSC, - pac.CLOCKS, - pac.PLL_SYS, - pac.PLL_USB, - &mut pac.RESETS, - &mut watchdog, - ) - .ok() - .unwrap(); - - let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz()); - - let pins = bsp::Pins::new( - pac.IO_BANK0, - pac.PADS_BANK0, - sio.gpio_bank0, - &mut pac.RESETS, - ); - - // Set up the USB driver - let usb_bus = UsbBusAllocator::new(usb::UsbBus::new( - pac.USBCTRL_REGS, - pac.USBCTRL_DPRAM, - clocks.usb_clock, - true, - &mut pac.RESETS, - )); - - // Set up the USB Communications Class Device driver - let mut serial = SerialPort::new(&usb_bus); - - let serialnum = if let Some(serialnum) = get_serialnum() { - serialnum - } else { - DEFAULT_SERIAL - }; - - let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x32ac, 0x0020)) - .manufacturer("Framework") - .product("Lotus LED Matrix") - .serial_number(serialnum) - .max_power(200) // Device uses roughly 164mW when all LEDs are at full brightness - .device_release(0x0011) // TODO: Assign dynamically based on crate version - .device_class(USB_CLASS_CDC) - .build(); - - // Enable LED controller - // SDB - let mut led_enable = pins.sdb.into_push_pull_output(); - led_enable.set_high().unwrap(); - // INTB. Currently ignoring - pins.intb.into_floating_input(); - - let sleep = pins.sleep.into_pull_down_input(); - - let i2c = bsp::hal::I2C::i2c1( - pac.I2C1, - pins.gpio26.into_mode::(), - pins.gpio27.into_mode::(), - 1000.kHz(), - &mut pac.RESETS, - &clocks.peripheral_clock, - ); - - let mut state = State { - grid: percentage(100), - col_buffer: Grid::default(), - animate: false, - brightness: 120, - sleeping: SleepState::Awake, - }; - - let mut matrix = LotusLedMatrix::configure(i2c); - matrix - .setup(&mut delay) - .expect("failed to setup rgb controller"); - - matrix - .set_scaling(state.brightness) - .expect("failed to set scaling"); - - let mut said_hello = false; - - fill_grid_pixels(&state.grid, &mut matrix); - - let timer = Timer::new(pac.TIMER, &mut pac.RESETS); - let mut prev_timer = timer.get_counter().ticks(); - - loop { - // TODO: Current hardware revision does not have the sleep pin wired up :( - // Go to sleep if the host is sleeping - let _host_sleeping = sleep.is_low().unwrap(); - //handle_sleep(host_sleeping, &mut state, &mut matrix, &mut delay); - - // Handle period display updates. Don't do it too often - if timer.get_counter().ticks() > prev_timer + 20_000 { - fill_grid_pixels(&state.grid, &mut matrix); - if state.animate { - for x in 0..WIDTH { - state.grid.0[x].rotate_right(1); - } - } - prev_timer = timer.get_counter().ticks(); - } - - // A welcome message at the beginning - if !said_hello && timer.get_counter().ticks() >= 2_000_000 { - said_hello = true; - let _ = serial.write(b"Hello, World!\r\n"); - - let time = timer.get_counter(); - let mut text: String<64> = String::new(); - writeln!(&mut text, "Current timer ticks: {}", time).unwrap(); - - // This only works reliably because the number of bytes written to - // the serial port is smaller than the buffers available to the USB - // peripheral. In general, the return value should be handled, so that - // bytes not transferred yet don't get lost. - let _ = serial.write(text.as_bytes()); - } - - // Check for new data - if usb_dev.poll(&mut [&mut serial]) { - let mut buf = [0u8; 64]; - match serial.read(&mut buf) { - Err(_e) => { - // Do nothing - } - Ok(0) => { - // Do nothing - } - Ok(count) => { - if let Some(command) = parse_command(count, &buf) { - if let Command::Sleep(go_sleeping) = command { - handle_sleep( - go_sleeping, - &mut state, - &mut matrix, - &mut delay, - &mut led_enable, - ); - } else if let SleepState::Awake = state.sleeping { - // While sleeping no command is handled, except waking up - handle_command(&command, &mut state, &mut matrix); - } - - fill_grid_pixels(&state.grid, &mut matrix); - } - } - } - } - } -} - -fn handle_sleep( - go_sleeping: bool, - state: &mut State, - matrix: &mut Foo, - delay: &mut Delay, - led_enable: &mut gpio::Pin>, -) { - match (state.sleeping.clone(), go_sleeping) { - (SleepState::Awake, false) => (), - (SleepState::Awake, true) => { - state.sleeping = SleepState::Sleeping(state.grid.clone()); - //state.grid = display_sleep(); - fill_grid_pixels(&state.grid, matrix); - - // Slowly decrease brightness - delay.delay_ms(1000); - let mut brightness = state.brightness; - loop { - delay.delay_ms(100); - brightness = if brightness <= 5 { 0 } else { brightness - 5 }; - matrix - .set_scaling(brightness) - .expect("failed to set scaling"); - if brightness == 0 { - break; - } - } - - // Turn LED controller off to save power - led_enable.set_low().unwrap(); - - // TODO: Set up SLEEP# pin as interrupt and wfi - //cortex_m::asm::wfi(); - } - (SleepState::Sleeping(_), true) => (), - (SleepState::Sleeping(old_grid), false) => { - // Restore back grid before sleeping - state.sleeping = SleepState::Awake; - state.grid = old_grid; - fill_grid_pixels(&state.grid, matrix); - - // Power LED controller back on - led_enable.set_high().unwrap(); - - // Slowly increase brightness - delay.delay_ms(1000); - let mut brightness = 0; - loop { - delay.delay_ms(100); - brightness = if brightness >= state.brightness - 5 { - state.brightness - } else { - brightness + 5 - }; - matrix - .set_scaling(brightness) - .expect("failed to set scaling"); - if brightness == state.brightness { - break; - } - } - } - } -} diff --git a/src/mapping.rs b/src/mapping.rs deleted file mode 100644 index 5c640fbe..00000000 --- a/src/mapping.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Taken from https://github.com/phip1611/max-7219-led-matrix-util/blob/main/src/mappings.rs - -/// We have 8 rows and 8 bits per row. -pub type SingleDisplayData = [u8; 8]; - -/// Capital letter A -pub const CAP_A: SingleDisplayData = [ - 0b00111000, 0b01000100, 0b01000100, 0b01000100, 0b01111100, 0b01000100, 0b01000100, 0b01000100, -]; -/// Capital letter B -pub const CAP_B: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01111000, -]; -/// Capital letter C -pub const CAP_C: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01111100, -]; -/// Capital letter D -pub const CAP_D: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01111000, -]; -/// Capital letter E -pub const CAP_E: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01111100, -]; -/// Capital letter F -pub const CAP_F: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01000000, -]; -/// Capital letter G -pub const CAP_G: SingleDisplayData = [ - 0b01111000, 0b11000100, 0b10000100, 0b10000000, 0b10011100, 0b10000100, 0b11000100, 0b01111100, -]; -/// Capital letter H -pub const CAP_H: SingleDisplayData = [ - 0b01000100, 0b01000100, 0b01000100, 0b01111100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, -]; -/// Capital letter I -pub const CAP_I: SingleDisplayData = [ - 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, -]; -/// Capital letter J -pub const CAP_J: SingleDisplayData = [0; 8]; // TODO -/// Capital letter K -pub const CAP_K: SingleDisplayData = [ - 0b01000100, 0b01001000, 0b01010000, 0b01100000, 0b01010000, 0b01001000, 0b01000100, 0b01000010, -]; -/// Capital letter L -/// I shifted it one left -pub const CAP_L: SingleDisplayData = [ - 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b10000000, 0b11111000, -]; -/// Capital letter M -pub const CAP_M: SingleDisplayData = [ - 0b10000010, 0b11000110, 0b10101010, 0b10111010, 0b10010010, 0b10000010, 0b10000010, 0b10000010, -]; -/// Capital letter N -pub const CAP_N: SingleDisplayData = [ - 0b01000100, 0b01100100, 0b01110100, 0b01010100, 0b01011100, 0b01001100, 0b01001100, 0b01000100, -]; -/// Capital letter O -pub const CAP_O: SingleDisplayData = [ - 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b00100100, 0b00011000, -]; -/// Capital letter P -pub const CAP_P: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01111000, 0b01000000, 0b01000000, 0b01000000, -]; -/// Capital letter Q -pub const CAP_Q: SingleDisplayData = [0; 8]; // TODO -/// Capital letter R -pub const CAP_R: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01111000, 0b01100000, 0b01010000, 0b01001000, 0b01000100, -]; -/// Capital letter S -/// I shifted it one to the right -pub const CAP_S: SingleDisplayData = [ - 0b00000111, 0b00001000, 0b00010000, 0b00001100, 0b00000010, 0b00000001, 0b00000001, 0b00011110, -]; -/// Capital letter T -pub const CAP_T: SingleDisplayData = [ - 0b11111110, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, -]; -/// Capital letter U -pub const CAP_U: SingleDisplayData = [ - 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b00111100, -]; -/// Capital letter V -pub const CAP_V: SingleDisplayData = [0; 8]; // TODO -/// Capital letter W -pub const CAP_W: SingleDisplayData = [0; 8]; // TODO -/// Capital letter X -pub const CAP_X: SingleDisplayData = [0; 8]; // TODO -/// Capital letter Y -pub const CAP_Y: SingleDisplayData = [0; 8]; // TODO -/// Capital letter Z -pub const CAP_Z: SingleDisplayData = [ - 0b01111110, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00100000, 0b01000000, 0b01111110, -]; -/// Number 0 -pub const ZERO: SingleDisplayData = [ - 0b00111000, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b00111000, -]; -/// Number 1 -pub const ONE: SingleDisplayData = [ - 0b00000100, 0b00011100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, -]; -/// " " character -pub const SPACE: SingleDisplayData = [0; 8]; -/// "." character -pub const DOT: SingleDisplayData = [0, 0, 0, 0, 0, 0, 0, 0b00010000]; -/// "!" character -pub const EXCLAMATION_MARK: SingleDisplayData = [ - 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0, 0b00010000, -];