Compare commits

..

5 commits
main ... main

Author SHA1 Message Date
9838c4ee64
feat: implement XInput mode (#27)
Reviewed-on: NaxdyOrg/NaxGCC-FW#27
2024-10-29 19:53:18 +00:00
6475e844a9
chore: improve flake & ci (#28)
Reviewed-on: NaxdyOrg/NaxGCC-FW#28
2024-10-27 20:49:20 +00:00
8f8d47704d
feat: implement "Pro-Controller" mode (#26)
Based on the pro-controller code from https://github.com/HandHeldLegend/HOJA-LIB-RP2040

Author has given their consent for this to be GPL'd:

![image](/attachments/c2551d3a-9b35-4c67-ad28-3778f9e04e29)

Reviewed-on: NaxdyOrg/NaxGCC-FW#26
2024-10-14 19:55:54 +00:00
4e1af50ebd
ci: adapt to new format 2024-09-01 17:21:38 +02:00
5cdd3348f4 implement filter for angled ftilts (#23)
Reviewed-on: NaxdyOrg/NaxGCC-FW#23
Reviewed-by: Naxdy <naxdy@naxdy.org>
Co-authored-by: Marcel Romagnuolo <marcello.r@gmx.net>
Co-committed-by: Marcel Romagnuolo <marcello.r@gmx.net>
2024-06-29 12:58:01 +00:00
19 changed files with 2457 additions and 871 deletions

12
.changelogs/v1.2.0.md Normal file
View file

@ -0,0 +1,12 @@
This release introduces a major new feature to combat a regression introduced in Switch firmware 19.0.0. For those unaware, Switch firmware 19.0.0 breaks compatibility with GCC adapters, including NaxGCC, which advertises itself as a GCC adapter.
Starting with this version, you will be able to connect NaxGCC in "Pro-Controller Mode" by pressing and holding `Start` while plugging it in. This will cause the NaxGCC to advertise itself as a Nintendo Switch Pro Controller, and therefore be unaffected by the bug in the latest Switch firmware. All input consistency modes remain fully functional while in this mode, and your settings (including calibration) carry over as well.
While in Pro-Controller Mode, pressing `Z+Start` will act like the home button on a regular Pro Controller. Additionally, pressing `L` will press both `L` and `ZL` at the same time (since the GCC only has one left shoulder button). This is useful if you want to map things like jump (short-hop macro) or shield (prevent rolling) to it.
> [!NOTE]
> As of this version, rumble will _not_ work while in Pro Controller Mode.
---
To update your firmware, plug in your controller to your computer while keeping the `A+X+Y` buttons held. Then drag & drop the `.uf2` file (found below, under Downloads) onto the storage device that appears.

10
.changelogs/v1.3.0.md Normal file
View file

@ -0,0 +1,10 @@
This release introduces XInput mode for the NaxGCC. This mode is mostly useful for playing games on PC, as it offers the best out-of-the-box compatiblity experience with most titles. Similar to Pro-Controller Mode, this is not a permanent configuration that you set, but a mode that is activated by pressing a button while plugging your controller in.
To enter XInput Mode, press and hold the `X` button while plugging in your controller. While in XInput mode, your NaxGCC will _always_ poll at 1ms intervals, regardless of your chosen input consistency mode setting. Once you go back to GCC or Pro-Controller Mode, your desired input consistency setting will be restored.
> [!NOTE]
> As of this version, rumble will _not_ work while in XInput mode.
---
To update your firmware, plug in your controller to your computer while keeping the `A+X+Y` buttons held. Then drag & drop the `.uf2` file (found below, under Downloads) onto the storage device that appears.

View file

@ -11,13 +11,16 @@ jobs:
runs-on: nix-flakes
steps:
- name: Set up packages
run: |
echo "extra-substituters = https://builder.naxdy.org/attic" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = attic:05LdE8Nav5Qd1E+KOJqSwdr+WE1z8AUmSb3oKL7s8dk=" >> /etc/nix/nix.conf
nix profile install nixpkgs#nodejs "github:zhaofengli/attic?ref=6eabc3f02fae3683bffab483e614bebfcd476b21"
echo "PATH=/nix/var/nix/profiles/per-user/root/profile/bin:$PATH" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- name: Set up attic binary cache
uses: https://git.naxdy.org/NaxdyOrg/attic-action@v0.3
with:
endpoint: "${{ vars.BINARY_CACHE_URL }}"
token: ""
cache: "${{ vars.PUBLIC_BINARY_CACHE_NAME }}"
skip-push: true
- name: Run Clippy
run: |
nix develop . --command cargo clippy -- -Dwarnings
nix flake check . --print-build-logs -j auto

View file

@ -13,24 +13,22 @@ jobs:
runs-on: nix-flakes
steps:
- name: Set up packages
run: |
echo "extra-substituters = https://builder.naxdy.org/attic" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = attic:05LdE8Nav5Qd1E+KOJqSwdr+WE1z8AUmSb3oKL7s8dk=" >> /etc/nix/nix.conf
nix profile install nixpkgs#nodejs "github:zhaofengli/attic?ref=6eabc3f02fae3683bffab483e614bebfcd476b21"
echo "PATH=/nix/var/nix/profiles/per-user/root/profile/bin:$PATH" >> "$GITHUB_ENV"
- name: Set up attic binary cache
run: |
attic login "${{ vars.PUBLIC_BINARY_CACHE_NAME }}" "${{ vars.BINARY_CACHE_URL }}" "${{ secrets.PUBLIC_BINARY_CACHE_AUTH_KEY }}"
attic use "${{ vars.PUBLIC_BINARY_CACHE_NAME }}"
uses: https://git.naxdy.org/NaxdyOrg/attic-action@v0.3
with:
endpoint: "${{ vars.BINARY_CACHE_URL }}"
token: "${{ secrets.PUBLIC_BINARY_CACHE_AUTH_KEY }}"
cache: "${{ vars.PUBLIC_BINARY_CACHE_NAME }}"
- uses: actions/checkout@v4
- name: Run flake checks
run: |
nix flake check . --print-build-logs -j auto
- name: Build firmware image
run: |
nix build .# -o dist --print-build-logs
- name: Push derivations to binary cache
run: |
cd /nix/store
attic push "${{ vars.PUBLIC_BINARY_CACHE_NAME }}" $(ls /nix/store --ignore='*.drv' --ignore='*fake_nixpkgs*')
- name: (Re-)generate tag
run: |
git config --global user.email "noreply@naxdy.org"
@ -40,6 +38,7 @@ jobs:
git tag nightly -m "Nightly Release"
git checkout nightly
git push --set-upstream origin nightly --force
- name: Publish nightly release
uses: https://gitea.com/actions/gitea-release-action@v1.3.0
with:

View file

@ -10,24 +10,16 @@ jobs:
runs-on: nix-flakes
steps:
- name: Set up packages
run: |
echo "extra-substituters = https://builder.naxdy.org/attic" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = attic:05LdE8Nav5Qd1E+KOJqSwdr+WE1z8AUmSb3oKL7s8dk=" >> /etc/nix/nix.conf
nix profile install nixpkgs#nodejs "github:zhaofengli/attic?ref=6eabc3f02fae3683bffab483e614bebfcd476b21"
echo "PATH=/nix/var/nix/profiles/per-user/root/profile/bin:$PATH" >> "$GITHUB_ENV"
- name: Set up attic binary cache
run: |
attic login "${{ vars.PUBLIC_BINARY_CACHE_NAME }}" "${{ vars.BINARY_CACHE_URL }}" "${{ secrets.PUBLIC_BINARY_CACHE_AUTH_KEY }}"
attic use "${{ vars.PUBLIC_BINARY_CACHE_NAME }}"
uses: https://git.naxdy.org/NaxdyOrg/attic-action@v0.3
with:
endpoint: "${{ vars.BINARY_CACHE_URL }}"
token: "${{ secrets.PUBLIC_BINARY_CACHE_AUTH_KEY }}"
cache: "${{ vars.PUBLIC_BINARY_CACHE_NAME }}"
- uses: actions/checkout@v4
- name: Build firmware image
run: |
nix build .# -o dist --print-build-logs
- name: Push derivations to binary cache
run: |
cd /nix/store
attic push "${{ vars.PUBLIC_BINARY_CACHE_NAME }}" $(ls /nix/store --ignore='*.drv' --ignore='*fake_nixpkgs*')
- name: Publish stable release
uses: https://gitea.com/actions/gitea-release-action@v1.3.0
with:

163
Cargo.lock generated
View file

@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
@ -77,6 +89,12 @@ 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"
@ -109,9 +127,9 @@ checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
[[package]]
name = "byteorder"
version = "1.4.3"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
@ -136,7 +154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [
"bare-metal",
"bitfield",
"bitfield 0.13.2",
"embedded-hal 0.2.7",
"volatile-register",
]
@ -309,7 +327,8 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "embassy-embedded-hal"
version = "0.1.0"
version = "0.2.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"defmt",
"embassy-futures",
@ -325,7 +344,8 @@ dependencies = [
[[package]]
name = "embassy-executor"
version = "0.5.0"
version = "0.6.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"cortex-m",
"critical-section",
@ -338,7 +358,8 @@ dependencies = [
[[package]]
name = "embassy-executor-macros"
version = "0.4.0"
version = "0.5.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"darling",
"proc-macro2",
@ -349,13 +370,15 @@ dependencies = [
[[package]]
name = "embassy-futures"
version = "0.1.1"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"defmt",
]
[[package]]
name = "embassy-hal-internal"
version = "0.1.0"
version = "0.2.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"cortex-m",
"critical-section",
@ -366,10 +389,12 @@ dependencies = [
[[package]]
name = "embassy-net-driver"
version = "0.2.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
[[package]]
name = "embassy-net-driver-channel"
version = "0.2.0"
version = "0.3.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"embassy-futures",
"embassy-net-driver",
@ -378,7 +403,8 @@ dependencies = [
[[package]]
name = "embassy-rp"
version = "0.1.0"
version = "0.2.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"atomic-polyfill",
"cfg-if",
@ -403,7 +429,6 @@ dependencies = [
"embedded-storage",
"embedded-storage-async",
"fixed",
"futures",
"nb 1.1.0",
"pio",
"pio-proc",
@ -414,7 +439,8 @@ dependencies = [
[[package]]
name = "embassy-sync"
version = "0.5.0"
version = "0.6.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"cfg-if",
"critical-section",
@ -426,7 +452,8 @@ dependencies = [
[[package]]
name = "embassy-time"
version = "0.3.0"
version = "0.3.2"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"cfg-if",
"critical-section",
@ -444,6 +471,7 @@ dependencies = [
[[package]]
name = "embassy-time-driver"
version = "0.1.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"document-features",
]
@ -451,10 +479,12 @@ dependencies = [
[[package]]
name = "embassy-time-queue-driver"
version = "0.1.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
[[package]]
name = "embassy-usb"
version = "0.1.0"
version = "0.3.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"defmt",
"embassy-futures",
@ -469,6 +499,7 @@ dependencies = [
[[package]]
name = "embassy-usb-driver"
version = "0.1.0"
source = "git+https://git.naxdy.org/NaxdyOrg/embassy.git?branch=naxgcc-fw#16151cce0271573a66206aa6e85932c20bbf0c70"
dependencies = [
"defmt",
]
@ -630,59 +661,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
@ -696,8 +680,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
@ -733,6 +715,15 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.3"
@ -768,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.3",
]
[[package]]
@ -875,7 +866,7 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "naxgcc-fw"
version = "1.1.1"
version = "1.3.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
@ -1453,15 +1444,19 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "usb-device"
version = "0.2.9"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f6cc3adc849b5292b4075fc0d5fdcf2f24866e88e336dd27a8943090a520508"
checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6"
dependencies = [
"heapless",
"portable-atomic",
]
[[package]]
name = "usbd-hid"
version = "0.6.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975bd411f4a939986751ea09992a24fa47c4d25c6ed108d04b4c2999a4fd0132"
checksum = "e6f291ab53d428685cc780f08a2eb9d5d6ff58622db2b36e239a4f715f1e184c"
dependencies = [
"serde",
"ssmarshal",
@ -1471,20 +1466,22 @@ dependencies = [
[[package]]
name = "usbd-hid-descriptors"
version = "0.1.2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbee8c6735e90894fba04770bc41e11fd3c5256018856e15dc4dd1e6c8a3dd1"
checksum = "0eee54712c5d778d2fb2da43b1ce5a7b5060886ef7b09891baeb4bf36910a3ed"
dependencies = [
"bitfield",
"bitfield 0.14.0",
]
[[package]]
name = "usbd-hid-macros"
version = "0.6.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261079a9ada015fa1acac7cc73c98559f3a92585e15f508034beccf6a2ab75a2"
checksum = "bb573c76e7884035ac5e1ab4a81234c187a82b6100140af0ab45757650ccda38"
dependencies = [
"byteorder",
"hashbrown 0.13.2",
"log",
"proc-macro2",
"quote",
"serde",
@ -1687,3 +1684,23 @@ checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]

View file

@ -1,6 +1,6 @@
[package]
name = "naxgcc-fw"
version = "1.1.1"
version = "1.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -9,33 +9,33 @@ edition = "2021"
embassy-time = { version = "0.3.0", features = [
"defmt",
"defmt-timestamp-uptime",
], path = "lib/embassy-rs/embassy-time" }
embassy-embedded-hal = { version = "0.1.0", features = [
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-embedded-hal = { version = "0.2.0", features = [
"defmt",
], path = "lib/embassy-rs/embassy-embedded-hal" }
embassy-sync = { version = "0.5.0", features = [
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-sync = { version = "0.6.0", features = [
"defmt",
], path = "lib/embassy-rs/embassy-sync" }
embassy-executor = { version = "0.5.0", features = [
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-executor = { version = "0.6.0", features = [
"task-arena-size-32768",
"arch-cortex-m",
"executor-thread",
"executor-interrupt",
"defmt",
"integrated-timers",
], path = "lib/embassy-rs/embassy-executor" }
embassy-rp = { version = "0.1.0", features = [
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-rp = { version = "0.2.0", features = [
"defmt",
"unstable-pac",
"time-driver",
"critical-section-impl",
], path = "lib/embassy-rs/embassy-rp" }
embassy-usb = { version = "0.1.0", features = [
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-usb = { version = "0.3.0", features = [
"defmt",
], path = "lib/embassy-rs/embassy-usb" }
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
embassy-futures = { version = "0.1.0", features = [
"defmt",
], path = "lib/embassy-rs/embassy-futures" }
], git = "https://git.naxdy.org/NaxdyOrg/embassy.git", branch = "naxgcc-fw" }
defmt = "0.3"
defmt-rtt = "0.4"
fixed = "1.23.1"
@ -48,7 +48,7 @@ libm = { version = "0.2.8" }
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
cortex-m-rt = "0.7.0"
panic-probe = { version = "0.3", features = ["print-defmt"] }
packed_struct = { version = "0.10.1", default_features = false }
packed_struct = { version = "0.10.1", default-features = false }
format_no_std = "1.0.2"
rand = { version = "0.8.5", default-features = false }
@ -79,14 +79,14 @@ overflow-checks = false
codegen-units = 8
debug = false
debug-assertions = false
opt-level = 0
opt-level = 3
overflow-checks = false
[profile.release.build-override]
codegen-units = 8
debug = false
debug-assertions = false
opt-level = 0
opt-level = 3
overflow-checks = false
# cargo test
@ -106,12 +106,3 @@ debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 3
# [patch.crates-io]
# embassy-rp = { path = "lib/embassy-rs/embassy-rp" }
# embassy-time = { path = "lib/embassy-rs/embassy-time" }
# embassy-embedded-hal = { path = "lib/embassy-rs/embassy-embedded-hal" }
# embassy-usb = { path = "lib/embassy-rs/embassy-usb" }
# embassy-sync = { path = "lib/embassy-rs/embassy-sync" }
# embassy-executor = { path = "lib/embassy-rs/embassy-executor" }
# embassy-futures = { path = "lib/embassy-rs/embassy-futures" }

View file

@ -1,22 +1,5 @@
{
"nodes": {
"embassy-rs-patched": {
"flake": false,
"locked": {
"lastModified": 1710754402,
"narHash": "sha256-HyTw5VQXlSqz9UOq0Dc6G/NWNzQOPXf3PNWffMkfLC4=",
"ref": "naxgcc-fw",
"rev": "2ee4657727b9679998d941de00b43e1754f570bf",
"revCount": 6754,
"type": "git",
"url": "https://gitea@git.naxdy.org/NaxdyOrg/embassy"
},
"original": {
"ref": "naxgcc-fw",
"type": "git",
"url": "https://gitea@git.naxdy.org/NaxdyOrg/embassy"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@ -35,24 +18,6 @@
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
@ -75,27 +40,27 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1710283656,
"narHash": "sha256-nI+AOy4uK6jLGBi9nsbHjL1EdSIzoo8oa+9oeVhbyFc=",
"lastModified": 1728740863,
"narHash": "sha256-u+rxA79a0lyhG+u+oPBRtTDtzz8kvkc9a6SWSt9ekVc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "51063ed4f2343a59fdeebb279bb81d87d453942b",
"rev": "a3f9ad65a0bf298ed5847629a57808b97e6e8077",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-23.11",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1706487304,
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
@ -107,7 +72,6 @@
},
"root": {
"inputs": {
"embassy-rs-patched": "embassy-rs-patched",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs",
@ -116,15 +80,14 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1710382258,
"narHash": "sha256-2FW1q+o34VBweYQiEkRaSEkNMq3ecrn83VzETeGiVbY=",
"lastModified": 1728700003,
"narHash": "sha256-Ox1pvEHxLK6lAdaKQW21Zvk65SPDag+cD8YA444R/og=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8ce81e71ab04a7e906fae62da086d6ee5d6cfc21",
"rev": "fc1e58ebabe0cef4442eedea07556ff0c9eafcfe",
"type": "github"
},
"original": {
@ -147,21 +110,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -2,17 +2,12 @@
description = "Firmware for the NaxGCC";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-23.11";
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-24.05";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
embassy-rs-patched = {
url = "git+https://gitea@git.naxdy.org/NaxdyOrg/embassy?ref=naxgcc-fw";
flake = false;
};
naersk = {
url = "github:nmattia/naersk";
inputs.nixpkgs.follows = "nixpkgs";
@ -25,7 +20,6 @@
, rust-overlay
, flake-utils
, naersk
, embassy-rs-patched
}: (flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
@ -51,35 +45,39 @@
};
CARGO_BUILD_TARGET = "thumbv6m-none-eabi";
prepCmd = ''
mkdir -p lib
rm lib/embassy-rs || true
ln -s "${embassy-rs-patched}" lib/embassy-rs
'';
in
{
packages.default = self.packages.${system}.naxgcc-fw-uf2;
packages = {
default = self.packages.${system}.naxgcc-fw-uf2;
packages.naxgcc-fw-uf2 = pkgs.runCommandLocal "${self.packages.${system}.naxgcc-fw.pname}-uf2-${self.packages.${system}.naxgcc-fw.version}" { } ''
mkdir -p $out/bin
${pkgs.elf2uf2-rs}/bin/elf2uf2-rs ${self.packages.${system}.naxgcc-fw}/bin/${self.packages.${system}.naxgcc-fw.pname} $out/bin/${self.packages.${system}.naxgcc-fw.pname}.uf2
'';
naxgcc-fw-uf2 = pkgs.runCommandLocal "${self.packages.${system}.naxgcc-fw.pname}-uf2-${self.packages.${system}.naxgcc-fw.version}" { } ''
mkdir -p $out/bin
${pkgs.elf2uf2-rs}/bin/elf2uf2-rs ${self.packages.${system}.naxgcc-fw}/bin/${self.packages.${system}.naxgcc-fw.pname} $out/bin/${self.packages.${system}.naxgcc-fw.pname}.uf2
'';
packages.naxgcc-fw = naersk_lib.buildPackage {
pname = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.name;
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
naxgcc-fw = pkgs.callPackage
({ mode ? "build" }: naersk_lib.buildPackage {
pname = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.name;
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
prePatch = prepCmd;
inherit mode;
src = self;
src = self;
cargoBuildOptions = _orig: _orig ++ [
"--target=${CARGO_BUILD_TARGET}"
];
cargoBuildOptions = _orig: _orig ++ [
"--target=${CARGO_BUILD_TARGET}"
];
# if a tree falls in the forest and no one is around to hear it, does it make a sound?
DEFMT_LOG = "off";
# if a tree falls in the forest and no one is around to hear it, does it make a sound?
DEFMT_LOG = "off";
})
{ };
};
checks = {
clippy = self.packages.${system}.naxgcc-fw.override {
mode = "clippy";
};
};
devShells.default = pkgs.mkShell {
@ -92,8 +90,6 @@
DEFMT_LOG = "debug";
inherit CARGO_BUILD_TARGET;
shellHook = prepCmd;
};
}));
}

View file

@ -16,8 +16,8 @@ use packed_struct::{
};
use crate::{
gcc_hid::{Buttons1, Buttons2, MUTEX_INPUT_CONSISTENCY_MODE, SIGNAL_CHANGE_RUMBLE_STRENGTH},
helpers::{PackedFloat, ToPackedFloatArray, ToRegularArray, XyValuePair},
hid::gcc::{GcButtons1, GcButtons2, GcState},
input::{
read_ext_adc, Stick, StickAxis, FLOAT_ORIGIN, SPI_ACS_SHARED, SPI_CCS_SHARED, SPI_SHARED,
},
@ -26,6 +26,7 @@ use crate::{
LinearizedCalibration, NotchCalibration, NotchStatus, CALIBRATION_ORDER,
NOTCH_ADJUSTMENT_ORDER, NO_OF_ADJ_NOTCHES, NO_OF_CALIBRATION_POINTS, NO_OF_NOTCHES,
},
usb_comms::{MUTEX_INPUT_CONSISTENCY_MODE, SIGNAL_CHANGE_RUMBLE_STRENGTH},
ADDR_OFFSET, FLASH_SIZE,
};
@ -36,7 +37,7 @@ use embassy_sync::{
};
use embassy_time::Timer;
use crate::{gcc_hid::GcReport, input::CHANNEL_GCC_STATE};
use crate::input::CHANNEL_GCC_STATE;
/// Whether we are currently calibrating the sticks. Updates are dispatched when the status changes.
/// Initial status is assumed to be false.
@ -78,7 +79,7 @@ const MAX_ANALOG_SCALER: u8 = 125;
/// a certain mode.
#[derive(Default, Debug, Clone, Format)]
pub struct OverrideGcReportInstruction {
pub report: GcReport,
pub report: GcState,
pub duration_ms: u64,
}
@ -563,6 +564,17 @@ pub enum InputConsistencyMode {
PC = 3,
}
/// Not saved, but set upon plugging in the controller.
#[derive(Debug, Clone, Copy, Format, PrimitiveEnum_u8, PartialEq, Eq)]
pub enum ControllerMode {
/// Advertise itself as a GCC adapter with 1 controller (itself) connected.
GcAdapter = 0,
/// Pretend to be a Nintendo Switch Pro Controller connected via USB.
Procon = 1,
/// Act as an XInput device, and also advertise itself with 1000Hz polling capability.
XInput = 2,
}
#[derive(Debug, Clone, Format, PackedStruct)]
#[packed_struct(endian = "msb")]
pub struct ControllerConfig {
@ -637,6 +649,8 @@ impl ControllerConfig {
/// Trait for providing button presses, used in the calibration process.
trait ButtonPressProvider {
/// Wait for a single button press.
// TODO: remove allow once this is used somewhere
#[allow(dead_code)]
async fn wait_for_button_press(&mut self, button_to_wait_for: &AwaitableButtons);
/// Wait for a single button release.
@ -670,7 +684,7 @@ trait ButtonPressProvider {
}
impl<'a, T: RawMutex, const I: usize, const J: usize, const K: usize> ButtonPressProvider
for Subscriber<'a, T, GcReport, I, J, K>
for Subscriber<'a, T, GcState, I, J, K>
{
async fn wait_for_button_press(&mut self, button_to_wait_for: &AwaitableButtons) {
loop {
@ -770,7 +784,7 @@ impl<'a, T: RawMutex, const I: usize, const J: usize, const K: usize> ButtonPres
}
pub fn is_awaitable_button_pressed(
report: &GcReport,
report: &GcState,
button_to_wait_for: &AwaitableButtons,
) -> bool {
match button_to_wait_for {
@ -1136,7 +1150,7 @@ async fn configuration_main_loop<
>(
current_config: &ControllerConfig,
flash: &mut Flash<'static, FLASH, Async, FLASH_SIZE>,
gcc_subscriber: &mut Subscriber<'a, M, GcReport, C, S, P>,
gcc_subscriber: &mut Subscriber<'a, M, GcState, C, S, P>,
) -> ControllerConfig {
let mut final_config = current_config.clone();
let config_options = [
@ -1194,15 +1208,15 @@ async fn configuration_main_loop<
// exit
0 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1222,15 +1236,15 @@ async fn configuration_main_loop<
// calibrate lstick
1 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1251,15 +1265,15 @@ async fn configuration_main_loop<
// calibrate rstick
2 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1310,15 +1324,15 @@ async fn configuration_main_loop<
.clamp(-ABS_MAX_SNAPBACK, ABS_MAX_SNAPBACK);
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1396,15 +1410,15 @@ async fn configuration_main_loop<
.clamp(0, MAX_WAVESHAPING) as u8;
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1482,15 +1496,15 @@ async fn configuration_main_loop<
.clamp(0, MAX_SMOOTHING) as u8;
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1557,15 +1571,15 @@ async fn configuration_main_loop<
.clamp(-1, MAX_CARDINAL_SNAP);
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1613,15 +1627,15 @@ async fn configuration_main_loop<
as u8;
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1661,16 +1675,16 @@ async fn configuration_main_loop<
SIGNAL_CHANGE_RUMBLE_STRENGTH.signal(*to_adjust);
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
button_z: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1697,15 +1711,15 @@ async fn configuration_main_loop<
};
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1731,15 +1745,15 @@ async fn configuration_main_loop<
// display waveshaping values on both sticks
38 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1757,15 +1771,15 @@ async fn configuration_main_loop<
// display stick smoothing values on both sticks
39 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1783,15 +1797,15 @@ async fn configuration_main_loop<
// display snapback values on both sticks
40 => {
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_r: true,
button_l: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,
@ -1854,15 +1868,15 @@ pub async fn config_task(mut flash: Flash<'static, FLASH, Async, FLASH_SIZE>) {
info!("Entering config mode.");
override_gcc_state_and_wait(&OverrideGcReportInstruction {
report: GcReport {
report: GcState {
trigger_r: 255,
trigger_l: 255,
buttons_2: Buttons2 {
buttons_2: GcButtons2 {
button_l: true,
button_r: true,
..Default::default()
},
buttons_1: Buttons1 {
buttons_1: GcButtons1 {
button_x: true,
button_y: true,
button_a: true,

View file

@ -1,509 +0,0 @@
/**
* Communication with the console / PC over USB HID.
* Includes the HID report descriptor, and the GcReport struct.
*/
use core::default::Default;
use defmt::{debug, info, trace, warn, Format};
use embassy_futures::join::join;
use embassy_rp::{
peripherals::{PIN_25, PIN_29, PWM_CH4, PWM_CH6, USB},
pwm::Pwm,
usb::Driver,
};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal};
use embassy_time::{Duration, Instant, Timer};
use embassy_usb::{
class::hid::{HidReaderWriter, ReportId, RequestHandler, State},
control::OutResponse,
msos::{self, windows_version},
Builder, Handler,
};
use libm::powf;
use packed_struct::{derive::PackedStruct, PackedStruct};
use crate::{config::InputConsistencyMode, input::CHANNEL_GCC_STATE};
static SIGNAL_RUMBLE: Signal<CriticalSectionRawMutex, bool> = Signal::new();
/// We could turn the config change signal into a PubSubChannel instead, but that
/// would just transmit unnecessary amounts of data.
pub static SIGNAL_CHANGE_RUMBLE_STRENGTH: Signal<CriticalSectionRawMutex, u8> = Signal::new();
/// Only dispatched ONCE after powerup, to determine how to advertise itself via USB.
pub static MUTEX_INPUT_CONSISTENCY_MODE: Mutex<
CriticalSectionRawMutex,
Option<InputConsistencyMode>,
> = Mutex::new(None);
/// Vendor-defined property data
const DEVICE_INTERFACE_GUID: &str = "{ecceff35-146c-4ff3-acd9-8f992d09acdd}";
#[rustfmt::skip]
pub const GCC_REPORT_DESCRIPTOR: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0xA1, 0x03, // Collection (Report)
0x85, 0x11, // Report ID (17)
0x19, 0x00, // Usage Minimum (Undefined)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x05, // Report Count (5)
0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xA1, 0x03, // Collection (Report)
0x85, 0x21, // Report ID (33)
0x05, 0x00, // Usage Page (Undefined)
0x15, 0x00, // Logical Minimum (0)
0x25, 0xFF, // Logical Maximum (-1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x08, // Usage Maximum (0x08)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x09, 0x33, // Usage (Rx)
0x09, 0x34, // Usage (Ry)
0x09, 0x35, // Usage (Rz)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x06, // Report Count (6)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0xA1, 0x03, // Collection (Report)
0x85, 0x13, // Report ID (19)
0x19, 0x00, // Usage Minimum (Undefined)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xC0, // End Collection
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct Buttons1 {
#[packed_field(bits = "0")]
pub button_a: bool,
#[packed_field(bits = "1")]
pub button_b: bool,
#[packed_field(bits = "2")]
pub button_x: bool,
#[packed_field(bits = "3")]
pub button_y: bool,
#[packed_field(bits = "4")]
pub dpad_left: bool,
#[packed_field(bits = "5")]
pub dpad_right: bool,
#[packed_field(bits = "6")]
pub dpad_down: bool,
#[packed_field(bits = "7")]
pub dpad_up: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct Buttons2 {
#[packed_field(bits = "0")]
pub button_start: bool,
#[packed_field(bits = "1")]
pub button_z: bool,
#[packed_field(bits = "2")]
pub button_r: bool,
#[packed_field(bits = "3")]
pub button_l: bool,
#[packed_field(bits = "4..=7")]
pub blank1: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PackedStruct, Format)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "8")]
pub struct GcReport {
#[packed_field(bits = "0..=7")]
pub buttons_1: Buttons1,
#[packed_field(bits = "8..=15")]
pub buttons_2: Buttons2,
#[packed_field(bits = "16..=23")]
pub stick_x: u8,
#[packed_field(bits = "24..=31")]
pub stick_y: u8,
#[packed_field(bits = "32..=39")]
pub cstick_x: u8,
#[packed_field(bits = "40..=47")]
pub cstick_y: u8,
#[packed_field(bits = "48..=55")]
pub trigger_l: u8,
#[packed_field(bits = "56..=63")]
pub trigger_r: u8,
}
impl Default for GcReport {
fn default() -> Self {
Self {
buttons_1: Buttons1::default(),
buttons_2: Buttons2::default(),
stick_x: 127,
stick_y: 127,
cstick_x: 127,
cstick_y: 127,
trigger_l: 0,
trigger_r: 0,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(C, align(8))]
pub struct RawConsoleReport {
pub packet: [u8; 64],
}
impl Default for RawConsoleReport {
fn default() -> Self {
Self { packet: [0u8; 64] }
}
}
struct GccRequestHandler {}
impl RequestHandler for GccRequestHandler {
fn get_report(&self, id: ReportId, _buf: &mut [u8]) -> Option<usize> {
info!("Get report for {:?}", id);
None
}
fn set_report(&self, id: ReportId, data: &[u8]) -> OutResponse {
info!("Set report for {:?}: {=[u8]}", id, data);
OutResponse::Accepted
}
fn set_idle_ms(&self, id: Option<ReportId>, dur: u32) {
info!("Set idle rate for {:?} to {:?}", id, dur);
}
fn get_idle_ms(&self, id: Option<ReportId>) -> Option<u32> {
info!("Get idle rate for {:?}", id);
None
}
}
fn get_gcinput_hid_report(input_state: &GcReport) -> [u8; 37] {
static mut GC_FIRST: bool = false;
let mut buffer = [0u8; 37];
buffer[0] = 0x21;
buffer[1] |= 0x14;
let data = input_state.pack().expect("Failed to pack GC input data");
if unsafe { !GC_FIRST } {
buffer[1] |= 0x04;
buffer[10] |= 0x04;
buffer[19] |= 0x04;
buffer[28] |= 0x04;
unsafe { GC_FIRST = true };
} else {
// controller in "port 1"
buffer[2..=9].copy_from_slice(&data[0..=7]);
}
buffer
}
struct MyDeviceHandler {
configured: bool,
}
impl MyDeviceHandler {
fn new() -> Self {
MyDeviceHandler { configured: false }
}
}
impl Handler for MyDeviceHandler {
fn enabled(&mut self, enabled: bool) {
self.configured = true;
if enabled {
info!("Device enabled");
} else {
info!("Device disabled");
}
}
fn reset(&mut self) {
self.configured = false;
info!("Bus reset, the Vbus current limit is 100mA");
}
fn addressed(&mut self, addr: u8) {
self.configured = false;
info!("USB address set to: {}", addr);
}
fn configured(&mut self, configured: bool) {
self.configured = configured;
if configured {
info!(
"Device configured, it may now draw up to the configured current limit from Vbus."
)
} else {
info!("Device is no longer configured, the Vbus current limit is 100mA.");
}
}
}
#[embassy_executor::task]
pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: Driver<'static, USB>) {
let input_consistency_mode = {
while MUTEX_INPUT_CONSISTENCY_MODE.lock().await.is_none() {
Timer::after(Duration::from_millis(100)).await;
}
MUTEX_INPUT_CONSISTENCY_MODE.lock().await.unwrap()
};
let mut serial_buffer = [0u8; 64];
let serial = format_no_std::show(
&mut serial_buffer,
format_args!(
"{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
raw_serial[0],
raw_serial[1],
raw_serial[2],
raw_serial[3],
raw_serial[4],
raw_serial[5],
raw_serial[6],
raw_serial[7]
),
)
.unwrap();
info!("Detected flash with unique serial number {}", serial);
trace!("Start of config");
let mut usb_config = embassy_usb::Config::new(0x057e, 0x0337);
usb_config.manufacturer = Some("Naxdy");
usb_config.product = Some(match input_consistency_mode {
InputConsistencyMode::Original => "NaxGCC (OG Mode)",
InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)",
InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)",
InputConsistencyMode::PC => "NaxGCC (PC Mode)",
});
usb_config.serial_number = Some(serial);
usb_config.max_power = 200;
usb_config.max_packet_size_0 = 64;
usb_config.device_class = 0;
usb_config.device_protocol = 0;
usb_config.self_powered = false;
usb_config.device_sub_class = 0;
usb_config.supports_remote_wakeup = true;
let mut device_descriptor = [0; 256];
let mut config_descriptor = [0; 256];
let mut bos_descriptor = [0; 256];
let mut msos_descriptor = [0; 256];
let mut control_buf = [0; 64];
let request_handler = GccRequestHandler {};
let mut device_handler = MyDeviceHandler::new();
let mut state = State::new();
let mut builder = Builder::new(
driver,
usb_config,
&mut device_descriptor,
&mut config_descriptor,
&mut bos_descriptor,
&mut msos_descriptor,
&mut control_buf,
);
builder.msos_descriptor(windows_version::WIN8_1, 2);
let msos_writer = builder.msos_writer();
msos_writer.device_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
msos_writer.device_feature(msos::RegistryPropertyFeatureDescriptor::new(
"DeviceInterfaceGUID",
msos::PropertyData::Sz(DEVICE_INTERFACE_GUID),
));
builder.handler(&mut device_handler);
let hid_config = embassy_usb::class::hid::Config {
report_descriptor: GCC_REPORT_DESCRIPTOR,
request_handler: Some(&request_handler),
poll_ms: match input_consistency_mode {
InputConsistencyMode::Original => 8,
InputConsistencyMode::ConsistencyHack
| InputConsistencyMode::SuperHack
| InputConsistencyMode::PC => 1,
},
max_packet_size_in: 37,
max_packet_size_out: 5,
};
let hid = HidReaderWriter::<_, 5, 37>::new(&mut builder, &mut state, hid_config);
let mut usb = builder.build();
let usb_fut = async {
loop {
usb.run_until_suspend().await;
debug!("Suspended");
usb.wait_resume().await;
debug!("RESUMED!");
}
};
let (mut reader, mut writer) = hid.split();
let in_fut = async {
let mut gcc_subscriber = CHANNEL_GCC_STATE.subscriber().unwrap();
let mut last_report_time = Instant::now();
let mut rate_limit_end_time = Instant::now();
loop {
// This is what we like to call a "hack".
// It forces reports to be sent at least every 8.33ms instead of every 8ms.
// 8.33ms is a multiple of the game's frame interval (16.66ms), so if we
// send a report every 8.33ms, it should (in theory) ensure (close to)
// 100% input accuracy.
//
// From the console's perspective, we are basically a laggy adapter, taking
// a minimum of 333 extra us to send a report every time it's polled, but it
// works to our advantage.
match input_consistency_mode {
InputConsistencyMode::SuperHack | InputConsistencyMode::ConsistencyHack => {
// "Ticker at home", so we can use this for both consistency and SuperHack mode
Timer::at(rate_limit_end_time).await;
}
InputConsistencyMode::Original | InputConsistencyMode::PC => {}
}
match writer
.write(&{
let state = gcc_subscriber.next_message_pure().await;
let report = get_gcinput_hid_report(&state);
trace!("Report Written: {:08b}", report);
report
})
.await
{
Ok(()) => {
let currtime = Instant::now();
let polltime = currtime.duration_since(last_report_time);
let micros = polltime.as_micros();
debug!("Report written in {}us", micros);
if input_consistency_mode != InputConsistencyMode::Original
&& input_consistency_mode != InputConsistencyMode::PC
{
while rate_limit_end_time < currtime {
rate_limit_end_time += Duration::from_micros(8333);
}
}
last_report_time = currtime;
}
Err(e) => warn!("Failed to send report: {:?}", e),
}
}
};
let out_fut = async {
loop {
trace!("Readery loop");
let mut buf = [0u8; 5];
match reader.read(&mut buf).await {
Ok(_e) => {
debug!("READ SOMETHIN: {:08b}", buf);
SIGNAL_RUMBLE.signal((buf[1] & 0x01) != 0);
}
Err(e) => {
warn!("Failed to read: {:?}", e);
}
}
}
};
let usb_fut_wrapped = async {
usb_fut.await;
debug!("USB FUT DED");
};
join(usb_fut_wrapped, join(in_fut, out_fut)).await;
}
fn calc_rumble_power(strength: u8) -> u16 {
if strength > 0 {
powf(2.0, 7.0 + ((strength as f32 - 3.0) / 8.0)) as u16
} else {
0
}
}
#[embassy_executor::task]
pub async fn rumble_task(
pin_rumble: PIN_25,
pin_brake: PIN_29,
pwm_ch_rumble: PWM_CH4,
pwm_ch_brake: PWM_CH6,
) {
let mut rumble_config: embassy_rp::pwm::Config = Default::default();
rumble_config.top = 255;
rumble_config.enable = true;
rumble_config.compare_b = 0;
let mut brake_config = rumble_config.clone();
brake_config.compare_b = 255;
let mut pwm_rumble = Pwm::new_output_b(pwm_ch_rumble, pin_rumble, rumble_config.clone());
let mut pwm_brake = Pwm::new_output_b(pwm_ch_brake, pin_brake, brake_config.clone());
let mut rumble_power = {
let strength = SIGNAL_CHANGE_RUMBLE_STRENGTH.wait().await;
calc_rumble_power(strength)
};
loop {
let new_rumble_status = SIGNAL_RUMBLE.wait().await;
debug!("Received rumble signal: {}", new_rumble_status);
if let Some(new_strength) = SIGNAL_CHANGE_RUMBLE_STRENGTH.try_take() {
rumble_power = calc_rumble_power(new_strength);
}
if new_rumble_status {
rumble_config.compare_b = rumble_power;
brake_config.compare_b = 0;
pwm_rumble.set_config(&rumble_config);
pwm_brake.set_config(&brake_config);
} else {
rumble_config.compare_b = 0;
brake_config.compare_b = 255;
pwm_rumble.set_config(&rumble_config);
pwm_brake.set_config(&brake_config);
}
}
}

198
src/hid/gcc.rs Normal file
View file

@ -0,0 +1,198 @@
use defmt::{info, trace, Format};
use embassy_usb::{
class::hid::{ReportId, RequestHandler},
control::OutResponse,
};
use crate::usb_comms::{HidReportBuilder, SIGNAL_RUMBLE};
use packed_struct::{derive::PackedStruct, PackedStruct};
#[rustfmt::skip]
pub const GCC_REPORT_DESCRIPTOR: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0xA1, 0x03, // Collection (Report)
0x85, 0x11, // Report ID (17)
0x19, 0x00, // Usage Minimum (Undefined)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x05, // Report Count (5)
0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xA1, 0x03, // Collection (Report)
0x85, 0x21, // Report ID (33)
0x05, 0x00, // Usage Page (Undefined)
0x15, 0x00, // Logical Minimum (0)
0x25, 0xFF, // Logical Maximum (-1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x08, // Usage Maximum (0x08)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x09, 0x33, // Usage (Rx)
0x09, 0x34, // Usage (Ry)
0x09, 0x35, // Usage (Rz)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x06, // Report Count (6)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0xA1, 0x03, // Collection (Report)
0x85, 0x13, // Report ID (19)
0x19, 0x00, // Usage Minimum (Undefined)
0x2A, 0xFF, 0x00, // Usage Maximum (0xFF)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x91, 0x00, // Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xC0, // End Collection
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct GcButtons1 {
#[packed_field(bits = "0")]
pub button_a: bool,
#[packed_field(bits = "1")]
pub button_b: bool,
#[packed_field(bits = "2")]
pub button_x: bool,
#[packed_field(bits = "3")]
pub button_y: bool,
#[packed_field(bits = "4")]
pub dpad_left: bool,
#[packed_field(bits = "5")]
pub dpad_right: bool,
#[packed_field(bits = "6")]
pub dpad_down: bool,
#[packed_field(bits = "7")]
pub dpad_up: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct GcButtons2 {
#[packed_field(bits = "0")]
pub button_start: bool,
#[packed_field(bits = "1")]
pub button_z: bool,
#[packed_field(bits = "2")]
pub button_r: bool,
#[packed_field(bits = "3")]
pub button_l: bool,
#[packed_field(bits = "4..=7")]
pub blank1: u8,
}
///
/// Struct representing the controller state. Used for HID communications in
/// GCC adapter mode, as well as for the internal representation of the controller.
///
#[derive(Clone, Copy, Debug, PartialEq, Eq, PackedStruct, Format)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "8")]
pub struct GcState {
#[packed_field(bits = "0..=7")]
pub buttons_1: GcButtons1,
#[packed_field(bits = "8..=15")]
pub buttons_2: GcButtons2,
#[packed_field(bits = "16..=23")]
pub stick_x: u8,
#[packed_field(bits = "24..=31")]
pub stick_y: u8,
#[packed_field(bits = "32..=39")]
pub cstick_x: u8,
#[packed_field(bits = "40..=47")]
pub cstick_y: u8,
#[packed_field(bits = "48..=55")]
pub trigger_l: u8,
#[packed_field(bits = "56..=63")]
pub trigger_r: u8,
}
impl Default for GcState {
fn default() -> Self {
Self {
buttons_1: GcButtons1::default(),
buttons_2: GcButtons2::default(),
stick_x: 127,
stick_y: 127,
cstick_x: 127,
cstick_y: 127,
trigger_l: 0,
trigger_r: 0,
}
}
}
#[derive(Default)]
pub struct GcReportBuilder {
gc_first: bool,
}
impl HidReportBuilder<37> for GcReportBuilder {
async fn get_hid_report(&mut self, state: &GcState) -> [u8; 37] {
let mut buffer = [0u8; 37];
buffer[0] = 0x21;
buffer[1] |= 0x14;
let data = state.pack().expect("Failed to pack GC input data");
if !self.gc_first {
buffer[1] |= 0x04;
buffer[10] |= 0x04;
buffer[19] |= 0x04;
buffer[28] |= 0x04;
self.gc_first = true;
} else {
// controller in "port 1"
buffer[2..=9].copy_from_slice(&data);
}
buffer
}
}
pub struct GccRequestHandler;
impl RequestHandler for GccRequestHandler {
fn get_report(&mut self, id: ReportId, _buf: &mut [u8]) -> Option<usize> {
info!("Get report for {:?}", id);
None
}
fn set_report(&mut self, id: ReportId, data: &[u8]) -> OutResponse {
trace!("Set report for {:?}: {:x}", id, data);
if data.len() > 1 {
SIGNAL_RUMBLE.signal((data[1] & 0x01) != 0);
}
OutResponse::Accepted
}
fn set_idle_ms(&mut self, id: Option<ReportId>, dur: u32) {
info!("Set idle rate for {:?} to {:?}", id, dur);
}
fn get_idle_ms(&mut self, id: Option<ReportId>) -> Option<u32> {
info!("Get idle rate for {:?}", id);
None
}
}

70
src/hid/mod.rs Normal file
View file

@ -0,0 +1,70 @@
use embassy_usb::{
class::hid::{HidReader, HidReaderWriter, HidWriter, ReadError, RequestHandler},
driver::{Driver, EndpointError},
};
pub mod gcc;
pub mod procon;
pub mod xinput;
/// Custom trait to unify the API between embassy's HID writer, and our XInput reader/writer (and any
/// custom writers we may create in the future)
pub trait HidReaderWriterSplit<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> {
fn split(
self,
) -> (
impl UsbReader<'d, D, READ_N>,
impl UsbWriter<'d, D, WRITE_N>,
);
}
/// Custom trait to unify the API between embassy's HID writer, and our XInput reader (and any
/// custom writers we may create in the future)
pub trait UsbReader<'d, D: Driver<'d>, const READ_N: usize> {
async fn run<T: RequestHandler>(self, use_report_ids: bool, handler: &mut T) -> !;
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError>;
}
/// Custom trait to unify the API between embassy's HID writer, and our XInput writer (and any
/// custom writers we may create in the future)
pub trait UsbWriter<'d, D: Driver<'d>, const WRITE_N: usize> {
async fn ready(&mut self);
async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError>;
}
impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize>
HidReaderWriterSplit<'d, D, READ_N, WRITE_N> for HidReaderWriter<'d, D, READ_N, WRITE_N>
{
fn split(
self,
) -> (
impl UsbReader<'d, D, READ_N>,
impl UsbWriter<'d, D, WRITE_N>,
) {
self.split()
}
}
impl<'d, D: Driver<'d>, const READ_N: usize> UsbReader<'d, D, READ_N> for HidReader<'d, D, READ_N> {
async fn run<T: RequestHandler>(self, use_report_ids: bool, handler: &mut T) -> ! {
self.run(use_report_ids, handler).await
}
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
self.read(buf).await
}
}
impl<'d, D: Driver<'d>, const WRITE_N: usize> UsbWriter<'d, D, WRITE_N>
for HidWriter<'d, D, WRITE_N>
{
async fn ready(&mut self) {
self.ready().await
}
async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> {
self.write(report).await
}
}

762
src/hid/procon.rs Normal file
View file

@ -0,0 +1,762 @@
///
/// The majority of the logic in this file is derived from HOJA-LIB-RP2040
/// https://github.com/HandHeldLegend/HOJA-LIB-RP2040
///
/// The original author gave their consent for this to be included in the NaxGCC firmware,
/// and for this file to be distributed under the GPLv3, see https://git.naxdy.org/NaxdyOrg/NaxGCC-FW/pulls/26
///
use core::ops::{Deref, DerefMut};
use defmt::{info, trace, Format};
use embassy_rp::clocks::RoscRng;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
use embassy_time::Instant;
use embassy_usb::{
class::hid::{ReportId, RequestHandler},
control::OutResponse,
};
use packed_struct::{derive::PackedStruct, PackedStruct};
use rand::RngCore;
use crate::usb_comms::HidReportBuilder;
use super::gcc::GcState;
const SW_INFO_SET_MAC: u8 = 0x01;
const SW_CMD_SET_INPUT_MODE: u8 = 0x03;
const SW_CMD_GET_DEVINFO: u8 = 0x02;
const SW_CMD_SET_SHIPMODE: u8 = 0x08;
const SW_CMD_GET_SPI: u8 = 0x10;
const SW_CMD_SET_PAIRING: u8 = 0x01;
const SW_CMD_GET_TRIGGERET: u8 = 0x04;
const ACK_GET_DEVINFO: u8 = 0x82;
const ACK_GET_SPI: u8 = 0x90;
const ACK_SET_PAIRING: u8 = 0x81;
const ACK_GET_TRIGERET: u8 = 0x83;
const ACK_GENERIC: u8 = 0x80;
const RM_SEND_STATE: u8 = 0x30;
const PRO_CONTROLLER_STRING: [u8; 24] = [
0x00, 0x25, 0x08, 0x50, 0x72, 0x6F, 0x20, 0x43, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x6C, 0x65,
0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68,
];
#[derive(Debug, Format, Clone, Copy)]
struct ProconRequestInfo {
report_id: ProconRequestId,
response_id: ProconResponseId,
command_id: u8,
raw_data: [u8; 64],
}
#[derive(Debug, Format, Clone, Copy)]
enum ProconRequestId {
GetInfo = 0x80,
Command = 0x01,
Rumble = 0x10,
}
#[derive(Debug, Format, Clone, Copy)]
enum ProconResponseId {
GetInfo = 0x81,
GetState = 0x30,
Command = 0x21,
}
static SIGNAL_PROCON_REQUEST: Signal<CriticalSectionRawMutex, ProconRequestInfo> = Signal::new();
#[rustfmt::skip]
pub const PROCON_REPORT_DESCRIPTOR: &[u8] = &[
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x15, 0x00, // Logical Minimum (0)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Application)
0x85, 0x30, // Report ID (48)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x0A, // Usage Maximum (0x0A)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x0A, // Report Count (10)
0x55, 0x00, // Unit Exponent (0)
0x65, 0x00, // Unit (None)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x0B, // Usage Minimum (0x0B)
0x29, 0x0E, // Usage Maximum (0x0E)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x95, 0x02, // Report Count (2)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x0B, 0x01, 0x00, 0x01, 0x00, // Usage (0x010001)
0xA1, 0x00, // Collection (Physical)
0x0B, 0x30, 0x00, 0x01, 0x00, // Usage (0x010030)
0x0B, 0x31, 0x00, 0x01, 0x00, // Usage (0x010031)
0x0B, 0x32, 0x00, 0x01, 0x00, // Usage (0x010032)
0x0B, 0x35, 0x00, 0x01, 0x00, // Usage (0x010035)
0x15, 0x00, // Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65534)
0x75, 0x10, // Report Size (16)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x0B, 0x39, 0x00, 0x01, 0x00, // Usage (0x010039)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x07, // Logical Maximum (7)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x3B, 0x01, // Physical Maximum (315)
0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x0F, // Usage Minimum (0x0F)
0x29, 0x12, // Usage Maximum (0x12)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x95, 0x34, // Report Count (52)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x85, 0x21, // Report ID (33)
0x09, 0x01, // Usage (0x01)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x81, // Report ID (-127)
0x09, 0x02, // Usage (0x02)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x01, // Report ID (1)
0x09, 0x03, // Usage (0x03)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x10, // Report ID (16)
0x09, 0x04, // Usage (0x04)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x80, // Report ID (-128)
0x09, 0x05, // Usage (0x05)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x82, // Report ID (-126)
0x09, 0x06, // Usage (0x06)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0xC0, // End Collection
// 203 bytes
];
#[derive(Clone, Copy, Debug, Format)]
struct ProconByteReport([u8; 64]);
impl Deref for ProconByteReport {
type Target = [u8; 64];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ProconByteReport {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ProconByteReport {
fn set_report_id(&mut self, id: u8) {
self[0] = id;
}
fn set_battery_status(&mut self) {
self[2] = BatteryStatus::default()
.pack()
.expect("Failed to pack fake procon battery status")[0];
}
fn set_timer(&mut self) {
self[1] = Instant::now().as_millis() as u8;
}
fn set_ack(&mut self, ack: u8) {
self[13] = ack;
}
fn set_subcommand(&mut self, cmd: u8) {
self[14] = cmd;
}
fn set_devinfo(&mut self) {
self[15] = 0x04; // NS Firmware primary (4.x)
self[16] = 0x33; // NS Firmware secondary (x.21)
self[17] = 0x03; // Controller ID primary (Pro Controller)
self[18] = 0x02; // Controller ID secondary
self[25] = 0x01;
self[26] = 0x02;
}
fn sw_spi_readfromaddress(
&mut self,
offset_address: u8,
address: u8,
length: u8,
switch_host_address: &[u8],
) {
let read_info = [address, offset_address, 0x00, 0x00, length];
self[15..(15 + read_info.len())].copy_from_slice(&read_info);
let mut output_spi_data = [0u8; 30];
output_spi_data.iter_mut().enumerate().for_each(|(i, e)| {
*e = sw_spi_getaddressdata(offset_address, address + i as u8, switch_host_address)
});
self[20..(20 + length as usize)].copy_from_slice(&output_spi_data[..(length as usize)]);
}
fn set_trigerret(&mut self, time_10_ms: u16) {
let [upper_ms, lower_ms] = time_10_ms.to_be_bytes();
for i in 0..14 {
self[15 + i] = upper_ms;
self[16 + i] = lower_ms;
}
}
}
fn sw_spi_getaddressdata(offset_address: u8, address: u8, switch_host_address: &[u8]) -> u8 {
match offset_address {
0x00 => 0x00,
0x20..=0x40 => match address {
0x26 | 0x00 => 0x95,
// Size of pairing data
0x27 | 0x01 => 0x22,
// Checksum
0x28 | 0x29 | 0x02 | 0x03 => 0x00,
// Host BT address (Big-endian)
0x2A..=0x2F => switch_host_address[(address - 0x2a) as usize],
0x04..=0x09 => switch_host_address[(address - 4) as usize],
// Bluetooth LTK (Little-endian) NOT IMPLEMENTED YET
0x30..=0x3F => 0x00,
0x0A..=0x19 => 0x00,
// Host capability 0x68 is Nintendo Switch. 0x08 is PC
0x4A | 0x24 => 0x68,
0x4B | 0x25 => 0,
_ => 0x00,
},
0x50 => 0x00,
0x60 => match address {
0x00..0x0f => 0xff,
0x12 => 0x03,
0x13 => 0x02,
0x1b => 0x01,
0x20 => 35,
0x21 => 0,
0x22 => 185,
0x23 => 255,
0x24 => 26,
0x25 => 1,
0x26 => 0,
0x27 => 64,
0x28 => 0,
0x29 => 64,
0x2A => 0,
0x2B => 64,
0x2C => 1,
0x2D => 0,
0x2E => 1,
0x2F => 0,
0x30 => 1,
0x31 => 0,
0x32 => 0x3B,
0x33 => 0x34,
0x34 => 0x3B,
0x35 => 0x34,
0x36 => 0x3B,
0x37 => 0x34,
0x3d..=0x45 => mk_switch_analog_calibration_data()[(address - 0x3d) as usize],
0x46..=0x4e => mk_switch_analog_calibration_data()[(address - 0x3d) as usize],
0x4F => 0xFF,
0x50 => 26,
0x51 => 26,
0x52 => 26,
0x53..=0x55 => 94,
0x56 => 255,
0x57 => 255,
0x58 => 255,
0x59..=0x5B => 255,
0x5C => 0x01,
0x80 => 80,
0x81 => 253,
0x82 => 0,
0x83 => 0,
0x84 => 198,
0x85 => 15,
0x98 | 0x86 => 15,
0x99 | 0x87 => 48,
0x9A | 0x88 => 97,
0x9B | 0x89 => 174,
0x9C | 0x8A => 144,
0x9D | 0x8B => 217,
0x9E | 0x8C => 212,
0x9F | 0x8D => 20,
0xA0 | 0x8E => 84,
0xA1 | 0x8F => 65,
0xA2 | 0x90 => 21,
0xA3 | 0x91 => 84,
0xA4 | 0x92 => 199,
0xA5 | 0x93 => 121,
0xA6 | 0x94 => 156,
0xA7 | 0x95 => 51,
0xA8 | 0x96 => 54,
0xA9 | 0x97 => 99,
_ => 0,
},
0x80 => match address {
0x10..=0x1a => 0xff,
0x1b..=0x25 => 0xff,
0x26..=0x3f => 0xff,
_ => 0xff,
},
_ => 0xff,
}
}
fn mk_switch_analog_calibration_data() -> [u8; 18] {
fn switch_analog_encode(in_lower: u16, in_upper: u16) -> [u8; 3] {
let mut out = [0u8; 3];
[out[0], out[1]] = in_lower.to_le_bytes();
out[1] |= ((in_upper & 0xf) << 4) as u8;
out[2] = ((in_upper & 0xff0) >> 4) as u8;
out
}
const MIN: u16 = 128 << 4;
const MAXX: u16 = 128 << 4;
const CENTER: u16 = 128 << 4;
let mut out = [0u8; 18];
out[0..3].copy_from_slice(&switch_analog_encode(MAXX, MAXX));
out[3..6].copy_from_slice(&switch_analog_encode(CENTER, CENTER));
out[6..9].copy_from_slice(&switch_analog_encode(MIN, MIN));
out[9..12].copy_from_slice(&switch_analog_encode(CENTER, CENTER));
out[12..15].copy_from_slice(&switch_analog_encode(MIN, MIN));
out[15..18].copy_from_slice(&switch_analog_encode(MAXX, MAXX));
info!("Returning switch data: {:x}", out);
out
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct ProconButtonsRight {
#[packed_field(bits = "0")]
pub button_y: bool,
#[packed_field(bits = "1")]
pub button_x: bool,
#[packed_field(bits = "2")]
pub button_b: bool,
#[packed_field(bits = "3")]
pub button_a: bool,
#[packed_field(bits = "4")]
pub trigger_r_sr: bool,
#[packed_field(bits = "5")]
pub trigger_r_sl: bool,
#[packed_field(bits = "6")]
pub trigger_r: bool,
#[packed_field(bits = "7")]
pub trigger_zr: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct ProconButtonsShared {
#[packed_field(bits = "0")]
pub button_minus: bool,
#[packed_field(bits = "1")]
pub button_plus: bool,
#[packed_field(bits = "2")]
pub button_sb_right: bool,
#[packed_field(bits = "3")]
pub button_sb_left: bool,
#[packed_field(bits = "4")]
pub button_home: bool,
#[packed_field(bits = "5")]
pub button_capture: bool,
#[packed_field(bits = "6")]
pub none: bool,
#[packed_field(bits = "7")]
pub change_grip_active: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct ProconButtonsLeft {
#[packed_field(bits = "0")]
pub dpad_down: bool,
#[packed_field(bits = "1")]
pub dpad_up: bool,
#[packed_field(bits = "2")]
pub dpad_right: bool,
#[packed_field(bits = "3")]
pub dped_left: bool,
#[packed_field(bits = "4")]
pub trigger_l_sr: bool,
#[packed_field(bits = "5")]
pub trigger_l_sl: bool,
#[packed_field(bits = "6")]
pub trigger_l: bool,
#[packed_field(bits = "7")]
pub trigger_zl: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "msb0", endian = "lsb", size_bytes = "9")]
pub struct ProconState {
#[packed_field(bits = "0..=7")]
pub buttons_right: ProconButtonsRight,
#[packed_field(bits = "8..=15")]
pub buttons_shared: ProconButtonsShared,
#[packed_field(bits = "16..=23")]
pub buttons_left: ProconButtonsLeft,
#[packed_field(bits = "24..=39")]
pub lstick_x: u16,
#[packed_field(bits = "40..=47")]
pub lstick_y: u8,
#[packed_field(bits = "48..=63")]
pub rstick_x: u16,
#[packed_field(bits = "64..=71")]
pub rstick_y: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", endian = "msb", size_bytes = "1")]
struct BatteryStatus {
#[packed_field(bits = "0..=3")]
connection: u8,
#[packed_field(bits = "4..=7")]
battery_level: u8,
}
impl Default for BatteryStatus {
fn default() -> Self {
Self {
connection: 1,
battery_level: 8,
}
}
}
impl From<&GcState> for ProconState {
fn from(value: &GcState) -> Self {
Self {
buttons_left: ProconButtonsLeft {
dpad_down: value.buttons_1.dpad_down,
dpad_right: value.buttons_1.dpad_right,
dpad_up: value.buttons_1.dpad_up,
dped_left: value.buttons_1.dpad_left,
trigger_l: value.buttons_2.button_l,
trigger_zl: value.buttons_2.button_l,
..Default::default()
},
buttons_right: ProconButtonsRight {
button_a: value.buttons_1.button_a,
button_b: value.buttons_1.button_b,
button_x: value.buttons_1.button_x,
button_y: value.buttons_1.button_y,
trigger_r: value.buttons_2.button_z,
trigger_zr: value.buttons_2.button_r,
..Default::default()
},
buttons_shared: ProconButtonsShared {
button_plus: value.buttons_2.button_start && !value.buttons_2.button_z,
button_home: value.buttons_2.button_start && value.buttons_2.button_z,
..Default::default()
},
lstick_x: value.stick_x as u16 * 16,
lstick_y: value.stick_y,
rstick_x: value.cstick_x as u16 * 16,
rstick_y: value.cstick_y,
}
}
}
pub struct ProconReportBuilder {
switch_reporting_mode: u8,
switch_mac_address: [u8; 6],
switch_host_address: [u8; 6],
switch_ltk: [u8; 16],
}
impl Default for ProconReportBuilder {
fn default() -> Self {
Self {
switch_reporting_mode: 0,
switch_mac_address: gen_switch_mac_address(),
switch_host_address: [0u8; 6],
switch_ltk: gen_switch_ltk(),
}
}
}
fn gen_switch_mac_address() -> [u8; 6] {
let mut mac_addr = [0u8; 6];
mac_addr.iter_mut().for_each(|e| {
*e = RoscRng.next_u64() as u8;
});
mac_addr[0] &= 0xfe;
mac_addr[5] = 0x9b;
mac_addr
}
fn gen_switch_ltk() -> [u8; 16] {
let mut switch_ltk = [0u8; 16];
switch_ltk.iter_mut().for_each(|e| {
*e = RoscRng.next_u64() as u8;
});
switch_ltk
}
impl ProconReportBuilder {
fn get_info_report(&self, current_report_info: &ProconRequestInfo) -> [u8; 64] {
let mut report = ProconByteReport([0u8; 64]);
report.set_report_id(ProconResponseId::GetInfo as u8);
if current_report_info.command_id == SW_INFO_SET_MAC {
report[1] = SW_INFO_SET_MAC;
report[3] = 0x03;
self.switch_mac_address
.iter()
.rev()
.enumerate()
.for_each(|(i, e)| {
report[4 + i] = *e;
});
} else {
report[1] = current_report_info.command_id;
}
*report
}
fn get_state_report(&self, state: &ProconState) -> [u8; 64] {
static mut UNKNOWN: u8 = 0xA;
unsafe {
UNKNOWN = match UNKNOWN {
0xA => 0xB,
0xB => 0xC,
_ => 0xA,
};
}
let mut report = ProconByteReport([0u8; 64]);
let data = state
.pack()
.expect("Failed to pack pro controller input data");
report.set_report_id(ProconResponseId::GetState as u8);
report.set_timer();
report.set_battery_status();
report[3..=11].copy_from_slice(&data);
report[12] = unsafe { UNKNOWN };
*report
}
fn get_command_report(&mut self, current_report_info: &ProconRequestInfo) -> [u8; 64] {
let mut report = ProconByteReport([0u8; 64]);
report.set_report_id(ProconResponseId::Command as u8);
report.set_timer();
report.set_battery_status();
report.set_subcommand(current_report_info.command_id);
match current_report_info.command_id {
SW_CMD_SET_INPUT_MODE => {
report.set_ack(ACK_GENERIC);
self.switch_reporting_mode = current_report_info.raw_data[11];
info!(
"Switch reporting mode is now {:x}",
self.switch_reporting_mode
);
}
SW_CMD_GET_DEVINFO => {
report.set_ack(ACK_GET_DEVINFO);
report.set_devinfo();
}
SW_CMD_GET_SPI => {
report.set_ack(ACK_GET_SPI);
report.sw_spi_readfromaddress(
current_report_info.raw_data[12],
current_report_info.raw_data[11],
current_report_info.raw_data[15],
&self.switch_host_address,
);
}
SW_CMD_SET_SHIPMODE => {
report.set_ack(ACK_GENERIC);
}
SW_CMD_SET_PAIRING => {
report.set_ack(ACK_SET_PAIRING);
self.perform_pairing(&mut report, current_report_info);
}
SW_CMD_GET_TRIGGERET => {
report.set_ack(ACK_GET_TRIGERET);
report.set_trigerret(100);
}
_ => {
report.set_ack(ACK_GENERIC);
}
}
*report
}
fn perform_pairing(
&mut self,
report: &mut ProconByteReport,
current_report_info: &ProconRequestInfo,
) {
let pairing_phase = current_report_info.raw_data[11];
let host_address = &current_report_info.raw_data[12..];
match pairing_phase {
1 => {
self.switch_host_address
.iter_mut()
.enumerate()
.for_each(|(i, e)| *e = host_address[5 - i]);
report[16..=21].copy_from_slice(&self.switch_mac_address);
report[22..(22 + PRO_CONTROLLER_STRING.len())]
.copy_from_slice(&PRO_CONTROLLER_STRING);
}
2 => {
report[15] = 2;
report[16..(16 + self.switch_ltk.len())].copy_from_slice(&self.switch_ltk);
}
3 => {
report[15] = 3;
}
_ => {}
}
}
}
impl HidReportBuilder<64> for ProconReportBuilder {
async fn get_hid_report(&mut self, state: &GcState) -> [u8; 64] {
let current_report_info = if self.switch_reporting_mode == RM_SEND_STATE {
SIGNAL_PROCON_REQUEST.try_take()
} else {
Some(SIGNAL_PROCON_REQUEST.wait().await)
};
if let Some(current_report_info) = current_report_info {
match current_report_info.report_id {
ProconRequestId::GetInfo => self.get_info_report(&current_report_info),
ProconRequestId::Command => self.get_command_report(&current_report_info),
ProconRequestId::Rumble => self.get_state_report(&ProconState::from(state)),
}
} else {
self.get_state_report(&ProconState::from(state))
}
}
}
pub struct ProconRequestHandler;
impl RequestHandler for ProconRequestHandler {
fn get_report(&mut self, id: ReportId, _buf: &mut [u8]) -> Option<usize> {
info!("Get report for {:?}", id);
None
}
fn set_report(&mut self, id: ReportId, data: &[u8]) -> OutResponse {
trace!("Set report for {:?}: {:x}", id, data);
let mut buf = [0u8; 64];
let len_to_copy = buf.len().min(data.len());
buf[..len_to_copy].copy_from_slice(&data[..len_to_copy]);
if let ReportId::Out(id) = id {
if id == ProconRequestId::GetInfo as u8 {
SIGNAL_PROCON_REQUEST.signal(ProconRequestInfo {
command_id: buf[1],
report_id: ProconRequestId::GetInfo,
response_id: ProconResponseId::GetInfo,
raw_data: buf,
});
} else if id == ProconRequestId::Command as u8 {
SIGNAL_PROCON_REQUEST.signal(ProconRequestInfo {
command_id: buf[10],
report_id: ProconRequestId::Command,
response_id: ProconResponseId::Command,
raw_data: buf,
});
} else if id == ProconRequestId::Rumble as u8 {
// TODO: handle rumble
}
}
OutResponse::Accepted
}
fn set_idle_ms(&mut self, id: Option<ReportId>, dur: u32) {
info!("Set idle rate for {:?} to {:?}", id, dur);
}
fn get_idle_ms(&mut self, id: Option<ReportId>) -> Option<u32> {
info!("Get idle rate for {:?}", id);
None
}
}

544
src/hid/xinput.rs Normal file
View file

@ -0,0 +1,544 @@
///
/// # XInput Protocol Implementation
///
/// The implementations for `XInputReader` and `XInputWriter` and the logic surrounding them is
/// mostly taken from embassy.
///
/// Unfortunately, the embassy hid classes don't allow us to specify a custom interface protocol,
/// hence the little bit of code duplication.
///
use core::{
mem::MaybeUninit,
sync::atomic::{AtomicUsize, Ordering},
};
use defmt::{info, trace, warn, Format};
use embassy_usb::{
class::hid::{Config, ReadError, ReportId, RequestHandler},
control::{InResponse, OutResponse, Recipient, Request, RequestType},
driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut},
types::InterfaceNumber,
Builder, Handler,
};
use packed_struct::{derive::PackedStruct, PackedStruct};
use crate::usb_comms::HidReportBuilder;
use super::{gcc::GcState, HidReaderWriterSplit, UsbReader, UsbWriter};
/// lol
pub const XINPUT_REPORT_DESCRIPTOR: &[u8] = &[];
const HID_DESC_DESCTYPE_HID: u8 = 0x21;
const HID_DESC_DESCTYPE_HID_REPORT: u8 = 0x22;
const HID_REQ_SET_IDLE: u8 = 0x0a;
const HID_REQ_GET_IDLE: u8 = 0x02;
const HID_REQ_GET_REPORT: u8 = 0x01;
const HID_REQ_SET_REPORT: u8 = 0x09;
const HID_REQ_GET_PROTOCOL: u8 = 0x03;
const HID_REQ_SET_PROTOCOL: u8 = 0x0b;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct XInputButtons1 {
#[packed_field(bits = "0")]
pub dpad_up: bool,
#[packed_field(bits = "1")]
pub dpad_down: bool,
#[packed_field(bits = "2")]
pub dpad_left: bool,
#[packed_field(bits = "3")]
pub dpad_right: bool,
#[packed_field(bits = "4")]
pub button_menu: bool,
#[packed_field(bits = "5")]
pub button_back: bool,
#[packed_field(bits = "6")]
pub button_stick_l: bool,
#[packed_field(bits = "7")]
pub button_stick_r: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")]
pub struct XInputButtons2 {
#[packed_field(bits = "0")]
pub bumper_l: bool,
#[packed_field(bits = "1")]
pub bumper_r: bool,
#[packed_field(bits = "2")]
pub button_guide: bool,
#[packed_field(bits = "3")]
pub blank_1: bool,
#[packed_field(bits = "4")]
pub button_a: bool,
#[packed_field(bits = "5")]
pub button_b: bool,
#[packed_field(bits = "6")]
pub button_x: bool,
#[packed_field(bits = "7")]
pub button_y: bool,
}
///
/// HID report that is sent back to the host.
///
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)]
#[packed_struct(bit_numbering = "msb0", endian = "lsb", size_bytes = "32")]
pub struct XInputReport {
#[packed_field(bits = "0..=7")]
pub report_id: u8,
#[packed_field(bits = "8..=15")]
pub report_size: u8,
#[packed_field(bits = "16..=23")]
pub buttons_1: XInputButtons1,
#[packed_field(bits = "24..=31")]
pub buttons_2: XInputButtons2,
#[packed_field(bits = "32..=39")]
pub analog_trigger_l: u8,
#[packed_field(bits = "40..=47")]
pub analog_trigger_r: u8,
#[packed_field(bits = "48..=63")]
pub stick_left_x: i16,
#[packed_field(bits = "64..=79")]
pub stick_left_y: i16,
#[packed_field(bits = "80..=95")]
pub stick_right_x: i16,
#[packed_field(bits = "96..=111")]
pub stick_right_y: i16,
#[packed_field(bits = "112..=255")]
pub reserved: [u8; 18],
}
impl From<&GcState> for XInputReport {
fn from(value: &GcState) -> Self {
Self {
report_id: 0,
report_size: 20,
buttons_1: XInputButtons1 {
dpad_up: value.buttons_1.dpad_up,
dpad_down: value.buttons_1.dpad_down,
dpad_right: value.buttons_1.dpad_right,
dpad_left: value.buttons_1.dpad_left,
button_menu: value.buttons_2.button_start,
button_back: false,
button_stick_l: false,
button_stick_r: false,
},
buttons_2: XInputButtons2 {
blank_1: false,
bumper_l: false,
bumper_r: value.buttons_2.button_z,
button_a: value.buttons_1.button_a,
button_b: value.buttons_1.button_b,
button_x: value.buttons_1.button_x,
button_y: value.buttons_1.button_y,
button_guide: false,
},
analog_trigger_l: value.trigger_l,
analog_trigger_r: value.trigger_r,
stick_left_x: (value.stick_x as i16 - 127).clamp(-127, 127) * 257,
stick_left_y: (value.stick_y as i16 - 127).clamp(-127, 127) * 257,
stick_right_x: (value.cstick_x as i16 - 127).clamp(-127, 127) * 257,
stick_right_y: (value.cstick_y as i16 - 127).clamp(-127, 127) * 257,
reserved: [0u8; 18],
}
}
}
///
/// Takes in a GcState, converts it to an `XInputReport` and returns its packed version.
///
pub struct XInputReportBuilder;
impl HidReportBuilder<32> for XInputReportBuilder {
async fn get_hid_report(&mut self, state: &super::gcc::GcState) -> [u8; 32] {
XInputReport::from(state)
.pack()
.expect("Failed to pack XInput State")
}
}
///
/// Handles packets sent from the host.
///
pub struct XInputRequestHandler;
impl RequestHandler for XInputRequestHandler {
fn get_report(
&mut self,
id: embassy_usb::class::hid::ReportId,
buf: &mut [u8],
) -> Option<usize> {
let _ = (id, buf);
None
}
fn set_report(
&mut self,
id: embassy_usb::class::hid::ReportId,
data: &[u8],
) -> embassy_usb::control::OutResponse {
let _ = (id, data);
info!("Set report for {:?}: {:x}", id, data);
embassy_usb::control::OutResponse::Accepted
}
fn get_idle_ms(&mut self, id: Option<embassy_usb::class::hid::ReportId>) -> Option<u32> {
let _ = id;
None
}
fn set_idle_ms(&mut self, id: Option<embassy_usb::class::hid::ReportId>, duration_ms: u32) {
let _ = (id, duration_ms);
}
}
/// Taken from embassy.
pub struct XInputWriter<'d, D: Driver<'d>, const N: usize> {
ep_in: D::EndpointIn,
}
impl<'d, D: Driver<'d>, const N: usize> UsbWriter<'d, D, N> for XInputWriter<'d, D, N> {
/// Waits for the interrupt in endpoint to be enabled.
async fn ready(&mut self) {
self.ep_in.wait_enabled().await;
}
/// Writes `report` to its interrupt endpoint.
async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> {
assert!(report.len() <= N);
let max_packet_size = usize::from(self.ep_in.info().max_packet_size);
let zlp_needed = report.len() < N && (report.len() % max_packet_size == 0);
for chunk in report.chunks(max_packet_size) {
self.ep_in.write(chunk).await?;
}
if zlp_needed {
self.ep_in.write(&[]).await?;
}
Ok(())
}
}
/// Taken from embassy.
pub struct XInputReader<'d, D: Driver<'d>, const N: usize> {
ep_out: D::EndpointOut,
offset: &'d AtomicUsize,
}
impl<'d, D: Driver<'d>, const N: usize> UsbReader<'d, D, N> for XInputReader<'d, D, N> {
async fn run<T: RequestHandler>(mut self, use_report_ids: bool, handler: &mut T) -> ! {
let offset = self.offset.load(Ordering::Acquire);
assert!(offset == 0);
let mut buf = [0; N];
loop {
match self.read(&mut buf).await {
Ok(len) => {
let id = if use_report_ids { buf[0] } else { 0 };
handler.set_report(ReportId::Out(id), &buf[..len]);
}
Err(ReadError::BufferOverflow) => warn!(
"Host ent output report larger than the configured maximum output report length ({})",
N
),
Err(ReadError::Disabled) => self.ep_out.wait_enabled().await,
Err(ReadError::Sync(_)) => unreachable!(),
}
}
}
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
assert!(N != 0);
assert!(buf.len() >= N);
// Read packets from the endpoint
let max_packet_size = usize::from(self.ep_out.info().max_packet_size);
let starting_offset = self.offset.load(Ordering::Acquire);
let mut total = starting_offset;
loop {
for chunk in buf[starting_offset..N].chunks_mut(max_packet_size) {
match self.ep_out.read(chunk).await {
Ok(size) => {
total += size;
if size < max_packet_size || total == N {
self.offset.store(0, Ordering::Release);
break;
}
self.offset.store(total, Ordering::Release);
}
Err(err) => {
self.offset.store(0, Ordering::Release);
return Err(err.into());
}
}
}
// Some hosts may send ZLPs even when not required by the HID spec, so we'll loop as long as total == 0.
if total > 0 {
break;
}
}
if starting_offset > 0 {
Err(ReadError::Sync(starting_offset..total))
} else {
Ok(total)
}
}
}
/// Taken from embassy, with a few modifications to the descriptor.
pub struct XInputReaderWriter<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> {
reader: XInputReader<'d, D, READ_N>,
writer: XInputWriter<'d, D, WRITE_N>,
}
impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize>
XInputReaderWriter<'d, D, READ_N, WRITE_N>
{
pub fn new(
builder: &mut Builder<'d, D>,
state: &'d mut XInputState<'d>,
config: Config<'d>,
) -> Self {
let mut func = builder.function(0xff, 0x5d, 0x01);
let mut iface = func.interface();
let if_num = iface.interface_number();
let mut alt = iface.alt_setting(0xff, 0x5d, 0x01, None);
#[rustfmt::skip]
alt.descriptor(0x21, &[
0x10, 0x01, // bcdHID 1.10
0x01, // bCountryCode
0x24, // bNumDescriptors
0x81, // bDescriptorType[0] (Unknown 0x81)
0x14, 0x03, // wDescriptorLength[0] 788
0x00, // bDescriptorType[1] (Unknown 0x00)
0x03, 0x13, // wDescriptorLength[1] 4867
0x02, // bDescriptorType[2] (Unknown 0x02)
0x00, 0x03, // wDescriptorLength[2] 768
0x00, // bDescriptorType[3] (Unknown 0x00)
]);
let ep_in = alt.endpoint_interrupt_in(config.max_packet_size_in, config.poll_ms);
let ep_out = alt.endpoint_interrupt_out(config.max_packet_size_out, config.poll_ms);
drop(func);
let control = state.control.write(XInputControl::new(
if_num,
config.report_descriptor,
config.request_handler,
&state.out_report_offset,
));
builder.handler(control);
Self {
reader: XInputReader {
ep_out,
offset: &state.out_report_offset,
},
writer: XInputWriter { ep_in },
}
}
}
impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize>
HidReaderWriterSplit<'d, D, READ_N, WRITE_N> for XInputReaderWriter<'d, D, READ_N, WRITE_N>
{
fn split(
self,
) -> (
impl UsbReader<'d, D, READ_N>,
impl UsbWriter<'d, D, WRITE_N>,
) {
(self.reader, self.writer)
}
}
pub struct XInputState<'d> {
control: MaybeUninit<XInputControl<'d>>,
out_report_offset: AtomicUsize,
}
impl<'d> Default for XInputState<'d> {
fn default() -> Self {
Self::new()
}
}
impl<'d> XInputState<'d> {
pub const fn new() -> Self {
XInputState {
control: MaybeUninit::uninit(),
out_report_offset: AtomicUsize::new(0),
}
}
}
/// Taken from embassy.
struct XInputControl<'d> {
if_num: InterfaceNumber,
report_descriptor: &'d [u8],
request_handler: Option<&'d mut dyn RequestHandler>,
out_report_offset: &'d AtomicUsize,
hid_descriptor: [u8; 16],
}
impl<'d> XInputControl<'d> {
fn new(
if_num: InterfaceNumber,
report_descriptor: &'d [u8],
request_handler: Option<&'d mut dyn RequestHandler>,
out_report_offset: &'d AtomicUsize,
) -> Self {
XInputControl {
if_num,
report_descriptor,
request_handler,
out_report_offset,
#[rustfmt::skip]
hid_descriptor: [
0x10, // bLength
0x21, // bDescriptorType (HID)
0x10, 0x01, // bcdHID 1.10
0x01, // bCountryCode
0x24, // bNumDescriptors
0x81, // bDescriptorType[0] (Unknown 0x81)
0x14, 0x03, // wDescriptorLength[0] 788
0x00, // bDescriptorType[1] (Unknown 0x00)
0x03, 0x13, // wDescriptorLength[1] 4867
0x02, // bDescriptorType[2] (Unknown 0x02)
0x00, 0x03, // wDescriptorLength[2] 768
0x00, // bDescriptorType[3] (Unknown 0x00)
],
}
}
}
/// Helper function, since the function in `ReportId` is private.
const fn try_u16_to_report_id(value: u16) -> Result<ReportId, ()> {
match value >> 8 {
1 => Ok(ReportId::In(value as u8)),
2 => Ok(ReportId::Out(value as u8)),
3 => Ok(ReportId::Feature(value as u8)),
_ => Err(()),
}
}
impl<'d> Handler for XInputControl<'d> {
fn reset(&mut self) {
self.out_report_offset.store(0, Ordering::Release);
}
fn control_out(&mut self, req: Request, data: &[u8]) -> Option<OutResponse> {
if (req.request_type, req.recipient, req.index)
!= (
RequestType::Class,
Recipient::Interface,
self.if_num.0 as u16,
)
{
return None;
}
trace!("HID control_out {:?} {=[u8]:x}", req, data);
match req.request {
HID_REQ_SET_IDLE => {
if let Some(handler) = self.request_handler.as_mut() {
let id = req.value as u8;
let id = (id != 0).then_some(ReportId::In(id));
let dur = u32::from(req.value >> 8);
let dur = if dur == 0 { u32::MAX } else { 4 * dur };
handler.set_idle_ms(id, dur);
}
Some(OutResponse::Accepted)
}
HID_REQ_SET_REPORT => {
match (
try_u16_to_report_id(req.value),
self.request_handler.as_mut(),
) {
(Ok(id), Some(handler)) => Some(handler.set_report(id, data)),
_ => Some(OutResponse::Rejected),
}
}
HID_REQ_SET_PROTOCOL => {
if req.value == 1 {
Some(OutResponse::Accepted)
} else {
warn!("HID Boot Protocol is unsupported.");
Some(OutResponse::Rejected) // UNSUPPORTED: Boot Protocol
}
}
_ => Some(OutResponse::Rejected),
}
}
fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> Option<InResponse<'a>> {
if req.index != self.if_num.0 as u16 {
return None;
}
match (req.request_type, req.recipient) {
(RequestType::Standard, Recipient::Interface) => match req.request {
Request::GET_DESCRIPTOR => match (req.value >> 8) as u8 {
HID_DESC_DESCTYPE_HID_REPORT => {
Some(InResponse::Accepted(self.report_descriptor))
}
HID_DESC_DESCTYPE_HID => Some(InResponse::Accepted(&self.hid_descriptor)),
_ => Some(InResponse::Rejected),
},
_ => Some(InResponse::Rejected),
},
(RequestType::Class, Recipient::Interface) => {
trace!("HID control_in {:?}", req);
match req.request {
HID_REQ_GET_REPORT => {
let size = match try_u16_to_report_id(req.value) {
Ok(id) => self
.request_handler
.as_mut()
.and_then(|x| x.get_report(id, buf)),
Err(_) => None,
};
if let Some(size) = size {
Some(InResponse::Accepted(&buf[0..size]))
} else {
Some(InResponse::Rejected)
}
}
HID_REQ_GET_IDLE => {
if let Some(handler) = self.request_handler.as_mut() {
let id = req.value as u8;
let id = (id != 0).then_some(ReportId::In(id));
if let Some(dur) = handler.get_idle_ms(id) {
let dur = u8::try_from(dur / 4).unwrap_or(0);
buf[0] = dur;
Some(InResponse::Accepted(&buf[0..1]))
} else {
Some(InResponse::Rejected)
}
} else {
Some(InResponse::Rejected)
}
}
HID_REQ_GET_PROTOCOL => {
// UNSUPPORTED: Boot Protocol
buf[0] = 1;
Some(InResponse::Accepted(&buf[0..1]))
}
_ => Some(InResponse::Rejected),
}
}
_ => None,
}
}
}

View file

@ -1,7 +1,7 @@
use defmt::{debug, info, trace, Format};
use embassy_futures::yield_now;
use embassy_rp::{
gpio::{AnyPin, Input, Output, Pin},
gpio::{Input, Output},
peripherals::SPI0,
spi::{Blocking, Spi},
};
@ -16,19 +16,20 @@ use libm::{fmaxf, fminf};
use crate::{
config::{
ControllerConfig, InputConsistencyMode, OverrideGcReportInstruction, OverrideStickState,
SIGNAL_CONFIG_CHANGE, SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE,
ControllerConfig, ControllerMode, InputConsistencyMode, OverrideGcReportInstruction,
OverrideStickState, SIGNAL_CONFIG_CHANGE, SIGNAL_IS_CALIBRATING, SIGNAL_OVERRIDE_GCC_STATE,
SIGNAL_OVERRIDE_STICK_STATE,
},
filter::{run_waveshaping, FilterGains, KalmanState, WaveshapingValues, FILTER_GAINS},
gcc_hid::{GcReport, MUTEX_INPUT_CONSISTENCY_MODE},
helpers::XyValuePair,
hid::gcc::GcState,
input_filter::{DummyFilter, InputFilter},
stick::{linearize, notch_remap, StickParams},
usb_comms::{MUTEX_CONTROLLER_MODE, MUTEX_INPUT_CONSISTENCY_MODE},
};
/// Used to send the button state to the usb task and the calibration task
pub static CHANNEL_GCC_STATE: PubSubChannel<CriticalSectionRawMutex, GcReport, 1, 4, 1> =
pub static CHANNEL_GCC_STATE: PubSubChannel<CriticalSectionRawMutex, GcState, 1, 4, 1> =
PubSubChannel::new();
/// Used to send the stick state from the stick task to the main input task
@ -36,10 +37,8 @@ static SIGNAL_STICK_STATE: Signal<CriticalSectionRawMutex, StickState> = Signal:
pub static SPI_SHARED: Mutex<ThreadModeRawMutex, Option<Spi<'static, SPI0, Blocking>>> =
Mutex::new(None);
pub static SPI_ACS_SHARED: Mutex<ThreadModeRawMutex, Option<Output<'static, AnyPin>>> =
Mutex::new(None);
pub static SPI_CCS_SHARED: Mutex<ThreadModeRawMutex, Option<Output<'static, AnyPin>>> =
Mutex::new(None);
pub static SPI_ACS_SHARED: Mutex<ThreadModeRawMutex, Option<Output<'static>>> = Mutex::new(None);
pub static SPI_CCS_SHARED: Mutex<ThreadModeRawMutex, Option<Output<'static>>> = Mutex::new(None);
const STICK_HYST_VAL: f32 = 0.3;
pub const FLOAT_ORIGIN: f32 = 127.5;
@ -84,18 +83,12 @@ pub enum StickAxis {
#[inline(never)]
#[link_section = ".time_critical.read_ext_adc"]
pub fn read_ext_adc<
'a,
Acs: Pin,
Ccs: Pin,
I: embassy_rp::spi::Instance,
M: embassy_rp::spi::Mode,
>(
pub fn read_ext_adc<'a, I: embassy_rp::spi::Instance, M: embassy_rp::spi::Mode>(
which_stick: Stick,
which_axis: StickAxis,
spi: &mut Spi<'a, I, M>,
spi_acs: &mut Output<'a, Acs>,
spi_ccs: &mut Output<'a, Ccs>,
spi_acs: &mut Output<'a>,
spi_ccs: &mut Output<'a>,
) -> u16 {
let mut buf = [0b11010000; 3];
@ -352,19 +345,19 @@ async fn update_stick_states(
#[allow(clippy::too_many_arguments)]
fn update_button_states(
gcc_state: &mut GcReport,
btn_a: &Input<'_, AnyPin>,
btn_b: &Input<'_, AnyPin>,
btn_x: &Input<'_, AnyPin>,
btn_y: &Input<'_, AnyPin>,
btn_start: &Input<'_, AnyPin>,
btn_l: &Input<'_, AnyPin>,
btn_r: &Input<'_, AnyPin>,
btn_z: &Input<'_, AnyPin>,
btn_dleft: &Input<'_, AnyPin>,
btn_dright: &Input<'_, AnyPin>,
btn_dup: &Input<'_, AnyPin>,
btn_ddown: &Input<'_, AnyPin>,
gcc_state: &mut GcState,
btn_a: &Input<'_>,
btn_b: &Input<'_>,
btn_x: &Input<'_>,
btn_y: &Input<'_>,
btn_start: &Input<'_>,
btn_l: &Input<'_>,
btn_r: &Input<'_>,
btn_z: &Input<'_>,
btn_dleft: &Input<'_>,
btn_dright: &Input<'_>,
btn_dup: &Input<'_>,
btn_ddown: &Input<'_>,
) {
gcc_state.buttons_1.button_a = btn_a.is_low();
gcc_state.buttons_1.button_b = btn_b.is_low();
@ -393,7 +386,7 @@ pub async fn input_integrity_benchmark() {
loop {
SIGNAL_OVERRIDE_GCC_STATE.signal(OverrideGcReportInstruction {
report: {
let mut report = GcReport::default();
let mut report = GcState::default();
report.buttons_1.dpad_up = true;
report
},
@ -409,18 +402,18 @@ pub async fn input_integrity_benchmark() {
#[allow(clippy::too_many_arguments)]
#[embassy_executor::task]
pub async fn update_button_state_task(
btn_z: Input<'static, AnyPin>,
btn_a: Input<'static, AnyPin>,
btn_b: Input<'static, AnyPin>,
btn_dright: Input<'static, AnyPin>,
btn_dup: Input<'static, AnyPin>,
btn_ddown: Input<'static, AnyPin>,
btn_dleft: Input<'static, AnyPin>,
btn_l: Input<'static, AnyPin>,
btn_r: Input<'static, AnyPin>,
btn_x: Input<'static, AnyPin>,
btn_y: Input<'static, AnyPin>,
btn_start: Input<'static, AnyPin>,
btn_z: Input<'static>,
btn_a: Input<'static>,
btn_b: Input<'static>,
btn_dright: Input<'static>,
btn_dup: Input<'static>,
btn_ddown: Input<'static>,
btn_dleft: Input<'static>,
btn_l: Input<'static>,
btn_r: Input<'static>,
btn_x: Input<'static>,
btn_y: Input<'static>,
btn_start: Input<'static>,
) {
// upon loop entry, we check for the reset combo once
if btn_a.is_low() && btn_x.is_low() && btn_y.is_low() {
@ -431,6 +424,17 @@ pub async fn update_button_state_task(
loop {}
}
{
let mut m = MUTEX_CONTROLLER_MODE.lock().await;
*m = if btn_start.is_low() {
Some(ControllerMode::Procon)
} else if btn_x.is_low() {
Some(ControllerMode::XInput)
} else {
Some(ControllerMode::GcAdapter)
};
}
let input_consistency_mode = {
while MUTEX_INPUT_CONSISTENCY_MODE.lock().await.is_none() {
Timer::after(Duration::from_millis(100)).await;
@ -438,9 +442,9 @@ pub async fn update_button_state_task(
MUTEX_INPUT_CONSISTENCY_MODE.lock().await.unwrap()
};
let mut previous_state = GcReport::default();
let mut previous_state = GcState::default();
let mut gcc_state = GcReport::default();
let mut gcc_state = GcState::default();
let gcc_publisher = CHANNEL_GCC_STATE.publisher().unwrap();
@ -546,10 +550,13 @@ pub async fn update_button_state_task(
#[link_section = ".time_critical.update_stick_states_task"]
pub async fn update_stick_states_task(
spi: Spi<'static, SPI0, embassy_rp::spi::Blocking>,
spi_acs: Output<'static, AnyPin>,
spi_ccs: Output<'static, AnyPin>,
spi_acs: Output<'static>,
spi_ccs: Output<'static>,
) {
Timer::after_secs(1).await;
// let some time pass before accepting stick inputs
// to ensure sticks are properly zeroed
Timer::after_secs(2).await;
*SPI_SHARED.lock().await = Some(spi);
*SPI_ACS_SHARED.lock().await = Some(spi_acs);
*SPI_CCS_SHARED.lock().await = Some(spi_ccs);
@ -608,7 +615,7 @@ pub async fn update_stick_states_task(
let n = Instant::now();
match (n - last_loop_time).as_micros() {
a if a > 1666 => debug!("Loop took {} us", a),
a if a > 800 => debug!("Loop took {} us", a),
_ => {}
};
last_loop_time = n;

View file

@ -2,15 +2,19 @@ use defmt::warn;
use crate::{
config::{is_awaitable_button_pressed, AwaitableButtons},
gcc_hid::GcReport,
hid::gcc::GcState,
};
/**
* Houses functionality for modifying GCC state before it is sent to the console.
*
* General info for implementing filters on the sticks:
* X and Y values of a stick go each from 0 to 255.
* 127.5 is the middle value and when both X and Y are 127.5 the stick is in neutral position.
*/
pub trait InputFilter: Sized {
fn apply_filter(&mut self, gcc_state: &mut GcReport);
fn apply_filter(&mut self, gcc_state: &mut GcState);
}
/// Presses a single button if another button is pressed.
@ -22,7 +26,7 @@ pub struct SingleButtonMacroFilter {
}
impl InputFilter for SingleButtonMacroFilter {
fn apply_filter(&mut self, gcc_state: &mut GcReport) {
fn apply_filter(&mut self, gcc_state: &mut GcState) {
if is_awaitable_button_pressed(gcc_state, &self.btn_instigator) {
match self.btn_to_press {
AwaitableButtons::A => {
@ -79,7 +83,7 @@ impl InputFilter for SingleButtonMacroFilter {
pub struct CStickUpTiltFilter;
impl InputFilter for CStickUpTiltFilter {
fn apply_filter(&mut self, gcc_state: &mut GcReport) {
fn apply_filter(&mut self, gcc_state: &mut GcState) {
if gcc_state.cstick_y > 157 {
if (137..=201).contains(&gcc_state.cstick_x) {
gcc_state.cstick_x = 201;
@ -100,10 +104,37 @@ impl InputFilter for CStickUpTiltFilter {
}
}
/// Improves hitting up/down angled forward tilt at the cost
/// of making it impossible to hit turnaround up & down tilt
/// and making it slightly harder to hit regular forward tilt.
pub struct CStickAngledFTiltFilter;
impl InputFilter for CStickAngledFTiltFilter {
fn apply_filter(&mut self, gcc_state: &mut GcState) {
if gcc_state.cstick_y > 147 {
if (147..=225).contains(&gcc_state.cstick_x) {
gcc_state.cstick_x = 205;
gcc_state.cstick_y = 205;
} else if (30..=107).contains(&gcc_state.cstick_x) {
gcc_state.cstick_x = 50;
gcc_state.cstick_y = 205;
}
} else if gcc_state.cstick_y < 107 {
if (147..=225).contains(&gcc_state.cstick_x) {
gcc_state.cstick_x = 205;
gcc_state.cstick_y = 50;
} else if (30..=107).contains(&gcc_state.cstick_x) {
gcc_state.cstick_x = 50;
gcc_state.cstick_y = 50;
}
}
}
}
/// Does nothing.
#[derive(Default)]
pub struct DummyFilter;
impl InputFilter for DummyFilter {
fn apply_filter(&mut self, _gcc_state: &mut GcReport) {}
fn apply_filter(&mut self, _gcc_state: &mut GcState) {}
}

View file

@ -1,19 +1,18 @@
//! This example test the RP Pico on board LED.
//!
//! It does not work with the RP Pico W board. See wifi_blinky.rs.
#![no_std]
#![no_main]
mod config;
mod filter;
mod gcc_hid;
mod helpers;
mod hid;
mod input;
mod input_filter;
mod stick;
mod usb_comms;
use core::ptr::addr_of_mut;
use config::config_task;
use defmt::{debug, info};
use defmt::info;
use embassy_executor::Executor;
use embassy_rp::{
bind_interrupts,
@ -24,14 +23,14 @@ use embassy_rp::{
spi::{self, Spi},
usb::{Driver, InterruptHandler},
};
use gcc_hid::usb_transfer_task;
use gpio::{Level, Output};
use usb_comms::usb_transfer_task;
use input::{update_button_state_task, update_stick_states_task};
use static_cell::StaticCell;
use crate::config::enter_config_mode_task;
use crate::gcc_hid::rumble_task;
use crate::usb_comms::rumble_task;
use {defmt_rtt as _, panic_probe as _};
@ -75,34 +74,37 @@ fn main() -> ! {
let spi_acs = Output::new(AnyPin::from(p_acs), Level::High); // active low
let spi_ccs = Output::new(AnyPin::from(p_ccs), Level::High); // active low
spawn_core1(p.CORE1, unsafe { &mut CORE1_STACK }, move || {
let executor1 = EXECUTOR1.init(Executor::new());
debug!("Mana");
executor1.run(|spawner| {
spawner.spawn(usb_transfer_task(uid, driver)).unwrap();
spawner.spawn(enter_config_mode_task()).unwrap();
spawner
.spawn(rumble_task(p.PIN_25, p.PIN_29, p.PWM_CH4, p.PWM_CH6))
.unwrap();
// spawner.spawn(input_integrity_benchmark()).unwrap();
spawner
.spawn(update_button_state_task(
Input::new(AnyPin::from(p.PIN_20), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_17), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_16), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_11), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_9), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_10), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_8), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_22), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_21), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_18), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_19), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_5), gpio::Pull::Up),
))
.unwrap()
});
});
spawn_core1(
p.CORE1,
unsafe { addr_of_mut!(CORE1_STACK).as_mut().unwrap() },
move || {
let executor1 = EXECUTOR1.init(Executor::new());
executor1.run(|spawner| {
spawner.spawn(usb_transfer_task(uid, driver)).unwrap();
spawner.spawn(enter_config_mode_task()).unwrap();
spawner
.spawn(rumble_task(p.PIN_25, p.PIN_29, p.PWM_SLICE4, p.PWM_SLICE6))
.unwrap();
// spawner.spawn(input_integrity_benchmark()).unwrap();
spawner
.spawn(update_button_state_task(
Input::new(AnyPin::from(p.PIN_20), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_17), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_16), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_11), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_9), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_10), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_8), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_22), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_21), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_18), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_19), gpio::Pull::Up),
Input::new(AnyPin::from(p.PIN_5), gpio::Pull::Up),
))
.unwrap()
});
},
);
let executor0 = EXECUTOR0.init(Executor::new());
info!("Initialized.");

499
src/usb_comms.rs Normal file
View file

@ -0,0 +1,499 @@
/**
* Communication with the console / PC over USB HID.
* Includes the HID report descriptor, and the GcReport struct.
*/
use core::{default::Default, future::Future};
use defmt::{debug, info, trace, warn};
use embassy_futures::join::join;
use embassy_rp::{
peripherals::{PIN_25, PIN_29, PWM_SLICE4, PWM_SLICE6, USB},
pwm::Pwm,
usb::Driver as EmbassyDriver,
};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal};
use embassy_time::{Duration, Instant, Timer};
use embassy_usb::{
class::hid::{HidReaderWriter, RequestHandler, State},
driver::Driver,
msos::{self, windows_version},
Builder, Handler, UsbDevice,
};
use libm::powf;
use crate::{
config::{ControllerMode, InputConsistencyMode},
hid::{
gcc::{GcReportBuilder, GcState, GccRequestHandler, GCC_REPORT_DESCRIPTOR},
procon::{ProconReportBuilder, ProconRequestHandler, PROCON_REPORT_DESCRIPTOR},
xinput::{
XInputReaderWriter, XInputReportBuilder, XInputRequestHandler, XInputState,
XINPUT_REPORT_DESCRIPTOR,
},
HidReaderWriterSplit, UsbReader, UsbWriter,
},
input::CHANNEL_GCC_STATE,
};
pub static SIGNAL_RUMBLE: Signal<CriticalSectionRawMutex, bool> = Signal::new();
/// We could turn the config change signal into a PubSubChannel instead, but that
/// would just transmit unnecessary amounts of data.
pub static SIGNAL_CHANGE_RUMBLE_STRENGTH: Signal<CriticalSectionRawMutex, u8> = Signal::new();
/// Only dispatched ONCE after powerup, to determine how to advertise itself via USB.
pub static MUTEX_INPUT_CONSISTENCY_MODE: Mutex<
CriticalSectionRawMutex,
Option<InputConsistencyMode>,
> = Mutex::new(None);
/// Only dispatched ONCE after powerup, to determine how to advertise itself via USB.
pub static MUTEX_CONTROLLER_MODE: Mutex<CriticalSectionRawMutex, Option<ControllerMode>> =
Mutex::new(None);
/// Vendor-defined property data
const DEVICE_INTERFACE_GUID: &str = "{ecceff35-146c-4ff3-acd9-8f992d09acdd}";
pub trait HidReportBuilder<const LEN: usize> {
async fn get_hid_report(&mut self, state: &GcState) -> [u8; LEN];
}
struct UsbConfig {
vid: u16,
pid: u16,
report_descriptor: &'static [u8],
}
impl From<ControllerMode> for UsbConfig {
fn from(value: ControllerMode) -> Self {
match value {
ControllerMode::GcAdapter => Self {
vid: 0x057e,
pid: 0x0337,
report_descriptor: GCC_REPORT_DESCRIPTOR,
},
ControllerMode::Procon => Self {
vid: 0x57e,
pid: 0x2009,
report_descriptor: PROCON_REPORT_DESCRIPTOR,
},
ControllerMode::XInput => Self {
vid: 0x045e,
pid: 0x028e,
report_descriptor: XINPUT_REPORT_DESCRIPTOR,
},
}
}
}
struct MyDeviceHandler {
configured: bool,
}
impl MyDeviceHandler {
fn new() -> Self {
MyDeviceHandler { configured: false }
}
}
impl Handler for MyDeviceHandler {
fn enabled(&mut self, enabled: bool) {
self.configured = true;
// reason: in production, info! compiles to nothing
#[allow(clippy::if_same_then_else)]
if enabled {
info!("Device enabled");
} else {
info!("Device disabled");
}
}
fn reset(&mut self) {
self.configured = false;
info!("Bus reset, the Vbus current limit is 100mA");
}
fn addressed(&mut self, addr: u8) {
self.configured = false;
info!("USB address set to: {}", addr);
}
fn configured(&mut self, configured: bool) {
self.configured = configured;
if configured {
info!(
"Device configured, it may now draw up to the configured current limit from Vbus."
)
} else {
info!("Device is no longer configured, the Vbus current limit is 100mA.");
}
}
}
fn mk_hid_reader_writer<'d, S, D, T, F, const R: usize, const W: usize>(
input_consistency_mode: InputConsistencyMode,
controller_mode: ControllerMode,
report_descriptor: &'d [u8],
mut builder: Builder<'d, D>,
state: &'d mut S,
mut init_func: F,
) -> (
UsbDevice<'d, D>,
impl UsbReader<'d, D, R>,
impl UsbWriter<'d, D, W>,
)
where
D: Driver<'d>,
T: HidReaderWriterSplit<'d, D, R, W> + 'd,
F: FnMut(&mut Builder<'d, D>, &'d mut S, embassy_usb::class::hid::Config<'d>) -> T + 'd,
{
let hid_config = embassy_usb::class::hid::Config {
report_descriptor,
request_handler: None,
poll_ms: if let ControllerMode::XInput = controller_mode {
1
} else {
match input_consistency_mode {
InputConsistencyMode::Original => 8,
InputConsistencyMode::ConsistencyHack
| InputConsistencyMode::SuperHack
| InputConsistencyMode::PC => 1,
}
},
max_packet_size_in: W as u16,
max_packet_size_out: R as u16,
};
let hid = init_func(&mut builder, state, hid_config);
let usb = builder.build();
let (reader, writer) = hid.split();
(usb, reader, writer)
}
#[allow(clippy::too_many_arguments)]
fn mk_usb_transfer_futures<'d, S, D, H, F, Rq, T, const R: usize, const W: usize>(
input_consistency_mode: InputConsistencyMode,
controller_mode: ControllerMode,
usb_config: &UsbConfig,
request_handler: &'d mut Rq,
builder: Builder<'d, D>,
mut hid_report_builder: H,
state: &'d mut S,
init_func: F,
) -> (
impl Future<Output = ()> + 'd,
impl Future<Output = ()> + 'd,
impl Future<Output = ()> + 'd,
)
where
D: Driver<'d> + 'd,
H: HidReportBuilder<W> + 'd,
Rq: RequestHandler,
T: HidReaderWriterSplit<'d, D, R, W> + 'd,
F: FnMut(&mut Builder<'d, D>, &'d mut S, embassy_usb::class::hid::Config<'d>) -> T + 'd,
{
let (mut usb, reader, mut writer) = mk_hid_reader_writer(
input_consistency_mode,
controller_mode,
usb_config.report_descriptor,
builder,
state,
init_func,
);
let usb_fut = async move {
loop {
usb.run_until_suspend().await;
debug!("Suspended");
usb.wait_resume().await;
debug!("RESUMED!");
}
};
let in_fut = async move {
let mut gcc_subscriber = CHANNEL_GCC_STATE.subscriber().unwrap();
let mut last_report_time = Instant::now();
let mut rate_limit_end_time = Instant::now();
loop {
// This is what we like to call a "hack".
// It forces reports to be sent at least every 8.33ms instead of every 8ms.
// 8.33ms is a multiple of the game's frame interval (16.66ms), so if we
// send a report every 8.33ms, it should (in theory) ensure (close to)
// 100% input accuracy.
//
// From the console's perspective, we are basically a laggy adapter, taking
// a minimum of 333 extra us to send a report every time it's polled, but it
// works to our advantage.
if controller_mode != ControllerMode::XInput {
match input_consistency_mode {
InputConsistencyMode::SuperHack | InputConsistencyMode::ConsistencyHack => {
// "Ticker at home", so we can use this for both consistency and SuperHack mode
Timer::at(rate_limit_end_time).await;
}
InputConsistencyMode::Original | InputConsistencyMode::PC => {}
}
}
writer.ready().await;
let state = gcc_subscriber.next_message_pure().await;
let report = hid_report_builder.get_hid_report(&state).await;
trace!("Writing report: {:08b}", report);
match writer.write(&report).await {
Ok(()) => {
let currtime = Instant::now();
let polltime = currtime.duration_since(last_report_time);
let micros = polltime.as_micros();
debug!("Report written in {}us", micros);
if input_consistency_mode != InputConsistencyMode::Original
&& input_consistency_mode != InputConsistencyMode::PC
&& controller_mode != ControllerMode::XInput
{
while rate_limit_end_time < currtime {
rate_limit_end_time += Duration::from_micros(8333);
}
}
last_report_time = currtime;
}
Err(e) => warn!("Failed to send report: {:?}", e),
}
}
};
let out_fut = async move {
trace!("Readery loop");
reader.run(true, request_handler).await;
};
let usb_fut_wrapped = async {
usb_fut.await;
debug!("USB FUT DED");
};
(usb_fut_wrapped, in_fut, out_fut)
}
#[embassy_executor::task]
pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: EmbassyDriver<'static, USB>) {
let input_consistency_mode = {
while MUTEX_INPUT_CONSISTENCY_MODE.lock().await.is_none() {
Timer::after(Duration::from_millis(100)).await;
}
MUTEX_INPUT_CONSISTENCY_MODE.lock().await.unwrap()
};
let controller_mode = {
while MUTEX_CONTROLLER_MODE.lock().await.is_none() {
Timer::after(Duration::from_millis(100)).await;
}
MUTEX_CONTROLLER_MODE.lock().await.unwrap()
};
let config = UsbConfig::from(controller_mode);
let mut serial_buffer = [0u8; 64];
let serial = format_no_std::show(
&mut serial_buffer,
format_args!(
"{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
raw_serial[0],
raw_serial[1],
raw_serial[2],
raw_serial[3],
raw_serial[4],
raw_serial[5],
raw_serial[6],
raw_serial[7]
),
)
.unwrap();
info!("Detected flash with unique serial number {}", serial);
trace!("Start of config");
let mut usb_config = embassy_usb::Config::new(config.vid, config.pid);
usb_config.manufacturer = Some("Naxdy");
usb_config.product = Some(if controller_mode == ControllerMode::XInput {
"NaxGCC (XInput Mode)"
} else {
match input_consistency_mode {
InputConsistencyMode::Original => "NaxGCC (OG Mode)",
InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)",
InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)",
InputConsistencyMode::PC => "NaxGCC (PC Mode)",
}
});
usb_config.serial_number = Some(serial);
usb_config.max_power = 200;
usb_config.max_packet_size_0 = 64;
usb_config.device_class = if controller_mode == ControllerMode::XInput {
0xff
} else {
0
};
usb_config.device_protocol = 0;
usb_config.self_powered = false;
usb_config.device_sub_class = if controller_mode == ControllerMode::XInput {
0xff
} else {
0
};
usb_config.supports_remote_wakeup = true;
usb_config.device_release = if controller_mode == ControllerMode::XInput {
0x0572
} else {
0x0010
};
let mut config_descriptor = [0; 256];
let mut bos_descriptor = [0; 256];
let mut msos_descriptor = [0; 256];
let mut control_buf = [0; 64];
let mut device_handler = MyDeviceHandler::new();
let mut builder = Builder::new(
driver,
usb_config,
&mut config_descriptor,
&mut bos_descriptor,
&mut msos_descriptor,
&mut control_buf,
);
builder.msos_descriptor(windows_version::WIN8_1, 2);
let msos_writer = builder.msos_writer();
msos_writer.device_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
msos_writer.device_feature(msos::RegistryPropertyFeatureDescriptor::new(
"DeviceInterfaceGUID",
msos::PropertyData::Sz(DEVICE_INTERFACE_GUID),
));
builder.handler(&mut device_handler);
match controller_mode {
ControllerMode::GcAdapter => {
let mut state = State::new();
let mut request_handler = GccRequestHandler;
let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures(
input_consistency_mode,
controller_mode,
&config,
&mut request_handler,
builder,
GcReportBuilder::default(),
&mut state,
HidReaderWriter::<_, 5, 37>::new,
);
join(usb_fut_wrapped, join(in_fut, out_fut)).await;
}
ControllerMode::Procon => {
let mut state = State::new();
let mut request_handler = ProconRequestHandler;
let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures(
input_consistency_mode,
controller_mode,
&config,
&mut request_handler,
builder,
ProconReportBuilder::default(),
&mut state,
HidReaderWriter::<_, 64, 64>::new,
);
join(usb_fut_wrapped, join(in_fut, out_fut)).await;
}
ControllerMode::XInput => {
let mut state = XInputState::new();
let mut request_handler = XInputRequestHandler;
let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures(
input_consistency_mode,
controller_mode,
&config,
&mut request_handler,
builder,
XInputReportBuilder,
&mut state,
XInputReaderWriter::<_, 32, 32>::new,
);
join(usb_fut_wrapped, join(in_fut, out_fut)).await;
}
};
}
fn calc_rumble_power(strength: u8) -> u16 {
if strength > 0 {
powf(2.0, 7.0 + ((strength as f32 - 3.0) / 8.0)) as u16
} else {
0
}
}
#[embassy_executor::task]
pub async fn rumble_task(
pin_rumble: PIN_25,
pin_brake: PIN_29,
pwm_ch_rumble: PWM_SLICE4,
pwm_ch_brake: PWM_SLICE6,
) {
let mut rumble_config: embassy_rp::pwm::Config = Default::default();
rumble_config.top = 255;
rumble_config.enable = true;
rumble_config.compare_b = 0;
let mut brake_config = rumble_config.clone();
brake_config.compare_b = 255;
let mut pwm_rumble = Pwm::new_output_b(pwm_ch_rumble, pin_rumble, rumble_config.clone());
let mut pwm_brake = Pwm::new_output_b(pwm_ch_brake, pin_brake, brake_config.clone());
let mut rumble_power = {
let strength = SIGNAL_CHANGE_RUMBLE_STRENGTH.wait().await;
calc_rumble_power(strength)
};
loop {
let new_rumble_status = SIGNAL_RUMBLE.wait().await;
debug!("Received rumble signal: {}", new_rumble_status);
if let Some(new_strength) = SIGNAL_CHANGE_RUMBLE_STRENGTH.try_take() {
rumble_power = calc_rumble_power(new_strength);
}
if new_rumble_status {
rumble_config.compare_b = rumble_power;
brake_config.compare_b = 0;
pwm_rumble.set_config(&rumble_config);
pwm_brake.set_config(&brake_config);
} else {
rumble_config.compare_b = 0;
brake_config.compare_b = 255;
pwm_rumble.set_config(&rumble_config);
pwm_brake.set_config(&brake_config);
}
}
}