commit d2bebc9e7b196f09a9126f1e32fe4ceaf1a5298a Author: nils Date: Fri Aug 21 08:44:19 2026 +0200 Initial import: Rust BLE-MIDI keypad for Pico W Port of the C/BTstack BLE-MIDI keypad firmware to Rust (embassy + cyw43 + trouble). 16-pad RGB keypad (APA102 SPI + I2C button expander), BLE-MIDI 1.0 GATT server (full codec: sysex, running-status, real-time), octave display on BOOTSEL, onboard LED heartbeat, USB serial logging. - src/keypad.rs Pimoroni RGB Keypad driver - src/midi_keypad.rs pad->Note On/Off + LED response state machine - src/codec.rs BLE-MIDI 1.0 codec (port of rppicomidi, MIT) - src/main.rs cyw43 BT bring-up + trouble GATT + MIDI pump - src/octave.rs octave digit display on BOOTSEL GPL-3.0-or-later; codec.rs retains its MIT (rppicomidi) attribution. diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..fc7c1c6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "thumbv6m-none-eabi" # Cortex-M0+ (RP2040) + +[env] +DEFMT_LOG = "info" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4751bea --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2074 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-device-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c051592f59fe68053524b4c4935249b806f72c1f544cfb7abe4f57c3be258e" +dependencies = [ + "aligned", +] + +[[package]] +name = "bt-hci" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63b3d6f0ef006d9b87a075705d6c142b03681ad34090659647f0e227c1c667f" +dependencies = [ + "bt-hci-transport", + "btuuid", + "defmt 1.1.1", + "embassy-sync", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "futures-intrusive", + "heapless 0.9.3", +] + +[[package]] +name = "bt-hci-transport" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb9819c69cf88877f5eebc8c5ade0ab50f2bf9475bbb3eb714d66c55b0cb64" +dependencies = [ + "defmt 1.1.1", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", +] + +[[package]] +name = "btuuid" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f48f1e9b0aad0a4f05d17bdeae0fa20ff798e272a03a6940ca27ad9c5a6ae7" +dependencies = [ + "defmt 0.3.100", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "defmt 1.1.1", + "num-traits", + "pure-rust-locales", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "cortex-m" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844b9697e922c99847eed515c6eb6d101e7ce62ff556fcaec243798291427ee8" +dependencies = [ + "bare-metal", + "bitfield 0.13.2", + "cortex-m-macros", + "critical-section", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "volatile-register", +] + +[[package]] +name = "cortex-m-macros" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1922be58519ad40368fc4ca595a2cefa51a7abf947be3b0c90586dc7dbd0e2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c439b23bf18154d8a634543caf1ee73cceb723f2902f7ea243634159a00fd47" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a75f3e7f3f01d649668d68e02d0ef92c7041a9c97b8fe6bc354ddbf40822d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-any" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46db9f663dfb869b80fcf59e32d7a80fc6c464a4f6328f3f06a00f5e36d05f8c" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cyw43" +version = "0.7.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "aligned", + "bt-hci-transport", + "cortex-m", + "cortex-m-rt", + "defmt 1.1.1", + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-net-driver-channel", + "embassy-sync", + "embassy-time", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-io-async 0.7.0", + "futures", + "heapless 0.9.3", + "sdio", + "trait-set", +] + +[[package]] +name = "cyw43-pio" +version = "0.10.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cyw43", + "defmt 1.1.1", + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-rp", + "fixed", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "debug-helper" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a4af69c60438a1a82af89d362f4729fd38db7b73f305a237636fad31ceb2bf" + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05144944c117db54127a8ac17a3c9a28d55e7047bfddb950bcd20b4a94c79b10" +dependencies = [ + "critical-section", + "defmt 1.1.1", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embassy-embedded-hal" +version = "0.6.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-hal-internal", + "embassy-sync", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-executor" +version = "0.10.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cordyceps", + "cortex-m", + "critical-section", + "defmt 1.1.1", + "document-features", + "embassy-executor-macros", + "embassy-executor-timer-queue", + "unitrait", +] + +[[package]] +name = "embassy-executor-macros" +version = "0.8.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "embassy-executor-timer-queue" +version = "0.1.1" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "unitrait", +] + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" + +[[package]] +name = "embassy-hal-internal" +version = "0.5.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cortex-m", + "critical-section", + "defmt 1.1.1", + "num-traits", +] + +[[package]] +name = "embassy-net-driver" +version = "0.2.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" + +[[package]] +name = "embassy-net-driver-channel" +version = "0.4.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-net-driver", + "embassy-sync", +] + +[[package]] +name = "embassy-rp" +version = "0.10.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cfg-if", + "chrono", + "cortex-m", + "cortex-m-rt", + "critical-section", + "defmt 1.1.1", + "document-features", + "embassy-embedded-hal", + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-hal-internal", + "embassy-sync", + "embassy-time", + "embassy-time-driver", + "embassy-time-queue-utils", + "embassy-usb-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "embedded-storage", + "embedded-storage-async", + "fixed", + "nb 1.1.0", + "pio", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_core 0.9.5", + "rp-pac", + "rp2040-boot2", + "sha2-const-stable", + "smart-leds-trait", +] + +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cfg-if", + "critical-section", + "defmt 1.1.1", + "embedded-io-async 0.7.0", + "futures-core", + "futures-sink", + "heapless 0.9.3", +] + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "cfg-if", + "critical-section", + "defmt 1.1.1", + "document-features", + "embassy-time-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-utils" +version = "0.3.2" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "embassy-executor-timer-queue", + "heapless 0.9.3", +] + +[[package]] +name = "embassy-usb" +version = "0.6.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "bitflags 2.13.1", + "defmt 1.1.1", + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-net-driver-channel", + "embassy-sync", + "embassy-time", + "embassy-usb-driver", + "embedded-io-async 0.7.0", + "heapless 0.9.3", + "ssmarshal", + "usbd-hid", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.2.2" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "defmt 1.1.1", + "embedded-io-async 0.6.1", + "embedded-io-async 0.7.0", +] + +[[package]] +name = "embassy-usb-logger" +version = "0.6.0" +source = "git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a#3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" +dependencies = [ + "embassy-futures 0.1.2 (git+https://github.com/embassy-rs/embassy.git?rev=3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a)", + "embassy-sync", + "embassy-usb", + "log", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "defmt 1.1.1", + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "embedded-storage-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fixed" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" +dependencies = [ + "az", + "bytemuck", + "defmt 1.1.1", + "half", + "typenum", +] + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "defmt 1.1.1", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "panic-probe" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" +dependencies = [ + "cortex-m", + "defmt 1.1.1", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pio" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ba4153cee9585abc451271aa437d9e8defdea8b468d48ba6b8f098cbe03d7f" +dependencies = [ + "pio-core", + "pio-proc", +] + +[[package]] +name = "pio-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61d90fddc3d67f21bbf93683bc461b05d6a29c708caf3ffb79947d7ff7095406" +dependencies = [ + "arrayvec", + "num_enum", + "paste", +] + +[[package]] +name = "pio-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825266c1eaddf54f636d06eefa4bf3c99d774c14ec46a4a6c6e5128a0f10d205" +dependencies = [ + "lalrpop", + "lalrpop-util", + "pio-core", +] + +[[package]] +name = "pio-proc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4a76571f5fe51af43cc80ac870fe0c79cc0cdd686b9002a6c4c84bfdd0176b" +dependencies = [ + "codespan-reporting", + "lalrpop-util", + "pio-core", + "pio-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +dependencies = [ + "critical-section", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pure-rust-locales" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869675ad2d7541aea90c6d88c81f46a7f4ea9af8cd0395d38f11a95126998a0d" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rgb-keypad" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "cyw43", + "cyw43-pio", + "defmt 1.1.1", + "defmt-rtt", + "embassy-executor", + "embassy-futures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-rp", + "embassy-sync", + "embassy-time", + "embassy-usb", + "embassy-usb-logger", + "embedded-hal-async", + "heapless 0.8.0", + "log", + "panic-probe", + "portable-atomic", + "static_cell", + "trouble-host", +] + +[[package]] +name = "rp-pac" +version = "7.0.0" +source = "git+https://github.com/embassy-rs/rp-pac?rev=c2e27609b021c444f634673155318977d7f0bdf6#c2e27609b021c444f634673155318977d7f0bdf6" +dependencies = [ + "cortex-m", + "cortex-m-rt", +] + +[[package]] +name = "rp2040-boot2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92f344f63f950ee36cf4080050e4dce850839b9175da38f9d2ffb69b4dbb21" +dependencies = [ + "crc-any", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdio" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7328e9dbad1337ef2d94a8404428a42d77a9916cda7c2e51287a1998da9afb" +dependencies = [ + "aligned", + "block-device-driver", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smart-leds-trait" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f4441a131924d58da6b83a7ad765c460e64630cce504376c3a87a2558c487f" +dependencies = [ + "rgb", +] + +[[package]] +name = "ssmarshal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" +dependencies = [ + "encode_unicode", + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trait-set" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79e2e9c9ab44c6d7c20d5976961b47e8f49ac199154daa514b77cd1ab536625" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "trouble-host" +version = "0.8.0" +source = "git+https://github.com/embassy-rs/trouble?rev=4eea72088fdbb91326a3ac95160b3154fdff802a#4eea72088fdbb91326a3ac95160b3154fdff802a" +dependencies = [ + "bt-hci", + "defmt 1.1.1", + "embassy-futures 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "embassy-sync", + "embassy-time", + "embedded-io 0.7.1", + "futures", + "heapless 0.9.3", + "static_cell", + "trouble-host-macros", + "zerocopy", +] + +[[package]] +name = "trouble-host-macros" +version = "0.6.0" +source = "git+https://github.com/embassy-rs/trouble?rev=4eea72088fdbb91326a3ac95160b3154fdff802a#4eea72088fdbb91326a3ac95160b3154fdff802a" +dependencies = [ + "convert_case", + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", + "uuid", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unitrait" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66b59b4e118746027256db08772826e9e0765e93b0bf15d2714a0ac0f940fbe" + +[[package]] +name = "usb-device" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6" +dependencies = [ + "heapless 0.8.0", + "portable-atomic", +] + +[[package]] +name = "usbd-hid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837741765c5aca78159bc475a788b71b731da71373fccef292e61744e76e3ac" +dependencies = [ + "usb-device", + "usbd-hid-macros", +] + +[[package]] +name = "usbd-hid-descriptors" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b297f021719c4308d5d0c61b6c1e7c6b3ba383deba774b49aa5484f996bdb8f1" +dependencies = [ + "bitfield 0.14.0", +] + +[[package]] +name = "usbd-hid-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c2e9e96d9ab0215ed5d01c72acf0582d8c1a9b73d990035da607dfc12e0b53" +dependencies = [ + "byteorder", + "log", + "proc-macro2", + "quote", + "serde", + "syn 3.0.3", + "usbd-hid-descriptors", +] + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2e8c0ee --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "rgb-keypad" +version = "0.1.0" +edition = "2021" +resolver = "2" + +[dependencies] +embassy-executor = { version = "0.10", default-features = false, features = ["platform-cortex-m", "executor-thread", "defmt", "executor-interrupt"] } +embassy-time = { version = "0.5", default-features = false, features = ["defmt", "defmt-timestamp-uptime"] } +embassy-rp = { version = "0.10", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl", "rp2040"] } +embassy-sync = { version = "0.8", features = ["defmt"] } +embassy-futures = "0.1.2" +embassy-usb = { version = "0.6", default-features = false, features = ["defmt"] } +embassy-usb-logger = { version = "0.6", default-features = false } +log = "0.4" + +# BLE: cyw43 (CYW43439 BT controller on Pico W) + trouble (BLE host / GATT). +# trouble-host 0.8.0 is unreleased on crates.io, so we pull it from git; it must +# match the bt-hci version used by cyw43, hence the embassy/cyw43 git patch +# below pins everything to a known-compatible revision (the same one the +# upstream rp-pico-w example uses). +trouble-host = { git = "https://github.com/embassy-rs/trouble", rev = "4eea72088fdbb91326a3ac95160b3154fdff802a", features = ["derive", "gatt", "defmt", "default-packet-pool-mtu-128"] } +cyw43 = { version = "0.7", features = ["defmt", "firmware-logs", "bluetooth"] } +cyw43-pio = { version = "0.10", features = ["defmt"] } + +defmt = "1.0" +defmt-rtt = "1.0" +panic-probe = { version = "1.0", features = ["print-defmt"] } + +cortex-m = { version = "0.7.6" } +cortex-m-rt = "0.7.0" +static_cell = "2" +portable-atomic = { version = "1.5", features = ["critical-section"] } +heapless = "0.8" + +embedded-hal-async = "1.0" + +[profile.release] +debug = 2 +opt-level = "s" + +# Pin the embassy + cyw43 crates to the same git revision the upstream +# rp-pico-w Bluetooth example validates, so cyw43's bt-hci and trouble's +# bt-hci unify on one version (released crates pull incompatible bt-hci majors). +[patch.crates-io] +cyw43 = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +cyw43-pio = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-usb = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } +embassy-usb-logger = { git = "https://github.com/embassy-rs/embassy.git", rev = "3cd51e6d8eb6aff8b0d64d9e56a75a538bcfc65a" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ea28dba --- /dev/null +++ b/LICENSE @@ -0,0 +1,652 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based on +the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that the work is licensed under this License and how to +view a copy of this License. If the interface presents a list of user +commands or options, such as a menu, a prominent item in the list meets +this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes this unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to the +covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of the source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall be +treated as though they were included in this License, to the extent that +they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not +to sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for or in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey the +Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFICULT, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w` and `show c` should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e18eef --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# pico-pi-midi (Rust) + +BLE-MIDI keypad firmware for the Raspberry Pi Pico W (RP2040 + CYW43439), written in Rust using [embassy](https://embassy.dev), the [cyw43](https://crates.io/crates/cyw43) driver (Bluetooth), and the [TrouBLE](https://github.com/embassy-rs/trouble) BLE host. + +This is the Rust port of the original C/BTstack firmware (now kept separately as `pico-pi-midi-c`). It talks BLE-MIDI 1.0 to iOS / macOS Core MIDI, turning the Pimoroni Pico RGB Keypad into a 16-pad wireless MIDI controller. + +## What it does + +- **16-pad keypad** (Pimoroni RGB Keypad Base): reads buttons over I²C (GPIO expander @ 0x20) and drives APA102 RGB LEDs over SPI (CS=17, SCK=18, MOSI=19). +- **BLE-MIDI 1.0** GATT server (MIDI Service `03B80E5A-…`), advertised so Core MIDI discovers it as a MIDI device. Pad presses → Note On/Off over Bluetooth; incoming Note On lights the pad. +- **Octave display**: the BOOTSEL button cycles the keypad's note offset by +12 and flashes the active octave as a digit on the 4×4 grid for 500 ms. +- **Onboard LED** heartbeat (driven via the CYW43 chip — the Pico W's LED is on the wireless chip, not an RP2040 pin). +- **USB serial logging** via `embassy-usb-logger` — connect and read with `screen /dev/cu.usbmodem* 115200` to see boot/advertising logs. + +## Roadmap + +This is v1: a BLE-MIDI keypad. The goal is a **flexible MIDI router** — MIDI over USB, BLE, keypad, TRS, and 4 stereo CV outputs, with channels configurable at runtime via an LCD. See the architecture notes (kept in the session memory) for the plan. + +## Building + +Requires Rust stable (≥1.93) and the `thumbv6m-none-eabi` target: + +```sh +rustup target add thumbv6m-none-eabi +cargo build --release +``` + +The CYW43 firmware blobs (`cyw43-firmware/*.bin`) are committed here so no network access is needed at build time. They are redistributable under the permissive binary license in the upstream [`embassy cyw43-firmware`](https://github.com/embassy-rs/embassy/tree/main/cyw43-firmware) directory. + +## Flashing + +```sh +cargo build --release +elf2uf2-rs convert target/thumbv6m-none-eabi/release/rgb-keypad target/uf2/rgb-keypad.uf2 +# hold BOOTSEL + plug in the Pico W, then: +picotool load target/uf2/rgb-keypad.uf2 -x +``` + +## License + +GPL-3.0-or-later (see [`LICENSE`](LICENSE)). + +`src/codec.rs` is a Rust port of the BLE-MIDI 1.0 packet codec from +[rppicomidi/pico-w-ble-midi-lib](https://github.com/rppicomidi/pico-w-ble-midi-lib), +which is MIT-licensed; that attribution is retained in the file header. MIT is +GPL-3.0-compatible, so the repository as a whole is GPL-3.0-or-later while that +file preserves its MIT provenance. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..1cb5fc7 --- /dev/null +++ b/build.rs @@ -0,0 +1,24 @@ +//! Build script: place memory.x on the linker search path and set up the +//! RP2040 + defmt linker scripts. The CYW43 firmware blobs are committed +//! under `cyw43-firmware/` (no network access needed at build time). + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo::rustc-link-search={}", out.display()); + + println!("cargo::rerun-if-changed=memory.x"); + + println!("cargo::rustc-link-arg-bins=--nmagic"); + println!("cargo::rustc-link-arg-bins=-Tlink.x"); + println!("cargo::rustc-link-arg-bins=-Tlink-rp.x"); + println!("cargo::rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/cyw43-firmware/43439A0.bin b/cyw43-firmware/43439A0.bin new file mode 100644 index 0000000..a05482f Binary files /dev/null and b/cyw43-firmware/43439A0.bin differ diff --git a/cyw43-firmware/43439A0_btfw.bin b/cyw43-firmware/43439A0_btfw.bin new file mode 100644 index 0000000..290ce8e Binary files /dev/null and b/cyw43-firmware/43439A0_btfw.bin differ diff --git a/cyw43-firmware/43439A0_clm.bin b/cyw43-firmware/43439A0_clm.bin new file mode 100644 index 0000000..dc4ee02 Binary files /dev/null and b/cyw43-firmware/43439A0_clm.bin differ diff --git a/cyw43-firmware/nvram_rp2040.bin b/cyw43-firmware/nvram_rp2040.bin new file mode 100644 index 0000000..7a24de5 Binary files /dev/null and b/cyw43-firmware/nvram_rp2040.bin differ diff --git a/memory.x b/memory.x new file mode 100644 index 0000000..6d065b6 --- /dev/null +++ b/memory.x @@ -0,0 +1,7 @@ +MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 + + /* RP2040: 264K RAM (6K stripe + 256K main, used as one block) */ + RAM : ORIGIN = 0x20000000, LENGTH = 264K +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..eaf13e5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +targets = ["thumbv6m-none-eabi"] diff --git a/src/codec.rs b/src/codec.rs new file mode 100644 index 0000000..a93909d --- /dev/null +++ b/src/codec.rs @@ -0,0 +1,719 @@ +//! BLE-MIDI 1.0 packet codec — faithful Rust port of `ble_midi_pkt_codec.c` +//! (rppicomidi, MIT). Encodes a MIDI 1.0 byte stream into BLE-MIDI packets and +//! decodes BLE-MIDI packets back into timestamped MIDI messages, with full +//! support for system-exclusive, running-status, and system real-time. +//! +//! Pure `no_std` logic (no allocator, no HAL/BLE deps) so it is independent of +//! the rest of the firmware and could be unit-tested on host. +//! +//! Spec: https://midi.org/specifications/midi-transports-specifications/midi-over-bluetooth-low-energy-ble-midi +//! +//! --- +//! +//! This file is a Rust port of MIT-licensed code by rppicomidi +//! (https://github.com/rppicomidi/pico-w-ble-midi-lib). The original MIT +//! notice and permission are retained below per its terms. The port is +//! contributed to this project under GPL-3.0-or-later (see the repository +//! LICENSE); MIT is GPL-3.0-compatible so both apply here. +//! +//! Original MIT license — Copyright (c) 2023 rppicomidi +//! +//! Permission is hereby granted, free of charge, to any person obtaining a copy +//! of this software and associated documentation files (the "Software"), to deal +//! in the Software without restriction, including without limitation the rights +//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//! copies of the Software, and to permit persons to whom the Software is +//! furnished to do so, subject to the following conditions: +//! +//! The above copyright notice and this permission notice shall be included in +//! all copies or substantial portions of the Software. +//! +//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//! THE SOFTWARE. +//! +//! SPDX-License-Identifier: GPL-3.0-or-later + +#![allow(clippy::needless_range_loop)] + +/// Max BLE-MIDI packet payload. Without LE Data Length Extension the ATT default +/// MTU is 23, giving a 20-byte MIDI payload (MTU - 3 ATT header). +pub const MAX_BLE_MIDI_PACKET: usize = 20; + +// Flag bits stored in the upper bits of `BleMidiMessage::nbytes`. +const IS_START_SYSEX: u8 = 0x80; +const IS_SYSEX: u8 = 0x20; +const IS_REAL_TIME: u8 = 0x10; +const IS_CHANNEL: u8 = 0x08; +const NBYTES_MASK: u8 = 0x03; + +const fn header(ts: u16) -> u8 { + 0x80 | ((ts >> 7) & 0x3F) as u8 +} +const fn timestamp_low(ts: u16) -> u8 { + 0x80 | (ts & 0x7F) as u8 +} + +/// A timestamped fragment of a MIDI message (the decode output unit). +/// +/// The upper bits of `nbytes` encode what kind of message is in `msg_bytes`; +/// the low 2 bits hold the number of valid bytes. +#[derive(Clone, Copy, Default)] +pub struct BleMidiMessage { + pub nbytes: u8, + pub msg_bytes: [u8; 3], + pub timestamp_ms: u16, +} + +impl BleMidiMessage { + /// Number of valid bytes in `msg_bytes`. + pub fn byte_count(&self) -> u8 { + self.nbytes & NBYTES_MASK + } +} + +/// A BLE-MIDI 1.0 data packet (the encode output / notify payload). +#[derive(Clone, Copy)] +pub struct BleMidiPacket { + /// Number of valid bytes in `pkt`. + pub nbytes: usize, + pub pkt: [u8; MAX_BLE_MIDI_PACKET], +} + +impl Default for BleMidiPacket { + fn default() -> Self { + Self { + nbytes: 0, + pkt: [0; MAX_BLE_MIDI_PACKET], + } + } +} + +/// Single-producer/single-consumer ring of fixed-size `Copy` elements. +struct Ring { + buf: [T; N], + head: usize, + tail: usize, + len: usize, +} + +impl Ring { + fn new() -> Self { + Self { + buf: core::array::from_fn(|_| T::default()), + head: 0, + tail: 0, + len: 0, + } + } + fn push(&mut self, v: T) -> bool { + if self.len < N { + self.buf[self.tail] = v; + self.tail = (self.tail + 1) % N; + self.len += 1; + true + } else { + false + } + } + fn pop(&mut self) -> Option { + if self.len > 0 { + let v = self.buf[self.head]; + self.head = (self.head + 1) % N; + self.len -= 1; + Some(v) + } else { + None + } + } + fn len(&self) -> usize { + self.len + } +} + +/// Encoder state for one connection (mirrors `to_ble_midi_stream_t`). +#[derive(Clone, Copy, Default)] +struct ToBleStream { + mes: BleMidiMessage, + next_msg_byte_idx: u16, + previous_timestamp: u16, + running_status: u8, + npending_rt: u8, + pending_rt_status: [u8; 4], + pending_rt_timestamp: [u16; 4], + pending_ble_pkt: BleMidiPacket, + pending_ble_midi_pkt_running_status: u8, + pending_ble_midi_pkt_prev_status: u8, +} + +/// Per-connection BLE-MIDI codec context. +pub struct Codec { + to_ble: Ring, + from_ble: Ring, + stream: ToBleStream, + ble_mtu: usize, +} + +impl Codec { + pub fn new() -> Self { + let mut c = Self { + to_ble: Ring::new(), + from_ble: Ring::new(), + stream: ToBleStream::default(), + ble_mtu: MAX_BLE_MIDI_PACKET, + }; + c.reset_stream(); + c + } + + pub fn set_mtu(&mut self, ble_mtu: usize) { + self.ble_mtu = ble_mtu; + } + + /// Update MTU, capped at the packet buffer size (matches the C `update_mtu` + /// which does `MIN(ble_mtu, sizeof(pending_ble_pkt.pkt))`). + pub fn update_mtu(&mut self, ble_mtu: usize) { + self.ble_mtu = core::cmp::min(ble_mtu, MAX_BLE_MIDI_PACKET); + } + + pub fn get_mtu(&self) -> usize { + self.ble_mtu + } + + fn reset_stream(&mut self) { + let s = &mut self.stream; + s.next_msg_byte_idx = 0; + s.previous_timestamp = 0xffff; // illegal sentinel + s.running_status = 0; + s.mes.nbytes = 0; + s.mes.timestamp_ms = 0xffff; + s.npending_rt = 0; + s.pending_ble_pkt.nbytes = 0; + s.pending_ble_midi_pkt_running_status = 0; + s.pending_ble_midi_pkt_prev_status = 0; + } + + // ---- encode (MIDI stream -> BLE-MIDI packets) -------------------------- + + /// Push a MIDI 1.0 byte stream to be encoded. Returns `(bytes_pushed, + /// ready_to_send)` where `ready_to_send` is true if at least one complete + /// BLE-MIDI packet is available to pop and notify. + pub fn push_midi(&mut self, midi_stream: &[u8], now_ms: u16) -> (u16, bool) { + let timestamp = now_ms & 0x1FFF; + let mut bytes_pushed: u16 = 0; + let mut ready_to_send = false; + + let s = &mut self.stream; + let to_ble = &mut self.to_ble; + let ble_mtu = self.ble_mtu; + + while (bytes_pushed as usize) < midi_stream.len() && s.pending_ble_pkt.nbytes < ble_mtu { + let ms_byte = midi_stream[bytes_pushed as usize]; + + if ms_byte & 0x80 != 0 { + // ---- status byte ---- + if ms_byte == 0xF0 { + s.previous_timestamp = timestamp; + s.mes.nbytes = IS_START_SYSEX | IS_SYSEX | 1; + bytes_pushed += 1; + s.running_status = 0; + s.mes.msg_bytes[0] = ms_byte; + s.next_msg_byte_idx = 1; + } else if ms_byte == 0xF7 { + if s.mes.nbytes & IS_SYSEX == 0 { + // EOX without being in a sysex — stream error + discard_stream(s); + return (bytes_pushed, ready_to_send); + } + if s.next_msg_byte_idx != 0 { + encode_bt_pkt(s, to_ble, ble_mtu); + } + s.mes.timestamp_ms = s.previous_timestamp; + s.mes.msg_bytes[0] = ms_byte; + s.mes.nbytes = 1; + bytes_pushed += 1; + encode_full_bt_pkt(s, to_ble, ble_mtu); + s.running_status = 0; + } else if ms_byte >= 0xF8 { + // system real-time (may occur inside sysex) + if s.mes.nbytes & IS_SYSEX != 0 && (s.mes.nbytes & NBYTES_MASK) > 0 { + encode_bt_pkt(s, to_ble, ble_mtu); + s.next_msg_byte_idx = 0; + s.mes.nbytes = IS_SYSEX; + } + if s.npending_rt < s.pending_rt_status.len() as u8 { + let i = s.npending_rt as usize; + s.pending_rt_status[i] = ms_byte; + s.pending_rt_timestamp[i] = timestamp; + s.npending_rt += 1; + } + bytes_pushed += 1; + if s.npending_rt >= s.pending_rt_status.len() as u8 { + discard_stream(s); + return (bytes_pushed, ready_to_send); + } + flush_rt_midi_to_pkt(s, to_ble, ble_mtu); + } else if ms_byte > 0xF0 { + // system common + s.mes.msg_bytes[0] = ms_byte; + s.previous_timestamp = timestamp; + s.mes.timestamp_ms = timestamp; + bytes_pushed += 1; + s.running_status = 0; + if ms_byte == 0xF1 || ms_byte == 0xF3 { + s.mes.nbytes = 2; + s.next_msg_byte_idx = 1; + } else if ms_byte == 0xF2 { + s.mes.nbytes = 3; + s.next_msg_byte_idx = 1; + } else { + s.mes.nbytes = 1; + encode_full_bt_pkt(s, to_ble, ble_mtu); + s.mes.nbytes = 0; + } + } else { + // channel message + s.running_status = ms_byte; + s.mes.msg_bytes[0] = ms_byte; + s.previous_timestamp = timestamp; + s.mes.timestamp_ms = timestamp; + bytes_pushed += 1; + s.next_msg_byte_idx = 1; + s.mes.nbytes = IS_CHANNEL | 3; + let chan_msg_base = (ms_byte >> 4) & 0xF; + if chan_msg_base == 0xC || chan_msg_base == 0xD { + s.mes.nbytes = IS_CHANNEL | 2; + } + } + } else { + // ---- data byte ---- + if s.mes.nbytes & IS_SYSEX != 0 { + s.mes.msg_bytes[s.next_msg_byte_idx as usize] = ms_byte; + s.next_msg_byte_idx += 1; + s.mes.nbytes += 1; + bytes_pushed += 1; + if s.next_msg_byte_idx >= 3 { + encode_full_bt_pkt(s, to_ble, ble_mtu); + s.mes.timestamp_ms = s.previous_timestamp; + s.mes.nbytes = IS_SYSEX; // length 0, still in sysex + } + } else if s.mes.nbytes & IS_CHANNEL != 0 && s.next_msg_byte_idx == 0 { + if s.running_status == 0 || s.running_status != s.mes.msg_bytes[0] { + discard_stream(s); + return (bytes_pushed, ready_to_send); + } else { + s.mes.msg_bytes[1] = ms_byte; + s.mes.timestamp_ms = timestamp; + bytes_pushed += 1; + s.next_msg_byte_idx = 2; + if (s.mes.nbytes & NBYTES_MASK) as u16 == s.next_msg_byte_idx { + encode_full_bt_pkt(s, to_ble, ble_mtu); + } + } + } else { + let len = s.mes.nbytes & NBYTES_MASK; + if (s.next_msg_byte_idx as u8) < len { + s.mes.msg_bytes[s.next_msg_byte_idx as usize] = ms_byte; + s.next_msg_byte_idx += 1; + bytes_pushed += 1; + if len == s.next_msg_byte_idx as u8 { + encode_full_bt_pkt(s, to_ble, ble_mtu); + if s.mes.nbytes & IS_CHANNEL == 0 { + s.mes.nbytes = 0; + } + } + } else { + discard_stream(s); + return (bytes_pushed, ready_to_send); + } + } + } + } + + if bytes_pushed > 0 && self.stream.pending_ble_pkt.nbytes > 0 { + self.to_ble.push(self.stream.pending_ble_pkt); + self.stream.pending_ble_pkt.nbytes = 0; + self.stream.pending_ble_midi_pkt_running_status = 0; + self.stream.pending_ble_midi_pkt_prev_status = 0; + ready_to_send = true; + } + (bytes_pushed, ready_to_send) + } + + /// Pop the next encoded BLE-MIDI packet to send (notify). + pub fn ble_pkt_pop(&mut self) -> Option { + self.to_ble.pop() + } + + /// True if at least one encoded packet is available to send. + pub fn ble_pkt_available(&self) -> bool { + self.to_ble.len() > 0 + } + + // ---- decode (BLE-MIDI packet -> timestamped MIDI messages) ------------ + + /// Decode a BLE-MIDI packet, pushing each resulting timestamped message to + /// the from-BLE ring. Returns the number of packet bytes consumed. + pub fn decode_push(&mut self, pkt: &[u8]) -> usize { + let nbytes = pkt.len(); + if nbytes < 2 { + return 0; + } + // header byte must be present + if (pkt[0] & 0xc0) != 0x80 { + return 0; + } + let mut timestamp: u16 = ((pkt[0] & 0x3f) as u16) << 7; + let mut prev_lsb: u8 = 0; + let mut ndecoded: usize = 1; + let mut running_status: u8 = 0; + let mut running_status_nbytes: u8 = 0; + let mut mes: BleMidiMessage = BleMidiMessage::default(); + + // sysex continuation data at the start of the packet? + if (pkt[ndecoded] & 0x80) == 0 { + mes.nbytes = IS_SYSEX; + if !decode_sysex_data(pkt, &mut mes, &mut self.from_ble, &mut ndecoded, 0) { + return ndecoded; + } + } + + while ndecoded < nbytes { + if ndecoded + 1 < nbytes { + if pkt[ndecoded] & 0x80 != 0 && pkt[ndecoded + 1] & 0x80 != 0 { + // timestamp + status byte (or rt inside sysex, or EOX) + if mes.nbytes & IS_SYSEX == 0 { + // not in sysex: timestamp + status + mes.timestamp_ms = timestamp_decode(&mut timestamp, pkt[ndecoded], &mut prev_lsb); + ndecoded += 1; + mes.msg_bytes[0] = pkt[ndecoded]; + ndecoded += 1; + if mes.msg_bytes[0] >= 0xF8 { + // real-time + mes.nbytes = IS_REAL_TIME + 1; + if !from_ble_push(&mut self.from_ble, &mes) { + return ndecoded; + } + } else if mes.msg_bytes[0] > 0xF0 && mes.msg_bytes[0] <= 0xF7 { + // system common + if mes.msg_bytes[0] == 0xF1 || mes.msg_bytes[0] == 0xF3 { + mes.nbytes = 2; + } else if mes.msg_bytes[0] == 0xF2 { + mes.nbytes = 3; + } else { + mes.nbytes = 1; + } + if ndecoded + (mes.nbytes as usize) - 1 <= nbytes { + for idx in 1..mes.nbytes as usize { + mes.msg_bytes[idx] = pkt[ndecoded]; + ndecoded += 1; + } + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= 1; + return ndecoded; + } + } else { + return ndecoded; + } + } else if mes.msg_bytes[0] < 0xF0 { + // channel message + running_status = mes.msg_bytes[0]; + let stat = (mes.msg_bytes[0] >> 4) & 0xF; + if stat == 0xC || stat == 0xD { + mes.nbytes = 2; + running_status_nbytes = 2; + } else { + running_status_nbytes = 3; + } + if (running_status_nbytes as usize) + ndecoded - 1 <= nbytes { + for idx in 1..running_status_nbytes as usize { + mes.msg_bytes[idx] = pkt[ndecoded]; + ndecoded += 1; + } + mes.nbytes = running_status_nbytes | IS_CHANNEL; + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= 1; + return ndecoded; + } + } else { + // not enough data; leave for next? C returns. + return ndecoded; + } + } else { + // 0xF0: start of sysex + mes.nbytes = IS_SYSEX + 1; + if !decode_sysex_data(pkt, &mut mes, &mut self.from_ble, &mut ndecoded, 1) { + return ndecoded; + } + } + } else if pkt[ndecoded + 1] >= 0xF8 { + // real-time timestamp+status within a sysex + let bytecount = mes.nbytes & NBYTES_MASK; + if bytecount > 0 { + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= bytecount as usize; + return ndecoded; + } + } + let mut mes_rt = BleMidiMessage::default(); + mes_rt.nbytes = IS_REAL_TIME + 1; + mes_rt.timestamp_ms = timestamp_decode(&mut timestamp, pkt[ndecoded], &mut prev_lsb); + ndecoded += 1; + mes_rt.msg_bytes[0] = pkt[ndecoded]; + ndecoded += 1; + if !from_ble_push(&mut self.from_ble, &mes_rt) { + ndecoded -= 2; + return ndecoded; + } + // continue parsing sysex data after the rt interleave + mes.nbytes = IS_SYSEX; + let mut idx: u8 = 0; + while ndecoded < nbytes && (pkt[ndecoded] & 0x80) == 0 { + mes.msg_bytes[idx as usize] = pkt[ndecoded]; + ndecoded += 1; + mes.nbytes += 1; + idx += 1; + if idx == 3 { + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= 3; + return ndecoded; + } + idx = 0; + mes.nbytes = IS_SYSEX; + } + } + let bytecount = mes.nbytes & NBYTES_MASK; + if bytecount > 0 { + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= bytecount as usize; + return ndecoded; + } + mes.nbytes = IS_SYSEX; + } + } else if pkt[ndecoded + 1] == 0xF7 { + // sysex ending: flush pending data; EOX handled next iter + let bytecount = mes.nbytes & NBYTES_MASK; + if bytecount > 0 { + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= bytecount as usize; + return ndecoded; + } + } + mes.nbytes = 0; // clear is_sysex flag + } + // else: timestamp+status where status is not handled — fall through + } else if pkt[ndecoded] & 0x80 != 0 && pkt[ndecoded + 1] & 0x80 == 0 { + // running-status channel message: timestamp + data byte(s) + if running_status != 0 && ndecoded + running_status_nbytes as usize <= nbytes { + mes.timestamp_ms = timestamp_decode(&mut timestamp, pkt[ndecoded], &mut prev_lsb); + ndecoded += 1; + mes.msg_bytes[0] = running_status; + mes.nbytes = running_status_nbytes | IS_CHANNEL; + for idx in 1..running_status_nbytes as usize { + mes.msg_bytes[idx] = pkt[ndecoded]; + ndecoded += 1; + } + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= running_status_nbytes as usize; + return ndecoded; + } + } else { + return ndecoded; + } + } else if pkt[ndecoded] & 0x80 == 0 && pkt[ndecoded + 1] & 0x80 == 0 { + // running status same as previous, same timestamp, new data + if running_status == mes.msg_bytes[0] + && mes.nbytes & IS_CHANNEL != 0 + && ndecoded + running_status_nbytes as usize - 1 <= nbytes + { + for idx in 1..running_status_nbytes as usize { + mes.msg_bytes[idx] = pkt[ndecoded]; + ndecoded += 1; + } + if !from_ble_push(&mut self.from_ble, &mes) { + ndecoded -= (running_status_nbytes as usize) - 1; + return ndecoded; + } + } else { + return ndecoded; + } + } + } else { + // only one byte left and not a timestamp+status pair — stop + break; + } + } + ndecoded + } + + /// Pop the next decoded timestamped MIDI message. + pub fn pop_midi(&mut self) -> Option { + self.from_ble.pop() + } +} + +impl Default for Codec { + fn default() -> Self { + Self::new() + } +} + +// ---- free encode helpers (take stream + ring + mtu to avoid overlapping +// borrows of `self`) ---------------------------------------------------- + +fn discard_stream(s: &mut ToBleStream) { + s.next_msg_byte_idx = 0; + s.pending_ble_pkt.nbytes = 0; + s.previous_timestamp = 0xffff; + s.running_status = 0; + s.mes.nbytes = 0; + s.mes.timestamp_ms = 0xffff; + s.npending_rt = 0; +} + +fn add_header_if_needed(pkt: &mut BleMidiPacket, timestamp: u16) { + if pkt.nbytes == 0 { + pkt.pkt[pkt.nbytes] = header(timestamp); + pkt.nbytes += 1; + } +} + +fn flush_rt_midi_to_pkt(s: &mut ToBleStream, to_ble: &mut Ring, ble_mtu: usize) { + for idx in 0..s.npending_rt as usize { + // make room for a 2-byte (timestamp + status) real-time entry + if s.pending_ble_pkt.nbytes + 2 >= ble_mtu { + to_ble.push(s.pending_ble_pkt); + s.pending_ble_pkt.nbytes = 0; + s.pending_ble_midi_pkt_running_status = 0; + s.pending_ble_midi_pkt_prev_status = 0; + } + if s.pending_ble_pkt.nbytes == 0 { + s.pending_ble_pkt.pkt[s.pending_ble_pkt.nbytes] = header(s.pending_rt_timestamp[0]); + s.pending_ble_pkt.nbytes += 1; + } + s.pending_ble_pkt.pkt[s.pending_ble_pkt.nbytes] = timestamp_low(s.pending_rt_timestamp[idx]); + s.pending_ble_pkt.nbytes += 1; + s.pending_ble_pkt.pkt[s.pending_ble_pkt.nbytes] = s.pending_rt_status[idx]; + s.pending_ble_pkt.nbytes += 1; + s.pending_ble_midi_pkt_prev_status = s.pending_rt_status[idx]; + } + s.npending_rt = 0; +} + +fn encode_bt_pkt(s: &mut ToBleStream, to_ble: &mut Ring, ble_mtu: usize) { + let nbytes = s.mes.nbytes & NBYTES_MASK; + if nbytes == 0 { + return; + } + if s.mes.nbytes & IS_REAL_TIME != IS_REAL_TIME { + flush_rt_midi_to_pkt(s, to_ble, ble_mtu); + } + + // channel messages may omit the status byte if running status matches + let requires_byte0 = (s.mes.nbytes & IS_CHANNEL != IS_CHANNEL) + || (s.pending_ble_midi_pkt_running_status != s.mes.msg_bytes[0]); + + // most messages need a timestamp prefix + let mut needs_timestamp = (s.mes.nbytes & (IS_SYSEX | IS_START_SYSEX) != IS_SYSEX) + && (requires_byte0 || s.previous_timestamp != s.mes.timestamp_ms); + if !needs_timestamp + && s.mes.nbytes & IS_CHANNEL == IS_CHANNEL + && s.pending_ble_midi_pkt_prev_status != s.pending_ble_midi_pkt_running_status + { + needs_timestamp = true; + } + + let first_byte_idx: u8 = if requires_byte0 { 0 } else { 1 }; + let total_bytes = nbytes as usize + (if needs_timestamp { 1 } else { 0 }) - first_byte_idx as usize; + + if s.pending_ble_pkt.nbytes + total_bytes >= ble_mtu { + to_ble.push(s.pending_ble_pkt); + s.pending_ble_pkt.nbytes = 0; + s.pending_ble_midi_pkt_running_status = 0; + s.pending_ble_midi_pkt_prev_status = 0; + needs_timestamp = true; + // requires_byte0 is set true in the C here but only used above; keep + // first_byte_idx consistent by re-evaluating would change total_bytes. + // The C sets requires_byte0=true after the flush but does not recompute + // first_byte_idx/total_bytes, so neither do we. + } + + if s.mes.nbytes & IS_CHANNEL != 0 { + s.pending_ble_midi_pkt_running_status = s.mes.msg_bytes[0]; + } + if s.mes.msg_bytes[0] & 0x80 != 0 { + s.pending_ble_midi_pkt_prev_status = s.mes.msg_bytes[0]; + } + + add_header_if_needed(&mut s.pending_ble_pkt, s.mes.timestamp_ms); + if needs_timestamp { + s.pending_ble_pkt.pkt[s.pending_ble_pkt.nbytes] = timestamp_low(s.mes.timestamp_ms); + s.pending_ble_pkt.nbytes += 1; + } + for idx in first_byte_idx as usize..nbytes as usize { + s.pending_ble_pkt.pkt[s.pending_ble_pkt.nbytes] = s.mes.msg_bytes[idx]; + s.pending_ble_pkt.nbytes += 1; + } +} + +fn encode_full_bt_pkt(s: &mut ToBleStream, to_ble: &mut Ring, ble_mtu: usize) { + encode_bt_pkt(s, to_ble, ble_mtu); + s.next_msg_byte_idx = 0; +} + +// ---- free decode helpers -------------------------------------------------- + +fn from_ble_push(ring: &mut Ring, mes: &BleMidiMessage) -> bool { + ring.push(*mes) +} + +/// Reconstruct a 14-bit timestamp from the accumulated 13-bit MSB and a 7-bit +/// LSB, incrementing the MSB when the LSB wraps backward. +fn timestamp_decode(msb: &mut u16, lsb_byte: u8, prev_lsb: &mut u8) -> u16 { + let lsb = lsb_byte; + if *prev_lsb > lsb { + *msb += 1; + } + *prev_lsb = lsb; + (*msb << 7) | (lsb as u16 & 0x7F) +} + +/// Decode sysex data bytes (MSB clear) starting at `start_idx` into `mes`, +/// flushing 3-byte chunks to the ring. Returns false if the ring is full. +fn decode_sysex_data( + pkt: &[u8], + mes: &mut BleMidiMessage, + ring: &mut Ring, + ndecoded: &mut usize, + start_idx: u8, +) -> bool { + let mut idx = start_idx; + while *ndecoded < pkt.len() && (pkt[*ndecoded] & 0x80) == 0 { + mes.msg_bytes[idx as usize] = pkt[*ndecoded]; + *ndecoded += 1; + mes.nbytes += 1; + idx += 1; + if idx == 3 { + if !ring.push(*mes) { + *ndecoded -= 3; + return false; + } + idx = 0; + mes.nbytes = IS_SYSEX; + } + } + let bytecount = mes.nbytes & NBYTES_MASK; + if bytecount > 0 { + if !ring.push(*mes) { + *ndecoded -= bytecount as usize; + return false; + } + // keep tracking an unterminated sysex message + mes.nbytes = IS_SYSEX; + } + true +} diff --git a/src/keypad.rs b/src/keypad.rs new file mode 100644 index 0000000..c86d40e --- /dev/null +++ b/src/keypad.rs @@ -0,0 +1,95 @@ +//! Pimoroni Pico RGB Keypad driver — Rust port of `pico_rgb_keypad.cpp`. +//! +//! Copyright (C) 2026 Nils — SPDX-License-Identifier: GPL-3.0-or-later +//! This file is part of pico-pi-midi. See LICENSE for the full notice. +//! +//! Hardware: 16 APA102-style RGB LEDs driven over SPI (CS=17, SCK=18, MOSI=19) +//! and a 16-bit button state read over I2C from a GPIO expander at address +//! 0x20 (SDA=4, SCL=5). Reading register 0 returns two bytes (little-endian +//! button bitmap); the C driver inverts them so a pressed pad reads as 1. +//! +//! APA102 frame: 4 start bytes (0x00), then per-LED 4 bytes +//! (0xE0|brightness, B, G, R), then a 4-byte end frame (0xFF). Total 72 bytes, +//! matching the C buffer layout `[start(4)][led0(4)]..[led15(4)][end(4)]`. + +use embassy_rp::gpio::Output; +use embassy_rp::i2c::{self, Blocking as I2cBlocking}; +use embassy_rp::spi::{self, Blocking as SpiBlocking}; + +pub const NUM_PADS: usize = 16; +const KEYPAD_ADDRESS: u8 = 0x20; + +/// 5-bit global brightness (0..=31), stored in each LED's frame header. +/// `set_brightness(1.0)` in the C driver -> 0b11111. +const BRIGHTNESS: u8 = 0b11111; + +pub struct RgbKeypad { + cs: Output<'static>, + spi: spi::Spi<'static, SpiBlocking>, + i2c: i2c::I2c<'static, I2cBlocking>, + /// APA102 frame buffer: 4 start + 16*4 LED + 4 end = 72 bytes. + buffer: [u8; 4 + NUM_PADS * 4 + 4], +} + +impl RgbKeypad { + /// Build the driver from already-constructed SPI (tx-only), I2C and CS + /// output. The peripherals are consumed in `main` and the resulting driver + /// is `'static`, so it can be moved into an embassy task. + pub fn new( + cs: Output<'static>, + spi: spi::Spi<'static, SpiBlocking>, + i2c: i2c::I2c<'static, I2cBlocking>, + ) -> Self { + let mut s = Self { + cs, + spi, + i2c, + buffer: [0; 4 + NUM_PADS * 4 + 4], + }; + + // APA102 start frame: 0x00 0x00 0x00 0x00. + s.buffer[0..4].copy_from_slice(&[0x00, 0x00, 0x00, 0x00]); + // Per-LED brightness header (0xE0 | 5-bit brightness); RGB stays 0 (off). + for i in 0..NUM_PADS { + s.buffer[4 + i * 4] = 0xE0 | BRIGHTNESS; + } + // End frame: 0xFF 0xFF 0xFF 0xFF (>= 1 bit per 2 LEDs). + let end = 4 + NUM_PADS * 4; + s.buffer[end..end + 4].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); + + s.update(); + s + } + + /// Push the frame buffer to the LEDs over SPI. + pub fn update(&mut self) { + self.cs.set_low(); + let _ = self.spi.blocking_write(&self.buffer); + self.cs.set_high(); + } + + /// Set an LED by linear index `i` (0..16) to `(r, g, b)`. Mirrors the C + /// `illuminate(i, r, g, b)`: frame byte order is brightness,B,G,R. + pub fn illuminate(&mut self, i: usize, r: u8, g: u8, b: u8) { + if i >= NUM_PADS { + return; + } + let off = 4 + i * 4; + // [0]=brightness (already set), [1]=B, [2]=G, [3]=R + self.buffer[off + 1] = b; + self.buffer[off + 2] = g; + self.buffer[off + 3] = r; + } + + /// Read the 16-bit button state bitmap. Bit `i` is 1 when pad `i` is + /// pressed (the raw expander value is inverted, matching the C driver). + pub fn get_button_states(&mut self) -> u16 { + let mut buf = [0u8; 2]; + // Write register 0, then read 2 bytes (repeated-start, like the C code). + let _ = self + .i2c + .blocking_write_read(KEYPAD_ADDRESS, &[0x00], &mut buf); + // C returns ~((buf[0]) | (buf[1] << 8)) + !(u16::from(buf[0]) | (u16::from(buf[1]) << 8)) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1508209 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,394 @@ +//! RGB MIDI Keypad — Rust port for Raspberry Pi Pico W (RP2040 + CYW43439). +//! +//! Copyright (C) 2026 Nils +//! +//! This program is free software: you can redistribute it and/or modify +//! it under the terms of the GNU General Public License as published by +//! the Free Software Foundation, either version 3 of the License, or +//! (at your option) any later version. +//! +//! This program is distributed in the hope that it will be useful, +//! but WITHOUT ANY WARRANTY; without even the implied warranty of +//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//! GNU General Public License for more details. +//! +//! You should have received a copy of the GNU General Public License +//! along with this program. If not, see . +//! +//! SPDX-License-Identifier: GPL-3.0-or-later +//! +//! Wires the Pimoroni RGB Keypad (16 pads + APA102 LEDs) to a BLE-MIDI 1.0 +//! GATT service so pad presses send Note On/Off over Bluetooth MIDI and +//! incoming Note messages light the pads. +//! +//! Concurrency: +//! - `cyw43_task` : drives the CYW43 controller (WiFi+BT firmware). +//! - `led_task` : blinks the onboard LED as a heartbeat. +//! - `keypad_task` : polls pads, sends Note On/Off on `OUTBOUND`, lights pads +//! from `INBOUND`. +//! - main (BLE) : runs the trouble host; advertises, accepts a connection, +//! and pumps MIDI in both directions through the codec. + +#![no_std] +#![no_main] + +use cyw43::Cyw43439; +use cyw43_pio::PioSpi; +use defmt::*; +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_futures::select::{select, Either}; +use embassy_rp::gpio::{Level, Output}; +use embassy_rp::peripherals::{DMA_CH0, DMA_CH1, PIO0, USB}; +use embassy_rp::pio::{InterruptHandler, Pio}; +use embassy_rp::usb::{Driver, InterruptHandler as UsbInterruptHandler}; +use embassy_rp::{bind_interrupts, dma}; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::channel::{Channel, Receiver, Sender}; +use embassy_time::{Instant, Timer}; +use heapless::Vec; +use static_cell::StaticCell; +use trouble_host::prelude::*; +use {defmt_rtt as _, panic_probe as _}; + +mod codec; +mod keypad; +mod midi_keypad; +mod octave; + +use keypad::RgbKeypad; +use midi_keypad::{MidiKeypad, MidiOut}; + +bind_interrupts!(struct Irqs { + PIO0_IRQ_0 => InterruptHandler; + DMA_IRQ_0 => dma::InterruptHandler, dma::InterruptHandler; + USBCTRL_IRQ => UsbInterruptHandler; +}); + +#[embassy_executor::task] +async fn logger_task(driver: Driver<'static, USB>) { + embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); +} + +/// Max number of simultaneous BLE connections. +const CONNECTIONS_MAX: usize = 1; +/// Max L2CAP channels ( signalling + ATT ). +const L2CAP_CHANNELS_MAX: usize = 2; + +/// Keypad -> BLE: each entry is a 3-byte Note On/Off message. +static OUTBOUND: Channel = Channel::new(); +/// BLE -> keypad: each entry is a 3-byte channel message to light a pad. +static INBOUND: Channel = Channel::new(); + +// ---- BLE-MIDI 1.0 GATT service definition -------------------------------- +// +// Service: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 (MIDI Service) +// Char: 7772E5DB-3868-4112-A1A9-F2669D106BF3 (MIDI I/O) +// Props: read | write-without-response | notify +#[gatt_server] +struct Server { + midi_service: MidiService, +} + +#[gatt_service(uuid = "03b80e5a-ede8-4b33-a751-6ce34ec4c700")] +struct MidiService { + #[characteristic(uuid = "7772e5db-3868-4112-a1a9-f2669d106bf3", read, write_without_response, notify)] + midi_io: (), +} + +// ---- CYW43 runner + heartbeat tasks -------------------------------------- + +#[embassy_executor::task] +async fn cyw43_task( + runner: cyw43::Runner< + 'static, + cyw43::SpiBus, PioSpi<'static, PIO0, 0>>, + Cyw43439, + >, +) { + runner.run().await +} + +#[embassy_executor::task] +async fn led_task(mut control: cyw43::Control<'static>) { + loop { + control.gpio_set(0, true).await; + Timer::after_millis(80).await; + control.gpio_set(0, false).await; + Timer::after_millis(920).await; + } +} + +/// Poll the keypad: send Note On/Off for changed pads, light pads from inbound +/// MIDI, repeat every 10ms (matching the C main loop cadence). +#[embassy_executor::task] +async fn keypad_task(mut keypad: RgbKeypad, mut bootsel: embassy_rp::Peri<'static, embassy_rp::peripherals::BOOTSEL>) { + // Startup blink: all LEDs white, then off — a visible "main is alive and + // the keypad/SPI driver works" signal that does not depend on the radio. + for i in 0..keypad::NUM_PADS { + keypad.illuminate(i, 64, 64, 64); + } + keypad.update(); + Timer::after_millis(400).await; + + let mut midi = MidiKeypad::new(&mut keypad, 48); // note offset C3; paints underlay + let mut out_buf: Vec = Vec::new(); + let outbound = OUTBOUND.sender(); + let inbound = INBOUND.receiver(); + let mut note_offset: u8 = 48; + let mut bootsel_held = false; + + loop { + // 1. read pads -> Note On/Off. Each message is routed two ways, matching + // the C firmware's connect_ports("MidiKeypad","MidiKeypad") local + // feedback + connect_ports("MidiKeypad","BluetoothMidi"): + // (a) local: light the pressed pad immediately, + // (b) outbound: send to the BLE side to notify the host. + out_buf.clear(); + midi.update(&mut keypad, &mut out_buf); + for m in out_buf.iter() { + midi.receive_midi_message(&mut keypad, &m.bytes); + let _ = outbound.try_send(m.bytes); + } + out_buf.clear(); + + // 2. light pads from inbound MIDI (host -> keypad) + while let Ok(msg) = inbound.try_receive() { + midi.receive_midi_message(&mut keypad, &msg); + } + + // 3. BOOTSEL: cycle octave +12 (wrap at 120) and flash the octave digit. + // Edge-triggered so one press = one step (C firmware re-checked every + // loop while held, which multi-triggered; we debounce to a press edge). + let pressed = embassy_rp::bootsel::is_bootsel_pressed(bootsel.reborrow()); + if pressed && !bootsel_held { + note_offset = (note_offset + 12) % 120; + midi.set_note_offset(&mut keypad, note_offset); + crate::octave::display_active_octave(&mut midi, &mut keypad, note_offset).await; + } + bootsel_held = pressed; + + Timer::after_millis(10).await; + } +} + +// ---- TrouBLE host runner -------------------------------------------------- + +async fn ble_task(mut runner: Runner<'_, C, P>) { + loop { + if let Err(e) = runner.run().await { + log::error!("[ble_task] runner error: {:?}", e); + } + } +} + +// ---- Advertising ---------------------------------------------------------- + +async fn advertise<'a, 'b, C: Controller>( + peripheral: &mut Peripheral<'a, C, DefaultPacketPool>, + server: &'b Server<'a>, +) -> Result, BleHostError> { + // adv_data: Flags + MIDI service UUID (so MIDI hosts auto-discover). + // scan_data: device name (won't fit in adv_data alongside the 128-bit UUID). + let mut advertiser_data = [0u8; 31]; + let mut scan_response = [0u8; 31]; + // MIDI Service UUID 03B80E5A-EDE8-4B33-A751-6CE34EC4C700, little-endian. + let midi_service_uuid: [u8; 16] = [ + 0x00, 0xc7, 0xc4, 0x4e, 0xe3, 0x6c, 0x51, 0xa7, 0x33, 0x4b, 0xe8, 0xed, 0x5a, 0x0e, 0xb8, + 0x03, + ]; + let adv_len = AdStructure::encode_slice( + &[ + AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), + AdStructure::CompleteServiceUuids128(&[midi_service_uuid]), + ], + &mut advertiser_data[..], + )?; + let scan_len = AdStructure::encode_slice( + &[AdStructure::CompleteLocalName("BLE-RGB Keypad".as_bytes())], + &mut scan_response[..], + )?; + log::info!("[adv] starting advertise..."); + let advertiser = peripheral + .advertise( + &Default::default(), + Advertisement::ConnectableScannableUndirected { + adv_data: &advertiser_data[..adv_len], + scan_data: &scan_response[..scan_len], + }, + ) + .await?; + log::info!("[adv] advertising as 'BLE-RGB Keypad'"); + let conn = advertiser.accept().await?.with_attribute_server(server)?; + log::info!("[adv] connection established"); + Ok(conn) +} + +// ---- Connection: pump MIDI both ways ------------------------------------- + +async fn run_connection<'a, 'b, P: PacketPool>( + conn: &GattConnection<'a, 'b, P>, + server: &'b Server<'a>, + codec: &mut codec::Codec, + outbound: &Receiver<'static, CriticalSectionRawMutex, [u8; 3], 16>, + inbound: &Sender<'static, CriticalSectionRawMutex, [u8; 3], 16>, +) { + let midi_handle = server.midi_service.midi_io.handle; + let midi_io = &server.midi_service.midi_io; + + loop { + match select(conn.next(), outbound.receive()).await { + Either::First(ev) => match ev { + GattConnectionEvent::Disconnected { reason } => { + info!("[gatt] disconnected: {:?}", reason); + return; + } + GattConnectionEvent::Gatt { event } => { + let reply = match event { + GattEvent::Write(e) => { + if e.handle() == midi_handle { + // Copy the written bytes out, then decode. + let mut buf = [0u8; codec::MAX_BLE_MIDI_PACKET]; + let mut len = 0usize; + e.with_data(|_offset, data| { + len = data.len().min(buf.len()); + buf[..len].copy_from_slice(&data[..len]); + }); + codec.decode_push(&buf[..len]); + while let Some(m) = codec.pop_midi() { + let bc = m.byte_count() as usize; + if bc == 3 { + let _ = inbound.try_send(m.msg_bytes); + } + } + } + e.accept() + } + other => other.accept(), + }; + if let Ok(reply) = reply { + let _ = reply.send().await; + } + } + _ => {} + }, + Either::Second(msg) => { + // Outbound Note On/Off: stamp + encode + notify. + let now = Instant::now().as_millis() as u16; + let (_, ready) = codec.push_midi(&msg, now); + if ready { + while let Some(pkt) = codec.ble_pkt_pop() { + let _ = midi_io.notify_raw(conn, &pkt.pkt[..pkt.nbytes], false).await; + } + } + } + } + } +} + +// ---- main ----------------------------------------------------------------- + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + info!("[boot] embassy_rp initialised"); + + // ---- USB serial logger (routes log::info!/error! to the serial port) - + let driver = Driver::new(p.USB, Irqs); + let _ = spawner.spawn(unwrap!(logger_task(driver))); + info!("[boot] usb logger task spawned"); + + // ---- Keypad task FIRST (I2C0 + SPI0; independent of the radio) ------ + let mut kp_spi_cfg = embassy_rp::spi::Config::default(); + kp_spi_cfg.frequency = 4_000_000; + let kp_spi = embassy_rp::spi::Spi::new_blocking_txonly(p.SPI0, p.PIN_18, p.PIN_19, kp_spi_cfg); + let mut kp_i2c_cfg = embassy_rp::i2c::Config::default(); + kp_i2c_cfg.frequency = 400_000; + kp_i2c_cfg.sda_pullup = true; + kp_i2c_cfg.scl_pullup = true; + let kp_i2c = embassy_rp::i2c::I2c::new_blocking(p.I2C0, p.PIN_5, p.PIN_4, kp_i2c_cfg); + let kp_cs = Output::new(p.PIN_17, Level::High); + let keypad = RgbKeypad::new(kp_cs, kp_spi, kp_i2c); + let _ = spawner.spawn(unwrap!(keypad_task(keypad, p.BOOTSEL))); + info!("[boot] keypad task spawned"); + + // ---- CYW43 firmware blobs (committed under cyw43-firmware/) ----------- + let fw = cyw43::aligned_bytes!("../cyw43-firmware/43439A0.bin"); + let clm = cyw43::aligned_bytes!("../cyw43-firmware/43439A0_clm.bin"); + let btfw = cyw43::aligned_bytes!("../cyw43-firmware/43439A0_btfw.bin"); + let nvram = cyw43::aligned_bytes!("../cyw43-firmware/nvram_rp2040.bin"); + + // ---- CYW43 PIO-SPI bus (Pico W wiring) ------------------------------- + let pwr = Output::new(p.PIN_23, Level::Low); + let cs = Output::new(p.PIN_25, Level::High); + let mut pio = Pio::new(p.PIO0, Irqs); + let spi = PioSpi::new( + &mut pio.common, + pio.sm0, + cyw43_pio::DEFAULT_CLOCK_DIVIDER, + pio.irq0, + cs, + p.PIN_24, + p.PIN_29, + dma::Channel::new(p.DMA_CH0, Irqs), + dma::Channel::new(p.DMA_CH1, Irqs), + ); + + static STATE: StaticCell = StaticCell::new(); + let state = STATE.init(cyw43::State::new()); + + let (_net_device, bt_device, mut control, runner) = + cyw43::new_with_bluetooth(state, pwr, spi, fw, btfw, nvram).await; + let _ = spawner.spawn(unwrap!(cyw43_task(runner))); + control.init(clm).await; + log::info!("[cyw43] initialised"); + + let _ = spawner.spawn(unwrap!(led_task(control))); + + // ---- TrouBLE BLE host ------------------------------------------------- + let controller: ExternalController<_, 10> = ExternalController::new(bt_device); + let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]); + log::info!("[ble] controller wrapped, address set"); + + static RES: StaticCell> = + StaticCell::new(); + let resources = RES.init(HostResources::new()); + log::info!("[ble] building stack..."); + let stack = trouble_host::new(controller, resources) + .set_random_address(address) + .build(); + log::info!("[ble] stack built"); + let runner = stack.runner(); + let mut peripheral = stack.peripheral(); + log::info!("[ble] peripheral acquired"); + + let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { + name: "BLE-RGB Keypad", + appearance: &appearance::power_device::GENERIC_POWER_DEVICE, + })) + .unwrap(); + log::info!("[ble] gatt server built"); + + let outbound = OUTBOUND.receiver(); + let inbound = INBOUND.sender(); + log::info!("[ble] entering advertise loop"); + + join(ble_task(runner), async { + let mut codec = codec::Codec::new(); + loop { + match advertise(&mut peripheral, &server).await { + Ok(conn) => { + log::info!("[ble] connected"); + // Fresh codec state per connection. + codec = codec::Codec::new(); + run_connection(&conn, &server, &mut codec, &outbound, &inbound).await; + } + Err(e) => { + log::error!("[adv] error: {:?}", e); + Timer::after_millis(500).await; + } + } + } + }) + .await; +} diff --git a/src/midi_keypad.rs b/src/midi_keypad.rs new file mode 100644 index 0000000..17614b2 --- /dev/null +++ b/src/midi_keypad.rs @@ -0,0 +1,190 @@ +//! MIDI keypad state machine — Rust port of `MidiKeypad.cpp`. +//! +//! Copyright (C) 2026 Nils — SPDX-License-Identifier: GPL-3.0-or-later +//! This file is part of pico-pi-midi. See LICENSE for the full notice. +//! +//! Reads the 16-pad button matrix from [`RgbKeypad`], turns presses/releases +//! into Note On/Off MIDI messages, and lights pads in response to incoming +//! Note messages. The underlay colours distinguish root / white / black keys +//! within the current octave, and an overlay can temporarily take over the +//! LEDs (used by the octave display). + +use crate::keypad::{RgbKeypad, NUM_PADS}; + +/// Root note (MIDI note number where the keypad's pad 0 starts). +pub type NoteOffset = u8; + +/// A MIDI message to send (Note On / Note Off here, 3 bytes each). +#[derive(Clone, Copy)] +pub struct MidiOut { + pub bytes: [u8; 3], +} + +/// Flip a 4x4 grid index so that physical pad 0 (top-left) maps to the +/// bottom-left note, matching the C `transform_index`. +fn transform_index(i: usize) -> usize { + let row = i / 4; + let column = i % 4; + (3 - row) * 4 + column +} + +pub struct MidiKeypad { + pub note_offset: NoteOffset, + previous_button_states: u16, + /// RGB underlay colour per physical pad index. + underlay_colors: [[u8; 3]; NUM_PADS], + overlay_active: bool, + underlay_active: bool, +} + +impl MidiKeypad { + pub fn new(keypad: &mut RgbKeypad, note_offset: NoteOffset) -> Self { + let mut s = Self { + note_offset, + previous_button_states: 0, + underlay_colors: [[0; 3]; NUM_PADS], + overlay_active: false, + underlay_active: true, + }; + s.set_base_colors(keypad); + s + } + + pub fn set_note_offset(&mut self, keypad: &mut RgbKeypad, offset: NoteOffset) { + self.note_offset = offset; + self.set_base_colors(keypad); + } + + /// Poll the button matrix; append a Note On/Off `[status, note, velocity]` + /// to `out` for every pad whose state changed since the last call. + pub fn update(&mut self, keypad: &mut RgbKeypad, out: &mut heapless::Vec) { + if self.overlay_active { + return; + } + let current = keypad.get_button_states(); + let changed = current ^ self.previous_button_states; + if changed != 0 { + for i in 0..NUM_PADS { + let mask = 1u16 << i; + if changed & mask != 0 { + let pressed = current & mask != 0; + let note = (transform_index(i) as u8).wrapping_add(self.note_offset); + let bytes = if pressed { + [0x90, note, 127] // Note On, full velocity + } else { + [0x80, note, 0] // Note Off + }; + let _ = out.push(MidiOut { bytes }); + } + } + } + self.previous_button_states = current; + } + + /// Light a single pad for the overlay (takes over the LEDs until cleared). + pub fn set_overlay_led(&mut self, keypad: &mut RgbKeypad, pad_index: usize, r: u8, g: u8, b: u8) { + let reversed = transform_index(pad_index); + keypad.illuminate(reversed, r, g, b); + keypad.update(); + self.overlay_active = true; + self.underlay_active = false; + } + + /// Mark the overlay active after painting LEDs directly (e.g. a bitmap via + /// `RgbKeypad` rather than `set_overlay_led`), so the keypad update loop + /// suspends button reads and `clear_overlay` knows to repaint the underlay. + pub fn set_overlay_active(&mut self) { + self.overlay_active = true; + self.underlay_active = false; + } + + /// Release the overlay and repaint the underlay. + pub fn clear_overlay(&mut self, keypad: &mut RgbKeypad) { + self.overlay_active = false; + self.underlay_active = true; + self.update_underlay(keypad); + } + + /// Handle an incoming 3-byte MIDI channel message, lighting the matching + /// pad if it falls in the keypad's note range. Mirrors the C + /// `receive_midi_message` (with the duplicated Note-On block de-duplicated). + pub fn receive_midi_message(&mut self, keypad: &mut RgbKeypad, message: &[u8]) { + if message.len() < 3 || self.overlay_active { + return; + } + let status = message[0]; + let note = message[1]; + let velocity = message[2]; + + let original = note as i32 - self.note_offset as i32; + if !(0..NUM_PADS as i32).contains(&original) { + return; + } + let original = original as usize; + let reversed = transform_index(original); + + let mut r = self.underlay_colors[original][0]; + let mut g = self.underlay_colors[original][1]; + let mut b = self.underlay_colors[original][2]; + + // Default to white if the underlay is off. + if r == 0 && g == 0 && b == 0 { + r = 255; + g = 255; + b = 255; + } else { + // Normalise the underlay colour to full brightness. + let max = r.max(g).max(b); + r = (r as u16 * 255 / max as u16) as u8; + g = (g as u16 * 255 / max as u16) as u8; + b = (b as u16 * 255 / max as u16) as u8; + } + + if status == 0x90 && velocity > 0 { + // Note On: scale the normalised colour by velocity. + r = (r as u16 * velocity as u16 / 127) as u8; + g = (g as u16 * velocity as u16 / 127) as u8; + b = (b as u16 * velocity as u16 / 127) as u8; + keypad.illuminate(reversed, r, g, b); + } else if status == 0x80 || (status == 0x90 && velocity == 0) { + // Note Off: revert to the original underlay colour. + keypad.illuminate( + reversed, + self.underlay_colors[original][0], + self.underlay_colors[original][1], + self.underlay_colors[original][2], + ); + } + keypad.update(); + } + + /// Paint the underlay colours across all pads. + fn update_underlay(&self, keypad: &mut RgbKeypad) { + if !self.overlay_active && self.underlay_active { + for i in 0..NUM_PADS { + let j = transform_index(i); + keypad.illuminate( + j, + self.underlay_colors[i][0], + self.underlay_colors[i][1], + self.underlay_colors[i][2], + ); + } + keypad.update(); + } + } + + /// Set the base underlay colours: root note purple, black keys dim, white + /// keys blue — keyed by `(note_offset + i) % 12`. + fn set_base_colors(&mut self, keypad: &mut RgbKeypad) { + for i in 0..NUM_PADS { + let note = (self.note_offset.wrapping_add(i as u8)) % 12; + self.underlay_colors[i] = match note { + 0 => [5, 1, 5], // root (purple) + 1 | 3 | 6 | 8 | 10 => [0, 0, 0], // black keys (dim) + _ => [1, 1, 10], // white keys (blue) + }; + } + self.update_underlay(keypad); + } +} diff --git a/src/octave.rs b/src/octave.rs new file mode 100644 index 0000000..fdde1da --- /dev/null +++ b/src/octave.rs @@ -0,0 +1,65 @@ +//! Octave display — Rust port of the octave bitmaps + `display_active_octave` +//! from `main.cpp`. On a BOOTSEL press, the active octave is shown as a digit +//! on the 4x4 keypad grid for 500ms via the keypad overlay, then cleared. +//! +//! Copyright (C) 2026 Nils — SPDX-License-Identifier: GPL-3.0-or-later +//! This file is part of pico-pi-midi. See LICENSE for the full notice. + +use crate::keypad::{RgbKeypad, NUM_PADS}; +use crate::midi_keypad::MidiKeypad; + +/// 4x4 bitmaps for octaves -1..=9. Each row is 4 bits (MSB = leftmost column), +/// rows ordered top-to-bottom. Index 0 = octave -1, index k = octave k-1. +/// (Direct copy of the C `digit_bitmaps[11][4]`.) +const DIGIT_BITMAPS: [[u8; 4]; 11] = [ + [0b0001, 0b0001, 0b1101, 0b0001], // -1 + [0b0010, 0b0101, 0b0101, 0b0010], // 0 + [0b0001, 0b0001, 0b0001, 0b0001], // 1 + [0b0111, 0b0011, 0b0110, 0b0111], // 2 + [0b0111, 0b0001, 0b0011, 0b0111], // 3 + [0b0101, 0b0111, 0b0001, 0b0001], // 4 + [0b0111, 0b0110, 0b0011, 0b0111], // 5 + [0b0100, 0b0111, 0b0101, 0b0111], // 6 + [0b0111, 0b0001, 0b0010, 0b0100], // 7 + [0b0111, 0b0111, 0b0111, 0b0111], // 8 + [0b0111, 0b0101, 0b0111, 0b0001], // 9 +]; + +/// Paint a 4x4 bitmap onto the keypad overlay (white where the bit is set). +/// Mirrors the C `display_bitmap`: row 0 is top, and the grid is written so +/// that physical row (3-row) is at the top — matching `transform_index`. +fn display_bitmap(keypad: &mut RgbKeypad, bitmap: &[u8; 4]) { + for row in 0..4 { + let row_data = bitmap[row]; + for col in 0..4 { + let on = row_data & (1 << (3 - col)) != 0; + let c = if on { 255 } else { 0 }; + // C: set_overlay_led((3 - row) * 4 + col, ...) + let pad_index = (3 - row) * 4 + col; + keypad.illuminate(pad_index, c, c, c); + } + } + keypad.update(); +} + +/// Show the active octave for `note_offset` on the grid for 500ms, then clear. +/// `octave = note_offset/12 - 1` (MIDI convention: note 0 = C-1). Out of range +/// flashes all pads red. +pub async fn display_active_octave(midi: &mut MidiKeypad, keypad: &mut RgbKeypad, note_offset: u8) { + let octave = (note_offset / 12) as i32 - 1; + + midi.clear_overlay(keypad); + + if (-1..=9).contains(&octave) { + display_bitmap(keypad, &DIGIT_BITMAPS[(octave + 1) as usize]); + midi.set_overlay_active(); + } else { + // Out of range: flash all LEDs red. + for i in 0..NUM_PADS { + midi.set_overlay_led(keypad, i, 255, 0, 0); + } + } + + embassy_time::Timer::after_millis(500).await; + midi.clear_overlay(keypad); +}