diff --git a/.github/workflows/auto-add-project.yml b/.github/workflows/auto-add-project.yml new file mode 100644 index 0000000..af93d5b --- /dev/null +++ b/.github/workflows/auto-add-project.yml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 hexaTune LLC +# SPDX-License-Identifier: MIT + +name: Auto add issues and PRs to hTuneSys Project 16 + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +jobs: + add-to-project: + runs-on: ubuntu-latest + + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/hTuneSys/projects/16 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..d513ad6 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2025 hexaTune LLC +# SPDX-License-Identifier: MIT + +name: 🧐 Commit Lint + +on: + pull_request: + branches: + - develop + types: [opened, synchronize, edited, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: read + +jobs: + commitlint: +<<<<<<< Updated upstream + name: Commit Lint (hexaGenMini) +======= + name: Commit Lint (hexaTuneWeb) +>>>>>>> Stashed changes + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install commitlint deps + run: | + npm i -D @commitlint/cli @commitlint/config-conventional + + - name: Show PR commit range (debug) + shell: bash + run: | + git fetch origin ${{ github.base_ref }} --depth=0 + BASE=$(git merge-base HEAD origin/${{ github.base_ref }}) + echo "Base ref: ${{ github.base_ref }}" + echo "Merge-base: $BASE" + echo "Commits to lint:" + git log --oneline "$BASE..HEAD" + + - name: Lint only commits introduced by this PR + shell: bash + run: | + git fetch origin ${{ github.base_ref }} --depth=0 + BASE=$(git merge-base HEAD origin/${{ github.base_ref }}) + npx commitlint \ + --from "$BASE" \ + --to "HEAD" \ + --config .github/.commitlintrc.yml \ + --verbose diff --git a/firmware/Cargo.lock b/firmware/Cargo.lock index 34559ee..cc6f576 100644 --- a/firmware/Cargo.lock +++ b/firmware/Cargo.lock @@ -16,9 +16,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -55,9 +55,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "az" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" [[package]] name = "bare-metal" @@ -68,12 +68,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bit-set" version = "0.8.0" @@ -109,9 +103,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -124,9 +118,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -136,9 +130,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "codespan-reporting" @@ -179,7 +173,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -214,9 +208,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -243,7 +237,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -254,7 +248,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -283,7 +277,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -297,9 +291,9 @@ dependencies = [ [[package]] name = "defmt-rtt" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cac3b8a5644a9e02b75085ebad3b6deafdbdbdec04bb25086523828aa4dfd1" +checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e" dependencies = [ "critical-section", "defmt", @@ -317,9 +311,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -402,7 +396,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -479,7 +473,7 @@ dependencies = [ "nb 1.1.0", "pio", "rand_core 0.6.4", - "rand_core 0.9.3", + "rand_core 0.9.5", "rp-pac", "rp2040-boot2", "sha2-const-stable", @@ -631,9 +625,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" dependencies = [ "log", ] @@ -652,9 +646,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fixed" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707070ccf8c4173548210893a0186e29c266901b71ed20cd9e2ca0193dfe95c3" +checksum = "c566da967934c6c7ee0458a9773de9b2a685bd2ce26a3b28ddfc740e640182f5" dependencies = [ "az", "bytemuck", @@ -676,15 +670,15 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "generic-array" @@ -698,12 +692,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -726,9 +721,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heapless" @@ -750,11 +745,30 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hexa-tune-proto" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083b47eef4a8c3b07d4235104a19b6991498cf59fe7eac89decbcbf542accde8" +dependencies = [ + "defmt", +] + +[[package]] +name = "hexa-tune-proto-embedded" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad329deaf227587caa833be9bf8f28b1f52da0cc3ec3ac1b4861af29335a583" +dependencies = [ + "defmt", + "heapless 0.9.1", + "hexa-tune-proto", +] + [[package]] name = "hexagenmini" version = "0.1.0" dependencies = [ - "base64", "cortex-m", "cortex-m-rt", "critical-section", @@ -770,9 +784,10 @@ dependencies = [ "embassy-usb", "embedded-storage", "heapless 0.9.1", + "hexa-tune-proto", + "hexa-tune-proto-embedded", "panic-probe", "portable-atomic", - "sha2", "smart-leds", "static_cell", ] @@ -785,12 +800,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -804,9 +819,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -845,37 +860,36 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nb" @@ -909,9 +923,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -919,13 +933,13 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -946,9 +960,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -956,15 +970,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] @@ -1043,14 +1057,14 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" dependencies = [ "critical-section", ] @@ -1080,23 +1094,23 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1109,24 +1123,24 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1136,9 +1150,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1147,18 +1161,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "rp-pac" @@ -1226,42 +1237,31 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", ] [[package]] name = "serde_core" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "syn 2.0.117", ] [[package]] @@ -1288,9 +1288,9 @@ checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "smallvec" @@ -1309,9 +1309,9 @@ dependencies = [ [[package]] name = "smart-leds-trait" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edeb89c73244414bb0568611690dd095b2358b3fda5bae65ad784806cca00157" +checksum = "a7f4441a131924d58da6b83a7ad765c460e64630cce504376c3a87a2558c487f" dependencies = [ "rgb", ] @@ -1328,9 +1328,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_cell" @@ -1372,9 +1372,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1383,9 +1383,9 @@ dependencies = [ [[package]] name = "term" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ "windows-sys", ] @@ -1401,35 +1401,35 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -1538,99 +1538,35 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index c192c6f..8fe112f 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -61,8 +61,10 @@ heapless = "0.9.1" static_cell = "2.1" portable-atomic = { version = "1.5", features = ["critical-section"] } -sha2 = { version = "0.10", default-features = false } -base64 = { version = "0.22.1", default-features = false } +hexa-tune-proto-embedded = { version = "0.1.1", default-features = false, features = [ + "defmt", +] } +hexa-tune-proto = "0.1.1" [profile.release] # Enable generation of debug symbols even on release builds diff --git a/firmware/src/at/at_task.rs b/firmware/src/at/at_task.rs index 5580a73..5da51af 100644 --- a/firmware/src/at/at_task.rs +++ b/firmware/src/at/at_task.rs @@ -1,53 +1,52 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT +use cortex_m::peripheral::SCB; use defmt::{error, info}; use embassy_executor::Spawner; use heapless::String; use {defmt_rtt as _, panic_probe as _}; +use hexa_tune_proto_embedded::command::HexaCommand; +use hexa_tune_proto_embedded::HexaError; + use crate::AT_CH; use crate::USB_CH; use crate::at::*; use crate::channel::*; +use crate::error::FirmwareError; use crate::hexa_config::*; #[embassy_executor::task] -pub async fn at_task(dispatcher: &'static AtDispatcher, spawner: Spawner) { +pub async fn at_task(spawner: Spawner) { info!("Starting AT task"); let mut last_operation_status: String<64> = String::new(); loop { match AT_CH.receive().await { Msg::AtRxLine(line) => { - if let Some(e) = dispatcher.dispatch(spawner, &line) { - let compiled = compile_at_error(get_empty_id(), e); + if let Err((id, e)) = dispatch_and_spawn(spawner, line.as_bytes()) { + let compiled = encode_error_response(id, &e); error!("Dispatch error: {:?}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } } - Msg::AtCmdOutput(at_command) => { - let compiled = at_command.compile(); - info!("Sending output: {}", compiled.as_str()); - USB_CH.send(Msg::UsbTxLine(compiled)).await; + Msg::AtCmdResponse(line) => { + info!("Sending response: {}", line.as_str()); + USB_CH.send(Msg::UsbTxLine(line)).await; } Msg::Done(msg_id) => { - let compiled = compile_at_done(msg_id); + let compiled = encode_done(msg_id); info!("Sending done: {}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } Msg::Err(msg_id, e) => { - let compiled = compile_at_error(msg_id, e); + let compiled = encode_error_response(msg_id, &e); error!("Sending error: {}", compiled.as_str()); USB_CH.send(Msg::UsbTxLine(compiled)).await; } Msg::SetDdsAvailable(status) => { set_dds_available(status); } - Msg::Completed(at_command) => { - let compiled = compile_at_completed(at_command); - info!("Sending done: {}", compiled.as_str()); - USB_CH.send(Msg::UsbTxLine(compiled)).await; - } Msg::SetOperationStatus(status) => { last_operation_status = status; } @@ -60,3 +59,62 @@ pub async fn at_task(dispatcher: &'static AtDispatcher, spawner: Spawner) { } } } + +fn dispatch_and_spawn(spawner: Spawner, payload: &[u8]) -> Result<(), (u32, FirmwareError)> { + let cmd = dispatch_at_payload(payload).map_err(|e| (0u32, e))?; + let id = command_id(&cmd); + + match cmd { + HexaCommand::VersionQuery => { + info!("Dispatching VERSION query"); + spawner.spawn(version_task()).ok(); + } + HexaCommand::SetRgb { id, r, g, b } => { + info!("Dispatching SETRGB command"); + spawner.spawn(setrgb_task(id, r, g, b)).ok(); + } + HexaCommand::Reset { .. } => { + info!("Dispatching RESET command"); + SCB::sys_reset(); + } + HexaCommand::FwUpdate { .. } => { + info!("Dispatching FWUPDATE command"); + spawner.spawn(fwupdate_task()).ok(); + } + HexaCommand::Freq { id, freq, time_ms } => { + if !is_dds_available() { + error!("DDS busy, cannot set FREQ"); + return Err((id, FirmwareError::Hexa(HexaError::DdsBusy))); + } + info!("Dispatching FREQ command"); + spawner.spawn(freq_task(id, freq, time_ms)).ok(); + } + HexaCommand::Operation { id, sub } => { + if !is_dds_available() { + error!("DDS busy, cannot set OPERATION"); + return Err((id, FirmwareError::Hexa(HexaError::DdsBusy))); + } + info!("Dispatching OPERATION command"); + spawner.spawn(operation_task(id, sub)).ok(); + } + HexaCommand::OperationQuery => { + info!("Dispatching OPERATION query"); + spawner.spawn(operation_status_task()).ok(); + } + _ => { + return Err((id, FirmwareError::Hexa(HexaError::UnknownCommand))); + } + } + Ok(()) +} + +fn command_id(cmd: &HexaCommand) -> u32 { + match cmd { + HexaCommand::SetRgb { id, .. } + | HexaCommand::Reset { id } + | HexaCommand::FwUpdate { id } + | HexaCommand::Freq { id, .. } + | HexaCommand::Operation { id, .. } => *id, + _ => 0, + } +} diff --git a/firmware/src/at/command.rs b/firmware/src/at/command.rs deleted file mode 100644 index 6318713..0000000 --- a/firmware/src/at/command.rs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use defmt::info; -use heapless::{String, Vec}; - -use crate::error::Error; - -#[derive(Debug, Clone)] -pub struct AtCommand { - pub id: String<16>, - pub name: String<16>, - pub params: Vec, 8>, - pub is_query: bool, -} - -impl AtCommand { - pub fn compile(&self) -> String<64> { - let mut s = String::<64>::new(); - s.push_str("AT+").unwrap(); - s.push_str(self.name.as_str()).unwrap(); - if self.is_query { - s.push('?').unwrap(); - } else { - s.push('=').unwrap(); - s.push_str(self.id.as_str()).unwrap(); - if !self.params.is_empty() { - for p in self.params.iter() { - s.push('#').unwrap(); - s.push_str(p.as_str()).unwrap(); - } - } - } - s - } -} - -pub fn parse(input: &str) -> Result { - info!("Received input: {}", input); - let input = input.trim(); - if !input.starts_with("AT+") { - return Err(Error::InvalidCommand); - } - let cmd = &input[3..]; - if let Some(eq_pos) = cmd.find('=') { - let (name, param_str) = cmd.split_at(eq_pos); - let mut id: String<16> = { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }; - let param_str = ¶m_str[1..]; - let mut params = Vec::, 8>::new(); - for (i, p) in param_str - .split('#') - .map(|p| p.trim()) - .filter(|p| !p.is_empty()) - .enumerate() - { - info!("Parsing param: {}", p); - let mut s_hl = String::<16>::new(); - s_hl.push_str(p).map_err(|_| Error::InvalidUtf8)?; - if i == 0 { - id = s_hl; - } else { - params.push(s_hl).map_err(|_| Error::ParamCount)?; - } - } - Ok(AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str(name).map_err(|_| Error::InvalidUtf8)?; - n - }, - params, - is_query: false, - }) - } else if let Some(name) = cmd.strip_suffix('?') { - Ok(AtCommand { - id: { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }, - name: { - let mut n = String::<16>::new(); - n.push_str(name).map_err(|_| Error::InvalidUtf8)?; - n - }, - params: Vec::, 8>::new(), - is_query: true, - }) - } else { - Ok(AtCommand { - id: { - let mut id = String::<16>::new(); - id.push_str("0").map_err(|_| Error::InvalidUtf8)?; - id - }, - name: { - let mut n = String::<16>::new(); - n.push_str(cmd).map_err(|_| Error::InvalidUtf8)?; - n - }, - params: Vec::, 8>::new(), - is_query: false, - }) - } -} - -pub fn get_empty_id() -> String<16> { - let mut id = String::<16>::new(); - id.push_str("0").unwrap(); - id -} - -pub fn compile_at_done(id: String<16>) -> String<64> { - let at_cmd = AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str("DONE").unwrap(); - n - }, - params: Vec::, 8>::new(), - is_query: false, - }; - at_cmd.compile() -} - -pub fn compile_at_completed(at_command: AtCommand) -> String<64> { - let mut at_cmd = AtCommand { - id: at_command.id, - name: at_command.name, - params: at_command.params, - is_query: false, - }; - let completede_param = String::<16>::try_from("COMPLETED").unwrap(); - at_cmd.params.push(completede_param).ok(); - at_cmd.compile() -} - -pub fn compile_at_error(id: String<16>, e: Error) -> String<64> { - let at_cmd = AtCommand { - id, - name: { - let mut n = String::<16>::new(); - n.push_str("ERROR").unwrap(); - n - }, - params: { - let mut p = Vec::, 8>::new(); - let mut code = String::<16>::new(); - code.push_str(e.code()).unwrap(); - p.push(code).unwrap(); - p - }, - is_query: false, - }; - at_cmd.compile() -} diff --git a/firmware/src/at/dispatcher.rs b/firmware/src/at/dispatcher.rs index 935341e..20ef041 100644 --- a/firmware/src/at/dispatcher.rs +++ b/firmware/src/at/dispatcher.rs @@ -1,60 +1,76 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; -use heapless::{LinearMap, String}; +use hexa_tune_proto::at::{self, AtOp}; +use hexa_tune_proto_embedded::command::HexaCommand; +use hexa_tune_proto_embedded::dispatch::resolve; -use crate::at::handler::{AtHandler, Handler, register_all}; -use crate::at::parse; -use crate::error::Error; +use crate::channel::MsgString; +use crate::error::FirmwareError; -pub struct AtDispatcher { - handlers: LinearMap, Handler, 8>, +/// Parse an AT payload and resolve it to a typed HexaCommand. +pub fn dispatch_at_payload(payload: &[u8]) -> Result { + let msg = at::parse(payload).map_err(FirmwareError::Proto)?; + resolve(&msg).map_err(FirmwareError::Hexa) } -impl AtDispatcher { - pub fn new() -> Self { - Self { - handlers: register_all(), +/// Encode an AT response (name=id#params...) into a MsgString. +pub fn encode_response(name: &[u8], id: u32, params: &[&[u8]]) -> MsgString { + let mut buf = [0u8; 64]; + if let Ok(n) = at::encode(name, id, AtOp::Response, params, &mut buf) { + if let Ok(s) = core::str::from_utf8(&buf[..n]) { + if let Ok(line) = MsgString::try_from(s) { + return line; + } } } + MsgString::new() +} - pub fn dispatch(&self, spawner: Spawner, input: &str) -> Option { - match parse(input) { - Ok(cmd) => match self.handlers.get(&cmd.name) { - Some(handler) => match handler { - Handler::Version(h) => { - info!("Dispatching VERSION command"); - h.handle(spawner, cmd) - } - Handler::SetRgb(h) => { - info!("Dispatching SETRGB command"); - h.handle(spawner, cmd) - } - Handler::Reset(h) => { - info!("Dispatching RESET command"); - h.handle(spawner, cmd) - } - Handler::FwUpdate(h) => { - info!("Dispatching FWUPDATE command"); - h.handle(spawner, cmd) - } - Handler::Freq(h) => { - info!("Dispatching FREQ command"); - h.handle(spawner, cmd) - } - Handler::Operation(h) => { - info!("Dispatching OPERATION command"); - h.handle(spawner, cmd) - } - }, - None => Some(Error::UnknownCommand), - }, - Err(e) => { - error!("Failed to parse command: {}", &e.code()); - Some(e) - } - } +/// Encode an AT+DONE=id response. +pub fn encode_done(id: u32) -> MsgString { + encode_response(b"DONE", id, &[]) +} + +/// Encode an AT+ERROR=id#code response using u8 error code. +pub fn encode_error_response(id: u32, e: &FirmwareError) -> MsgString { + let code = e.error_code(); + let mut code_buf = [0u8; 3]; + let code_len = u8_to_ascii(code, &mut code_buf); + encode_response(b"ERROR", id, &[&code_buf[..code_len]]) +} + +/// Convert a u32 value to ASCII decimal bytes in a buffer. +pub fn u32_to_ascii_buf(val: u32, buf: &mut [u8; 10]) -> usize { + if val == 0 { + buf[0] = b'0'; + return 1; + } + let mut tmp = [0u8; 10]; + let mut n = val; + let mut i = 10usize; + while n > 0 { + i -= 1; + tmp[i] = b'0' + (n % 10) as u8; + n /= 10; + } + let len = 10 - i; + buf[..len].copy_from_slice(&tmp[i..]); + len +} + +fn u8_to_ascii(val: u8, buf: &mut [u8; 3]) -> usize { + if val >= 100 { + buf[0] = b'0' + val / 100; + buf[1] = b'0' + (val / 10) % 10; + buf[2] = b'0' + val % 10; + 3 + } else if val >= 10 { + buf[0] = b'0' + val / 10; + buf[1] = b'0' + val % 10; + 2 + } else { + buf[0] = b'0' + val; + 1 } } diff --git a/firmware/src/at/handler.rs b/firmware/src/at/handler.rs deleted file mode 100644 index 8567778..0000000 --- a/firmware/src/at/handler.rs +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use embassy_executor::Spawner; -use heapless::{LinearMap, String}; - -use crate::at::*; -use crate::error::Error; - -pub trait AtHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option; -} - -pub enum Handler { - Version(VersionHandler), - SetRgb(SetRgbHandler), - Reset(ResetHandler), - FwUpdate(FwUpdateHandler), - Freq(FreqHandler), - Operation(OperationHandler), -} - -pub fn register_all() -> LinearMap, Handler, 8> { - let mut map: LinearMap, Handler, 8> = LinearMap::new(); - - map.insert( - String::<16>::try_from("VERSION").unwrap(), - Handler::Version(VersionHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("SETRGB").unwrap(), - Handler::SetRgb(SetRgbHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("RESET").unwrap(), - Handler::Reset(ResetHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("FWUPDATE").unwrap(), - Handler::FwUpdate(FwUpdateHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("FREQ").unwrap(), - Handler::Freq(FreqHandler), - ) - .ok(); - - map.insert( - String::<16>::try_from("OPERATION").unwrap(), - Handler::Operation(OperationHandler), - ) - .ok(); - - map -} diff --git a/firmware/src/at/handlers/freq_handler.rs b/firmware/src/at/handlers/freq_handler.rs index 7d5ea45..9e770df 100644 --- a/firmware/src/at/handlers/freq_handler.rs +++ b/firmware/src/at/handlers/freq_handler.rs @@ -1,36 +1,14 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; +use defmt::info; use crate::DDS_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; -use crate::hexa_config::*; - -pub struct FreqHandler; - -impl AtHandler for FreqHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if is_dds_available() { - info!("Setting FREQ: {:?}", at_command.id.as_str()); - let _ = spawner.spawn(freq_task(at_command)); - None - } else { - error!("DDS busy, cannot set FREQ ({})", at_command.id.as_str()); - Some(Error::DdsBusy) - } - } -} #[embassy_executor::task] -async fn freq_task(at_command: AtCommand) { - info!( - "Sending FREQ command to DDS task: {}", - at_command.id.as_str() - ); - DDS_CH.send(Msg::FreqWithValue(at_command)).await; +pub async fn freq_task(id: u32, freq: u32, time_ms: u32) { + info!("Sending FREQ command to DDS task"); + DDS_CH.send(Msg::FreqSet { id, freq, time_ms }).await; info!("FREQ command sent to DDS task"); } diff --git a/firmware/src/at/handlers/fwupdate_handler.rs b/firmware/src/at/handlers/fwupdate_handler.rs index 4effdae..0ab7c87 100644 --- a/firmware/src/at/handlers/fwupdate_handler.rs +++ b/firmware/src/at/handlers/fwupdate_handler.rs @@ -2,23 +2,9 @@ // SPDX-License-Identifier: MIT use defmt::info; -use embassy_executor::Spawner; - -use crate::at::*; -use crate::error::Error; - -pub struct FwUpdateHandler; - -impl AtHandler for FwUpdateHandler { - fn handle(&self, spawner: Spawner, _at_command: AtCommand) -> Option { - info!("Firmware update requested - spawning BOOTSEL task"); - let _ = spawner.spawn(fwupdate_task()); - None - } -} #[embassy_executor::task] -async fn fwupdate_task() { +pub async fn fwupdate_task() { info!("Entering BOOTSEL mode for firmware update"); embassy_time::Timer::after_millis(100).await; embassy_rp::rom_data::reset_to_usb_boot(0, 0); diff --git a/firmware/src/at/handlers/mod.rs b/firmware/src/at/handlers/mod.rs index 7c4329a..239d6d4 100644 --- a/firmware/src/at/handlers/mod.rs +++ b/firmware/src/at/handlers/mod.rs @@ -7,8 +7,6 @@ mod setrgb_handler; pub use setrgb_handler::*; mod fwupdate_handler; pub use fwupdate_handler::*; -mod reset_handler; -pub use reset_handler::*; mod freq_handler; pub use freq_handler::*; mod operation_handler; diff --git a/firmware/src/at/handlers/operation_handler.rs b/firmware/src/at/handlers/operation_handler.rs index 564dcc4..cc4f6c8 100644 --- a/firmware/src/at/handlers/operation_handler.rs +++ b/firmware/src/at/handlers/operation_handler.rs @@ -1,52 +1,23 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::{error, info}; -use embassy_executor::Spawner; +use defmt::info; + +use hexa_tune_proto_embedded::command::OperationSub; use crate::AT_CH; use crate::DDS_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; -use crate::hexa_config::*; - -pub struct OperationHandler; - -impl AtHandler for OperationHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if at_command.is_query { - let _ = spawner.spawn(operation_status_task()); - None - } else { - info!("Handling OPERATION command"); - if is_dds_available() { - info!("Setting OPERATION: {:?}", at_command.id.as_str()); - let _ = spawner.spawn(operation_task(at_command)); - None - } else { - error!( - "DDS busy, cannot set OPERATION ({})", - at_command.id.as_str() - ); - Some(Error::DdsBusy) - } - } - } -} #[embassy_executor::task] -async fn operation_task(at_command: AtCommand) { - info!( - "Sending OPERATION command to DDS task: {}", - at_command.id.as_str() - ); - DDS_CH.send(Msg::Operation(at_command)).await; +pub async fn operation_task(id: u32, sub: OperationSub) { + info!("Sending OPERATION command to DDS task"); + DDS_CH.send(Msg::OperationCmd { id, sub }).await; info!("OPERATION command sent to DDS task"); } #[embassy_executor::task] -async fn operation_status_task() { +pub async fn operation_status_task() { info!("Requesting OPERATION status"); AT_CH.send(Msg::GetOperationStatus).await; } diff --git a/firmware/src/at/handlers/reset_handler.rs b/firmware/src/at/handlers/reset_handler.rs deleted file mode 100644 index f3785fc..0000000 --- a/firmware/src/at/handlers/reset_handler.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use cortex_m::peripheral::SCB; -use embassy_executor::Spawner; - -use crate::at::*; -use crate::error::Error; - -pub struct ResetHandler; - -impl AtHandler for ResetHandler { - fn handle(&self, _spawner: Spawner, _at_command: AtCommand) -> Option { - SCB::sys_reset(); - } -} diff --git a/firmware/src/at/handlers/setrgb_handler.rs b/firmware/src/at/handlers/setrgb_handler.rs index f6413f8..bb2f83a 100644 --- a/firmware/src/at/handlers/setrgb_handler.rs +++ b/firmware/src/at/handlers/setrgb_handler.rs @@ -1,23 +1,10 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use embassy_executor::Spawner; - use crate::RGB_CH; -use crate::at::{AtCommand, AtHandler}; use crate::channel::*; -use crate::error::Error; - -pub struct SetRgbHandler; - -impl AtHandler for SetRgbHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - let _ = spawner.spawn(setrgb_task(at_command)); - None - } -} #[embassy_executor::task] -async fn setrgb_task(at_command: AtCommand) { - RGB_CH.send(Msg::RgbWithValue(at_command)).await; +pub async fn setrgb_task(id: u32, r: u8, g: u8, b: u8) { + RGB_CH.send(Msg::RgbSet { id, r, g, b }).await; } diff --git a/firmware/src/at/handlers/version_handler.rs b/firmware/src/at/handlers/version_handler.rs index 4a2849b..b024aa3 100644 --- a/firmware/src/at/handlers/version_handler.rs +++ b/firmware/src/at/handlers/version_handler.rs @@ -1,44 +1,13 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use defmt::error; -use embassy_executor::Spawner; -use heapless::{String, Vec}; - use crate::AT_CH; -use crate::at::*; +use crate::at::encode_response; use crate::channel::*; -use crate::error::Error; use crate::hexa_config::CONF_VERSION; -pub struct VersionHandler; - -impl AtHandler for VersionHandler { - fn handle(&self, spawner: Spawner, at_command: AtCommand) -> Option { - if at_command.is_query { - let _ = spawner.spawn(version_task()); - None - } else { - error!("VERSION command is a query only"); - Some(Error::NotAQuery) - } - } -} - #[embassy_executor::task] -async fn version_task() { - let mut name = String::<16>::new(); - name.push_str("VERSION").unwrap(); - - let mut params = Vec::, 8>::new(); - let version_param = String::<16>::try_from(CONF_VERSION).unwrap(); - params.push(version_param).ok(); - - let at_command = AtCommand { - id: get_empty_id(), - name, - params, - is_query: false, - }; - AT_CH.send(Msg::AtCmdOutput(at_command)).await; +pub async fn version_task() { + let line = encode_response(b"VERSION", 0, &[CONF_VERSION.as_bytes()]); + AT_CH.send(Msg::AtCmdResponse(line)).await; } diff --git a/firmware/src/at/mod.rs b/firmware/src/at/mod.rs index 902ff41..d44969d 100644 --- a/firmware/src/at/mod.rs +++ b/firmware/src/at/mod.rs @@ -1,12 +1,8 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -mod command; -pub use command::*; mod dispatcher; pub use dispatcher::*; -mod handler; -pub use handler::*; mod at_task; pub use at_task::*; mod handlers; diff --git a/firmware/src/channel/message_type.rs b/firmware/src/channel/message_type.rs index b4b42ff..2fca2a9 100644 --- a/firmware/src/channel/message_type.rs +++ b/firmware/src/channel/message_type.rs @@ -3,24 +3,22 @@ use heapless::String; -use crate::at::AtCommand; -use crate::error::Error; +use crate::error::FirmwareError; +use hexa_tune_proto_embedded::command::OperationSub; -pub type MsgId = String<16>; +pub type MsgId = u32; pub type MsgString = String<64>; -pub type IsDdsAvailable = bool; pub enum Msg { AtRxLine(MsgString), - AtCmdOutput(AtCommand), + AtCmdResponse(MsgString), Done(MsgId), - Completed(AtCommand), - Err(MsgId, Error), + Err(MsgId, FirmwareError), UsbTxLine(MsgString), - RgbWithValue(AtCommand), - FreqWithValue(AtCommand), - SetDdsAvailable(IsDdsAvailable), + RgbSet { id: u32, r: u8, g: u8, b: u8 }, + FreqSet { id: u32, freq: u32, time_ms: u32 }, + SetDdsAvailable(bool), SetOperationStatus(MsgString), GetOperationStatus, - Operation(AtCommand), + OperationCmd { id: u32, sub: OperationSub }, } diff --git a/firmware/src/dds/ad985x.rs b/firmware/src/dds/ad985x.rs index e1c4b98..90b798b 100644 --- a/firmware/src/dds/ad985x.rs +++ b/firmware/src/dds/ad985x.rs @@ -5,7 +5,7 @@ use defmt::*; use embassy_rp::gpio::Output; use embassy_time::{Duration, Timer}; -use crate::error::Error; +use crate::error::FirmwareError; const CTRL_PWRDOWN: u8 = 1 << 7; const CTRL_SLEEP: u8 = 1 << 6; @@ -76,7 +76,7 @@ impl Ad985x { ((num + den / 2) / den) as u32 } - pub async fn reset(&mut self) -> Option { + pub async fn reset(&mut self) -> Option { self.rst.set_high(); Timer::after(Duration::from_micros(5)).await; self.rst.set_low(); @@ -93,23 +93,23 @@ impl Ad985x { None } - /*pub async fn sleep(&mut self) -> Option { + /*pub async fn sleep(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base | CTRL_SLEEP).await; None }*/ - pub async fn down(&mut self) -> Option { + pub async fn down(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base | CTRL_PWRDOWN).await; None } - pub async fn up(&mut self) -> Option { + pub async fn up(&mut self) -> Option { self.write_ftw_ctrl(0, self.ctrl_base & !(CTRL_SLEEP | CTRL_PWRDOWN)) .await; None } - pub async fn set_freq(&mut self, freq_hz: u32, dwell_ms: u32) -> Option { + pub async fn set_freq(&mut self, freq_hz: u32, dwell_ms: u32) -> Option { if let Some(e) = self.down().await { return Some(e); } @@ -135,7 +135,7 @@ impl Ad985x { None } - async fn set_freq_immediate(&mut self, freq_hz: u32) -> Option { + async fn set_freq_immediate(&mut self, freq_hz: u32) -> Option { let ftw = self.hz_to_ftw(freq_hz); self.write_ftw_ctrl(ftw, self.ctrl_base | CTRL_PHASE0).await; None diff --git a/firmware/src/dds/dds_task.rs b/firmware/src/dds/dds_task.rs index a2886d2..7134e5c 100644 --- a/firmware/src/dds/dds_task.rs +++ b/firmware/src/dds/dds_task.rs @@ -6,13 +6,14 @@ use defmt::*; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex as Cs; use embassy_sync::mutex::Mutex; -use heapless::{String, Vec}; use {defmt_rtt as _, panic_probe as _}; -use crate::at::*; +use hexa_tune_proto_embedded::command::OperationSub; + +use crate::at::{encode_error_response, encode_response, u32_to_ascii_buf}; use crate::channel::*; use crate::dds::*; -use crate::error::Error; +use crate::error::FirmwareError; use crate::{AT_CH, DDS_CH}; static OPERATION: Mutex> = Mutex::new(RefCell::new(Operation::new())); @@ -22,136 +23,86 @@ pub async fn dds_task(mut ad985x: Ad985x) { info!("Starting DDS task"); loop { match DDS_CH.receive().await { - Msg::Operation(at_command) => { - info!( - "Received OPERATION command in DDS task: {}", - at_command.id.as_str() - ); - info!("Parsing OPERATION command parameters"); - if at_command.params.len() != 1 { - error!( - "OPERATION command requires 1 parameters, got {}", - at_command.params.len() - ); - AT_CH.send(Msg::Err(at_command.id, Error::ParamCount)).await; - continue; - } - info!("Parameters parsed successfully"); - info!("Extracting command_type values"); - let command_type = match at_command.params[0].parse::>() { - Ok(v) => v, - Err(_) => { - error!("Invalid command_type value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - continue; - } - }; + Msg::OperationCmd { id, sub } => { + info!("Received OPERATION command in DDS task: {}", id); - match command_type.as_str() { - "PREPARE" => { + match sub { + OperationSub::Prepare => { info!("Preparing DDS operation"); - // Reset the operation by replacing the inner value let operation = OPERATION.lock().await; *operation.borrow_mut() = Operation::new(); - { let mut guard = operation.borrow_mut(); - guard.set_id(at_command.id.clone()); + guard.set_id(id); } drop(operation); info!("DDS operation prepared"); - AT_CH.send(Msg::Completed(at_command.clone())).await; + let completed = + encode_response(b"OPERATION", id, &[b"PREPARE", b"COMPLETED"]); + AT_CH.send(Msg::AtCmdResponse(completed.clone())).await; info!("Completed sent for PREPARE command"); - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + AT_CH.send(Msg::SetOperationStatus(completed)).await; } - "GENERATE" => { + OperationSub::Generate => { info!("Starting DDS operation"); - let mut result: Option = None; + let mut result: Option = None; info!("Setting Device Available to false"); AT_CH.send(Msg::SetDdsAvailable(false)).await; info!("Set Device Available to false"); - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; - - let steps = { + let gen_completed = + encode_response(b"OPERATION", id, &[b"GENERATE", b"COMPLETED"]); + AT_CH + .send(Msg::SetOperationStatus(gen_completed.clone())) + .await; + + // Clone steps out of the mutex + let step_count; + let mut step_ids = [0u32; 64]; + let mut step_freqs = [0u32; 64]; + let mut step_times = [0u32; 64]; + { let operation = OPERATION.lock().await; let guard = operation.borrow(); - guard.get_steps().clone() - }; - - let mut at_command_status = at_command.clone(); - at_command_status.params.clear(); - - for at_command_step in steps.iter() { - let mut params = Vec::, 8>::new(); - let generating_param = String::<16>::try_from("GENERATING").unwrap(); - params.push(generating_param).ok(); - - at_command_status.params = params; - - info!("Parsing FREQ command parameters"); - if at_command_step.params.len() != 2 { - error!( - "FREQ command requires 2 parameters, got {}", - at_command_step.params.len() - ); - result = Some(Error::ParamCount); - break; + let steps = guard.get_steps(); + step_count = steps.len(); + for (i, s) in steps.iter().enumerate() { + step_ids[i] = s.id; + step_freqs[i] = s.freq; + step_times[i] = s.time_ms; } - info!("Parameters parsed successfully"); - - info!("Extracting frequency and time_ms values"); - let freq = match at_command_step.params[0].parse::() { - Ok(v) => v, - Err(_) => { - error!( - "Invalid frequency value: {}", - at_command_step.id.as_str() - ); - result = Some(Error::ParamValue); - break; - } - }; - info!("Frequency value extracted: {}", freq); - let time_ms = match at_command_step.params[1].parse::() { - Ok(v) => v, - Err(_) => { - error!( - "Invalid time_ms value: {}", - at_command_step.id.as_str() - ); - result = Some(Error::ParamValue); - break; - } - }; - info!("time_ms value extracted: {}", time_ms); - - info!( - "Setting FREQ to ({}) over {} ms", - at_command_step.id.as_str(), - time_ms - ); + } - let step_id_param = - String::<16>::try_from(at_command_step.id.as_str()).unwrap(); - at_command_status.params.push(step_id_param).ok(); - let compiled_status = compile_at_completed(at_command_status.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + for i in 0..step_count { + let step_id = step_ids[i]; + let freq = step_freqs[i]; + let time_ms = step_times[i]; + + // Build status: AT+OPERATION=id#GENERATING#step_id#COMPLETED + let mut sid_buf = [0u8; 10]; + let sid_len = u32_to_ascii_buf(step_id, &mut sid_buf); + let status = encode_response( + b"OPERATION", + id, + &[b"GENERATING", &sid_buf[..sid_len], b"COMPLETED"], + ); + AT_CH.send(Msg::SetOperationStatus(status)).await; - info!("Starting frequency set..."); + info!("Setting FREQ to {} over {} ms", freq, time_ms); let err = ad985x.set_freq(freq, time_ms).await; info!("Frequency set complete."); if let Some(err) = err { - error!("Error setting FREQ: {:?}", err.code()); - result = Some(err); + error!("Error setting FREQ"); + result = Some(FirmwareError::Hexa( + hexa_tune_proto_embedded::HexaError::InvalidParam, + )); + let _ = err; break; } else { info!("FREQ set successfully"); @@ -163,46 +114,47 @@ pub async fn dds_task(mut ad985x: Ad985x) { info!("Set Device Available to true"); if let Some(err) = result { - error!("DDS operation failed: {:?}", err.code()); - AT_CH - .send(Msg::Err(at_command.id.clone(), err.clone())) - .await; + error!("DDS operation failed"); + AT_CH.send(Msg::Err(id, err)).await; - let compiled_status = compile_at_error(at_command.id.clone(), err); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + let error_status = encode_error_response(id, &err); + AT_CH.send(Msg::SetOperationStatus(error_status)).await; } else { - let compiled_status = compile_at_completed(at_command.clone()); - AT_CH.send(Msg::SetOperationStatus(compiled_status)).await; + AT_CH.send(Msg::SetOperationStatus(gen_completed)).await; } } - _ => { - error!("Unknown command_type value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - continue; - } } } - Msg::FreqWithValue(at_command) => { - info!( - "Received FREQ command in DDS task: {}", - at_command.id.as_str() - ); + Msg::FreqSet { id, freq, time_ms } => { + info!("Received FREQ command in DDS task: {}", id); - info!("Adding FREQ command to operation"); + info!("Adding FREQ step to operation"); let operation = OPERATION.lock().await; + let step = FreqStep { id, freq, time_ms }; let add_result = { let mut guard = operation.borrow_mut(); - guard.add_step(at_command.clone()) + guard.add_step(step) }; drop(operation); if let Err(e) = add_result { error!("Failed to add step: operation is full"); - AT_CH.send(Msg::Err(at_command.id, e)).await; + AT_CH.send(Msg::Err(id, e)).await; } else { - info!("FREQ command added to operation"); - AT_CH.send(Msg::Completed(at_command)).await; + info!("FREQ step added to operation"); + + // Build completed response: AT+FREQ=id#freq#time_ms#COMPLETED + let mut freq_buf = [0u8; 10]; + let freq_len = u32_to_ascii_buf(freq, &mut freq_buf); + let mut time_buf = [0u8; 10]; + let time_len = u32_to_ascii_buf(time_ms, &mut time_buf); + let completed = encode_response( + b"FREQ", + id, + &[&freq_buf[..freq_len], &time_buf[..time_len], b"COMPLETED"], + ); + AT_CH.send(Msg::AtCmdResponse(completed)).await; info!("Completed sent for FREQ command"); } } diff --git a/firmware/src/dds/dds_type.rs b/firmware/src/dds/dds_type.rs index b68532a..d247a4b 100644 --- a/firmware/src/dds/dds_type.rs +++ b/firmware/src/dds/dds_type.rs @@ -1,40 +1,45 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -use heapless::{String, Vec}; +use heapless::Vec; -use crate::at::AtCommand; -use crate::error::Error; +use crate::error::FirmwareError; -pub type OperationId = String<16>; +pub struct FreqStep { + pub id: u32, + pub freq: u32, + pub time_ms: u32, +} pub struct Operation { - id: OperationId, - steps: Vec, + id: u32, + steps: Vec, } impl Operation { pub const fn new() -> Self { Self { - id: String::new(), + id: 0, steps: Vec::new(), } } #[allow(dead_code)] - pub fn get_id(&self) -> &OperationId { - &self.id + pub fn get_id(&self) -> u32 { + self.id } - pub fn set_id(&mut self, id: OperationId) { + pub fn set_id(&mut self, id: u32) { self.id = id; } - pub fn get_steps(&self) -> &heapless::Vec { + pub fn get_steps(&self) -> &Vec { &self.steps } - pub fn add_step(&mut self, step: AtCommand) -> Result<(), Error> { - self.steps.push(step).map_err(|_| Error::OperationStepsFull) + pub fn add_step(&mut self, step: FreqStep) -> Result<(), FirmwareError> { + self.steps + .push(step) + .map_err(|_| FirmwareError::OperationStepsFull) } } diff --git a/firmware/src/error/mod.rs b/firmware/src/error/mod.rs index 40c79a3..46e700f 100644 --- a/firmware/src/error/mod.rs +++ b/firmware/src/error/mod.rs @@ -1,34 +1,48 @@ // SPDX-FileCopyrightText: 2025 hexaTune LLC // SPDX-License-Identifier: MIT -#[derive(Debug, Clone)] -pub enum Error { - InvalidCommand, // "Not an AT command" - DdsBusy, // "DDS is busy" - InvalidUtf8, // "Invalid UTF-8 in param" - InvalidSysEx, // "Invalid SysEx" - InvalidDataLength, // "Invalid data length" - ParamCount, // "Param count" - ParamValue, // "Param value" - NotAQuery, // "Not a query" - UnknownCommand, // "Unknown command" - OperationStepsFull, // "Operation steps full" +pub use hexa_tune_proto::ProtoError; +pub use hexa_tune_proto_embedded::HexaError; + +#[derive(Debug, Clone, Copy)] +pub enum FirmwareError { + Proto(ProtoError), + Hexa(HexaError), + OperationStepsFull, +} + +impl From for FirmwareError { + fn from(e: ProtoError) -> Self { + Self::Proto(e) + } +} + +impl From for FirmwareError { + fn from(e: HexaError) -> Self { + Self::Hexa(e) + } } -impl Error { - pub fn code(&self) -> &'static str { +impl FirmwareError { + /// Returns a numeric error code (u8) for wire-format encoding. + pub fn error_code(&self) -> u8 { match self { - // Command errors - Error::InvalidCommand => "E001001", - Error::DdsBusy => "E001002", - Error::InvalidUtf8 => "E001003", - Error::InvalidSysEx => "E001004", - Error::InvalidDataLength => "E001005", - Error::ParamCount => "E001006", - Error::ParamValue => "E001007", - Error::NotAQuery => "E001008", - Error::UnknownCommand => "E001009", - Error::OperationStepsFull => "E001010", + FirmwareError::Proto(e) => e.code(), + FirmwareError::Hexa(e) => match e { + HexaError::Proto(pe) => pe.code(), + HexaError::UnknownCommand => 11, + HexaError::DdsBusy => 12, + HexaError::NotAQuery => 13, + HexaError::MissingParam => 14, + HexaError::InvalidParam => 15, + }, + FirmwareError::OperationStepsFull => 20, } } } + +impl defmt::Format for FirmwareError { + fn format(&self, f: defmt::Formatter) { + defmt::write!(f, "FirmwareError({})", self.error_code()); + } +} diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 6f425bb..5dc62cd 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -16,7 +16,6 @@ mod dds; mod error; mod hexa_config; mod rgb; -mod sysex; mod usb; use crate::channel::*; @@ -51,8 +50,6 @@ async fn main(spawner: embassy_executor::Spawner) { //AT module info!("Initializing AT command dispatcher"); - static DISPATCHER: static_cell::StaticCell = static_cell::StaticCell::new(); - let dispatcher = DISPATCHER.init(at::AtDispatcher::new()); //Led module info!("Initializing RGB LED"); @@ -82,7 +79,7 @@ async fn main(spawner: embassy_executor::Spawner) { //Dummy Led let led = embassy_rp::gpio::Output::new(p.PIN_25, embassy_rp::gpio::Level::Low); - spawner.spawn(at::at_task(dispatcher, spawner)).unwrap(); + spawner.spawn(at::at_task(spawner)).unwrap(); spawner.spawn(rgb::rgb_task(rgb_led)).unwrap(); spawner.spawn(usb::dev_task(device)).unwrap(); spawner.spawn(usb::usb_io_task(midi_mutex)).unwrap(); diff --git a/firmware/src/rgb/rgb_task.rs b/firmware/src/rgb/rgb_task.rs index b3bce79..2d2c48f 100644 --- a/firmware/src/rgb/rgb_task.rs +++ b/firmware/src/rgb/rgb_task.rs @@ -7,53 +7,18 @@ use {defmt_rtt as _, panic_probe as _}; use crate::AT_CH; use crate::RGB_CH; use crate::channel::*; -use crate::error::Error; use crate::rgb::RgbLed; #[embassy_executor::task] pub async fn rgb_task(mut rgb_led: RgbLed) { info!("Starting RGB task"); loop { - if let Msg::RgbWithValue(at_command) = RGB_CH.receive().await { - if at_command.params.len() != 3 { - error!( - "SETRGB command {} requires 3 parameters, got {}", - at_command.id.as_str(), - at_command.params.len() - ); - AT_CH.send(Msg::Err(at_command.id, Error::ParamCount)).await; - return; - } - // check params are u8 - let r = match at_command.params[0].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid R value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; - let g = match at_command.params[1].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid G value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; - let b = match at_command.params[2].parse::() { - Ok(v) => v, - Err(_) => { - error!("Invalid B value: {}", at_command.id.as_str()); - AT_CH.send(Msg::Err(at_command.id, Error::ParamValue)).await; - return; - } - }; + if let Msg::RgbSet { id, r, g, b } = RGB_CH.receive().await { info!("Setting RGB to ({}, {}, {})", r, g, b); rgb_led.set_rgb(r, g, b).await; info!("RGB set"); info!("Sending DONE from RGB task"); - AT_CH.send(Msg::Done(at_command.id)).await; + AT_CH.send(Msg::Done(id)).await; } } } diff --git a/firmware/src/sysex/mod.rs b/firmware/src/sysex/mod.rs deleted file mode 100644 index 11eb6a3..0000000 --- a/firmware/src/sysex/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2025 hexaTune LLC -// SPDX-License-Identifier: MIT - -use heapless::Vec; - -pub fn build_sysex(payload: &str) -> Option> { - let mut out: Vec = Vec::new(); - - // SysEx start - out.push(0xF0).ok()?; - - // Payload - for b in payload.as_bytes() { - out.push(*b).ok()?; - } - - // SysEx end - out.push(0xF7).ok()?; - - Some(out) -} - -pub fn sysex_to_usb_midi_packets(sysex: &[u8]) -> Vec<[u8; 4], M> { - let mut out: Vec<[u8; 4], M> = Vec::new(); - - let mut i = 0usize; - while i < sysex.len() { - let rem = sysex.len() - i; - if rem >= 3 { - if rem == 3 && sysex[sysex.len() - 1] == 0xF7 { - out.push([0x07, sysex[i], sysex[i + 1], sysex[i + 2]]).ok(); // end with 3 - i += 3; - } else { - out.push([0x04, sysex[i], sysex[i + 1], sysex[i + 2]]).ok(); // start/continue - i += 3; - } - } else if rem == 2 { - out.push([0x06, sysex[i], sysex[i + 1], 0x00]).ok(); // end with 2 - i += 2; - } else { - out.push([0x05, sysex[i], 0x00, 0x00]).ok(); // end with 1 - i += 1; - } - } - out -} - -pub fn extract_sysex_payload(data: &[u8]) -> Option> { - let mut out: Vec = Vec::new(); - - for chunk in data.chunks_exact(4) { - let cin = chunk[0] & 0x0F; - let b1 = chunk[1]; - let b2 = chunk[2]; - let b3 = chunk[3]; - - match cin { - 0x4 => { - // SysEx continue/start (3 byte data) - for &b in &[b1, b2, b3] { - if b != 0 { - out.push(b).ok(); - } - } - } - 0x5 => { - if b1 != 0 { - out.push(b1).ok(); - } - } // end with 1 - 0x6 => { - for &b in &[b1, b2] { - if b != 0 { - out.push(b).ok(); - } - } - } // end with 2 - 0x7 => { - for &b in &[b1, b2, b3] { - if b != 0 { - out.push(b).ok(); - } - } - } // end with 3 - _ => {} - } - } - - if out.first().copied() == Some(0xF0) && out.last().copied() == Some(0xF7) { - let mut payload: Vec = Vec::new(); - payload.extend_from_slice(&out[1..out.len() - 1]).ok()?; - Some(payload) - } else { - None - } -} diff --git a/firmware/src/usb/usb_task.rs b/firmware/src/usb/usb_task.rs index 32bd300..30304f3 100644 --- a/firmware/src/usb/usb_task.rs +++ b/firmware/src/usb/usb_task.rs @@ -6,12 +6,13 @@ use embassy_futures::select::{Either, select}; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex as Cs; use embassy_sync::mutex::Mutex; +use hexa_tune_proto::sysex; +use hexa_tune_proto::usb_midi; + use crate::AT_CH; use crate::USB_CH; -use crate::at::get_empty_id; use crate::channel::*; -use crate::error::Error; -use crate::sysex::{build_sysex, extract_sysex_payload, sysex_to_usb_midi_packets}; +use crate::error::FirmwareError; use crate::usb::{MyMidiClass, MyUsbDevice}; #[embassy_executor::task] @@ -43,48 +44,108 @@ pub async fn usb_io_task(midi: &'static Mutex>) { let data = &buf[..n]; info!("Received MIDI packet: {:?}", data); - if let Some(payload) = extract_sysex_payload(data) { - if let Ok(input) = core::str::from_utf8(&payload) { - match heapless::String::<64>::try_from(input) { - Ok(line) => { - AT_CH.send(Msg::AtRxLine(line)).await; - } - Err(_) => { - error!("Error Code: {}", Error::InvalidDataLength.code()); - AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidDataLength)) - .await; - } + let num_packets = n / 4; + if num_packets == 0 { + continue; + } + + // Collect 4-byte packets from raw data + let mut packets = [[0u8; 4]; 16]; + for i in 0..num_packets.min(16) { + packets[i] = [ + data[i * 4], + data[i * 4 + 1], + data[i * 4 + 2], + data[i * 4 + 3], + ]; + } + + // Depacketize USB MIDI → SysEx + let mut sysex_buf = [0u8; 128]; + let sysex_len = match usb_midi::depacketize( + &packets[..num_packets.min(16)], + &mut sysex_buf, + ) { + Ok(len) => len, + Err(e) => { + error!("USB MIDI depacketize error"); + AT_CH + .send(Msg::Err(0, FirmwareError::Proto(e))) + .await; + continue; + } + }; + + // Unframe SysEx → payload + let payload = match sysex::unframe(&sysex_buf[..sysex_len]) { + Ok(p) => p, + Err(e) => { + error!("SysEx unframe error"); + AT_CH + .send(Msg::Err(0, FirmwareError::Proto(e))) + .await; + continue; + } + }; + + // Convert to UTF-8 string and send to AT channel + match core::str::from_utf8(payload) { + Ok(input) => match MsgString::try_from(input) { + Ok(line) => { + AT_CH.send(Msg::AtRxLine(line)).await; + } + Err(_) => { + error!("AT payload too long for buffer"); + AT_CH + .send(Msg::Err( + 0, + FirmwareError::Proto( + hexa_tune_proto::ProtoError::BufferTooSmall, + ), + )) + .await; } - } else { - error!("Error Code: {}", Error::InvalidUtf8.code()); + }, + Err(_) => { + error!("Invalid UTF-8 in payload"); AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidUtf8)) + .send(Msg::Err( + 0, + FirmwareError::Proto(hexa_tune_proto::ProtoError::InvalidUtf8), + )) .await; } - } else { - error!("Error Code: {}", Error::InvalidSysEx.code()); - /*AT_CH - .send(Msg::Err(get_empty_id(), Error::InvalidSysEx)) - .await;*/ } } Either::Second(msg) => match msg { Msg::UsbTxLine(line) => { - if let Some(sysex) = build_sysex::<64>(&line) { - let packets = sysex_to_usb_midi_packets::<64>(&sysex); - info!("Sending {} MIDI packets", packets.len()); - - let mut m = midi.lock().await; - for pkt in packets.iter() { - //info!("Sending MIDI packet: {:?}", pkt); - if let Err(e) = m.write_packet(pkt).await { - error!("USB write error: {:?}", e); + let line_bytes = line.as_bytes(); + let mut sysex_buf = [0u8; 128]; + match sysex::frame(line_bytes, &mut sysex_buf) { + Ok(sysex_len) => { + let mut packets = [[0u8; 4]; 32]; + match usb_midi::packetize( + &sysex_buf[..sysex_len], + &mut packets, + ) { + Ok(np) => { + info!("Sending {} MIDI packets", np); + let mut m = midi.lock().await; + for pkt in packets[..np].iter() { + if let Err(e) = m.write_packet(pkt).await { + error!("USB write error: {:?}", e); + } + } + } + Err(_) => { + error!("USB MIDI packetize error"); + } } } - } else { - error!("UsbTxLine too long to fit into SysEx"); + Err(_) => { + error!("UsbTxLine too long to fit into SysEx"); + } } } _ => {