bluerobotics / bluerobotics/cockpit

Stale joystick input keeps driving the vehicle after a Bluetooth controller reconnect (Electron/SDL)

Open
#2,863 2 comments 0 reactions 1 assignee Claimed by @rafaellehmkuhl View on GitHub
safety
Dominant language
TypeScript
Stars
198
Forks
63
Avg merge
2d 7h
Merged PRs (30d)
57

Description

### Bug description

**Safety-relevant.** After a Bluetooth game controller briefly disconnects and reconnects while the operator is holding a stick deflected, the standalone Electron app (SDL controller path) keeps sending `MANUAL_CONTROL` messages containing the **pre-disconnect** stick values even after the operator releases the stick. The vehicle keeps moving (e.g. keeps yawing) although the physical stick is centered. Because `MANUAL_CONTROL` messages keep flowing at the normal rate, the autopilot's pilot-input-loss failsafe never triggers, so nothing stops the vehicle.

What we traced internally:

1. When the Bluetooth link drops, the OS can keep the HID node alive for a while, so the SDL controller instance is **not** reported as closed. The main-process polling loop (`src/electron/services/joystick.ts`) keeps reading the instance every 50 ms and keeps re-sending the last (now frozen) axis values to the renderer — the cleanup path only runs once `instance.closed` becomes true, which for Bluetooth can take a very long time or not happen at all during the session.
2. When the controller reconnects, SDL enumerates it as a **new device with a new device id**, so two device streams now reach the renderer: the live one, and a "zombie" one still repeating the frozen pre-disconnect axes at 20 Hz.
3. The renderer-side SDL state handler (`onElectronSDLControllerJoystickStateChange` in `src/libs/joystick/manager.ts`) forwards **every** incoming frame unconditionally — unlike the browser Gamepad API path, which compares each frame against the previous state before emitting. The two streams therefore alternately overwrite the manual-control state, and the frozen deflected values win roughly half the time.
4. Result: after the stick is released, the transmitted `MANUAL_CONTROL` alternates between the real value (0) and the frozen deflection, and the vehicle keeps moving until the zombie device eventually disappears.

### Steps to reproduce

1. Standalone Electron app (SDL path active), Bluetooth gamepad, vehicle connected and armed, joystick forwarding enabled.
2. Hold a stick deflected (e.g. full yaw) and keep holding it through the next steps.
3. Turn Bluetooth off (or walk out of range).
4. Turn Bluetooth back on shortly after, so the controller reconnects.
5. Keep holding the stick for a moment, then release it.

**Expected:** the vehicle stops when the stick is released.
**Actual:** the vehicle keeps moving. Observing the mavlink2rest websocket shows `MANUAL_CONTROL` messages alternating between the released value (0) and the pre-disconnect deflection.

### Primary pain point(s)

- The vehicle continues to move with no operator input — effectively uncommanded motion that is hard to stop without disarming.
- The autopilot's lost-manual-control failsafe never triggers, because messages keep flowing at the normal rate.
- Nothing in the UI indicates that a stale device stream is still being consumed; the joystick screen looks normal.

### Additional context

- Windows 11, standalone Electron build, Bluetooth Xbox-style controller.
- Observed on a downstream build; the relevant code paths (unconditional 50 ms state re-emit and `instance.closed`-only cleanup in `src/electron/services/joystick.ts`, no per-frame change comparison in the SDL handler in `src/libs/joystick/manager.ts`) are unchanged on current `master`.
- The browser version does **not** reproduce this, consistent with the Gamepad API path having per-frame change comparison while the SDL path does not.
- Related but distinct from #215 (there the joystick was unplugged via USB, messages stopped and the failsafe worked; here messages keep flowing, so the failsafe never engages).

### Prerequisites

- [x] I have checked to make sure that a similar issue has not already been filed or fixed.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.