Skip to content

GameSir Super Nova: gyro (and sometimes buttons) not recognized after BT reconnect #30

Description

@petegordon

Symptom

Testing the GameSir Super Nova (BT, spoofs Sony DS4 v2 054c:09cc) in-game via WebHID (petegordon/Tandemonium#305). It works great on first connect, but a disconnect→reconnect cycle lands in one of two bad states:

  • State 1 — gyro not recognized: the motion toggle shows off and can't be turned on, but the Gamepad-API joystick/buttons work.
  • State 2 — neither the joystick/buttons nor the gyro is recognized.

Easy to reproduce: the Super Nova auto-disconnects when placed on its charger and auto-reconnects over Bluetooth the instant it's picked up, so the reconnect path gets exercised constantly.

Root cause — State 1 (confirmed, fixed)

The GameSir is a dual-path device: buttons/sticks via the Gamepad API, gyro via WebHID (IMU-only DS4 report). The HID entry is bound to the slot by ControllerManager._attachMatchingPoolEntry, which only runs at claim time.

On reconnect, the Gamepad-API pad re-appears and re-claims the slot before the async WebHID connectpoolDevice finishes re-pooling the device. So the claim-time bind finds an empty pool and never retries. The HID-pool claim loop that could otherwise re-bind is gated on hasFreshButtonPress() — but the GameSir's HID interface is IMU-only (no buttons), so that gate is never satisfied. Net: claimed slot, working buttons, slot._hidEntry === nullslot.fusion/slot.driver null → gyro toggle off and un-enableable.

Fix

Add a per-frame reconciliation pass in ingestFrame that binds any pool entry to an already-claimed-but-unbound slot it matches (vid:pid OR productName), independent of button activity. Binding-only — it never creates a slot — so it's safe to run every frame. Extracted the slot-match into _findClaimedUnboundSlotForEntry (also DRY-ing the existing HID-pool dedupe). Regression test in test/manager-hid-reconcile.test.js.

State 2 — candidate causes (needs console logs to confirm)

Buttons also dead means no slot claimed the pad at all. Candidates:

  1. Orphan never recovers. On gamepad disconnect, ingestFrame calls slot.orphan() (state claimedorphan), but nothing ever transitions orphanempty. The claim loops only target state === 'empty'. A reconnect that reuses the same slot can't reclaim it → permanently stuck.
  2. Reclaim cooldown (reclaimCooldownMs 1500ms) / _recentlyReleasedByIndex keyed on a reused pad index.
  3. Ghost-pad suppression (gamepadHasFreshActivity returns false for a pad with stuck axes and no prior frame) — possible on a BT reconnect where Chrome surfaces the pad with non-centered axes before the first real input.
  4. Browser/OS simply not re-enumerating the Gamepad-API pad on that cycle (not fixable here).

Ask: next repro of State 2, grab the DevTools console (the manager logs claim/defer/skip decisions) so we can pin which of the above it is. The orphan-never-recovers leak (#1) looks like the most likely real bug and is a candidate follow-up fix.

Found while verifying petegordon/Tandemonium#305.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions