SDL: Added support for pairing controllers to the Steam Controller dongle

From b71151a6975e626853dd3296e374aef91fc98924 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 14 Nov 2024 20:04:11 -0800
Subject: [PATCH] Added support for pairing controllers to the Steam Controller
 dongle

---
 src/joystick/hidapi/SDL_hidapi_steam.c | 90 +++++++++++++++++++++++++-
 1 file changed, 89 insertions(+), 1 deletion(-)

diff --git a/src/joystick/hidapi/SDL_hidapi_steam.c b/src/joystick/hidapi/SDL_hidapi_steam.c
index f14b5d229099e..336bc2ff7d7bc 100644
--- a/src/joystick/hidapi/SDL_hidapi_steam.c
+++ b/src/joystick/hidapi/SDL_hidapi_steam.c
@@ -22,11 +22,14 @@
 
 #ifdef SDL_JOYSTICK_HIDAPI
 
+#include "../../SDL_hints_c.h"
 #include "../SDL_sysjoystick.h"
 #include "SDL_hidapijoystick_c.h"
 
 #ifdef SDL_JOYSTICK_HIDAPI_STEAM
 
+#define SDL_HINT_JOYSTICK_HIDAPI_STEAM_PAIRING_ENABLED    "SDL_JOYSTICK_HIDAPI_STEAM_PAIRING_ENABLED"
+
 #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS)
 // This requires prompting for Bluetooth permissions, so make sure the application really wants it
 #define SDL_HINT_JOYSTICK_HIDAPI_STEAM_DEFAULT  false
@@ -620,6 +623,31 @@ static int ReadSteamController(SDL_hid_device *dev, uint8_t *pData, int nDataSiz
     return SDL_hid_read(dev, pData, nDataSize);
 }
 
+//---------------------------------------------------------------------------
+// Set Steam Controller pairing state
+//---------------------------------------------------------------------------
+static void SetPairingState(SDL_HIDAPI_Device *dev, bool bEnablePairing)
+{
+    unsigned char buf[65];
+    SDL_memset(buf, 0, 65);
+    buf[1] = ID_ENABLE_PAIRING;
+    buf[2] = 2; // 2 payload bytes: bool + timeout
+    buf[3] = bEnablePairing ? 1 : 0;
+    buf[4] = bEnablePairing ? 60 : 0;
+    SetFeatureReport(dev, buf, 5);
+}
+
+//---------------------------------------------------------------------------
+// Commit Steam Controller pairing
+//---------------------------------------------------------------------------
+static void CommitPairing(SDL_HIDAPI_Device *dev)
+{
+    unsigned char buf[65];
+    SDL_memset(buf, 0, 65);
+    buf[1] = ID_DONGLE_COMMIT_DEVICE;
+    SetFeatureReport(dev, buf, 2);
+}
+
 //---------------------------------------------------------------------------
 // Close a Steam Controller
 //---------------------------------------------------------------------------
@@ -967,6 +995,7 @@ static bool UpdateSteamControllerState(const uint8_t *pData, int nDataSize, Stea
 
 typedef struct
 {
+    SDL_HIDAPI_Device *device;
     bool connected;
     bool report_sensors;
     uint32_t update_rate_in_us;
@@ -977,6 +1006,11 @@ typedef struct
     SteamControllerStateInternal_t m_last_state;
 } SDL_DriverSteam_Context;
 
+static bool IsDongle(Uint16 product_id)
+{
+    return (product_id == USB_PRODUCT_VALVE_STEAM_CONTROLLER_DONGLE);
+}
+
 static void HIDAPI_DriverSteam_RegisterHints(SDL_HintCallback callback, void *userdata)
 {
     SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM, callback, userdata);
@@ -997,6 +1031,45 @@ static bool HIDAPI_DriverSteam_IsSupportedDevice(SDL_HIDAPI_Device *device, cons
     return SDL_IsJoystickSteamController(vendor_id, product_id);
 }
 
+static void HIDAPI_DriverSteam_SetPairingState(SDL_DriverSteam_Context *ctx, bool enabled)
+{
+    // Only have one dongle in pairing mode at a time
+    static SDL_DriverSteam_Context *s_PairingContext = NULL;
+
+    if (enabled && s_PairingContext != NULL) {
+        return;
+    }
+
+    if (!enabled && s_PairingContext != ctx) {
+        return;
+    }
+
+    if (ctx->connected) {
+        return;
+    }
+
+    SetPairingState(ctx->device, enabled);
+
+    if (enabled) {
+        s_PairingContext = ctx;
+    } else {
+        s_PairingContext = NULL;
+    }
+}
+
+static void HIDAPI_DriverSteam_CommitPairing(SDL_DriverSteam_Context *ctx)
+{
+    CommitPairing(ctx->device);
+}
+
+static void SDLCALL SDL_PairingEnabledHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    SDL_DriverSteam_Context *ctx = (SDL_DriverSteam_Context *)userdata;
+    bool enabled = SDL_GetStringBoolean(hint, false);
+
+    HIDAPI_DriverSteam_SetPairingState(ctx, enabled);
+}
+
 static bool HIDAPI_DriverSteam_InitDevice(SDL_HIDAPI_Device *device)
 {
     SDL_DriverSteam_Context *ctx;
@@ -1005,6 +1078,7 @@ static bool HIDAPI_DriverSteam_InitDevice(SDL_HIDAPI_Device *device)
     if (!ctx) {
         return false;
     }
+    ctx->device = device;
     device->context = ctx;
 
 #ifdef SDL_PLATFORM_WIN32
@@ -1018,7 +1092,7 @@ static bool HIDAPI_DriverSteam_InitDevice(SDL_HIDAPI_Device *device)
     HIDAPI_SetDeviceName(device, "Steam Controller");
 
     // If this is a wireless dongle, request a wireless state update
-    if (device->product_id == USB_PRODUCT_VALVE_STEAM_CONTROLLER_DONGLE) {
+    if (IsDongle(device->product_id)) {
         unsigned char buf[65];
         int res;
 
@@ -1029,6 +1103,9 @@ static bool HIDAPI_DriverSteam_InitDevice(SDL_HIDAPI_Device *device)
             return SDL_SetError("Failed to send ID_DONGLE_GET_WIRELESS_STATE request");
         }
 
+        SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM_PAIRING_ENABLED,
+                            SDL_PairingEnabledHintChanged, ctx);
+
         // We will enumerate any attached controllers in UpdateDevice()
         return true;
     } else {
@@ -1255,6 +1332,9 @@ static bool HIDAPI_DriverSteam_UpdateDevice(SDL_HIDAPI_Device *device)
                 return false;
             }
 
+            // We'll automatically accept this controller if we're in pairing mode
+            HIDAPI_DriverSteam_CommitPairing(ctx);
+
             joystick = SDL_GetJoystickFromID(device->joysticks[0]);
             ctx->connected = true;
         }
@@ -1277,6 +1357,14 @@ static void HIDAPI_DriverSteam_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joys
 
 static void HIDAPI_DriverSteam_FreeDevice(SDL_HIDAPI_Device *device)
 {
+    SDL_DriverSteam_Context *ctx = (SDL_DriverSteam_Context *)device->context;
+
+    if (IsDongle(device->product_id)) {
+        SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM_PAIRING_ENABLED,
+                               SDL_PairingEnabledHintChanged, ctx);
+
+        HIDAPI_DriverSteam_SetPairingState(ctx, false);
+    }
 }
 
 SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteam = {