feat: recognize Amazon Luna Controller (wired USB) on the verified fast lane - #153
Conversation
…st lane Its wired mode reports a plain 20-byte XInput frame, byte-for-byte identical to a real Xbox 360 pad, and its interface 0 even matches the generic vendor-class 0xFF/0x5D/0x01 XInput sniff already in classifyDevice. Confirmed on hardware: every button, both triggers, both sticks, and rumble all decode/drive correctly via the existing Parser::XINPUT_360 path, end-to-end through the app into a satellite host.
|
Sorry, noticing problems.... closing. |
|
While testing this on real hardware, I found that the Amazon Luna Controller doesn't send a continuous report stream at rest the way most wired XInput-class pads do — a real Xbox 360 controller emits dozens of reports per second even sitting untouched, but Luna appears to be purely event-driven: it only sends a report when a button, stick, or trigger actually changes. I confirmed this directly: reading from the controller for 8 seconds while leaving it completely untouched produced exactly one report (presumably whatever was already buffered) followed by 40 consecutive timeouts — no further data for the full 8 seconds. This interacts badly with probeDecodable() in usb_host.cpp, which claims Direct mode by waiting up to ~4 consecutive 80ms timeouts (roughly 320ms) for a decodable report before giving up. Since Luna sends nothing at all while idle, switching to Direct mode fails with "Direct mode unavailable" unless the controller happens to be actively moving something during that exact window. Wiggling a stick or pressing a button while making the switch works reliably — the byte format itself decodes correctly via the existing XINPUT_360 parser once a report actually arrives, so this isn't a decode bug, just a probe-timing mismatch with this specific controller's idle behavior. TLDR: Direct mode works great with this change, the user just needs to be wiggling a stick while it binds or it will fail. |
|
I'll leave it to you @emir-hasanbegovic if you want to merge this, I'm not sure how many luna users you have on your app. |
|
@MichaelWilsonSNC missed this. Reviewing today! |
|
@MichaelWilsonSNC Reviewed, and your diagnosis is exactly right. Thanks for the careful hardware work, the byte-offset capture pass especially. I checked what the reference stacks do with this device. The Linux xpad driver has carried So the fix belongs on our side, and it's up as #158: the probe is now tri-state. A decoded report claims as before, traffic that never decodes still releases, and pure silence claims only when the model is in the hardware-verified table, which your row makes true for the Luna. Unverified and descriptor-sniffed models still require a decoded report. The steady-state poll loop already tolerates silence after claim (URBs just stay pending), which lines up with what you saw once bound. Your PR stays as-is apart from two mechanical things I'll push to this branch (thanks for leaving maintainer edits on): a merge with main to resolve the CHANGELOG conflict that grew under it, and moving the new row a few lines down into the third-party vendor block so the Microsoft grouping stays contiguous. Once #158 is merged and this branch is updated, would you re-verify on your hardware, no wiggle anywhere: (1) plug in while idle and let it auto-claim Direct, (2) toggle Standard to Direct while idle, (3) toggle while moving a stick? If all three bind, I'll merge. One note for the record from the research: the wired Luna has a second USB personality, |
…upport # Conflicts: # CHANGELOG.md
…158) ## Description The USB Direct attach probe treated ~320ms of endpoint silence as proof the chosen parser was wrong and released the device back to the framework. That assumption held for every current verified table entry because wired XInput-class pads stream reports at rest. The Amazon Luna Controller (wired USB, `1949:041A`, #153) is event-driven: it sends nothing until a button, stick, or trigger changes, so switching an idle Luna to Direct failed unless the user happened to be moving a stick during the probe window, and putting the model on the verified fast lane would make the plug-in auto-claim fail the same way. `probeEndpoint` (was `probeDecodable`) now distinguishes three outcomes, and `probePermitsClaim` in `usb_parsers` decides per trust level: - A decoded report, or a recognized wireless link event, claims at any trust level. - Traffic that never decodes releases at any trust level: bytes in the wrong shape are positive evidence of a wrong parser. - Pure silence claims only a model in the hardware-verified `kKnown` table. Imported and descriptor-sniffed models still require a decoded report, because for them the probe is the only verification. This mirrors what the reference stacks do with this device class: the Linux xpad driver (5.13+) and SDL both classify the Luna by ID alone and are event-driven end to end, with no idle-traffic requirement. Neither has a wake or init packet we could send instead; none exists for this protocol family. The steady-state poll loop is already silence-tolerant (interrupt URBs stay pending until data arrives; only ENODEV exits), so a claimed idle pad is healthy. Behavior for every currently verified model is unchanged, since they all stream at rest. Prepares for #153, which puts the Luna on the verified fast lane. ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit tests - [ ] Instrumented tests - [ ] Manual testing on device/emulator Native host suite 321/321 passing, including three new `ProbeRule` cases covering the outcome/trust truth table. `:app:externalNativeBuildDebug` compiles clean on both ABIs. clang-format 22.1.4 clean on all touched files. Hardware verification on an actual Luna is requested in #153 from @MichaelWilsonSNC, who has the device: idle plug-in auto-claim, idle Standard-to-Direct toggle, and toggle while active. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] Any non-obvious code explains why, not what (no new comments; rationale is in this PR and the commit message) - [x] I have made corresponding changes to documentation (`CHANGELOG.md`) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
…6.5) (#160) The 1.1.0 tag run failed in two independent jobs ([run 32032681945](https://github.com/TinkerNorth/dish-android/actions/runs/32032681945)). ## Play metadata gate `Upload AAB to Google Play` failed the pre-upload lint: every locale was missing `changelogs/10100.txt`. The notes had been staged as `10002.txt` for a 1.0.2 that never shipped, then the release went out as 1.1.0 (versionCode 10100). - Renamed `10002.txt` to `10100.txt` in all 5 locales. - Reworked the notes to also cover the Amazon Luna support (#153) that landed in this range; the header and bullets now match the release scope (Steam Controller, Luna, PDP wired Switch fix) while staying under Play's 500-char cap. - Dated the CHANGELOG `[Unreleased]` section as 1.1.0. Validated locally: `EXPECTED_VERSION_CODE=10100 python scripts/check_play_metadata.py` reports 0 errors (the 512x512 icon warnings are the known open store item). ## Harden job: cosign keyless signing `cosign sign-blob` died with `fetching ambient OIDC credentials: invalid character 'u' looking for beginning of value`: the pinned cosign 2.4.1 (Nov 2024) failed to parse a non-JSON response from the Actions OIDC token endpoint. The same job passed on July 25, so this broke on the service side, not in the workflow. - Bumped the pinned cosign to 2.6.5, the current release of the maintained v2 line (Aug 6, 2026, includes the GHSA-fx35-mq7g-6g98 fix). The `sign-blob --output-signature/--output-certificate` CLI used here is unchanged in v2; v3 was deliberately avoided because it changes sign-blob's output format. - `COSIGN_SHA256` verified by downloading `cosign-linux-amd64` and matching it against the upstream `cosign_checksums.txt`. ## Re-releasing after merge The 1.1.0 tag still points at the broken snapshot (`d3006c0`). After merging: ``` git fetch origin git tag -f 1.1.0 origin/main git push origin 1.1.0 --force ``` The tag push reruns the release workflow with these fixes in the tagged tree. One remaining risk outside this PR: if the Play app record has never had a build uploaded manually, the upload step can still fail; the Play API cannot create the first build of an app (see the note in release.yml).
…y) (#162) ## Description `decodeXInput360` (the shared decoder for every `Parser::XINPUT_360` / `XINPUT_360_WIRELESS` device in the `kKnown` table — genuine Xbox 360 controllers and its many licensed clones, plus the Amazon Luna Controller) never read byte 3 bit `0x04` of the wired report. That bit is the standard Guide/Xbox button position in the XInput wired protocol (the same bit Linux's `xpad` driver treats as `BTN_MODE`); the other bits in that byte (`0x01`/`0x02` bumpers, `0x10..0x80` face buttons) were already decoded, but `0x04` was dead, so any pad's center/Guide button silently produced no input at all instead of `XUSB_GUIDE`. Found on real hardware: a live raw-report capture from an Amazon Luna Controller (USB VID:PID `1949:041A`, wired mode, from #153) showed byte 3 going to exactly `0x04` and nothing else when its center button was pressed, with A/B/X/Y independently confirmed against their own existing bits (`0x10`/`0x20`/`0x40`/`0x80`) in the same capture. Fix is a single added line, decoding that bit the same way every other button in the function already is, with no change to the report layout or any other bit. No wire-protocol changes; nothing to coordinate with `satellite`, `dish-linux`, or `dish-mac`. Scope note for review: this is a generic decoder fix, not a Luna-only special case, so it affects every device riding `XINPUT_360`/ `XINPUT_360_WIRELESS` in `kKnown` (Mad Catz, PDP, Hori, PowerA, Afterglow, dance pads, fight sticks, etc.). Pads without a physical Guide button never assert that bit in hardware, so this is a no-op for them. The only theoretical residual risk is an unlicensed clone that reused bit `0x04` for some other custom function instead of leaving it unused; if one exists, worst case is that button now also raises Guide/Home, not a crash or corrupted input. ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit tests - [ ] Instrumented tests - [x] Manual testing on device/emulator Added `Decode.Xbox360WiredGuideButtonSetsGuideBit` to `usb_parsers_test.cpp`, asserting byte 3 `0x04` sets `XUSB_GUIDE` through `decodeReport(Parser::XINPUT_360, ...)`. Full native host suite: 241/241 passing (`ctest` across all `app/src/test/cpp` binaries). Manual testing: real Amazon Luna Controller, wired directly to an Android phone running dish, Direct mode. Captured raw USB interrupt reports for A/B/X/Y and the center button before the fix (center button produced no bit at all in `wButtons`); after the fix, rebuilt and reinstalled the app and confirmed the center button now reaches the Guide/Home action end-to-end through a `satellite` host. ## Checklist - [x] My code follows the project's style guidelines (ktlint/detekt untouched — no Kotlin changed; clang-format skipped locally, not installed at the pinned 22.1.4 version, but the change is a single line matching the exact formatting of every neighboring line in the same `if` chain, and CI's own clang-format gate will catch it if not) - [x] I have performed a self-review of my code - [x] Any non-obvious code explains why, not what (no comment added — the fix reads identically to every other bit check in the same function, so a comment here would be inconsistent with the rest of the file) - [x] I have made corresponding changes to documentation (`CHANGELOG.md`, under `[Unreleased]`) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes (241/241 native host tests, `assembleDebug`) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
A dual-mode pad already connected over Bluetooth that is then plugged in over USB is tracked by UsbGamepadManager but never surfaces anywhere: the attach-time auto path pick deliberately stays silent without permission, so the entry parks in Routed with no framework id and no synthetic, and no card offers the wired path (reported on the Amazon Luna Controller in PR #153 discussion). These tests pin that mechanism at the FSM level so the fix has the behavior it builds on spelled out; the FSM itself is intentionally left unchanged.
A dual-mode pad connected over Bluetooth and then plugged in over USB kept streaming over Bluetooth with nothing on its card acknowledging the cable: transport resolution lets Bluetooth win, Bluetooth cards hide the whole Standard/Direct section, and the tracked USB entry sits in Routed with no framework or synthetic device representing it. The only route to wired was knowing to walk the setup guide's Wired branch (reported on the Amazon Luna Controller in the PR #153 discussion). The card now surfaces that tracked-but-unrepresented USB entry: a "USB available" pill on the connection row plus a "Use wired" action that opens the wired setup flow, which already owns enumeration, the permission prompt, and claim recovery. Association is by vendor, since dual-mode pads may enumerate a different product id per link, and by exact framework id for pads that report the same id on both links. Nothing auto-switches: plugging in just to charge while playing over Bluetooth behaves exactly as before, and the path FSM is untouched.
… plugged in (#163) ## Description A pad connected over Bluetooth and then plugged in over USB showed only a Bluetooth badge with no way to reach the wired path from the card (#153 comments, Amazon Luna Controller). The plugged pad is tracked by UsbGamepadManager but has no framework or synthetic device, so nothing rendered it. - New `wiredUsbPresentFor` in `ui/main/WiredUsbPresence.kt`: detects that tracked-but-unrendered USB entry and ties it to the Bluetooth card by vendor id, or by exact framework id when both links report the same VID:PID. - `PathCard` gains `wiredSwitchAvailable`; the card shows a "USB available" pill and a "Use wired" button that opens the existing wired setup flow. - No auto-switching. Path FSM unchanged. Strings added in all six locales, CHANGELOG under [Unreleased]. Commit 1 adds characterization tests that pass on `main` and pin the pre-fix behavior; commit 2 is the fix plus its tests. ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit tests - [ ] Instrumented tests - [ ] Manual testing on device/emulator 28 new tests: `UsbDualPresenceGhostTest` (9, FSM behavior before and after, unchanged), `WiredUsbPresenceTest` (13, the new signal), `PathCardMapperTest` (+6, card gating). ktlintCheck and detekt clean locally; full suite runs in CI. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] Any non-obvious code explains why, not what - [x] I have made corresponding changes to documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
|
@MichaelWilsonSNC I merged a fix for the bluetooth. I meant to just check it but I merged it by accident. Feel free to test the bluetooth thing off of main |
|
That was the intention, try it out, does it work as expected? any feedback? |
|
Sorry I think I see what you mean now. If the controller is connected via Bluetooth to the phone then it won't change until you plug it in via usb. If it is plugged in via usb and still showing up via Bluetooth, that is unexpected. It might be a quirk of that controller, or your phone, or both. I'll test a little more on my end with other devices. Can you confirm the state of your controller connection to your device before the app (is it connected via bluetooth?) and when you see that, is it connected via usb ? Note also that nothing gets "used" until you complete the binding. |
|
My understanding was that this is a case of the Luna specifically: The Amazon Luna Controller stays paired, input keeps arriving over BT, and the cable is only power. dish never initiates the BT link either, so there's nothing on our side to tear down. You need to "claim" the wired before you can confirgure the binding |
|
Yep, you're right, this all makes sense to me. Thanks for the tweaks, appreciate it. I think you got a really cool project here and I've been happy to submit 2 cents on it. I originally bought the Luna with the hopes that somebody would be able to use the onboard wifi for this exact idea - utilize wifi to make a direct connection to your gaming PC. Your app has realized that, and in a controller agnostic way. 🤘 Cheers! |
|
Super happy to see it being used! Feel free to recommend features! |
|
Also a review on the app store would be delicious! |


Description
Adds the Amazon Luna Controller (wired USB mode) to the verified
fast-lane
kKnowntable inusb_parsers.cpp, riding the existingParser::XINPUT_360decoder unchanged. Its wired report is a plain20-byte XInput frame, byte-for-byte identical to a real Xbox 360
controller, and interface 0's descriptor (class 0xFF, subclass 0x5D,
protocol 0x01) is the exact vendor-class triple
classifyDevice()already sniffs generically — so the device was already decodable
before this change, just unnamed and not on the verified fast lane
(requiring a manual "Direct" opt-in instead of auto-claim).
No wire-protocol changes; nothing to coordinate with
satellite,dish-linux, ordish-mac.Device confirmed: Amazon Luna Controller (Lab126, Inc.), USB VID:PID
1949:041A, wired mode.Type of Change
How Has This Been Tested?
Existing
usb_parsers_test.cpphost suite (174/174 passing, unchanged —no new decode logic was added; individual
KnownDevicerows aren'tindependently unit-tested, only parser families, per existing
convention).
Manual test matrix, wired directly to an Android phone running dish,
Direct mode, verified twice — once via a raw USB capture against the
same byte offsets
decodeXInput360/buildRumbleReportexpect, andonce end-to-end through the real app into a
satellitehost's virtualXbox 360 controller:
clicks, Start/Back) — correct on both passes
DirectInput test panel shows them combined onto one Z-axis, which is
standard Xbox-360 behavior, not specific to this change)
via raw USB report injection and via the app's Diagnostics rumble
tester
Checklist
Android lint all green locally; clang-format skipped locally —
not installed at the pinned 22.1.4 version — but the change is a
single-line table row matching the exact formatting of every
neighboring row, and CI's own clang-format gate will catch it if not)
every other row in this table is undocumented data, so a
per-row comment would be inconsistent with the rest of the file)
CHANGELOG.md)feature works — not applicable, see note above; hardware
verification stood in for a new automated test
(174/174 native host tests, JVM unit tests,
assembleDebug)