diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java index bf1ca2149d..97b4c127f4 100644 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -19,9 +19,13 @@ import android.os.*; import java.lang.Runnable; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.UUID; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { private static final String TAG = "hidapi"; @@ -37,6 +41,11 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private LinkedList mOperations; GattOperation mCurrentOperation = null; private Handler mHandler; + private int mProductId = -1; + + private static final int D0G_BLE2_PID = 0x1106; + private static final int TRITON_BLE_PID = 0x1303; + private static final int TRANSPORT_AUTO = 0; private static final int TRANSPORT_BREDR = 1; @@ -45,10 +54,13 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3"); static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + private HashMap mOutputReportChars = new HashMap(); + static class GattOperation { private enum Operation { CHR_READ, @@ -314,8 +326,38 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); + boolean bShouldStartNotifications = false; + + if (chr.getUuid().equals(inputCharacteristicTriton)) { + Log.v(TAG, "Found Triton input characteristic"); + mProductId = TRITON_BLE_PID; + bShouldStartNotifications = true; + } else if (chr.getUuid().equals(inputCharacteristicD0G)) { + Log.v(TAG, "Found D0G input characteristic"); + mProductId = D0G_BLE2_PID; + bShouldStartNotifications = true; + } else { + Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE); + Matcher matcher = reportPattern.matcher(chr.getUuid().toString()); + + if (matcher.find()) { + try { + int reportId = Integer.parseInt(matcher.group(1), 16); + + reportId -= 0x35; + if (reportId >= 0x80) { + // This is a Triton output report characteristic that we need to care about. + Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16)); + mOutputReportChars.put(reportId, chr); + } + } + catch (NumberFormatException nfe) { + Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString()); + } + } + } + + if (bShouldStartNotifications) { // Start notifications BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); if (cccd != null) { @@ -448,8 +490,16 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mIsConnected = false; gatt.disconnect(); mGatt = connectGatt(false); - } - else { + } else { + if (getProductId() == TRITON_BLE_PID) { + // Android will not properly play well with Data Length Extensions without manually requesting a large MTU, + // and Triton controllers require DLE support. + // + // 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change + // this value. It is functionally "please enable data length extensions" on some Android builds. + mGatt.requestMtu(517); + } + probeService(this); } } @@ -487,7 +537,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // Enable this for verbose logging of controller input reports //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + if (characteristic.getUuid().equals(getInputCharacteristic()) && !mFrozen) { mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); } } @@ -502,13 +552,21 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); - if (chr.getUuid().equals(inputCharacteristic)) { + if (chr.getUuid().equals(getInputCharacteristic())) { boolean hasWrittenInputDescriptor = true; BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); if (reportChr != null) { - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); + if (getProductId() == TRITON_BLE_PID) { + // For Triton we just mark things registered. + Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true); + setRegistered(); + } else { + // For the original controller, we need to manually enter Valve mode. + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + gatt.writeCharacteristic(reportChr); + } } } @@ -548,9 +606,28 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe @Override public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; + if (mProductId > 0) { + // We've already set a product ID. + return mProductId; + } + + if (mDevice.getName().startsWith("Steam Ctrl")) { + // We're a newer Triton device + mProductId = TRITON_BLE_PID; + } else { + // We're an OG Steam Controller + mProductId = D0G_BLE2_PID; + } + + return mProductId; + } + + private UUID getInputCharacteristic() { + if (getProductId() == TRITON_BLE_PID) { + return inputCharacteristicTriton; + } else { + return inputCharacteristicD0G; + } } @Override @@ -601,10 +678,29 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe writeCharacteristic(reportCharacteristic, actual_report); return report.length; } else { - //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; + // If we're an original-recipe Steam Controller we just write to the characteristic directly. + if (getProductId() == D0G_BLE2_PID) { + //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + // If we're a Triton, we need to find the correct report characteristic. + if (report.length > 0) { + int reportId = report[0]; + BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId); + if (targetedReportCharacteristic != null) { + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report)); + writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report); + return report.length; + } else { + Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16)); + } + } } + + return -1; } @Override diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index b91a8211b1..6476d4f9c4 100644 --- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -528,7 +528,13 @@ public class HIDDeviceManager { return false; } - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + // Steam Controllers will always support Bluetooth Low Energy + if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) { + return false; + } + + // Match on the name either the original Steam Controller or the new second-generation one advertise with. + return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl"); } private void close() { diff --git a/src/hidapi/android/hid.cpp b/src/hidapi/android/hid.cpp index 934912962a..e1aa7e13d7 100644 --- a/src/hidapi/android/hid.cpp +++ b/src/hidapi/android/hid.cpp @@ -91,6 +91,9 @@ extern "C" { typedef uint32_t uint32; typedef uint64_t uint64; +#define D0G_BLE2_PID 0x1106 +#define TRITON_BLE_PID 0x1303 + struct hid_device_ { @@ -438,8 +441,7 @@ public: // The Bluetooth Steam Controller needs special handling const int VALVE_USB_VID = 0x28DE; - const int D0G_BLE2_PID = 0x1106; - if ( pInfo->vendor_id == VALVE_USB_VID && pInfo->product_id == D0G_BLE2_PID ) + if ( pInfo->vendor_id == VALVE_USB_VID && ( pInfo->product_id == D0G_BLE2_PID || pInfo->product_id == TRITON_BLE_PID ) ) { m_bIsBLESteamController = true; } @@ -604,7 +606,14 @@ public: size_t nDataLen = buffer.size() > length ? length : buffer.size(); if ( m_bIsBLESteamController ) { - data[0] = 0x03; + if ( m_pInfo->product_id == TRITON_BLE_PID ) + { + data[0] = 0x45; + } + else + { + data[0] = 0x03; + } SDL_memcpy( data + 1, buffer.data(), nDataLen ); ++nDataLen; }