diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml index bc94b585..cba0a1a5 100644 --- a/.github/workflows/firmware.yml +++ b/.github/workflows/firmware.yml @@ -1,7 +1,19 @@ -name: Firmware CI Checks +name: Firmware on: - push + push: + branches: + - main + - dev-* + paths-ignore: + - '*.py' + - 'inputmodule-control/**' + pull_request: + branches: + - '*' + paths-ignore: + - '*.py' + - 'inputmodule-control/**' env: CARGO_TERM_COLOR: always @@ -12,61 +24,120 @@ jobs: name: Building runs-on: [ubuntu-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show + - run: cargo install cargo-make + - run: cargo install flip-link - - run: cargo build -p ledmatrix - - run: cargo build -p b1display - - run: cargo build -p c1minimal - - run: cargo build -p ledmatrix --release - - run: cargo build -p b1display --release - - run: cargo build -p c1minimal --release + # 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 - - run: | + - name: Convert to UF2 format + run: | sudo apt-get update sudo apt-get install -y libudev-dev - cargo install elf2uf2-rs - elf2uf2-rs target/thumbv6m-none-eabi/release/b1display b1display.uf2 - elf2uf2-rs target/thumbv6m-none-eabi/release/c1minimal c1minimal.uf2 - elf2uf2-rs target/thumbv6m-none-eabi/release/ledmatrix ledmatrix.uf2 + 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 Linux tool - uses: actions/upload-artifact@v3 + - name: Upload ledmatrix files + uses: actions/upload-artifact@v4 with: - name: inputmodule_fw + name: ledmatrix_fw_${{github.sha}} path: | - b1display.uf2 - c1minimal.uf2 - ledmatrix.uf2 + # 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@v3 - with: - submodules: true + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show + - run: cargo install cargo-make + - run: | - cargo clippy -p b1display -- --deny=warnings - cargo clippy -p c1minimal -- --deny=warnings - cargo clippy -p ledmatrix -- --deny=warnings + 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@v3 - with: - submodules: true + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show - - run: cargo fmt --all -- --check + - 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 index bd26c461..105dde7b 100644 --- a/.github/workflows/software.yml +++ b/.github/workflows/software.yml @@ -1,6 +1,23 @@ -name: Software CI Checks +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 @@ -12,7 +29,7 @@ jobs: # name: Cross-Build for FreeBSD # runs-on: 'ubuntu-22.04' # steps: - # - uses: actions/checkout@v3 + # - uses: actions/checkout@v4 # - name: Setup Rust toolchain # run: rustup show @@ -24,7 +41,7 @@ jobs: # run: cross build --target=x86_64-unknown-freebsd # - name: Upload FreeBSD App - # uses: actions/upload-artifact@v3 + # uses: actions/upload-artifact@v4 # with: # name: qmk_hid_freebsd # path: target/x86_64-unknown-freebsd/debug/qmk_hid @@ -33,7 +50,7 @@ jobs: name: Build Linux runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -43,14 +60,16 @@ jobs: - name: Setup Rust toolchain run: rustup show + - run: cargo install cargo-make + - name: Build Linux tool - run: cargo build --release --target x86_64-unknown-linux-gnu -p inputmodule-control + run: cargo make --cwd inputmodule-control build-release - name: Check if Linux tool can start - run: cargo run --release --target x86_64-unknown-linux-gnu -p inputmodule-control -- --help + run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' - name: Upload Linux tool - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: inputmodule-control path: target/x86_64-unknown-linux-gnu/release/inputmodule-control @@ -59,28 +78,68 @@ jobs: name: Build Windows runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Rust toolchain run: rustup show + - run: cargo install cargo-make + - name: Build Windows tool - run: cargo build --release --target x86_64-pc-windows-msvc -p inputmodule-control + run: cargo make --cwd inputmodule-control build-release - name: Check if Windows tool can start - run: cargo run --release --target x86_64-pc-windows-msvc -p inputmodule-control -- --help + run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline' - name: Upload Windows App - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: inputmodule-control.exe path: target/x86_64-pc-windows-msvc/release/inputmodule-control.exe + # 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@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -90,8 +149,10 @@ jobs: - 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 clippy --target x86_64-unknown-linux-gnu -p inputmodule-control -- -D warnings + 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 ac6f0627..579047c8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .gdb_history target/ +venv + # editor files .vscode/* !.vscode/*.md @@ -11,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 index 607dff71..afe9fa55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,18 @@ dependencies = [ "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" @@ -197,7 +209,7 @@ checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" [[package]] name = "b1display" -version = "0.1.5" +version = "0.2.0" dependencies = [ "cortex-m", "cortex-m-rt", @@ -208,7 +220,7 @@ dependencies = [ "fl16-inputmodules", "fugit", "heapless", - "rp2040-boot2", + "rp2040-boot2 0.3.0", "rp2040-hal", "rp2040-panic-usb-boot", "st7306", @@ -306,7 +318,7 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "c1minimal" -version = "0.1.5" +version = "0.2.0" dependencies = [ "cortex-m", "cortex-m-rt", @@ -316,7 +328,7 @@ dependencies = [ "fl16-inputmodules", "fugit", "heapless", - "rp2040-boot2", + "rp2040-boot2 0.3.0", "rp2040-hal", "rp2040-panic-usb-boot", "smart-leds", @@ -561,6 +573,15 @@ dependencies = [ "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" @@ -570,6 +591,12 @@ 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" @@ -759,10 +786,11 @@ dependencies = [ [[package]] name = "fl16-inputmodules" -version = "0.1.5" +version = "0.2.0" dependencies = [ "cortex-m", "cortex-m-rt", + "crc", "defmt", "defmt-rtt", "embedded-graphics", @@ -773,7 +801,7 @@ dependencies = [ "num", "num-derive", "num-traits", - "rp2040-boot2", + "rp2040-boot2 0.3.0", "rp2040-hal", "rp2040-panic-usb-boot", "smart-leds", @@ -963,7 +991,7 @@ dependencies = [ [[package]] name = "inputmodule-control" -version = "0.1.5" +version = "0.2.0" dependencies = [ "chrono", "clap", @@ -1005,8 +1033,9 @@ dependencies = [ [[package]] name = "is31fl3741" -version = "0.2.1" -source = "git+https://github.com/JohnAZoidberg/is31fl3741?branch=all-at-once-update-deps#52ae4d277fd5decb148683d6aa5665c1153c8768" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9abf02bbdd939fe7f46924002c004d8ab811f093392d8e354e8f583a40badf" dependencies = [ "embedded-graphics-core", "embedded-hal", @@ -1087,7 +1116,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "ledmatrix" -version = "0.1.5" +version = "0.2.0" dependencies = [ "cortex-m", "cortex-m-rt", @@ -1098,7 +1127,7 @@ dependencies = [ "fugit", "heapless", "is31fl3741", - "rp2040-boot2", + "rp2040-boot2 0.3.0", "rp2040-hal", "rp2040-panic-usb-boot", "usb-device", @@ -1599,6 +1628,29 @@ 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" @@ -1816,6 +1868,15 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index ec5e8cb3..cf32392a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "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 @@ -19,61 +20,58 @@ members = [ # target. But we set the default target to thumbv6m-none-eabi default-members = ["fl16-inputmodules"] -#[patch.'https://github.com/rp-rs/rp-hal.git'] -#rp2040-hal = { path = "./rp2040-hal" } -# -#[patch.crates-io] -#rp2040-hal = { path = "./rp2040-hal" } +[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" +# USB Serial +usb-device = "0.2.9" +heapless = "0.7.16" +usbd-serial = "0.1.1" +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/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 20879703..a6c102b9 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ See pages of the individual modules for details about how they work and how they're controlled. - [LED Matrix](ledmatrix/README.md) -- [2nd Display](b1display/README.md) - [Minimal C1 Input Module](c1minimal/README.md) +- [2nd Display](b1display/README.md) +- [QT PY RP2040](qtpy/README.md) ## Generic Features @@ -32,13 +33,25 @@ Features that all modules share To build your own application see the: [API command documentation](commands.md) Or use our `inputmodule-control` app, which you can download from the latest -[GH Actions](https://github.com/FrameworkComputer/led_matrix_fw/actions) run or -the [release page](https://github.com/FrameworkComputer/led_matrix_fw/releases). -Optionally there are is also a [Python script](python.md). +[GH Actions](https://github.com/FrameworkComputer/inputmodule-rs/actions) run or +the [release page](https://github.com/FrameworkComputer/inputmodule-rs/releases). For device specific commands, see their individual documentation pages. -Common commands: +### 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 +``` + +##### Common commands: ###### Listing available devices @@ -63,7 +76,7 @@ to a single device, specify the COM port. In this example the command is targeted at `b1-display`, so it will only apply to this module type. -``` +```sh # Example on Linux > inputmodule-control --serial-dev /dev/ttyACM0 b1-display --pattern black @@ -79,7 +92,7 @@ connected and then send the command. ``` > inputmodule-control b1-display --pattern black -Failed to find serial devivce. Please manually specify with --serial-dev +Failed to find serial device. Please manually specify with --serial-dev # No failure, waits until the device is connected, sends command and exits > inputmodule-control --wait-for-device b1-display --pattern black @@ -96,7 +109,7 @@ Device already present. No need to wait. Not executing command. First, put the module into bootloader mode. This can be done either by pressing the bootsel button while plugging it in or -by using one of the following commands: +by using one of the following commands: ```sh inputmodule-control led-matrix --bootloader @@ -116,42 +129,49 @@ cargo run -p c1minimal ## Building the firmware -Dependencies: Rust +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev Prepare Rust toolchain (once): ```sh rustup target install thumbv6m-none-eabi cargo install flip-link -cargo install elf2uf2-rs --locked +cargo install cargo-make +cargo install elf2uf2-rs ``` Build: ```sh -cargo build -p ledmatrix -cargo build -p b1display -cargo build -p c1minimal +cargo make --cwd ledmatrix +cargo make --cwd b1display +cargo make --cwd c1minimal ``` -Generate the UF2 update file: +Generate the UF2 update file into `target/thumbv6m-none-eabi/release/`: ```sh -elf2uf2-rs target/thumbv6m-none-eabi/debug/ledmatrix ledmatrix.uf2 -elf2uf2-rs target/thumbv6m-none-eabi/debug/b1display b1dipslay.uf2 -elf2uf2-rs target/thumbv6m-none-eabi/debug/c1minimal c1minimal.uf2 +cargo make --cwd ledmatrix uf2 +cargo make --cwd b1display uf2 +cargo make --cwd c1minimal uf2 ``` ## Building the Application -Dependencies: Rust, pkg-config, libudev +Dependencies: [Rust/rustup](https://rustup.rs/), pkg-config, libudev Currently have to specify the build target because it's not possible to specify a per package build target. Tracking issue: https://github.com/rust-lang/cargo/issues/9406 -``` -> cargo build --target x86_64-unknown-linux-gnu -p inputmodule-control -> cargo run --target x86_64-unknown-linux-gnu -p inputmodule-control +```sh +# 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 ``` ### Check the firmware version of the device @@ -159,8 +179,8 @@ Tracking issue: https://github.com/rust-lang/cargo/issues/9406 ###### In-band using commandline ```sh -> inputmodule-control b1-display --version -Device Version: 0.1.3 +> inputmodule-control led-matrix --version +Device Version: 0.2.0 ``` ###### By looking at the USB descriptor diff --git a/b1display/Cargo.toml b/b1display/Cargo.toml index 2cf15126..49d878fc 100644 --- a/b1display/Cargo.toml +++ b/b1display/Cargo.toml @@ -1,34 +1,33 @@ [package] edition = "2021" name = "b1display" -version = "0.1.5" +version = "0.2.0" [dependencies] -cortex-m = "0.7" -cortex-m-rt = "0.7.3" -embedded-hal = { version = "0.2.7", features = ["unproven"] } +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true -defmt = "0.3" -defmt-rtt = "0.4" +defmt.workspace = true +defmt-rtt.workspace = true -#panic-probe = { version = "0.3", features = ["print-defmt"] } -rp2040-panic-usb-boot = "0.5.0" +#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 = { version="0.8", features=["rt", "critical-section-impl"] } -rp2040-boot2 = "0.3" +rp2040-hal.workspace = true +rp2040-boot2.workspace = true # USB Serial -usb-device= "0.2.9" - -heapless = "0.7.16" -usbd-serial = "0.1.1" -usbd-hid = "0.6.1" -fugit = "0.3.7" - -st7306 = { git = "https://github.com/FrameworkComputer/st7306-rs", branch = "update-deps" } -embedded-graphics = "0.8" -tinybmp = "0.5.0" +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" 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/src/main.rs b/b1display/src/main.rs index ff5bbf85..81ac88a2 100644 --- a/b1display/src/main.rs +++ b/b1display/src/main.rs @@ -114,7 +114,7 @@ fn main() -> ! { let mut serial = SerialPort::new(&usb_bus); let serialnum = if let Some(serialnum) = get_serialnum() { - serialnum + serialnum.serialnum } else { DEFAULT_SERIAL }; diff --git a/c1minimal/Cargo.toml b/c1minimal/Cargo.toml index fdd04623..a932ccb3 100644 --- a/c1minimal/Cargo.toml +++ b/c1minimal/Cargo.toml @@ -1,33 +1,33 @@ [package] edition = "2021" name = "c1minimal" -version = "0.1.5" +version = "0.2.0" [dependencies] -cortex-m = "0.7" -cortex-m-rt = "0.7.3" -embedded-hal = { version = "0.2.7", features = ["unproven"] } +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true -defmt = "0.3" -defmt-rtt = "0.4" +defmt.workspace = true +defmt-rtt.workspace = true -#panic-probe = { version = "0.3", features = ["print-defmt"] } -rp2040-panic-usb-boot = "0.5.0" +#panic-probe.workspace = true +rp2040-panic-usb-boot.workspace = true -# Not using an external BSP, we've got the Framework Laptop BSPs locally in this crate -rp2040-hal = { version="0.8", features=["rt", "critical-section-impl"] } -rp2040-boot2 = "0.3" +# 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= "0.2.9" - -heapless = "0.7.16" -usbd-serial = "0.1.1" -usbd-hid = "0.6.1" -fugit = "0.3.7" - -smart-leds = "0.3.0" -ws2812-pio = "0.6.0" +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" 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 index a4d6c92e..a64b58bf 100644 --- a/c1minimal/README.md +++ b/c1minimal/README.md @@ -8,11 +8,11 @@ When booting up this LED is lit in green color. Its color and brightness can be controlled via the commands: ```sh -> ./control.py --brightness 255 -> ./control.py --get-brightness +> ./ledmatrix_control.py --brightness 255 +> ./ledmatrix_control.py --get-brightness Current brightness: 255 -> ./control.py --set-color yellow -> ./control.py --get-color +> ./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 index 0da4d8f0..9967774e 100644 --- a/c1minimal/src/main.rs +++ b/c1minimal/src/main.rs @@ -103,7 +103,7 @@ fn main() -> ! { let mut serial = SerialPort::new(&usb_bus); let serialnum = if let Some(serialnum) = get_serialnum() { - serialnum + serialnum.serialnum } else { DEFAULT_SERIAL }; diff --git a/commands.md b/commands.md index 00ddfb66..46322edb 100644 --- a/commands.md +++ b/commands.md @@ -9,8 +9,8 @@ Simple example in Python: ```python import serial -def send_command(command_id, parameters, with_response=False) - with serial.Serial(/dev/ttyACM0, 115200) as s: +def send_command(command_id, parameters, with_response=False): + with serial.Serial("/dev/ttyACM0", 115200) as s: s.write([0x32, 0xAC, command_id] + parameters) if with_response: @@ -20,7 +20,7 @@ def send_command(command_id, parameters, with_response=False) # Go to sleep and check the status send_command(0x03, [True]) res = send_command(0x03, [], with_response=True) -print(f"Is currently sleeping: {bool(res[0])}) +print(f"Is currently sleeping: {bool(res[0])}") ``` Many commands support setting and writing a value, with the same command ID. @@ -56,7 +56,7 @@ When no parameters are given, the current value is queried and returned. | InvertScreen | 0x15 | ` D ` | | bool | Invert scren on/off | | SetPxCol | 0x16 | ` D ` | | 50 Bytes | Send a column of pixels | | FlushFB | 0x17 | ` D ` | | | Flush all columns | -| Version | 0x20 | ` D ` | 3 Bytes | | Get firmware version | +| Version | 0x20 | `LDM` | 3 Bytes | | Get firmware version | #### Pattern (0x01) @@ -93,8 +93,15 @@ TODO Response: -``` +```plain Byte 0: USB bcdDevice MSB Byte 1: USB bcdDevice LSB Byte 2: 1 if pre-release version, 0 otherwise + ++-- Major version +| +-- Minor version +| | +-- Patch version +| | | +-- 1 if is pre-release, +| | | | 0 otherwise +MMMMMMMM mmmmPPPP 0000000p ``` diff --git a/control.py b/control.py deleted file mode 100755 index 40c24404..00000000 --- a/control.py +++ /dev/null @@ -1,1605 +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 -from enum import IntEnum - - -# Need to install -import serial - -# Optional dependencies: -# from PIL import Image -# import PySimpleGUI as sg - -FWK_MAGIC = [0x32, 0xAC] - - -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 - 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 - - -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)', -] -DRAW_PATTERNS = ['off', 'on', 'foo'] -GREYSCALE_DEPTH = 32 -RESPONSE_SIZE = 32 -WIDTH = 9 -HEIGHT = 34 -B1_WIDTH = 300 -B1_HEIGHT = 400 - -ARG_UP = 0 -ARG_DOWN = 1 -ARG_LEFT = 2 -ARG_RIGHT = 3 -ARG_QUIT = 4 -ARG_2LEFT = 5 -ARG_2RIGHT = 6 - -RGB_COLORS = ['white', 'black', 'red', 'green', - 'blue', 'cyan', 'yellow', 'purple'] -SCREEN_FPS = ['quarter', 'half', 'one', 'two', 'four', 'eight', 'sixteen', 'thirtytwo'] -HIGH_FPS_MASK = 0b00010000 -LOW_FPS_MASK = 0b00000111 - -SERIAL_DEV = None - -STOP_THREAD = False - - -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('--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("--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("--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", - default='/dev/ttyACM0') - - 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() - - 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: - send_command(CommandVals.Sleep, [args.sleep]) - elif args.is_sleeping: - res = send_command(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(args.brightness) - elif args.get_brightness: - br = get_brightness() - print(f"Current brightness: {br}") - 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.get_animate: - animating = get_animate() - print(f"Currently animating: {animating}") - elif args.panic: - send_command(CommandVals.Panic, [0x00]) - 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.set_color: - set_color(args.set_color) - elif args.get_color: - (red, green, blue) = get_color() - print(f"Current color: RGB:({red}, {green}, {blue})") - elif args.gui: - gui() - elif args.blink: - blinking() - elif args.breathing: - breathing() - elif args.wpm: - wpm_demo() - elif args.snake: - snake() - elif args.snake_embedded: - snake_embedded() - elif args.game_of_life_embedded is not None: - game_of_life_embedded(args.game_of_life_embedded) - elif args.quit_embedded_game: - send_command(CommandVals.GameControl, [GameControlVal.Quit]) - elif args.pong_embedded: - pong_embedded() - 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) - elif args.disp_str is not None: - display_string(args.disp_str) - elif args.display_on is not None: - display_on_cmd(args.display_on) - elif args.invert_screen is not None: - invert_screen_cmd(args.invert_screen) - elif args.screen_saver is not None: - screen_saver_cmd(args.screen_saver) - elif args.set_fps is not None: - set_fps_cmd(args.set_fps) - elif args.set_power_mode is not None: - set_power_mode_cmd(args.set_power_mode) - elif args.get_fps: - get_fps_cmd() - elif args.get_power_mode: - get_power_mode_cmd() - elif args.b1image is not None: - b1image_bl(args.b1image) - elif args.version: - version = get_version() - print(f"Device version: {version}") - else: - parser.print_help(sys.stderr) - sys.exit(1) - - -def bootloader(): - """Reboot into the bootloader to flash new firmware""" - send_command(CommandVals.BootloaderReset, [0x00]) - - -def percentage(p): - """Fill a percentage of the screen. Bottom to top""" - send_command(CommandVals.Pattern, [PatternVals.Percentage, p]) - - -def brightness(b: int): - """Adjust the brightness scaling of the entire screen. - """ - send_command(CommandVals.Brightness, [b]) - - -def get_brightness(): - """Adjust the brightness scaling of the entire screen. - """ - res = send_command(CommandVals.Brightness, with_response=True) - return int(res[0]) - - -def get_version(): - """Get the device's firmware version""" - res = send_command(CommandVals.Version, with_response=True) - major = res[0] - minor = (res[1] & 0xF0) >> 4 - patch = res[1] & 0xF - pre_release = res[2] - - version = f"{major}.{minor}.{patch}" - if pre_release: - version += " (Pre-release)" - return version - - -def animate(b: bool): - """Tell the firmware to start/stop animation. - Scrolls the currently saved grid vertically down.""" - send_command(CommandVals.Animate, [b]) - - -def get_animate(): - """Tell the firmware to start/stop animation. - Scrolls the currently saved grid vertically down.""" - res = send_command(CommandVals.Animate, with_response=True) - return bool(res[0]) - - -def b1image_bl(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(command) - - # Flush - command = FWK_MAGIC + [0x17] - 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) - - send_command(CommandVals.Draw, vals) - - -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 + [CommandVals.StageGreyCol, 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 + [CommandVals.DrawGreyColBuffer, 0x00] - send_serial(s, command) - - -def get_color(): - res = send_command(CommandVals.SetColor, with_response=True) - return (int(res[0]), int(res[1]), int(res[2])) - - -def set_color(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(CommandVals.SetColor, rgb) - - -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 - global STOP_THREAD - while True: - if STOP_THREAD: - STOP_THREAD = False - return - now = datetime.now() - passed_time = (now - start) / timedelta(microseconds=1) - - ratio = passed_time / target - if passed_time >= target: - break - - leds = int(306 * ratio) - light_leds(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 snake_keyscan(): - from getkey import getkey, keys - global direction - global body - - while True: - current_dir = direction - key = getkey() - if key in [keys.RIGHT, keys.UP, keys.LEFT, keys.DOWN]: - # Don't allow accidental suicide if we have a body - if key == opposite_direction(current_dir) and body: - continue - direction = key - - -def snake_embedded_keyscan(): - from getkey import getkey, keys - - while True: - key_arg = None - key = getkey() - if key == keys.UP: - key_arg = GameControlVal.Up - elif key == keys.DOWN: - key_arg = GameControlVal.Down - elif key == keys.LEFT: - key_arg = GameControlVal.Left - elif key == keys.RIGHT: - key_arg = GameControlVal.Right - elif key == 'q': - # Quit - key_arg = GameControlVal.Quit - if key_arg is not None: - send_command(CommandVals.GameControl, [key_arg]) - - -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 pong_embedded(): - # Start game - send_command(CommandVals.StartGame, [Game.Pong]) - - from getkey import getkey, keys - - while True: - key_arg = None - key = getkey() - if key == keys.LEFT: - key_arg = ARG_LEFT - elif key == keys.RIGHT: - key_arg = ARG_RIGHT - elif key == 'a': - key_arg = ARG_2LEFT - elif key == 'd': - key_arg = ARG_2RIGHT - elif key == 'q': - # Quit - key_arg = ARG_QUIT - if key_arg is not None: - send_command(CommandVals.GameControl, [key_arg]) - - -def game_of_life_embedded(arg): - # Start game - # TODO: Add a way to stop it - print("Game", int(arg)) - send_command(CommandVals.StartGame, [Game.GameOfLife, int(arg)]) - - -def snake_embedded(): - # Start game - send_command(CommandVals.StartGame, [Game.Snake]) - - snake_embedded_keyscan() - - -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=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() - 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. - """ - global STOP_THREAD - while True: - if STOP_THREAD: - STOP_THREAD = False - return - # Lower values more likely, makes it look nicer - weights = [i*i for i in range(33, 0, -1)] - population = list(range(1, 34)) - vals = random.choices(population, weights=weights, k=9) - eq(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) - - send_command(CommandVals.Draw, vals) - - -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 - send_command(CommandVals.Draw, vals) - - -def pattern(p): - """Display a pattern that's already programmed into the firmware""" - if p == 'All LEDs on': - send_command(CommandVals.Pattern, [PatternVals.FullBrightness]) - elif p == 'Gradient (0-13% Brightness)': - send_command(CommandVals.Pattern, [PatternVals.Gradient]) - elif p == 'Double Gradient (0-7-0% Brightness)': - send_command(CommandVals.Pattern, [PatternVals.DoubleGradient]) - elif p == '"LOTUS" sideways': - send_command(CommandVals.Pattern, [PatternVals.DisplayLotus]) - elif p == 'Zigzag': - send_command(CommandVals.Pattern, [PatternVals.ZigZag]) - elif p == '"PANIC"': - send_command(CommandVals.Pattern, [PatternVals.DisplayPanic]) - elif p == '"LOTUS" Top Down': - send_command(CommandVals.Pattern, [PatternVals.DisplayLotus2]) - elif p == 'All brightness levels (1 LED each)': - all_brightnesses() - 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) - - send_command(CommandVals.Draw, vals) - - -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""" - global STOP_THREAD - while True: - if STOP_THREAD: - STOP_THREAD = False - return - 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, parameters=[], with_response=False): - return send_command_raw(FWK_MAGIC + [command] + parameters, with_response) - - -def send_command_raw(command, with_response=False): - """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) - - if with_response: - res = s.read(RESPONSE_SIZE) - # print(f"Received: {res}") - return res - - -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")], - # TODO: Get default from device - [sg.Slider((0, 255), orientation='h', default_value=120, - k='-BRIGHTNESS-', enable_events=True)], - - [sg.Text("Animation")], - [sg.Button("Start Animation"), sg.Button("Stop Animation")], - - [sg.Text("Pattern")], - [sg.Combo(PATTERNS, k='-PATTERN-', enable_events=True)], - - [sg.Text("Fill screen X% (could be volume indicator)")], - [sg.Slider((0, 100), orientation='h', - k='-PERCENTAGE-', enable_events=True)], - - [sg.Text("Countdown Timer")], - [ - sg.Spin([i for i in range(1, 60)], - initial_value=10, k='-COUNTDOWN-'), - sg.Text("Seconds"), - sg.Button("Start", k='-START-COUNTDOWN-'), - sg.Button("Stop", k='-STOP-COUNTDOWN-'), - ], - - [sg.Text("Black&White Image")], - [sg.Button("Send stripe.gif", k='-SEND-BL-IMAGE-')], - - [sg.Text("Greyscale Image")], - [sg.Button("Send greyscale.gif", k='-SEND-GREY-IMAGE-')], - - [sg.Text("Display Current Time")], - [ - sg.Button("Start", k='-START-TIME-'), - sg.Button("Stop", k='-STOP-TIME-') - ], - - [sg.Text("Display Text with Symbols")], - [sg.Button("Send '2 5 degC thunder'", k='-SEND-TEXT-')], - - # TODO - # [sg.Text("Play Snake")], - # [sg.Button("Start Game", k='-PLAY-SNAKE-')], - - [sg.Text("Equalizer")], - [ - sg.Button("Start random equalizer", k='-RANDOM-EQ-'), - sg.Button("Stop", k='-STOP-EQ-') - ], - - [sg.Text("Sleep")], - [sg.Button("Sleep"), sg.Button("Wake")], - # [sg.Button("Panic")] - - [sg.Button("Quit")] - ] - window = sg.Window("LED Matrix Control", layout) - global STOP_THREAD - 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 == '-PATTERN-': - pattern(values['-PATTERN-']) - - 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 == '-STOP-COUNTDOWN-': - STOP_THREAD = True - - if event == '-SEND-BL-IMAGE-': - image_bl('stripe.gif') - - if event == '-SEND-GREY-IMAGE-': - image_greyscale('greyscale.gif') - - if event == '-START-TIME-': - thread = threading.Thread(target=clock, args=(), daemon=True) - thread.start() - if event == '-STOP-TIME-': - STOP_THREAD = True - - if event == '-SEND-TEXT-': - show_symbols(['2', '5', 'degC', ' ', 'thunder']) - - if event == '-PLAY-SNAKE-': - snake() - - if event == '-RANDOM-EQ-': - thread = threading.Thread(target=random_eq, args=(), daemon=True) - thread.start() - if event == '-STOP-EQ-': - STOP_THREAD = True - - if event == 'Sleep': - send_command(CommandVals.Sleep, [True]) - - if event == 'Wake': - send_command(CommandVals.Sleep, [False]) - - window.close() - - -def display_string(disp_str): - b = [ord(x) for x in disp_str] - send_command(CommandVals.SetText, [len(disp_str)] + b) - - -def display_on_cmd(on): - send_command(CommandVals.DisplayOn, [on]) - - -def invert_screen_cmd(invert): - send_command(CommandVals.InvertScreen, [invert]) - - -def screen_saver_cmd(on): - send_command(CommandVals.ScreenSaver, [on]) - - -def set_fps_cmd(mode): - res = send_command(CommandVals.SetFps, with_response=True) - current_fps = res[0] - - if mode == 'quarter': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b000 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'half': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b001 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'one': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b010 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'two': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b011 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'four': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b100 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'eight': - fps = current_fps & ~LOW_FPS_MASK - fps |= 0b101 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('low') - elif mode == 'sixteen': - fps = current_fps & ~HIGH_FPS_MASK - fps |= 0b00000000 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('high') - elif mode == 'thirtytwo': - fps = current_fps & ~HIGH_FPS_MASK - fps |= 0b00010000 - send_command(CommandVals.SetFps, [fps]) - set_power_mode_cmd('high') - - -def set_power_mode_cmd(mode): - if mode == 'low': - send_command(CommandVals.SetPowerMode, [0]) - elif mode == 'high': - send_command(CommandVals.SetPowerMode, [1]) - else: - print("Unsupported power mode") - sys.exit(1) - -def get_power_mode_cmd(): - res = send_command(CommandVals.SetPowerMode, with_response=True) - current_mode = int(res[0]) - if current_mode == 0: - print(f"Current Power Mode: Low Power") - elif current_mode == 1: - print(f"Current Power Mode: High Power") - -def get_fps_cmd(): - res = send_command(CommandVals.SetFps, with_response=True) - current_fps = res[0] - res = send_command(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}") - - -# 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/fl16-inputmodules/Cargo.toml b/fl16-inputmodules/Cargo.toml index 28250327..4af886d8 100644 --- a/fl16-inputmodules/Cargo.toml +++ b/fl16-inputmodules/Cargo.toml @@ -1,48 +1,50 @@ [package] edition = "2021" name = "fl16-inputmodules" -version = "0.1.5" +version = "0.2.0" [dependencies] -cortex-m = "0.7" -cortex-m-rt = "0.7.3" -embedded-hal = { version = "0.2.7", features = ["unproven"] } +crc = "3.0" +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true -defmt = "0.3" -defmt-rtt = "0.4" +defmt.workspace = true +defmt-rtt.workspace = true -#panic-probe = { version = "0.3", features = ["print-defmt"] } -rp2040-panic-usb-boot = "0.5.0" +#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 = { version="0.8", features=["rt", "critical-section-impl"] } -rp2040-boot2 = "0.3" +rp2040-hal.workspace = true +rp2040-boot2.workspace = true # USB Serial -usb-device = "0.2.9" +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true -heapless = "0.7.16" -usbd-serial = "0.1.1" -usbd-hid = "0.6.1" -fugit = "0.3.7" num = { version = "0.4", default-features = false } num-derive = "0.3" num-traits = { version = "0.2", default-features = false } # LED Matrix -is31fl3741 = { git = "https://github.com/JohnAZoidberg/is31fl3741", branch = "all-at-once-update-deps", optional = true } +is31fl3741 = { workspace = true, optional = true } # B1 Display -st7306 = { git = "https://github.com/FrameworkComputer/st7306-rs", branch = "update-deps", optional = true } -embedded-graphics = { version = "0.8", optional = true } -tinybmp = { version = "0.5.0", optional = true } +st7306 = { workspace = true, optional = true } +embedded-graphics = { workspace = true, optional = true } +tinybmp = { workspace = true, optional = true } # C1 Minimal -smart-leds = { version = "0.3.0", optional = true } -ws2812-pio = { version = "0.6.0", optional = true } +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" ] +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/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 index ec28c542..ed1fbd32 100644 --- a/fl16-inputmodules/src/control.rs +++ b/fl16-inputmodules/src/control.rs @@ -35,6 +35,8 @@ use crate::games::snake; use crate::matrix::*; #[cfg(feature = "ledmatrix")] use crate::patterns::*; +#[cfg(feature = "ledmatrix")] +use is31fl3741::PwmFreq; #[cfg(feature = "c1minimal")] use smart_leds::{SmartLedsWrite, RGB8}; @@ -66,6 +68,8 @@ pub enum CommandVals { SetFps = 0x1A, SetPowerMode = 0x1B, AnimationPeriod = 0x1C, + PwmFreq = 0x1E, + DebugMode = 0x1F, Version = 0x20, } @@ -115,6 +119,7 @@ pub enum GameOfLifeStartParam { Toad = 0x03, Beacon = 0x04, Glider = 0x05, + BeaconToadBlinker = 0x06, } #[derive(Copy, Clone, num_derive::FromPrimitive)] @@ -125,6 +130,30 @@ pub enum DisplayMode { 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 @@ -173,8 +202,15 @@ pub enum Command { GetFps, SetPowerMode(u8), GetPowerMode, + /// Set the animation period in milliseconds SetAnimationPeriod(u16), + /// Get the animation period in milliseconds GetAnimationPeriod, + #[cfg(feature = "ledmatrix")] + SetPwmFreq(PwmFreqArg), + GetPwmFreq, + SetDebugMode(bool), + GetDebugMode, _Unknown, } @@ -215,6 +251,7 @@ pub struct B1DIsplayState { pub screensaver: Option, pub power_mode: PowerMode, pub fps_config: FpsConfig, + /// Animation period in microseconds pub animation_period: u64, } @@ -348,6 +385,18 @@ pub fn parse_module_command(count: usize, buf: &[u8]) -> Option { 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 { @@ -567,6 +616,25 @@ pub fn handle_command( 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), } } diff --git a/fl16-inputmodules/src/fl16.rs b/fl16-inputmodules/src/fl16.rs index 7bf3efce..4a6bdd43 100644 --- a/fl16-inputmodules/src/fl16.rs +++ b/fl16-inputmodules/src/fl16.rs @@ -1,368 +1,317 @@ -// #[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 LedMatrix { - pub device: IS31FL3741, -} - -impl LedMatrix -where - I2C: Write, -{ - pub fn unwrap(self) -> I2C { - self.device.i2c +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) } - - // TODO: Maybe make this private and set it once in the constructor - pub fn set_scaling(&mut self, scale: u8) -> Result<(), I2cError> { - self.device.set_scaling(scale) - } - - pub fn configure(i2c: I2C) -> LedMatrix { - LedMatrix { - device: IS31FL3741 { - i2c, - address: 0x30, - width: 9, - height: 34, - calc_pixel: |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) - } - }, - }, - } - } - - 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/fl16-inputmodules/src/games/game_of_life.rs b/fl16-inputmodules/src/games/game_of_life.rs index c51d4e15..1670bacf 100644 --- a/fl16-inputmodules/src/games/game_of_life.rs +++ b/fl16-inputmodules/src/games/game_of_life.rs @@ -1,7 +1,7 @@ use crate::control::{GameControlArg, GameOfLifeStartParam}; use crate::matrix::{GameState, Grid, LedmatrixState, HEIGHT, WIDTH}; -#[derive(Clone, Copy, num_derive::FromPrimitive)] +#[derive(Clone, Copy, num_derive::FromPrimitive, PartialEq, Eq)] pub enum Cell { Dead = 0, Alive = 1, @@ -12,6 +12,20 @@ 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(); @@ -34,6 +48,7 @@ pub fn game_step(state: &mut LedmatrixState, _random: u8) { } 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(), @@ -57,6 +72,9 @@ impl GameOfLifeState { 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 { @@ -81,12 +99,12 @@ impl GameOfLifeState { // X // X let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; - cells[10][5] = Cell::Alive; - cells[10][6] = Cell::Alive; - cells[10][7] = Cell::Alive; - cells[14][5] = Cell::Alive; - cells[14][6] = Cell::Alive; - cells[14][7] = Cell::Alive; + 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 { @@ -99,12 +117,12 @@ impl GameOfLifeState { // X X // X let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; - cells[10][4] = Cell::Alive; - cells[10][5] = Cell::Alive; - cells[10][6] = Cell::Alive; - cells[11][5] = Cell::Alive; - cells[11][6] = Cell::Alive; - cells[11][7] = Cell::Alive; + 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 { @@ -119,15 +137,15 @@ impl GameOfLifeState { // X // XX let mut cells = [[Cell::Dead; WIDTH]; HEIGHT]; - cells[10][4] = Cell::Alive; - cells[10][5] = Cell::Alive; - cells[11][4] = Cell::Alive; - cells[11][5] = Cell::Alive; - - cells[12][6] = Cell::Alive; - cells[12][7] = Cell::Alive; - cells[13][6] = Cell::Alive; - cells[13][7] = Cell::Alive; + 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 } } @@ -197,7 +215,7 @@ impl GameOfLifeState { self.cells = next_generation; } - fn draw_matrix(&self) -> Grid { + pub fn draw_matrix(&self) -> Grid { let mut grid = Grid::default(); for row in 0..HEIGHT { diff --git a/fl16-inputmodules/src/games/mod.rs b/fl16-inputmodules/src/games/mod.rs index b16c5f82..6263c98d 100644 --- a/fl16-inputmodules/src/games/mod.rs +++ b/fl16-inputmodules/src/games/mod.rs @@ -1,3 +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 index 37a09028..c063386c 100644 --- a/fl16-inputmodules/src/games/pong.rs +++ b/fl16-inputmodules/src/games/pong.rs @@ -28,49 +28,114 @@ pub struct PongState { pub speed: u64, } -pub fn start_game(state: &mut LedmatrixState, _random: u8) { - state.game = Some(GameState::Pong(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 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, + } + } } -pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { - if let Some(GameState::Pong(ref mut pong_state)) = state.game { + +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 pong_state.paddles.0 + PADDLE_WIDTH < WIDTH { - pong_state.paddles.0 += 1; + if self.paddles.0 + PADDLE_WIDTH < WIDTH { + self.paddles.0 += 1; } } GameControlArg::Right => { - if pong_state.paddles.0 >= 1 { - pong_state.paddles.0 -= 1; + if self.paddles.0 >= 1 { + self.paddles.0 -= 1; } } GameControlArg::SecondLeft => { - if pong_state.paddles.1 + PADDLE_WIDTH < WIDTH { - pong_state.paddles.1 += 1; + if self.paddles.1 + PADDLE_WIDTH < WIDTH { + self.paddles.1 += 1; } } GameControlArg::SecondRight => { - if pong_state.paddles.1 >= 1 { - pong_state.paddles.1 -= 1; + if self.paddles.1 >= 1 { + self.paddles.1 -= 1; } } - GameControlArg::Exit => state.game = None, _ => {} } } } +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: @@ -98,55 +163,7 @@ fn hit_paddle(ball: Position, paddles: (usize, usize)) -> Option { pub fn game_step(state: &mut LedmatrixState, _random: u8) { if let Some(GameState::Pong(ref mut pong_state)) = state.game { - pong_state.ball.pos = { - let (vx, vy) = pong_state.ball.direction; - let (x, y) = add_velocity(pong_state.ball.pos, pong_state.ball.direction); - let x = if x > WIDTH - 1 { WIDTH - 1 } else { x }; - if x == 0 || x == WIDTH - 1 { - // Hit wall, bounce back - pong_state.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), pong_state.paddles) { - // Hit paddle, bounce back - // TODO: Change vy direction slightly depending on where the paddle was hit - let (vx, vy) = pong_state.ball.direction; - pong_state.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 - //pong_state.speed += 1; - (x, y) - } else if y == 0 || y == HEIGHT - 1 { - pong_state.speed = 0; - pong_state.ball.direction = (1, 1); //random_v(random); - (WIDTH / 2, HEIGHT / 2) - } else { - (x, y) - }; - (x, y) - }; - state.grid = draw_matrix(pong_state); - } -} - -fn draw_matrix(state: &PongState) -> Grid { - let mut grid = Grid::default(); - - for x in state.paddles.0..state.paddles.0 + PADDLE_WIDTH { - grid.0[x][0] = 0xFF; + pong_state.tick(); + state.grid = pong_state.draw_matrix(); } - for x in state.paddles.1..state.paddles.1 + PADDLE_WIDTH { - grid.0[x][HEIGHT - 1] = 0xFF; - } - grid.0[state.ball.pos.0][state.ball.pos.1] = 0xFF; - - grid } 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 index b985ec65..57f52e2f 100644 --- a/fl16-inputmodules/src/games/snake.rs +++ b/fl16-inputmodules/src/games/snake.rs @@ -26,66 +26,41 @@ pub struct SnakeState { food: Position, } -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 { - head: (4, 0), - direction: HeadDirection::Down, - body: Vec::new(), - game_over: false, - food: place_food(random), - })); -} -pub fn handle_control(state: &mut LedmatrixState, arg: &GameControlArg) { - if let Some(GameState::Snake(ref mut snake_state)) = state.game { - match arg { - GameControlArg::Up => snake_state.direction = HeadDirection::Up, - GameControlArg::Down => snake_state.direction = HeadDirection::Down, - GameControlArg::Left => snake_state.direction = HeadDirection::Left, - GameControlArg::Right => snake_state.direction = HeadDirection::Right, - GameControlArg::Exit => state.game = None, - _ => {} +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 game_step(state: &mut LedmatrixState, random: u8) -> (HeadDirection, bool, usize, Position) { - if let Some(GameState::Snake(ref mut snake_state)) = state.game { - if snake_state.game_over { - return ( - snake_state.direction, - snake_state.game_over, - snake_state.body.len(), - snake_state.head, - ); + pub fn tick(&mut self, random: u8) { + if self.game_over { + return; } - let (x, y) = snake_state.head; - let oldhead = snake_state.head; - snake_state.head = match snake_state.direction { + 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) = snake_state.head; + let (x, y) = self.head; let width = WIDTH as i8; let height = HEIGHT as i8; - if snake_state.body.contains(&snake_state.head) { + if self.body.contains(&self.head) { // Ran into itself - snake_state.game_over = true + self.game_over = true } else if x >= width || x < 0 || y >= height || y < 0 { // Hit an edge if WRAP_ENABLE { - snake_state.head = if x >= width { + self.head = if x >= width { (0, y) } else if x < 0 { (width - 1, y) @@ -97,20 +72,69 @@ pub fn game_step(state: &mut LedmatrixState, random: u8) -> (HeadDirection, bool (x, y) }; } else { - snake_state.game_over = true + self.game_over = true } - } else if snake_state.head == snake_state.food { + } else if self.head == self.food { // Eating food and growing - snake_state.body.insert(0, oldhead).unwrap(); - snake_state.food = place_food(random); - } else if !snake_state.body.is_empty() { + self.body.insert(0, oldhead).unwrap(); + self.food = place_food(random); + } else if !self.body.is_empty() { // Move body along - snake_state.body.pop(); - snake_state.body.insert(0, oldhead).unwrap(); + 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 = draw_matrix(snake_state); + state.grid = snake_state.draw_matrix(); } ( snake_state.direction, @@ -122,17 +146,3 @@ pub fn game_step(state: &mut LedmatrixState, random: u8) -> (HeadDirection, bool (HeadDirection::Down, true, 0, (0, 0)) } } - -fn draw_matrix(state: &SnakeState) -> Grid { - let (x, y) = state.head; - let mut grid = Grid::default(); - - grid.0[x as usize][y as usize] = 0xFF; - grid.0[state.food.0 as usize][state.food.1 as usize] = 0xFF; - for bodypart in &state.body { - let (x, y) = bodypart; - grid.0[*x as usize][*y as usize] = 0xFF; - } - - grid -} 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/led_hal.rs b/fl16-inputmodules/src/led_hal.rs index 5c3795e2..efe9e790 100644 --- a/fl16-inputmodules/src/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 index 7b2c45a1..8c445376 100644 --- a/fl16-inputmodules/src/lib.rs +++ b/fl16-inputmodules/src/lib.rs @@ -1,6 +1,13 @@ #![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")] @@ -8,8 +15,11 @@ 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; @@ -19,7 +29,7 @@ pub mod graphics; #[cfg(feature = "b1display")] pub mod lcd_hal; -#[cfg(feature = "c1minimal")] +#[cfg(all(feature = "c1minimal", not(feature = "qtpy")))] pub mod minimal_hal; pub mod control; diff --git a/fl16-inputmodules/src/mapping.rs b/fl16-inputmodules/src/mapping.rs index 5c640fbe..de17157b 100644 --- a/fl16-inputmodules/src/mapping.rs +++ b/fl16-inputmodules/src/mapping.rs @@ -5,105 +5,313 @@ pub type SingleDisplayData = [u8; 8]; /// Capital letter A pub const CAP_A: SingleDisplayData = [ - 0b00111000, 0b01000100, 0b01000100, 0b01000100, 0b01111100, 0b01000100, 0b01000100, 0b01000100, + 0b00010000, + 0b00101000, + 0b00101000, + 0b01000100, + 0b01111100, + 0b01000100, + 0b01000100, + 0b01000100, ]; /// Capital letter B pub const CAP_B: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01111000, + 0b01111000, + 0b01000100, + 0b01000100, + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, ]; /// Capital letter C pub const CAP_C: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01000000, 0b01111100, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01111100, ]; /// Capital letter D pub const CAP_D: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01111000, + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, ]; /// Capital letter E pub const CAP_E: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01111100, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01111100, ]; /// Capital letter F pub const CAP_F: SingleDisplayData = [ - 0b01111100, 0b01000000, 0b01000000, 0b01111100, 0b01000000, 0b01000000, 0b01000000, 0b01000000, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01111100, + 0b01000000, + 0b01000000, + 0b01000000, + 0b01000000, ]; /// Capital letter G pub const CAP_G: SingleDisplayData = [ - 0b01111000, 0b11000100, 0b10000100, 0b10000000, 0b10011100, 0b10000100, 0b11000100, 0b01111100, + 0b01111000, + 0b11000100, + 0b10000100, + 0b10000000, + 0b10011100, + 0b10000100, + 0b11000100, + 0b01111100, ]; /// Capital letter H pub const CAP_H: SingleDisplayData = [ - 0b01000100, 0b01000100, 0b01000100, 0b01111100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, ]; /// Capital letter I pub const CAP_I: SingleDisplayData = [ - 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, ]; /// Capital letter J -pub const CAP_J: SingleDisplayData = [0; 8]; // TODO +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, + 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, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b10000000, + 0b11111000, ]; /// Capital letter M pub const CAP_M: SingleDisplayData = [ - 0b10000010, 0b11000110, 0b10101010, 0b10111010, 0b10010010, 0b10000010, 0b10000010, 0b10000010, + 0b10000010, + 0b11000110, + 0b10101010, + 0b10111010, + 0b10010010, + 0b10000010, + 0b10000010, + 0b10000010, ]; /// Capital letter N pub const CAP_N: SingleDisplayData = [ - 0b01000100, 0b01100100, 0b01110100, 0b01010100, 0b01011100, 0b01001100, 0b01001100, 0b01000100, + 0b01000100, + 0b01100100, + 0b01110100, + 0b01010100, + 0b01011100, + 0b01001100, + 0b01001100, + 0b01000100, ]; /// Capital letter O pub const CAP_O: SingleDisplayData = [ - 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b00100100, 0b00011000, + 0b00011000, + 0b00100100, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b00100100, + 0b00011000, ]; /// Capital letter P pub const CAP_P: SingleDisplayData = [ - 0b01111000, 0b01000100, 0b01000100, 0b01000100, 0b01111000, 0b01000000, 0b01000000, 0b01000000, + 0b01111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01111000, + 0b01000000, + 0b01000000, + 0b01000000, ]; /// Capital letter Q -pub const CAP_Q: SingleDisplayData = [0; 8]; // TODO +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, + 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, + 0b00000111, + 0b00001000, + 0b00010000, + 0b00001100, + 0b00000010, + 0b00000001, + 0b00000001, + 0b00011110, ]; /// Capital letter T pub const CAP_T: SingleDisplayData = [ - 0b11111110, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, + 0b11111110, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, + 0b00010000, ]; /// Capital letter U pub const CAP_U: SingleDisplayData = [ - 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b01000010, 0b00111100, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b01000010, + 0b00111100, ]; /// Capital letter V -pub const CAP_V: SingleDisplayData = [0; 8]; // TODO +pub const CAP_V: SingleDisplayData = [ + 0b10000001, + 0b10000001, + 0b10000001, + 0b10000001, + 0b10000010, + 0b01000100, + 0b00101000, + 0b00010000, +]; /// Capital letter W -pub const CAP_W: SingleDisplayData = [0; 8]; // TODO +pub const CAP_W: SingleDisplayData = [ + 0b10000010, + 0b10010010, + 0b11010110, + 0b01010100, + 0b01111100, + 0b00110000, + 0b00010000, + 0b00000000, +]; /// Capital letter X -pub const CAP_X: SingleDisplayData = [0; 8]; // TODO +pub const CAP_X: SingleDisplayData = [ + 0b00000000, + 0b10000010, + 0b01000100, + 0b00101000, + 0b00010000, + 0b00101000, + 0b01000100, + 0b10000010, +]; /// Capital letter Y -pub const CAP_Y: SingleDisplayData = [0; 8]; // TODO +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, + 0b01111110, + 0b00000010, + 0b00000100, + 0b00001000, + 0b00010000, + 0b00100000, + 0b01000000, + 0b01111110, ]; /// Number 0 pub const ZERO: SingleDisplayData = [ - 0b00111000, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b01000100, 0b00111000, + 0b00111000, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b01000100, + 0b00111000, ]; /// Number 1 pub const ONE: SingleDisplayData = [ - 0b00000100, 0b00011100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, 0b00000100, + 0b00000100, + 0b00011100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, + 0b00000100, ]; /// " " character pub const SPACE: SingleDisplayData = [0; 8]; @@ -111,5 +319,22 @@ pub const SPACE: SingleDisplayData = [0; 8]; 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, + 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 index 02f3529e..71a625fd 100644 --- a/fl16-inputmodules/src/matrix.rs +++ b/fl16-inputmodules/src/matrix.rs @@ -1,3 +1,5 @@ +use crate::animations::*; +use crate::control::PwmFreqArg; use crate::games::game_of_life::GameOfLifeState; use crate::games::pong::PongState; use crate::games::snake::SnakeState; @@ -14,25 +16,60 @@ impl Default for Grid { } } +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), diff --git a/fl16-inputmodules/src/patterns.rs b/fl16-inputmodules/src/patterns.rs index 5f46b556..5ed6fc9b 100644 --- a/fl16-inputmodules/src/patterns.rs +++ b/fl16-inputmodules/src/patterns.rs @@ -3,10 +3,10 @@ use rp2040_hal::{ pac::I2C1, }; -use crate::fl16::LedMatrix; use crate::led_hal as bsp; use crate::mapping::*; use crate::matrix::*; +use is31fl3741::devices::LedMatrix; /// Bytes needed to represent all LEDs with a single bit /// math.ceil(WIDTH * HEIGHT / 8) @@ -50,6 +50,37 @@ pub fn draw_grey_col(grid: &mut Grid, col: u8, levels: &[u8; HEIGHT]) { 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 { 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 @@ -328,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 index 7d7850b1..545054ac 100644 --- a/fl16-inputmodules/src/serialnum.rs +++ b/fl16-inputmodules/src/serialnum.rs @@ -3,16 +3,43 @@ const FLASH_OFFSET: usize = 0x10000000; const LAST_4K_BLOCK: usize = 0xff000; const SERIALNUM_LEN: usize = 18; -pub fn get_serialnum() -> Option<&'static str> { +#[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; - 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() + 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 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 index 8d37c041..27be7699 100644 --- a/inputmodule-control/Cargo.toml +++ b/inputmodule-control/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "inputmodule-control" -version = "0.1.5" +version = "0.2.0" [dependencies] clap = { version = "4.3", features = ["derive"] } 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/src/font.rs b/inputmodule-control/src/font.rs index fc06f30b..49c9db8d 100644 --- a/inputmodule-control/src/font.rs +++ b/inputmodule-control/src/font.rs @@ -388,7 +388,7 @@ pub fn convert_font(c: char) -> Vec { 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, ], 'G' => vec![ 0, 1, 1, 1, 0, diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 1085a510..d2d4e83c 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -47,6 +47,8 @@ enum Command { Fps = 0x1A, PowerMode = 0x1B, AnimationPeriod = 0x1C, + PwmFreq = 0x1E, + DebugMode = 0x1F, Version = 0x20, } @@ -88,6 +90,12 @@ fn match_serialdevs( // Find all supported Framework devices for p in ports { if let SerialPortType::UsbPort(usbinfo) = &p.port_type { + // macOS creates a /dev/cu.* and /dev/tty.* device. + // The latter can only be used for reading, not writing, so we have to ignore it. + #[cfg(target_os = "macos")] + if !p.port_name.starts_with("/dev/tty.") { + continue; + } if usbinfo.vid == FRAMEWORK_VID && pids.contains(&usbinfo.pid) { compatible_devs.push(p.port_name.clone()); } @@ -152,7 +160,7 @@ pub fn find_serialdevs(args: &crate::ClapCli, wait_for_device: bool) -> (Vec, bool) = find_serialdevs(args, args.wait_for_device); if serialdevs.is_empty() { - println!("Failed to find serial devivce. Please manually specify with --serial-dev"); + println!("Failed to find serial device. Please manually specify with --serial-dev"); return; } else if args.wait_for_device && !waited { println!("Device already present. No need to wait. Not executing command. Sleep 1s"); @@ -221,6 +229,13 @@ pub fn serial_commands(args: &crate::ClapCli) { 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, @@ -388,12 +403,17 @@ fn simple_cmd_multiple(serialdevs: &Vec, command: Command, args: &[u8]) } fn simple_cmd(serialdev: &str, command: Command, args: &[u8]) { - let mut port = serialport::new(serialdev, 115_200) + let port_result = serialport::new(serialdev, 115_200) .timeout(SERIAL_TIMEOUT) - .open() - .expect("Failed to open port"); + .open(); - simple_cmd_port(&mut port, command, args); + 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 { @@ -436,6 +456,26 @@ fn sleeping_cmd(serialdev: &str, arg: Option) { } } +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) @@ -966,7 +1006,13 @@ fn animation_fps_cmd(serialdev: &str, arg: Option) { .expect("Failed to open port"); if let Some(fps) = arg { - let period = (1000 / fps).to_le_bytes(); + const MS: u16 = 1000; + if fps < MS { + // It would need to set the animation period lower than 1ms + println!("Unable to set FPS over 1000"); + return; + } + let period = (MS / fps).to_le_bytes(); simple_cmd_port(&mut port, Command::AnimationPeriod, &[period[0], period[1]]); } else { simple_cmd_port(&mut port, Command::AnimationPeriod, &[]); @@ -975,12 +1021,44 @@ fn animation_fps_cmd(serialdev: &str, arg: Option) { port.read_exact(response.as_mut_slice()) .expect("Found no data!"); - println!("Response: {:X?}", response); 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], diff --git a/inputmodule-control/src/ledmatrix.rs b/inputmodule-control/src/ledmatrix.rs index da9c1e6d..30ae5750 100644 --- a/inputmodule-control/src/ledmatrix.rs +++ b/inputmodule-control/src/ledmatrix.rs @@ -31,6 +31,7 @@ pub enum GameOfLifeStartParam { Toad = 0x03, Beacon = 0x04, Glider = 0x05, + BeaconToadBlinker = 0x06, } /// LED Matrix @@ -126,6 +127,15 @@ pub struct LedMatrixSubcommand { #[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, diff --git a/led-matrix.py b/led-matrix.py index fc178dfb..39bb616c 100755 --- a/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,6 +37,12 @@ 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 = [] @@ -45,7 +52,7 @@ def get_leds(): # 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 index 720383da..563971c6 100644 --- a/ledmatrix/Cargo.toml +++ b/ledmatrix/Cargo.toml @@ -1,33 +1,36 @@ [package] edition = "2021" name = "ledmatrix" -version = "0.1.5" +version = "0.2.0" + +[features] +10k = [] +evt = [] [dependencies] -cortex-m = "0.7" -cortex-m-rt = "0.7.3" -embedded-hal = { version = "0.2.7", features = ["unproven"] } +cortex-m.workspace = true +cortex-m-rt.workspace = true +embedded-hal.workspace = true -defmt = "0.3" -defmt-rtt = "0.4" +defmt.workspace = true +defmt-rtt.workspace = true -#panic-probe = { version = "0.3", features = ["print-defmt"] } -rp2040-panic-usb-boot = "0.5.0" +#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 = { version="0.8", features=["rt", "critical-section-impl"] } -rp2040-boot2 = "0.3" +rp2040-hal.workspace = true +rp2040-boot2.workspace = true # USB Serial -usb-device= "0.2.9" - -heapless = "0.7.16" -usbd-serial = "0.1.1" -usbd-hid = "0.6.1" -fugit = "0.3.7" +usb-device.workspace = true +heapless.workspace = true +usbd-serial.workspace = true +usbd-hid.workspace = true +fugit.workspace = true -is31fl3741 = { git = "https://github.com/JohnAZoidberg/is31fl3741", branch = "all-at-once-update-deps", optional = true } +is31fl3741.workspace = true [dependencies.fl16-inputmodules] path = "../fl16-inputmodules" -features = [ "ledmatrix" ] +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 index b9805b26..d2f6cc25 100644 --- a/ledmatrix/README.md +++ b/ledmatrix/README.md @@ -155,10 +155,10 @@ Instead try out the [Python script](../python.md): ```sh # Snake -./control.py --snake +./ledmatrix_control.py --snake # Pong (Seems broken at the moment) -./control.py --pong-embedded +./ledmatrix_control.py --pong-embedded ``` ###### Game of Life @@ -182,5 +182,68 @@ If you want to display something else, either reset the module (unplugging) or run the stop command. ```sh -inputmodule-control led-amtrix --stop-game +inputmodule-control led-matrix --stop-game ``` + +## Sleep Behavior + +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 index d8681f7e..c4082c72 100644 --- a/ledmatrix/src/main.rs +++ b/ledmatrix/src/main.rs @@ -16,15 +16,39 @@ use rp2040_hal::{ //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 INSTANT_SLEEP: bool = false; +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 -/// Set to 94 because that results in just below 500mA current draw. +/// 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))] @@ -85,7 +109,15 @@ const MAX_BRIGHTNESS: u8 = 94; // 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; @@ -110,7 +142,6 @@ use core::fmt::Write; use heapless::String; use fl16_inputmodules::control::*; -use fl16_inputmodules::fl16::LedMatrix; use fl16_inputmodules::games::{pong, snake}; use fl16_inputmodules::matrix::*; use fl16_inputmodules::patterns::*; @@ -118,10 +149,10 @@ use fl16_inputmodules::serialnum::{device_release, get_serialnum}; // FRA - Framwork // KDE - C1 LED Matrix -// AM - Atemitech -// 00 - Default Configuration +// BZ - BizLink +// 01 - SKU, Default Configuration // 00000000 - Device Identifier -const DEFAULT_SERIAL: &str = "FRAKDEAM0000000000"; +const DEFAULT_SERIAL: &str = "FRAKDEBZ0100000000"; #[entry] fn main() -> ! { @@ -167,7 +198,7 @@ fn main() -> ! { let mut serial = SerialPort::new(&usb_bus); let serialnum = if let Some(serialnum) = get_serialnum() { - serialnum + serialnum.serialnum } else { DEFAULT_SERIAL }; @@ -197,6 +228,8 @@ fn main() -> ! { &clocks.peripheral_clock, ); + let dip1 = pins.dip1.into_pull_up_input(); + let mut state = LedmatrixState { grid: percentage(0), col_buffer: Grid::default(), @@ -205,24 +238,64 @@ fn main() -> ! { 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); }; - let mut matrix = LedMatrix::configure(i2c); + #[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 prev_timer = timer.get_counter().ticks(); + let mut animation_timer = timer.get_counter().ticks(); let mut game_timer = timer.get_counter().ticks(); - - let mut startup_percentage = Some(0); + 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 @@ -237,28 +310,89 @@ fn main() -> ! { 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(); - handle_sleep( - host_sleeping, - &mut state, - &mut matrix, - &mut delay, - &mut led_enable, + 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 - if timer.get_counter().ticks() > prev_timer + state.animation_period { - // On startup slowly turn the screen on - it's a pretty effect :) - match startup_percentage { - Some(p) if p <= 100 => { - state.grid = percentage(p); - startup_percentage = Some(p + 5); + 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); @@ -267,11 +401,29 @@ fn main() -> ! { state.grid.0[x].rotate_right(1); } } - prev_timer = timer.get_counter().ticks(); + 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) => { @@ -283,28 +435,49 @@ fn main() -> ! { Ok(count) => { let random = get_random_byte(&rosc); match (parse_command(count, &buf), &state.sleeping) { - (Some(Command::Sleep(go_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( - go_sleeping, + sleep_reason, &mut state, &mut matrix, &mut delay, &mut led_enable, ); - } - (Some(c @ Command::BootloaderReset), _) - | (Some(c @ Command::IsSleeping), _) => { - if let Some(response) = - handle_command(&c, &mut state, &mut matrix, random) - { - let _ = serial.write(&response); - }; - } - (Some(command), SleepState::Awake) => { + // If there's a very early command, cancel the startup animation - startup_percentage = None; + 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(); - // While sleeping no command is handled, except waking up if let Some(response) = handle_command(&command, &mut state, &mut matrix, random) { @@ -319,12 +492,29 @@ fn main() -> ! { 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 @@ -378,25 +568,55 @@ fn get_random_byte(rosc: &RingOscillator) -> u8 { 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( - go_sleeping: bool, + sleep_reason: Option, state: &mut LedmatrixState, matrix: &mut Foo, delay: &mut Delay, led_enable: &mut gpio::Pin>, ) { - match (state.sleeping.clone(), go_sleeping) { - (SleepState::Awake, false) => (), - (SleepState::Awake, true) => { + 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)); - // Perhaps we could have a sleep pattern. Probbaly not Or maybe - // just for the first couple of minutes? - // state.grid = display_sleep(); - // fill_grid_pixels(&state, matrix); - // Slowly decrease brightness - if !INSTANT_SLEEP { - delay.delay_ms(1000); + if dyn_sleep_mode(state) == SleepMode::Fading { let mut brightness = state.brightness; loop { delay.delay_ms(100); @@ -408,25 +628,39 @@ fn handle_sleep( } } - // Turn LED controller off to save power - led_enable.set_low().unwrap(); + 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(); } - (SleepState::Sleeping(_), true) => (), - (SleepState::Sleeping((old_grid, old_brightness)), false) => { + // 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 - led_enable.set_high().unwrap(); + if !debug_mode(state) { + led_enable.set_high().unwrap(); + } // Slowly increase brightness - if !INSTANT_SLEEP { - delay.delay_ms(1000); + if dyn_sleep_mode(state) == SleepMode::Fading { let mut brightness = 0; loop { delay.delay_ms(100); diff --git a/python.md b/python.md deleted file mode 100644 index 894be3ac..00000000 --- a/python.md +++ /dev/null @@ -1,58 +0,0 @@ -# Python script to control Framework Laptop 16 Input Modules - -Requirements: Python, [PySimpleGUI](https://www.pysimplegui.org) and optionally [pillow](https://pillow.readthedocs.io/en/stable/index.html) - -Use `control.py`. Either the commandline, see `control.py --help` or the graphical version: `control.py --gui` - -``` -options: - -h, --help show this help message and exit - --bootloader Jump to the bootloader to flash new firmware - --sleep, --no-sleep Simulate the host going to sleep or waking up - --brightness BRIGHTNESS - Adjust the brightness. Value 0-255 - --animate, --no-animate - Start/stop vertical scrolling - --pattern {full,lotus,gradient,double-gradient,zigzag,panic,lotus2} - Display a pattern - --image IMAGE Display a PNG or GIF image in black and white only) - --image-grey IMAGE_GREY - Display a PNG or GIF image in greyscale - --percentage PERCENTAGE - Fill a percentage of the screen - --clock Display the current time - --string STRING Display a string or number, like FPS - --symbols SYMBOLS [SYMBOLS ...] - Show symbols (degF, degC, :), snow, cloud, ...) - --gui Launch the graphical version of the program - --blink Blink the current pattern - --breathing Breathing of the current pattern - --eq EQ [EQ ...] Equalizer - --random-eq Random Equalizer - --wpm WPM Demo - --snake Snake - --all-brightnesses Show every pixel in a different brightness - --set-color {white,black,red,green,blue,cyan,yellow,purple} - Set RGB color (C1 Minimal Input Module) - --get-color Get RGB color (C1 Minimal Input Module) - -v, --version Get device version - --serial-dev SERIAL_DEV - Change the serial dev. Probably /dev/ttyACM0 on Linux, COM0 on Windows -``` - -Examples - -```sh -# Launch graphical application -./control.py --gui - -# Show current time and keep updating it -./control.py --clock - -# Draw PNG or GIF -./control.py --image stripe.gif -./control.py --image stripe.png - -# Change brightness (0-255) -./control.py --brightness 50 -``` 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 index 5ce400b6..460b3c4b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.69.0" +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