Repository navigation
Support the NSO GameCube controller - #30
Conversation
JoeGeC
left a comment
There was a problem hiding this comment.
Thanks for this, it's a really thorough PR. Fixing it at the product ID and having one ControllerModel instead of guessing the side in several places is a big improvement. So is the shared SpiReadCommand/SpiReadReply, and the trigger calibration that learns only from a held first stop is a nice way round the hardware. The docs are exactly how I'd want them too: measurements dated, with the hardware named. A few things before I merge:
1. Rebase onto main. It conflicts with the clone-support and GATT-queue commits in JoyconConnection.kt, PacketParser.kt, ConnectionPool.kt and JoyconInput.kt. JoyconConnection needs the most care, since both changes touch the init writes. Please also check how the 0x3C/0x3D trigger bytes behave on the console-protocol path the clone commit added.
2. Eden: ZL also clicks the left stick (I think). ZL and Z move onto the stick clicks, but the GameCube body reuses the Pro layout, which still binds LStickClick → LS and RStickClick → RS. So in Eden, button_zl and button_lstick both end up on 106, and pressing ZL would fire ZL plus L3 (Z the same with R3). The mapping editor already leaves LS/RS out of physicalButtons(GAMECUBE), so the default layout disagrees with the editor. Returning null from emittedFor(GAMECUBE) for LS/RS, or dropping them from the GameCube entries in SwitchProMapping, would fix it. A test along the lines of EdenGamepadConfigTest asserting no button_lstick line would pin it down.
3. The controller type menu. I like having an override, but I'd rather it didn't add a full-width picker to every unassigned card, since Auto is right almost every time. Could it move to the type label in the card header instead?
- Make the header's type label ("GameCube", "Pro", "Left"…) tappable, with a small ▾, and open the menu from there using
PanelDropdownMenu. When detection works, the card looks as it does today. - When the model is
UNKNOWN, show aWarningBoxon the card ("Type not recognised — tap the name to set it"), so it only asks when it needs to. - Put Detect automatically first in the menu instead of "Auto: X", and mark a manually set type (e.g. "Pro Controller · set manually") so it's easy to spot and undo.
- Add GameCube controller to the choices. That means storing the override as a
ControllerModelrather than aSide. As it is, a misdetected GameCube controller can only be forced to Pro, and then loses analog triggers, its layouts and its Dolphin port.
Smaller things, take or leave:
- The saved overrides start out empty (
stateIn(..., emptyMap())), so a controller found before DataStore emits connects on Auto. Waiting for the first emission before connecting would avoid that. JoyconConnection.factoryis written from BLE callbacks and read inoverrideTypeon the main thread. It should probably be@Volatile.EdenGamepadConfig.sideForandEdenDsuConfig.sideForduplicatePlayerState.joyconSide(). Since you're adding a line to each, it'd be nice to have both calljoyconSide().- DSU still sends L2/R2 as on or off and never sees the analog travel. That's fine for now, but worth a line in
virtual-gamepad.md.
The NSO GameCube controller advertises product ID 0x2073, which the scanner didn't know. It connected as an unknown side, and a lone unknown controller is treated as a sideways Joy-Con. So its stick and D-pad were turned 90 degrees, its C-stick was never decoded, L + R didn't assign it, and it played as whichever Joy-Con the first button pressed suggested. The scanner now names a ControllerModel from the product ID. The GameCube controller reports in the Pro Controller's layout, so it's parsed and assigned as one: one body, both sticks. It shares nothing else with the Pro. For button mapping it's a body of its own, beside a pair and a lone Joy-Con, with its own layouts and saved bindings. Its default puts Z on its Z button (reported as ZR) and the triggers on L/R. The mapping editor offers only the buttons it has, with ZR shown as Z. Dolphin setup configures a GameCube player and still skips Pro players, as before. Joy-Con pairs and Pro Controllers keep their layouts exactly as they were. Each controller card now has a Controller type menu: Auto, Pro Controller, Left or Right Joy-Con. This covers any controller whose advertisement names no type or the wrong one. The choice is stored per BLE address, which captures show is a fixed public address, so it applies again on reconnect. On a connected controller it takes effect at once: the parser reads the new side's sticks, and stick calibration starts over. A controller assigned to a player is unassigned first, since its slot was checked against the old type. Analog L/R now reach Dolphin. The controller reports trigger travel at 0x3C/0x3D. TriggerCalibrator maps it from the factory zero (SPI 0x013140) to the first stop, where analog travel ends, and drives the virtual gamepad's trigger bytes with it. Dolphin setup binds L-Analog and R-Analog to those axes and keeps the click at the second stop on L/R. Every other controller keeps ZL/ZR on the trigger bytes. On this controller ZL and Z also move off BUTTON_L2/R2 and onto the stick clicks, which it doesn't have. On a Retroid Pocket Nova those keys followed the trigger axes, so pulling R pressed Z and Z itself did nothing. With the move, both work. At connect it also reads its factory stick calibration (0x0130A8 for the main stick, 0x0130E8 for the C-stick, decoded as the Linux and SDL Switch 2 drivers do), so its sticks start centred with their full range. Learning still applies on top, and takes over when the flash is unset or implausible. Only the GameCube controller's values have been checked against its sticks, so Joy-Con 2 and Pro Controllers aren't read and keep learning theirs. Tested on a Retroid Pocket Nova (Android 13), with three GameCube controllers connected at once as players 1–3 in Dolphin. Recognition, L + R assign, both sticks, the D-pad, every button, analog L/R with a separate click, and the type menu all behave. Packet captures from the three controllers back the offsets and calibration values in protocol.md. Four controllers and Android 14+ are untested. Rumble is left out: the app has no rumble path yet. Closes JoeGeC#27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dd6e9b8 to
8ca60ab
Compare
A GameCube controller's ZL and Z travel on the stick-click bits, which it has no buttons for. Its layouts still bound the stick clicks there, so each press fired twice: in Eden, ZL also pressed L3 and Z pressed R3, on both the virtual gamepad and DSU. In the Joy-Con Wii layout, Z pressed Two and Recenter at once. emittedFor now resolves LS and RS to nothing for a GameCube controller, so no config writer can bind them, whatever the mapping says. Its Switch Pro layout drops the stick-click targets, and its Joy-Con Wii layout leaves Recenter unbound: there is no stick click to put it on, and the Wii layout's default, R, is already One there. The Joy-Con layout's no-double-fire test now checks the buttons each body emits rather than the ones it names. That catches this case and caught the R clash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The analog triggers already drive the trigger axes, so the bumper keys are where Android expects ZL and Z: Z is the GameCube's bumper. They move from the stick clicks to BUTTON_L1/BUTTON_R1, and the trigger clicks at the bottom of L/R take the free stick clicks instead. Nothing goes on BUTTON_L2/R2, which can follow the trigger axes. Dolphin and Eden bind whatever each button sends, so they play as before. This changes what apps the app doesn't configure see by default: a full trigger pull no longer presses a bumper, and Z no longer presses the right stick in. Tested on a Retroid Pocket Nova (Android 13) with an NSO GameCube controller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Joy-Con layout puts − on the controller's own − and Recenter on a stick click. A GameCube controller has neither, so − pressed nothing and Recenter was left unbound. − now takes X, as it already does on a lone right Joy-Con, and Recenter takes Y, which was free. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The override was stored as a Side, so a GameCube controller the scanner didn't recognise could only be forced to Pro Controller. That gave it a Pro's layouts, with no analog triggers and no C-stick calibration. The override is now a ControllerModel: Left or Right Joy-Con, Pro Controller or GameCube controller. The model in effect is the override or, without one, what the advertisement named, and the side follows from it. ConnectedJoycon carries both, so the menu's Auto entry still names what was detected. Switching a connected controller to GameCube also reads its factory calibration, as connecting as one does. Overrides saved as a side no longer parse and are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The type picker was a full-width dropdown on every unassigned card, recognised or not. The type label at the top of the card now opens it instead, so a recognised controller costs no extra row. The menu lists Detect automatically first, sub-labelled with what was detected, then the four types. A manual choice shows SET MANUALLY beside the label. Only an unrecognised controller gets a warning, and tapping it opens the same menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The saved type overrides started as an empty map until DataStore answered, so a controller found in that window connected as its advertised type and kept it. A scan now starts once they have loaded: at once in the usual case, when they already have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It is written on the binder thread a reply arrives on and read on the main thread when the type changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EdenGamepadConfig, EdenDsuConfig and DolphinGcpadConfig each kept their own copy of PlayerState.joyconSide(), so the GameCube case had to be added three times. They now call it. Dolphin's GameCube setup still leaves a Pro Controller to the player, now under one name in both places it checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The console fallback takes over after 1.5 s of silence on the common channel, or on an SMP security request. The console report has no trigger bytes and its parser knows only the Joy-Con's buttons, so a GameCube controller moved there would lose its triggers. It answers on the common channel, so this hasn't been seen, but nothing stopped it. Now a controller with analog triggers never starts a console session, and on a security request it goes on to the common channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both rules lived inside Android classes no unit test can build. They now sit in ConsoleFallback and StoredControllerModel, with tests: a GameCube controller never falls back to the console protocol, and a side saved by an earlier build reads as no override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bc4d180 to
3a999f1
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough review! Everything's addressed, and I've updated the description to match. Rebased onto
Eden double-binding: good catch, and it was wider than Eden. ZL and Z travelled on the stick-click bits, so any layout binding a stick click fired twice: Eden over both the pad and DSU, and in the Joy-Con Wii layout, Z pressed both Two and Recenter. Bumpers (not something you asked for): ZL and Z are now on Type menu: the override is a Console protocol and trigger bytes: the GameCube controller answers on the common channel, so in practice it never reached the console path. To rule it out, DSU: its L2/R2 analog bytes stay 0 for this controller, because the encoder fills them from buttons (0 or 255). Sending real travel there would be a small follow-up if you want it. Tested in Dolphin on a Retroid Pocket Nova (Android 13) with NSO GameCube controllers up to |
JoeGeC
left a comment
There was a problem hiding this comment.
Thanks, this is looking really good. I installed it on an AYN Thor and the type menu works nicely. Two changes before merge, both about the GameCube controller showing up as a Pro Controller:
1. Label it as a GameCube controller everywhere, not just on the assignment card
The assignment card says GameCube, but once the controller is assigned it reads "Pro" again:
PlayerMappingCard.kt,ControllerChips: the mapping screen chip usesR.string.controller_profor anyhasProplayer.CompactPlayerRow.kt:62: same in the compact view.
Please give these a GameCube label when hasGameCube. A shared helper for "which label does this player's controller get" would stop the three places drifting apart again.
2. Show only the GameCube controller's own buttons in the detailed view
ControllerLayout.kt sends any hasPro player to ProControllerLayout, which draws inputs the GameCube controller doesn't have: GL/GR, −, and the LS/RS click on the stick cards. Its Z also shows as "ZR". Keep the overall layout the same, but for a GameCube controller:
- leave out GL, GR, − and the stick clicks
- label ZR as "Z", as the mapping editor already does
Driving this from one list of the buttons each model has would be the cleanest way. MappingOptions.physicalButtons(GAMECUBE) already has that list, so it's worth sharing rather than writing it out a second time.
… listed The assignment card said GameCube, but once assigned, the compact player row and the mapping card said Pro for any Pro-shaped body. All three now name a controller by its model through the same exhaustive when, so a new model fails to build until each has a word for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It used the Pro Controller's layout as is: GL, GR, − and the stick clicks, with Z labelled ZR. GameCubeButtons in core:model now holds what it has and its Z marking; the detailed view leaves out the rest and marks Z, and the mapping editor offers from the same list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L and R are its triggers, so they head the shoulder columns, the place ZL and ZR take on a Pro Controller. The two columns now share one ShoulderColumn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both done:
Labels: the three call sites live in Detailed view: Tested on a Retroid Pocket Nova (Android 13): an NSO GameCube controller is labelled GameCube on the compact row and the mapping card, and its detailed view shows only its own buttons, with L/R above ZL/Z and each lighting on press. A Pro Controller and left and right Joy-Cons still show and behave as before. |
Dolphin passes an uncalibrated stick straight through, so a full tilt reaches GameCube ±127, where a real pad's gate stops near ±101 on the main stick and ±92 on the C-stick. Every push lands about a quarter further than on hardware, so walking slowly or aiming finely is hard: a light push already runs, and a scope overshoots. Dolphin for Android has no Calibrate button, and nothing else sets it. Dolphin setup now writes `Calibration = 100.00`, a full circle, for each stick it binds. Dolphin then maps the stick's rim onto its default Gate Size octagon: 79.37% for the main stick and 72.21% for the C-stick, values Dolphin averaged from genuine controllers. A full push still reaches the real gate. Sources are Dolphin's GCPadEmu.h and StickGate.cpp, and Dolphin PR #8963. Setup still leaves a Pro Controller player's section to the player. It only adds the two calibration keys there, keeping the bindings they made, and only to a section that already exists. A new Dolphin section in Settings has a Calibrate sticks switch. It's on by default, for anyone who prefers the old full-range feel, and takes effect at the next Set up. The README covers the setting and adds a troubleshooting row. Tested on a Retroid Pocket Nova (Android 13) with NSO GameCube controllers, using the GameCube controller support from JoeGeC#30: walking and scope aiming feel like an original controller. Joy-Con 2 and Pro Controllers are untested. The hand-mapped step skips a GameCube controller player, since setup writes their whole section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Calibrate Dolphin's sticks so they stop at the GameCube gate Dolphin passes an uncalibrated stick straight through, so a full tilt reaches GameCube ±127, where a real pad's gate stops near ±101 on the main stick and ±92 on the C-stick. Every push lands about a quarter further than on hardware, so walking slowly or aiming finely is hard: a light push already runs, and a scope overshoots. Dolphin for Android has no Calibrate button, and nothing else sets it. Dolphin setup now writes `Calibration = 100.00`, a full circle, for each stick it binds. Dolphin then maps the stick's rim onto its default Gate Size octagon: 79.37% for the main stick and 72.21% for the C-stick, values Dolphin averaged from genuine controllers. A full push still reaches the real gate. Sources are Dolphin's GCPadEmu.h and StickGate.cpp, and Dolphin PR #8963. Setup still leaves a Pro Controller player's section to the player. It only adds the two calibration keys there, keeping the bindings they made, and only to a section that already exists. A new Dolphin section in Settings has a Calibrate sticks switch. It's on by default, for anyone who prefers the old full-range feel, and takes effect at the next Set up. The README covers the setting and adds a troubleshooting row. Tested on a Retroid Pocket Nova (Android 13) with NSO GameCube controllers, using the GameCube controller support from #30: walking and scope aiming feel like an original controller. Joy-Con 2 and Pro Controllers are untested. The hand-mapped step skips a GameCube controller player, since setup writes their whole section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Always calibrate Dolphin's sticks, and keep a calibration already set Review feedback: the Calibrate sticks switch only served someone who prefers the twitchier full-range feel, and turning it off never undid the keys setup had added to a Pro Controller's section. Setup now always calibrates, and the switch, its setting and its use case are gone. Setup keeps a Calibration the section already has instead of overwriting it, in a Joy-Con section it writes as well as a Pro Controller's. A player who calibrated by hand keeps it across Set up. The full-circle constant moves next to STANDARD_CONTROLLER, out from under the hat-switch comment. A duplicate Pro Controller test goes, and two tests cover a kept calibration. The README drops the settings entry and keeps the troubleshooting row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The NSO GameCube controller advertises product ID 0x2073, which the scanner didn't know. It connected as an unknown side, and a lone unknown controller is treated as a sideways Joy-Con. So its stick and D-pad were turned 90 degrees, its C-stick was never decoded, L + R didn't assign it, and it played as whichever Joy-Con the first button pressed suggested.
The scanner now names a
ControllerModelfrom the product ID. The GameCube controller reports in the Pro Controller's layout, so it's parsed and assigned as one: one body, both sticks.It shares nothing else with the Pro. For button mapping it's a body of its own, beside a pair and a lone Joy-Con, with its own layouts and saved bindings. Its GameCube default puts Z on its Z button (reported as ZR) and the triggers on L/R. The mapping editor offers only the buttons it has, with ZR shown as Z. It has no stick clicks, so no layout or saved binding can use them. In the Joy-Con Wii layout, which puts − on − and Recenter on a stick click, it gets − on X and Recenter on Y instead. Dolphin setup configures a GameCube player and still leaves a Pro player's section alone. Joy-Con pairs and Pro Controllers keep their layouts exactly as they were.
Analog L/R reach Dolphin. The controller reports trigger travel at 0x3C/0x3D.
TriggerCalibratormaps it from the factory zero (SPI 0x013140) to the first stop, where analog travel ends, and drives the virtual gamepad's trigger bytes with it. Dolphin setup binds L-Analog and R-Analog to those axes, and the click at the second stop stays its own button. DSU carries no trigger travel: its packet fills the analog bytes from buttons. Every other controller keeps ZL/ZR on the trigger bytes.Its other shoulder buttons move so each lands where Android expects it: ZL and Z on
BUTTON_L1/BUTTON_R1, and the trigger clicks onBUTTON_THUMBL/BUTTON_THUMBR, which it has no stick clicks for. Nothing goes onBUTTON_L2/R2: on a Retroid Pocket Nova those keys followed the trigger axes, so with Z there, pulling R pressed Z and Z itself did nothing. Dolphin and Eden bind whatever each button sends; this decides what apps the app doesn't configure see.At connect it also reads its factory stick calibration (0x0130A8 for the main stick, 0x0130E8 for the C-stick, decoded as the Linux and SDL Switch 2 drivers do), so its sticks start centred with their full range. Learning still applies on top, and takes over when the flash is unset or implausible. Only the GameCube controller's values have been checked against its sticks, so Joy-Con 2 and Pro Controllers aren't read and keep learning theirs.
A controller whose advertisement names no type, or the wrong one, can be corrected from its card: tapping its type opens a menu with Detect automatically first, then Left or Right Joy-Con, Pro Controller or GameCube controller. The choice is a
ControllerModelstored per BLE address, which captures show is a fixed public address, so it applies again on reconnect. Scanning waits for the saved choices to load, so none is missed. On a connected controller it takes effect at once: the parser reads the new side's sticks, stick calibration starts over, and switching to GameCube reads its factory calibration. A controller assigned to a player is unassigned first, since its slot was checked against the old type. A manual choice is marked on the card, and only an unrecognised controller gets a warning.A controller with analog triggers never falls back to the console protocol. Its report has no trigger bytes, and the GameCube controller answers on the common channel anyway.
Tested in Dolphin on a Retroid Pocket Nova (Android 13), with three GameCube controllers connected at once as players 1–3. Recognition, L + R assign, both sticks, the D-pad, every button, analog L/R with a separate click, the bumper layout, and the type menu all behave, including switching a controller to Pro and back to GameCube. Packet captures from the three controllers back the offsets and calibration values in protocol.md. Eden is untested on hardware: its config is covered by unit tests only. Four controllers and Android 14+ are untested, and scanning waiting for saved types has no unit test because
Joycon2Managerbuilds its own scanner.Rumble is left out: the app has no rumble path yet.
Closes #27