From 1998b650452bdf0bee5209e20e4715b4295abe8c Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Wed, 12 Nov 2025 11:32:32 -0800 Subject: [PATCH] Added support for the new Steam Controller --- VisualC/SDL/SDL.vcxproj | 9 +- VisualC/SDL/SDL.vcxproj.filters | 4 + Xcode/SDL/SDL.xcodeproj/project.pbxproj | 4 + src/hidapi/ios/hid.m | 283 +++++++--- src/joystick/SDL_gamepad.c | 3 + src/joystick/SDL_joystick.c | 6 + src/joystick/SDL_joystick_c.h | 3 + src/joystick/controller_list.h | 10 +- src/joystick/controller_type.h | 2 + src/joystick/hidapi/SDL_hidapi_steam_triton.c | 532 ++++++++++++++++++ src/joystick/hidapi/SDL_hidapijoystick.c | 3 + src/joystick/hidapi/SDL_hidapijoystick_c.h | 2 + .../hidapi/steam/controller_constants.h | 8 +- .../hidapi/steam/controller_structs.h | 225 ++++++-- src/joystick/usb_ids.h | 2 + 15 files changed, 970 insertions(+), 126 deletions(-) create mode 100644 src/joystick/hidapi/SDL_hidapi_steam_triton.c diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 7af7132b29..f5a1612f4d 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -82,16 +82,16 @@ C:\Program Files %28x86%29\Microsoft DirectX SDK %28June 2010%29\Lib\x86;$(LibraryPath) - $(ProjectDir)/../../src;$(IncludePath) + $(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath) - $(ProjectDir)/../../src;$(IncludePath) + $(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath) - $(ProjectDir)/../../src;$(IncludePath) + $(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath) - $(ProjectDir)/../../src;$(IncludePath) + $(ProjectDir)/../../src;$(ProjectDir)/../../src/core/windows;$(IncludePath) @@ -625,6 +625,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 77bd59d5fb..7e5aa144c7 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -1660,6 +1660,10 @@ + + joystick\hidapi + + core\windows diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index bd3c41722c..6d57ae7e17 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -415,6 +415,7 @@ F386F6F92884663E001840AA /* SDL_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = F386F6E62884663E001840AA /* SDL_utils.c */; }; F388C95528B5F6F700661ECF /* SDL_hidapi_ps3.c in Sources */ = {isa = PBXBuildFile; fileRef = F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */; }; F39344CE2E99771B0056986F /* SDL_dlopennote.h in Headers */ = {isa = PBXBuildFile; fileRef = F39344CD2E99771B0056986F /* SDL_dlopennote.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F38C72492CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c in Sources */ = {isa = PBXBuildFile; fileRef = F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */; }; F395BF6525633B2400942BFF /* SDL_crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = F395BF6425633B2400942BFF /* SDL_crc32.c */; }; F395C1932569C68F00942BFF /* SDL_iokitjoystick_c.h in Headers */ = {isa = PBXBuildFile; fileRef = F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */; }; F395C19C2569C68F00942BFF /* SDL_iokitjoystick.c in Sources */ = {isa = PBXBuildFile; fileRef = F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */; }; @@ -998,6 +999,7 @@ F386F6E62884663E001840AA /* SDL_utils.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_utils.c; sourceTree = ""; }; F388C95428B5F6F600661ECF /* SDL_hidapi_ps3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps3.c; sourceTree = ""; }; F39344CD2E99771B0056986F /* SDL_dlopennote.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_dlopennote.h; sourceTree = ""; }; + F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_triton.c; sourceTree = ""; }; F395BF6425633B2400942BFF /* SDL_crc32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_crc32.c; sourceTree = ""; }; F395C1912569C68E00942BFF /* SDL_iokitjoystick_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_iokitjoystick_c.h; sourceTree = ""; }; F395C1922569C68E00942BFF /* SDL_iokitjoystick.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_iokitjoystick.c; sourceTree = ""; }; @@ -1962,6 +1964,7 @@ F3984CCF25BCC92800374F43 /* SDL_hidapi_stadia.c */, A75FDAAC23E2795C00529352 /* SDL_hidapi_steam.c */, F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */, + F38C72482CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c */, A797456F2B2E9D39009D224A /* SDL_hidapi_steamdeck.c */, A7D8A7C623E2513E00DCD162 /* SDL_hidapi_switch.c */, A7D8A7C623E2513E00DCD163 /* SDL_hidapi_switch2.c */, @@ -3009,6 +3012,7 @@ F316ABD92B5C3185002EF551 /* SDL_memcpy.c in Sources */, A7D8B97A23E2514400DCD162 /* SDL_render.c in Sources */, A7D8ABD323E2514100DCD162 /* SDL_stretch.c in Sources */, + F38C72492CEEB1DE000B0A90 /* SDL_hidapi_steam_triton.c in Sources */, A7D8AC3923E2514100DCD162 /* SDL_blit_copy.c in Sources */, A7D8B5CF23E2514300DCD162 /* SDL_syspower.m in Sources */, F3B439512C935C2400792030 /* SDL_dummyprocess.c in Sources */, diff --git a/src/hidapi/ios/hid.m b/src/hidapi/ios/hid.m index cb5e231759..24a3c791ee 100644 --- a/src/hidapi/ios/hid.m +++ b/src/hidapi/ios/hid.m @@ -63,6 +63,7 @@ #define VALVE_USB_VID 0x28DE #define D0G_BLE2_PID 0x1106 +#define TRITON_BLE_PID 0x1303 typedef uint32_t uint32; typedef uint64_t uint64; @@ -76,7 +77,8 @@ typedef uint64_t uint64; #define VALVE_SERVICE @"100F6C32-1735-4313-B402-38567131E5F3" // (READ/NOTIFICATIONS) -#define VALVE_INPUT_CHAR @"100F6C33-1735-4313-B402-38567131E5F3" +#define VALVE_INPUT_CHAR_0x1106 @"100F6C33-1735-4313-B402-38567131E5F3" +#define VALVE_INPUT_CHAR_0x1303 @"100F6C77-1735-4313-B402-38567131E5F3" //  (READ/WRITE) #define VALVE_REPORT_CHAR @"100F6C34-1735-4313-B402-38567131E5F3" @@ -101,21 +103,7 @@ typedef struct typedef struct { uint8_t id; - union { - bluetoothSegment segment; - struct { - uint8_t segmentHeader; - uint8_t featureReportMessageID; - uint8_t length; - uint8_t settingIdentifier; - union { - uint16_t usPayload; - uint32_t uPayload; - uint64_t ulPayload; - uint8_t ucPayload[15]; - }; - }; - }; + bluetoothSegment segment; } hidFeatureReport; #pragma pack(pop) @@ -125,34 +113,62 @@ size_t GetBluetoothSegmentSize(bluetoothSegment *segment) return segment->length + 3; } -#define RingBuffer_cbElem 19 -#define RingBuffer_nElem 4096 +#define RingBuffer_nElem 256 typedef struct { int _first, _last; - uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ]; + int _cbElem; + uint8_t *_data; pthread_mutex_t accessLock; } RingBuffer; -static void RingBuffer_init( RingBuffer *this ) +static RingBuffer *RingBuffer_alloc( int cbElem ) { + RingBuffer *this = (RingBuffer *)malloc( sizeof(*this) ); + if (!this) +{ + return NULL; + } + this->_first = -1; this->_last = 0; + this->_cbElem = cbElem; + this->_data = (uint8_t *)malloc(RingBuffer_nElem * cbElem); + if ( !this->_data ) + { + free( this ); + return NULL; + } pthread_mutex_init( &this->accessLock, 0 ); + return this; +} + +static void RingBuffer_free( RingBuffer *this ) +{ + if ( this ) + { + free( this->_data ); + free( this ); + } } static bool RingBuffer_write( RingBuffer *this, const uint8_t *src ) { + if ( !this ) + { + return false; + } + pthread_mutex_lock( &this->accessLock ); - memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem ); + memcpy( &this->_data[ this->_last ], src, this->_cbElem ); if ( this->_first == -1 ) { this->_first = this->_last; } - this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); + this->_last = ( this->_last + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem); if ( this->_last == this->_first ) { - this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); + this->_first = ( this->_first + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem); pthread_mutex_unlock( &this->accessLock ); return false; } @@ -162,14 +178,19 @@ static bool RingBuffer_write( RingBuffer *this, const uint8_t *src ) static bool RingBuffer_read( RingBuffer *this, uint8_t *dst ) { + if ( !this ) + { + return false; + } + pthread_mutex_lock( &this->accessLock ); if ( this->_first == -1 ) { pthread_mutex_unlock( &this->accessLock ); return false; } - memcpy( dst, &this->_data[ this->_first ], RingBuffer_cbElem ); - this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); + memcpy( dst, &this->_data[ this->_first ], this->_cbElem ); + this->_first = ( this->_first + this->_cbElem ) % (RingBuffer_nElem * this->_cbElem); if ( this->_first == this->_last ) { this->_first = -1; @@ -191,12 +212,14 @@ typedef enum @interface HIDBLEDevice : NSObject { - RingBuffer _inputReports; - uint8_t _featureReport[20]; + RingBuffer *_inputReports; + NSData *_featureReport; + NSMutableDictionary *_outputReports; BLEDeviceWaitState _waitStateForReadFeatureReport; BLEDeviceWaitState _waitStateForWriteFeatureReport; } +@property (nonatomic, readwrite) uint16_t pid; @property (nonatomic, readwrite) bool connected; @property (nonatomic, readwrite) bool ready; @@ -205,6 +228,7 @@ typedef enum @property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport; - (id)initWithPeripheral:(CBPeripheral *)peripheral; +- (void)onDisconnect; @end @@ -278,8 +302,7 @@ typedef enum HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; if ( steamController ) { - steamController.connected = NO; - steamController.ready = NO; + [steamController onDisconnect]; [self.centralManager cancelPeripheralConnection:peripheral]; } } @@ -474,8 +497,7 @@ typedef enum HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; if ( steamController ) { - steamController.connected = NO; - steamController.ready = NO; + [steamController onDisconnect]; [self.deviceMap removeObjectForKey:peripheral]; } } @@ -500,12 +522,14 @@ static void process_pending_events(void) { if ( self = [super init] ) { - RingBuffer_init( &_inputReports ); + self.pid = 0; + _inputReports = NULL; + _outputReports = [[NSMutableDictionary alloc] init]; + _connected = NO; + _ready = NO; self.bleSteamController = nil; self.bleCharacteristicInput = nil; self.bleCharacteristicReport = nil; - _connected = NO; - _ready = NO; } return self; } @@ -514,7 +538,9 @@ static void process_pending_events(void) { if ( self = [super init] ) { - RingBuffer_init( &_inputReports ); + self.pid = 0; + _inputReports = NULL; + _outputReports = [[NSMutableDictionary alloc] init]; _connected = NO; _ready = NO; self.bleSteamController = peripheral; @@ -528,6 +554,18 @@ static void process_pending_events(void) return self; } +- (void)onDisconnect +{ + self.connected = NO; + self.ready = NO; + + if ( _inputReports ) + { + RingBuffer_free( _inputReports ); + _inputReports = NULL; + } +} + - (void)setConnected:(bool)connected { _connected = connected; @@ -543,94 +581,134 @@ static void process_pending_events(void) - (size_t)read_input_report:(uint8_t *)dst { - if ( RingBuffer_read( &_inputReports, dst+1 ) ) + if ( RingBuffer_read( _inputReports, dst+1 ) ) { - *dst = 0x03; - return 20; + switch ( self.pid ) + { + case D0G_BLE2_PID: + *dst = 0x03; + break; + case TRITON_BLE_PID: + *dst = 0x42; + break; + default: + abort(); + } + return _inputReports->_cbElem + 1; } return 0; } - (int)send_report:(const uint8_t *)data length:(size_t)length { + if ( self.pid == D0G_BLE2_PID ) + { [_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; return (int)length; } -- (int)send_feature_report:(hidFeatureReport *)report + // We need to look up the correct characteristic for this output report + if ( length > 0 ) + { + CBCharacteristic *aChar = [_outputReports objectForKey:[NSNumber numberWithInt:data[0]]]; + if ( aChar != nil ) + { + [_bleSteamController writeValue:[NSData dataWithBytes:&data[1] length:(length - 1)] forCharacteristic:aChar type:CBCharacteristicWriteWithResponse]; + return (int)length; + } + } + return -1; +} + +- (int)send_feature_report:(hidFeatureReport *)report length:(size_t)length { #if FEATURE_REPORT_LOGGING uint8_t *reportBytes = (uint8_t *)report; - NSLog( @"HIDBLE:send_feature_report (%02zu/19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", GetBluetoothSegmentSize( report->segment ), + NSLog( @"HIDBLE:send_feature_report (%02zu/19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", length, reportBytes[1], reportBytes[2], reportBytes[3], reportBytes[4], reportBytes[5], reportBytes[6], reportBytes[7], reportBytes[8], reportBytes[9], reportBytes[10], reportBytes[11], reportBytes[12], reportBytes[13], reportBytes[14], reportBytes[15], reportBytes[16], reportBytes[17], reportBytes[18], reportBytes[19] ); #endif - int sendSize = (int)GetBluetoothSegmentSize( &report->segment ); - if ( sendSize > 20 ) - sendSize = 20; - #if 1 // fire-and-forget - we are going to not wait for the response here because all Steam Controller BLE send_feature_report's are ignored, // except errors. - [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; + [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:MIN(length, 64)] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; // pretend we received a result anybody cares about - return 19; + return (int)length; #else // this is technically the correct send_feature_report logic if you want to make sure it gets through and is // acknowledged or errors out _waitStateForWriteFeatureReport = BLEDeviceWaitState_Waiting; - [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize + [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:MIN(length, 64) ] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; - while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting ) + while ( _connected && _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting ) { process_pending_events(); } - if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error ) + if ( !_connected || _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error ) { _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; return -1; } _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; - return 19; + return (int)length; #endif } -- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer +- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer length:(size_t)length { _waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting; [_bleSteamController readValueForCharacteristic:_bleCharacteristicReport]; - while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting ) - process_pending_events(); + while ( _connected && _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting ) + { + process_pending_events(); + } - if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error ) + if ( !_connected || _waitStateForReadFeatureReport == BLEDeviceWaitState_Error ) { _waitStateForReadFeatureReport = BLEDeviceWaitState_None; return -1; } - memcpy( buffer, _featureReport, sizeof(_featureReport) ); + int amount = 0; + if ( _featureReport.length > 0 ) + { + uint8_t *data = (uint8_t *)_featureReport.bytes; + if ( *data == *buffer ) + { + amount = (int)MIN( length, _featureReport.length ); + memcpy( buffer, _featureReport.bytes, amount ); + } + else + { + // Leave the report in the buffer + amount = (int)MIN( length - 1, _featureReport.length ); + memcpy( &buffer[ 1 ], _featureReport.bytes, amount ); + ++amount; + } + } _waitStateForReadFeatureReport = BLEDeviceWaitState_None; #if FEATURE_REPORT_LOGGING - NSLog( @"HIDBLE:get_feature_report (19) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", - buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], - buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12], - buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18], - buffer[19] ); + NSLog( @"HIDBLE:get_feature_report (%lu/%zu) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", + _featureReport.length, length, + buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], + buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12], + buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18], + buffer[19] ); #endif - return 19; + return amount; } #pragma mark CBPeripheralDelegate Implementation @@ -667,8 +745,14 @@ static void process_pending_events(void) { NSLog( @"Found Characteristic %@", aChar ); - if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] ) + if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR_0x1106]] ) { + self.pid = D0G_BLE2_PID; + self.bleCharacteristicInput = aChar; + } + else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR_0x1303]] ) + { + self.pid = TRITON_BLE_PID; self.bleCharacteristicInput = aChar; } else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] ) @@ -676,6 +760,21 @@ static void process_pending_events(void) self.bleCharacteristicReport = aChar; [self.bleSteamController discoverDescriptorsForCharacteristic: aChar]; } + else + { + NSString *UUIDString = [aChar.UUID UUIDString]; + int report_id = 0; + if ( sscanf( UUIDString.UTF8String, "100F6C%x", &report_id ) == 1 && report_id > 0x35 ) + { + report_id -= 0x35; + //NSLog( @"Found characteristic for output report 0x%.2x", report_id ); + + if (report_id >= 0x80) { + // An output report + [_outputReports setObject:aChar forKey:[NSNumber numberWithInt:report_id]]; + } + } + } } } } @@ -690,17 +789,33 @@ static void process_pending_events(void) if ( self.ready == NO ) { self.ready = YES; + if ( _inputReports == NULL ) + { + int cbElem = 0; + switch ( self.pid ) + { + case D0G_BLE2_PID: + cbElem = 19; + break; + case TRITON_BLE_PID: + cbElem = 53; + break; + default: + abort(); + } + _inputReports = RingBuffer_alloc( cbElem ); + } HIDBLEManager.sharedInstance.nPendingPairs -= 1; } if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] ) { NSData *data = [characteristic value]; - if ( data.length != 19 ) + if ( _inputReports && data.length != _inputReports->_cbElem ) { - NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length ); + NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly %d", (unsigned long)data.length, _inputReports->_cbElem ); } - if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) ) + if ( !RingBuffer_write( _inputReports, (const uint8_t *)data.bytes ) ) { uint64_t ticksNow = mach_approximate_time(); if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) ) @@ -712,8 +827,6 @@ static void process_pending_events(void) } else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] ) { - memset( _featureReport, 0, sizeof(_featureReport) ); - if ( error != nil ) { NSLog( @"HIDBLE: get_feature_report error: %@", error ); @@ -721,12 +834,7 @@ static void process_pending_events(void) } else { - NSData *data = [characteristic value]; - if ( data.length != 20 ) - { - NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 20", (unsigned long)data.length ); - } - memcpy( _featureReport, data.bytes, MIN( data.length, sizeof(_featureReport) ) ); + _featureReport = [characteristic value]; _waitStateForReadFeatureReport = BLEDeviceWaitState_Complete; } } @@ -850,7 +958,7 @@ static struct hid_device_info *create_device_info_for_hid_device(HIDBLEDevice *d memset( device_info, 0, sizeof(struct hid_device_info) ); device_info->path = strdup( device.bleSteamController.identifier.UUIDString.UTF8String ); device_info->vendor_id = VALVE_USB_VID; - device_info->product_id = D0G_BLE2_PID; + device_info->product_id = device.pid; device_info->product_string = wcsdup( L"Steam Controller" ); device_info->manufacturer_string = wcsdup( L"Valve Corporation" ); device_info->bus_type = HID_API_BUS_BLUETOOTH; @@ -861,14 +969,6 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, { @autoreleasepool { struct hid_device_info *root = NULL; - /* See if there are any devices we should skip in enumeration */ - if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, D0G_BLE2_PID, 0, 0)) { - return NULL; - } - - if ( ( vendor_id == 0 || vendor_id == VALVE_USB_VID ) && - ( product_id == 0 || product_id == D0G_BLE2_PID ) ) - { HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; [bleManager updateConnectedSteamControllers:false]; NSEnumerator *devices = [bleManager.deviceMap objectEnumerator]; @@ -891,11 +991,22 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, } continue; } + + if ( ( vendor_id != 0 && vendor_id != VALVE_USB_VID ) || + ( product_id != 0 && product_id != device.pid ) ) + { + continue; + } + + /* See if there are any devices we should skip in enumeration */ + if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, device.pid, 0, 0)) { + continue; + } + struct hid_device_info *device_info = create_device_info_for_hid_device(device); device_info->next = root; root = device_info; } - } return root; }} @@ -975,7 +1086,7 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char if ( !device_handle.connected ) return -1; - return [device_handle send_feature_report:(hidFeatureReport *)(void *)data]; + return [device_handle send_feature_report:(hidFeatureReport *)(void *)data length:length]; } int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) @@ -985,7 +1096,7 @@ int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, if ( !device_handle.connected ) return -1; - size_t written = [device_handle get_feature_report:data[0] into:data]; + size_t written = [device_handle get_feature_report:data[0] into:data length:length]; return written == length-1 ? (int)length : (int)written; } @@ -1018,7 +1129,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t NSLog( @"hid_read_timeout with non-zero wait" ); } int result = (int)[device_handle read_input_report:data]; -#if FEATURE_REPORT_LOGGING +#if 0 //FEATURE_REPORT_LOGGING NSLog( @"HIDBLE:hid_read_timeout (%d) [%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x]", result, data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index cab6221d12..b63a47a879 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -1252,6 +1252,9 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) if (SDL_IsJoystickSteamController(vendor, product)) { // Steam controllers have 2 back paddle buttons SDL_strlcat(mapping_string, "paddle1:b11,paddle2:b12,", sizeof(mapping_string)); + } else if (SDL_IsJoystickSteamTriton(vendor, product)) { + // Steam controllers have 2 back paddle buttons + SDL_strlcat(mapping_string, "misc1:b11,paddle1:b12,paddle2:b13,paddle3:b14,paddle4:b15", sizeof(mapping_string)); } else if (SDL_IsJoystickNintendoSwitchPro(vendor, product) || SDL_IsJoystickNintendoSwitchProInputOnly(vendor, product)) { // Nintendo Switch Pro controllers have a screenshot button diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index d099928282..831cc541ca 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -3295,6 +3295,12 @@ bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id) return eType == k_eControllerType_SteamControllerNeptune; } +bool SDL_IsJoystickSteamTriton(Uint16 vendor_id, Uint16 product_id) +{ + EControllerType eType = GuessControllerType(vendor_id, product_id); + return eType == k_eControllerType_SteamControllerTriton; +} + bool SDL_IsJoystickXInput(SDL_GUID guid) { return (guid.data[14] == 'x') ? true : false; diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h index c6e1a7b792..f186616454 100644 --- a/src/joystick/SDL_joystick_c.h +++ b/src/joystick/SDL_joystick_c.h @@ -144,6 +144,9 @@ extern bool SDL_IsJoystickFlydigiController(Uint16 vendor_id, Uint16 product_id) // Function to return whether a joystick is a Steam Deck extern bool SDL_IsJoystickSteamDeck(Uint16 vendor_id, Uint16 product_id); +// Function to return whether a joystick is a Steam Triton +extern bool SDL_IsJoystickSteamTriton(Uint16 vendor_id, Uint16 product_id); + // Function to return whether a joystick guid comes from the XInput driver extern bool SDL_IsJoystickXInput(SDL_GUID guid); diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h index 4abd813551..080983fa47 100644 --- a/src/joystick/controller_list.h +++ b/src/joystick/controller_list.h @@ -588,9 +588,9 @@ static const ControllerDescription_t arrControllers[] = { { MAKE_CONTROLLER_ID( 0x20d6, 0xa715 ), k_eControllerType_SwitchInputOnlyController, NULL }, // Power A Fusion Wireless Arcade Stick (USB Mode) Over BT is shows up as 057e 2009 { MAKE_CONTROLLER_ID( 0x20d6, 0xa716 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PowerA Nintendo Switch Fusion Pro Controller - USB requires toggling switch on back of device { MAKE_CONTROLLER_ID( 0x20d6, 0xa718 ), k_eControllerType_SwitchInputOnlyController, NULL }, // PowerA Nintendo Switch Nano Wired Controller - { MAKE_CONTROLLER_ID( 0x33dd, 0x0001 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch Black - { MAKE_CONTROLLER_ID( 0x33dd, 0x0002 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch ?? - { MAKE_CONTROLLER_ID( 0x33dd, 0x0003 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch Red + { MAKE_CONTROLLER_ID( 0x33dd, 0x0001 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch Black + { MAKE_CONTROLLER_ID( 0x33dd, 0x0002 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch ?? + { MAKE_CONTROLLER_ID( 0x33dd, 0x0003 ), k_eControllerType_SwitchInputOnlyController, NULL }, // ZUIKI MasCon for Nintendo Switch Red // Valve products { MAKE_CONTROLLER_ID( 0x0000, 0x11fb ), k_eControllerType_MobileTouch, NULL }, // Streaming mobile touch virtual controls @@ -603,4 +603,8 @@ static const ControllerDescription_t arrControllers[] = { { MAKE_CONTROLLER_ID( 0x28de, 0x1201 ), k_eControllerType_SteamControllerV2, NULL }, // Valve wired Steam Controller (HEADCRAB) { MAKE_CONTROLLER_ID( 0x28de, 0x1202 ), k_eControllerType_SteamControllerV2, NULL }, // Valve Bluetooth Steam Controller (HEADCRAB) { MAKE_CONTROLLER_ID( 0x28de, 0x1205 ), k_eControllerType_SteamControllerNeptune, NULL }, // Valve Steam Deck Builtin Controller + { MAKE_CONTROLLER_ID( 0x28de, 0x1302 ), k_eControllerType_SteamControllerTriton, NULL }, // Valve Steam Triton Controller + { MAKE_CONTROLLER_ID( 0x28de, 0x1303 ), k_eControllerType_SteamControllerTriton, NULL }, // Valve Steam Triton Controller (BLE) + { MAKE_CONTROLLER_ID( 0x28de, 0x1304 ), k_eControllerType_SteamControllerTriton, NULL }, // Valve Steam Proteus Dongle (Proprietary) + { MAKE_CONTROLLER_ID( 0x28de, 0x1305 ), k_eControllerType_SteamControllerTriton, NULL }, // Valve Steam Nereid Dongle (Proprietary) }; diff --git a/src/joystick/controller_type.h b/src/joystick/controller_type.h index 155c8ad11e..bb94840aff 100644 --- a/src/joystick/controller_type.h +++ b/src/joystick/controller_type.h @@ -39,6 +39,8 @@ typedef enum k_eControllerType_SteamControllerV2 = 3, k_eControllerType_SteamControllerNeptune = 4, + k_eControllerType_SteamControllerTriton = 10, + // Other Controllers k_eControllerType_UnknownNonSteamController = 30, k_eControllerType_XBox360Controller = 31, diff --git a/src/joystick/hidapi/SDL_hidapi_steam_triton.c b/src/joystick/hidapi/SDL_hidapi_steam_triton.c new file mode 100644 index 0000000000..315d803d8a --- /dev/null +++ b/src/joystick/hidapi/SDL_hidapi_steam_triton.c @@ -0,0 +1,532 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2023 Max Maisel + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_JOYSTICK_HIDAPI + +#include "../SDL_sysjoystick.h" +#include "SDL_hidapijoystick_c.h" + +#ifdef SDL_JOYSTICK_HIDAPI_STEAM_TRITON + +/*****************************************************************************************************/ + +#include "steam/controller_constants.h" +#include "steam/controller_structs.h" + +// Always 1kHz according to USB descriptor, but actually about 4 ms. +#define TRITON_SENSOR_UPDATE_INTERVAL_US 4032 + +enum +{ + SDL_GAMEPAD_BUTTON_STEAM_DECK_QAM = 11, + SDL_GAMEPAD_BUTTON_STEAM_DECK_RIGHT_PADDLE1, + SDL_GAMEPAD_BUTTON_STEAM_DECK_LEFT_PADDLE1, + SDL_GAMEPAD_BUTTON_STEAM_DECK_RIGHT_PADDLE2, + SDL_GAMEPAD_BUTTON_STEAM_DECK_LEFT_PADDLE2, + SDL_GAMEPAD_NUM_TRITON_BUTTONS, +}; + +typedef enum +{ + + TRITON_LBUTTON_A = 0x00000001, + TRITON_LBUTTON_B = 0x00000002, + TRITON_LBUTTON_X = 0x00000004, + TRITON_LBUTTON_Y = 0x00000008, + + TRITON_HBUTTON_QAM = 0x00000010, + TRITON_LBUTTON_R3 = 0x00000020, + TRITON_LBUTTON_VIEW = 0x00000040, + TRITON_HBUTTON_R4 = 0x00000080, + + TRITON_LBUTTON_R5 = 0x00000100, + TRITON_LBUTTON_R = 0x00000200, + TRITON_LBUTTON_DPAD_DOWN = 0x00000400, + TRITON_LBUTTON_DPAD_RIGHT = 0x00000800, + + TRITON_LBUTTON_DPAD_LEFT = 0x00001000, + TRITON_LBUTTON_DPAD_UP = 0x00002000, + TRITON_LBUTTON_MENU = 0x00004000, + TRITON_LBUTTON_L3 = 0x00008000, + + TRITON_LBUTTON_STEAM = 0x00010000, + TRITON_HBUTTON_L4 = 0x00020000, + TRITON_LBUTTON_L5 = 0x00040000, + TRITON_LBUTTON_L = 0x00080000, + + /* + STEAM_RIGHTSTICK_FINGERDOWN_MASK, // Right Stick Touch 0x00100000 + STEAM_RIGHTPAD_FINGERDOWN_MASK, // Right Pad Touch 0x00200000 + STEAM_BUTTON_RIGHTPAD_CLICKED_MASK, // Right Pressure Click 0x00400000 + STEAM_RIGHT_TRIGGER_MASK, // Right Trigger Click 0x00800000 + + STEAM_LEFTSTICK_FINGERDOWN_MASK, // Left Stick Touch 0x01000000 + STEAM_LEFTPAD_FINGERDOWN_MASK, // Left Pad Touch 0x02000000 + STEAM_BUTTON_LEFTPAD_CLICKED_MASK, // Left Pressure Click 0x04000000 + STEAM_LEFT_TRIGGER_MASK, // Left Trigger Click 0x08000000 + STEAM_RIGHT_AUX_MASK, // Right Pinky Touch 0x10000000 + STEAM_LEFT_AUX_MASK, // Left Pinky Touch 0x20000000 + */ +} TritonButtons; + +typedef struct +{ + bool connected; + bool report_sensors; + Uint32 last_sensor_tick; + Uint64 sensor_timestamp_ns; + Uint64 last_button_state; + Uint64 last_lizard_update; +} SDL_DriverSteamTriton_Context; + +static bool IsProteusDongle(Uint16 product_id) +{ + return (product_id == USB_PRODUCT_VALVE_STEAM_PROTEUS_DONGLE || + product_id == USB_PRODUCT_VALVE_STEAM_NEREID_DONGLE); +} + +static bool DisableSteamTritonLizardMode(SDL_hid_device *dev) +{ + int rc; + Uint8 buffer[HID_FEATURE_REPORT_BYTES] = { 1 }; + FeatureReportMsg *msg = (FeatureReportMsg *)(buffer + 1); + + msg->header.type = ID_SET_SETTINGS_VALUES; + msg->header.length = 1 * sizeof(ControllerSetting); + msg->payload.setSettingsValues.settings[0].settingNum = SETTING_LIZARD_MODE; + msg->payload.setSettingsValues.settings[0].settingValue = LIZARD_MODE_OFF; + + rc = SDL_hid_send_feature_report(dev, buffer, sizeof(buffer)); + if (rc != sizeof(buffer)) { + return false; + } + + return true; +} + +static void HIDAPI_DriverSteamTriton_HandleState(SDL_HIDAPI_Device *device, + SDL_Joystick *joystick, + TritonMTUFull_t *pTritonReport) +{ + float values[3]; + SDL_DriverSteamTriton_Context *ctx = (SDL_DriverSteamTriton_Context *)device->context; + Uint64 timestamp = SDL_GetTicksNS(); + + if (pTritonReport->uButtons != ctx->last_button_state) { + Uint8 hat = 0; + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, + ((pTritonReport->uButtons & TRITON_LBUTTON_A) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, + ((pTritonReport->uButtons & TRITON_LBUTTON_B) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, + ((pTritonReport->uButtons & TRITON_LBUTTON_X) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, + ((pTritonReport->uButtons & TRITON_LBUTTON_Y) != 0)); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, + ((pTritonReport->uButtons & TRITON_LBUTTON_L) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, + ((pTritonReport->uButtons & TRITON_LBUTTON_R) != 0)); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, + ((pTritonReport->uButtons & TRITON_LBUTTON_MENU) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, + ((pTritonReport->uButtons & TRITON_LBUTTON_VIEW) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, + ((pTritonReport->uButtons & TRITON_LBUTTON_STEAM) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_STEAM_DECK_QAM, + ((pTritonReport->uButtons & TRITON_HBUTTON_QAM) != 0)); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, + ((pTritonReport->uButtons & TRITON_LBUTTON_L3) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, + ((pTritonReport->uButtons & TRITON_LBUTTON_R3) != 0)); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_STEAM_DECK_RIGHT_PADDLE1, + ((pTritonReport->uButtons & TRITON_HBUTTON_R4) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_STEAM_DECK_LEFT_PADDLE1, + ((pTritonReport->uButtons & TRITON_HBUTTON_L4) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_STEAM_DECK_RIGHT_PADDLE2, + ((pTritonReport->uButtons & TRITON_LBUTTON_R5) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_STEAM_DECK_LEFT_PADDLE2, + ((pTritonReport->uButtons & TRITON_LBUTTON_L5) != 0)); + + if (pTritonReport->uButtons & TRITON_LBUTTON_DPAD_UP) { + hat |= SDL_HAT_UP; + } + if (pTritonReport->uButtons & TRITON_LBUTTON_DPAD_DOWN) { + hat |= SDL_HAT_DOWN; + } + if (pTritonReport->uButtons & TRITON_LBUTTON_DPAD_LEFT) { + hat |= SDL_HAT_LEFT; + } + if (pTritonReport->uButtons & TRITON_LBUTTON_DPAD_RIGHT) { + hat |= SDL_HAT_RIGHT; + } + SDL_SendJoystickHat(timestamp, joystick, 0, hat); + + ctx->last_button_state = pTritonReport->uButtons; + } + + // RKRK There're button bits for this if you so choose. + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, + (int)pTritonReport->sTriggerLeft * 2 - 32768); + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, + (int)pTritonReport->sTriggerRight * 2 - 32768); + + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, + pTritonReport->sLeftStickX); + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, + -pTritonReport->sLeftStickY); + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, + pTritonReport->sRightStickX); + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, + -pTritonReport->sRightStickY); + + if (ctx->report_sensors && pTritonReport->imu.uTimestamp != ctx->last_sensor_tick) { + Uint32 delta_us = (pTritonReport->imu.uTimestamp - ctx->last_sensor_tick); + + ctx->sensor_timestamp_ns += SDL_US_TO_NS(delta_us); + + values[0] = (pTritonReport->imu.sGyroX / 32768.0f) * (2000.0f * (SDL_PI_F / 180.0f)); + values[1] = (pTritonReport->imu.sGyroZ / 32768.0f) * (2000.0f * (SDL_PI_F / 180.0f)); + values[2] = (-pTritonReport->imu.sGyroY / 32768.0f) * (2000.0f * (SDL_PI_F / 180.0f)); + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, ctx->sensor_timestamp_ns, values, 3); + + values[0] = (pTritonReport->imu.sAccelX / 32768.0f) * 2.0f * SDL_STANDARD_GRAVITY; + values[1] = (pTritonReport->imu.sAccelZ / 32768.0f) * 2.0f * SDL_STANDARD_GRAVITY; + values[2] = (-pTritonReport->imu.sAccelY / 32768.0f) * 2.0f * SDL_STANDARD_GRAVITY; + SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, ctx->sensor_timestamp_ns, values, 3); + + ctx->last_sensor_tick = pTritonReport->imu.uTimestamp; + } +} + +static void HIDAPI_DriverSteamTriton_HandleBatteryStatus(SDL_HIDAPI_Device *device, + SDL_Joystick *joystick, + TritonBatteryStatus_t *pTritonBatteryStatus) +{ + SDL_PowerState state; + + if (device->is_bluetooth) { + state = SDL_POWERSTATE_ON_BATTERY; + } else if (IsProteusDongle(device->product_id)) { + state = SDL_POWERSTATE_ON_BATTERY; + } else if (pTritonBatteryStatus->ucBatteryLevel == 100) { + state = SDL_POWERSTATE_CHARGED; + } else { + state = SDL_POWERSTATE_CHARGING; + } + SDL_SendJoystickPowerInfo(joystick, state, pTritonBatteryStatus->ucBatteryLevel); +} + +static bool HIDAPI_DriverSteamTriton_SetControllerConnected(SDL_HIDAPI_Device *device, bool connected) +{ + SDL_DriverSteamTriton_Context *ctx = (SDL_DriverSteamTriton_Context *)device->context; + + if (ctx->connected != connected) { + if (connected) { + SDL_JoystickID joystickID; + if (!HIDAPI_JoystickConnected(device, &joystickID)) { + return false; + } + } else { + if (device->num_joysticks > 0) { + HIDAPI_JoystickDisconnected(device, device->joysticks[0]); + } + } + ctx->connected = connected; + } + return true; +} + +static void HIDAPI_DriverSteamTriton_HandleWirelessStatus(SDL_HIDAPI_Device *device, + TritonWirelessStatus_t *pTritonWirelessStatus) +{ + switch (pTritonWirelessStatus->state) { + case k_ETritonWirelessStateConnect: + HIDAPI_DriverSteamTriton_SetControllerConnected(device, true); + break; + case k_ETritonWirelessStateDisconnect: + HIDAPI_DriverSteamTriton_SetControllerConnected(device, false); + break; + default: + break; + } +} + +/*****************************************************************************************************/ + +static void HIDAPI_DriverSteamTriton_RegisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM, callback, userdata); +} + +static void HIDAPI_DriverSteamTriton_UnregisterHints(SDL_HintCallback callback, void *userdata) +{ + SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM, callback, userdata); +} + +static bool HIDAPI_DriverSteamTriton_IsEnabled(void) +{ + return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_STEAM, + SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT)); +} + +static bool HIDAPI_DriverSteamTriton_IsSupportedDevice( + SDL_HIDAPI_Device *device, + const char *name, + SDL_GamepadType type, + Uint16 vendor_id, + Uint16 product_id, + Uint16 version, + int interface_number, + int interface_class, + int interface_subclass, + int interface_protocol) +{ + + if (IsProteusDongle(product_id)) { + if (interface_number >= 2 && interface_number <= 5) { + // The set of controller interfaces for Proteus & Nereid...currently + return true; + } + } else if (SDL_IsJoystickSteamTriton(vendor_id, product_id)) { + return true; + } + return false; +} + +static bool HIDAPI_DriverSteamTriton_InitDevice(SDL_HIDAPI_Device *device) +{ + SDL_DriverSteamTriton_Context *ctx; + + ctx = (SDL_DriverSteamTriton_Context *)SDL_calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return false; + } + + device->context = ctx; + + HIDAPI_SetDeviceName(device, "Steam Controller"); + + if (IsProteusDongle(device->product_id)) { + return true; + } + + // Wired controller, connected! + return HIDAPI_DriverSteamTriton_SetControllerConnected(device, true); +} + +static int HIDAPI_DriverSteamTriton_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id) +{ + return -1; +} + +static void HIDAPI_DriverSteamTriton_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index) +{ +} + +static bool HIDAPI_DriverSteamTriton_UpdateDevice(SDL_HIDAPI_Device *device) +{ + SDL_DriverSteamTriton_Context *ctx = (SDL_DriverSteamTriton_Context *)device->context; + SDL_Joystick *joystick = NULL; + + if (device->num_joysticks > 0) { + joystick = SDL_GetJoystickFromID(device->joysticks[0]); + } + + if (ctx->connected && joystick) { + Uint64 now = SDL_GetTicks(); + if (!ctx->last_lizard_update || (now - ctx->last_lizard_update) >= 3000) { + DisableSteamTritonLizardMode(device->dev); + ctx->last_lizard_update = now; + } + } + + for (;;) { + uint8_t data[64]; + int r = SDL_hid_read(device->dev, data, sizeof(data)); + + if (r == 0) { + return true; + } + if (r < 0) { + // Failed to read from controller + HIDAPI_DriverSteamTriton_SetControllerConnected(device, false); + return false; + } + + switch (data[0]) { + case ID_TRITON_CONTROLLER_STATE: + if (!joystick) { + HIDAPI_DriverSteamTriton_SetControllerConnected(device, true); + if (device->num_joysticks > 0) { + joystick = SDL_GetJoystickFromID(device->joysticks[0]); + } + } + if (joystick && r >= (1 + sizeof(TritonMTUFull_t))) { + TritonMTUFull_t *pTritonReport = (TritonMTUFull_t *)&data[1]; + HIDAPI_DriverSteamTriton_HandleState(device, joystick, pTritonReport); + } + break; + case ID_TRITON_BATTERY_STATUS: + if (joystick && r >= (1 + sizeof(TritonBatteryStatus_t))) { + TritonBatteryStatus_t *pTritonBatteryStatus = (TritonBatteryStatus_t *)&data[1]; + HIDAPI_DriverSteamTriton_HandleBatteryStatus(device, joystick, pTritonBatteryStatus); + } + break; + case ID_TRITON_WIRELESS_STATUS_X: + case ID_TRITON_WIRELESS_STATUS: + if (r >= (1 + sizeof(TritonWirelessStatus_t))) { + TritonWirelessStatus_t *pTritonWirelessStatus = (TritonWirelessStatus_t *)&data[1]; + HIDAPI_DriverSteamTriton_HandleWirelessStatus(device, pTritonWirelessStatus); + } + break; + default: + break; + } + } +} + +static bool HIDAPI_DriverSteamTriton_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ + float update_rate_in_hz = 1000000.0f / TRITON_SENSOR_UPDATE_INTERVAL_US; + + SDL_AssertJoysticksLocked(); + + // Initialize the joystick capabilities + joystick->nbuttons = SDL_GAMEPAD_NUM_TRITON_BUTTONS; + joystick->naxes = SDL_GAMEPAD_AXIS_COUNT; + joystick->nhats = 1; + + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, update_rate_in_hz); + SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, update_rate_in_hz); + + return true; +} + +static bool HIDAPI_DriverSteamTriton_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) +{ + int rc; + + //RKRK Not sure about size. Probalby 64+1 is OK for ORs + Uint8 buffer[HID_RUMBLE_OUTPUT_REPORT_BYTES]; + OutputReportMsg *msg = (OutputReportMsg *)(buffer); + + msg->report_id = ID_OUT_REPORT_HAPTIC_RUMBLE; + msg->payload.hapticRumble.type = 0; + msg->payload.hapticRumble.intensity = 0; + msg->payload.hapticRumble.left.speed = low_frequency_rumble; + msg->payload.hapticRumble.left.gain = 0; + msg->payload.hapticRumble.right.speed = high_frequency_rumble; + msg->payload.hapticRumble.right.gain = 0; + + + rc = SDL_hid_write(device->dev, buffer, sizeof(buffer)); + if (rc != sizeof(buffer)) { + return false; + } + return true; +} + +static bool HIDAPI_DriverSteamTriton_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble) +{ + return SDL_Unsupported(); +} + +static Uint32 HIDAPI_DriverSteamTriton_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ + return SDL_JOYSTICK_CAP_RUMBLE; +} + +static bool HIDAPI_DriverSteamTriton_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) +{ + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSteamTriton_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size) +{ + return SDL_Unsupported(); +} + +static bool HIDAPI_DriverSteamTriton_SetSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled) +{ + SDL_DriverSteamTriton_Context *ctx = (SDL_DriverSteamTriton_Context *)device->context; + int rc; + Uint8 buffer[HID_FEATURE_REPORT_BYTES] = { 1 }; + FeatureReportMsg *msg = (FeatureReportMsg *)(buffer + 1); + + msg->header.type = ID_SET_SETTINGS_VALUES; + msg->header.length = 1 * sizeof(ControllerSetting); + msg->payload.setSettingsValues.settings[0].settingNum = SETTING_IMU_MODE; + if (enabled) { + msg->payload.setSettingsValues.settings[0].settingValue = (SETTING_GYRO_MODE_SEND_RAW_ACCEL | SETTING_GYRO_MODE_SEND_RAW_GYRO); + } else { + msg->payload.setSettingsValues.settings[0].settingValue = SETTING_GYRO_MODE_OFF; + } + + rc = SDL_hid_send_feature_report(device->dev, buffer, sizeof(buffer)); + if (rc != sizeof(buffer)) { + return false; + } + + ctx->report_sensors = enabled; + + return true; +} + +static void HIDAPI_DriverSteamTriton_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) +{ + // Lizard mode id automatically re-enabled by watchdog. Nothing to do here. +} + +static void HIDAPI_DriverSteamTriton_FreeDevice(SDL_HIDAPI_Device *device) +{ +} + +SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamTriton = { + SDL_HINT_JOYSTICK_HIDAPI_STEAM, + true, + HIDAPI_DriverSteamTriton_RegisterHints, + HIDAPI_DriverSteamTriton_UnregisterHints, + HIDAPI_DriverSteamTriton_IsEnabled, + HIDAPI_DriverSteamTriton_IsSupportedDevice, + HIDAPI_DriverSteamTriton_InitDevice, + HIDAPI_DriverSteamTriton_GetDevicePlayerIndex, + HIDAPI_DriverSteamTriton_SetDevicePlayerIndex, + HIDAPI_DriverSteamTriton_UpdateDevice, + HIDAPI_DriverSteamTriton_OpenJoystick, + HIDAPI_DriverSteamTriton_RumbleJoystick, + HIDAPI_DriverSteamTriton_RumbleJoystickTriggers, + HIDAPI_DriverSteamTriton_GetJoystickCapabilities, + HIDAPI_DriverSteamTriton_SetJoystickLED, + HIDAPI_DriverSteamTriton_SendJoystickEffect, + HIDAPI_DriverSteamTriton_SetSensorsEnabled, + HIDAPI_DriverSteamTriton_CloseJoystick, + HIDAPI_DriverSteamTriton_FreeDevice, +}; + +#endif // SDL_JOYSTICK_HIDAPI_STEAM_TRITON + +#endif // SDL_JOYSTICK_HIDAPI diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 07e7e3552e..7ce4815130 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -70,6 +70,9 @@ static SDL_HIDAPI_DeviceDriver *SDL_HIDAPI_drivers[] = { #ifdef SDL_JOYSTICK_HIDAPI_STEAMDECK &SDL_HIDAPI_DriverSteamDeck, #endif +#ifdef SDL_JOYSTICK_HIDAPI_STEAMDECK + &SDL_HIDAPI_DriverSteamTriton, +#endif #ifdef SDL_JOYSTICK_HIDAPI_SWITCH &SDL_HIDAPI_DriverNintendoClassic, &SDL_HIDAPI_DriverJoyCons, diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index 64a0f53ce1..12d87fc3b9 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -43,6 +43,7 @@ #define SDL_JOYSTICK_HIDAPI_XBOXONE #define SDL_JOYSTICK_HIDAPI_SHIELD #define SDL_JOYSTICK_HIDAPI_STEAM_HORI +#define SDL_JOYSTICK_HIDAPI_STEAM_TRITON #define SDL_JOYSTICK_HIDAPI_LG4FF #define SDL_JOYSTICK_HIDAPI_8BITDO #define SDL_JOYSTICK_HIDAPI_FLYDIGI @@ -168,6 +169,7 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360W; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXboxOne; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamHori; +extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamTriton; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverLg4ff; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_Driver8BitDo; extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverFlydigi; diff --git a/src/joystick/hidapi/steam/controller_constants.h b/src/joystick/hidapi/steam/controller_constants.h index 3a62d27245..b60021e0c5 100644 --- a/src/joystick/hidapi/steam/controller_constants.h +++ b/src/joystick/hidapi/steam/controller_constants.h @@ -411,6 +411,12 @@ typedef enum { TRACKPAD_NUM_MODES } TrackpadDPadMode; +typedef enum +{ + LIZARD_MODE_OFF, + LIZARD_MODE_ON, +} LizardModeState_t; + // Read-write controller settings (only add to this enum and never change the order) typedef enum { @@ -423,7 +429,7 @@ typedef enum SETTING_USB_DEBUG_MODE, SETTING_LEFT_TRACKPAD_MODE, SETTING_RIGHT_TRACKPAD_MODE, - SETTING_MOUSE_POINTER_ENABLED, + SETTING_LIZARD_MODE, // 10 SETTING_DPAD_DEADZONE, diff --git a/src/joystick/hidapi/steam/controller_structs.h b/src/joystick/hidapi/steam/controller_structs.h index 90160db3a6..0496f8308e 100644 --- a/src/joystick/hidapi/steam/controller_structs.h +++ b/src/joystick/hidapi/steam/controller_structs.h @@ -154,6 +154,100 @@ typedef struct } FeatureReportMsg; +// Triton and derivatives utilize output reports for haptic commands. This is a +// snapshot from Nov 2024 -- things may change. + +// Triton Output Report Lengths #defs include +1 for the OR ID + +// Output Report Haptic Messages for Triton +typedef struct +{ + uint8_t type; + uint16_t intensity; + struct + { + uint16_t speed; + int8_t gain; + } left, right; +} MsgHapticRumble; +#define HID_RUMBLE_OUTPUT_REPORT_BYTES 10 + + +typedef struct +{ + uint8_t side; + uint16_t on_us; + uint16_t off_us; + uint16_t repeat_count; + uint16_t gain_db; +} MsgHapticPulse; +#define HID_HAPTIC_PULSE_OUTPUT_REPORT_BYTES 10 + +typedef struct +{ + uint8_t side; + uint8_t command; + int8_t gain_db; +} MsgHapticCommand; +#define HID_HAPTIC_COMMAND_REPORT_BYTES 4 + +typedef struct +{ + uint8_t side; + int8_t gain_db; + uint16_t frequency; + uint16_t duration_ms; + uint16_t lfo_freq; + uint8_t lfo_depth; +} MsgHapticLfoTone; +#define HID_HAPTIC_LFO_TONE_REPORT_BYTES 10 + +typedef struct +{ + uint8_t side; + int8_t gain_db; + uint16_t duration_ms; + struct + { + uint16_t frequency; + } start, end; +} MsgHapticLogSweep; +#define HID_HAPTIC_LOG_SWEEP_REPORT_BYTES 9 + +typedef struct m +{ + uint8_t side; + uint8_t script_id; + int8_t gain_db; +} MsgHapticScript; +#define HID_HAPTIC_SCRIPT_REPORT_BYTES 4 + +typedef enum +{ + ID_OUT_REPORT_HAPTIC_RUMBLE = 0x80, + ID_OUT_REPORT_HAPTIC_PULSE = 0x81, + ID_OUT_REPORT_HAPTIC_COMMAND = 0x82, + ID_OUT_REPORT_HAPTIC_LFO_TONE = 0x83, + ID_OUT_REPORT_HAPTIC_LOG_SWEEP = 0x85, + ID_OUT_REPORT_HAPTIC_SCRIPT = 0x86, +} ValveTritonOutReportMessageIDs; + +typedef struct +{ + uint8_t report_id; + union + { + MsgHapticRumble hapticRumble; + MsgHapticPulse hapticPulse; + MsgHapticCommand hapticCommand; + MsgHapticLfoTone hapticLfoTone; + MsgHapticLogSweep hapticLogSweep; + MsgHapticScript hapticScript; + } payload; + +} OutputReportMsg; + + // Roll this version forward anytime that you are breaking compatibility of existing // message types within ValveInReport_t or the header itself. Hopefully this should // be super rare and instead you should just add new message payloads to the union, @@ -413,51 +507,118 @@ typedef struct unsigned short sPressurePadRight; } SteamDeckStatePacket_t; + typedef struct { - ValveInReportHeader_t header; - - union - { - ValveControllerStatePacket_t controllerState; - ValveControllerBLEStatePacket_t controllerBLEState; - ValveControllerDebugPacket_t debugState; - ValveControllerTrackpadImage_t padImage; - ValveControllerRawTrackpadImage_t rawPadImage; - SteamControllerWirelessEvent_t wirelessEvent; - SteamControllerStatusEvent_t statusEvent; - SteamDeckStatePacket_t deckState; - } payload; - + ValveInReportHeader_t header; + + union + { + ValveControllerStatePacket_t controllerState; + ValveControllerBLEStatePacket_t controllerBLEState; + ValveControllerDebugPacket_t debugState; + ValveControllerTrackpadImage_t padImage; + ValveControllerRawTrackpadImage_t rawPadImage; + SteamControllerWirelessEvent_t wirelessEvent; + SteamControllerStatusEvent_t statusEvent; + SteamDeckStatePacket_t deckState; + } payload; + } ValveInReport_t; - -// Enumeration for BLE packet protocol enum EBLEPacketReportNums { - // Skipping past 2-3 because they are escape characters in Uart protocol - k_EBLEReportState = 4, - k_EBLEReportStatus = 5, + k_EBLEReportState = 4, + k_EBLEReportStatus = 5, }; - - // Enumeration of data chunks in BLE state packets enum EBLEOptionDataChunksBitmask { - // First byte upper nibble - k_EBLEButtonChunk1 = 0x10, - k_EBLEButtonChunk2 = 0x20, - k_EBLEButtonChunk3 = 0x40, - k_EBLELeftJoystickChunk = 0x80, + // First byte upper nibble + k_EBLEButtonChunk1 = 0x10, + k_EBLEButtonChunk2 = 0x20, + k_EBLEButtonChunk3 = 0x40, + k_EBLELeftJoystickChunk = 0x80, - // Second full byte - k_EBLELeftTrackpadChunk = 0x100, - k_EBLERightTrackpadChunk = 0x200, - k_EBLEIMUAccelChunk = 0x400, - k_EBLEIMUGyroChunk = 0x800, - k_EBLEIMUQuatChunk = 0x1000, + // Second full byte + k_EBLELeftTrackpadChunk = 0x100, + k_EBLERightTrackpadChunk = 0x200, + k_EBLEIMUAccelChunk = 0x400, + k_EBLEIMUGyroChunk = 0x800, + k_EBLEIMUQuatChunk = 0x1000, }; +// Triton and derivatives do not use the ValveInReport_t structure + +enum ETritonReportIDTypes +{ + ID_TRITON_CONTROLLER_STATE = 0x42, + ID_TRITON_BATTERY_STATUS = 0x43, + ID_TRITON_WIRELESS_STATUS_X = 0x46, + ID_TRITON_WIRELESS_STATUS = 0x79, +}; + +enum ETritonWirelessState +{ + k_ETritonWirelessStateDisconnect = 1, + k_ETritonWirelessStateConnect = 2, +}; + +typedef struct +{ + uint32_t uTimestamp; + short sAccelX; + short sAccelY; + short sAccelZ; + + short sGyroX; + short sGyroY; + short sGyroZ; + + short sGyroQuatW; + short sGyroQuatX; + short sGyroQuatY; + short sGyroQuatZ; +} TritonMTUIMU_t; + +typedef struct +{ + uint8_t cSeq_num; + uint32_t uButtons; + short sTriggerLeft; + short sTriggerRight; + + short sLeftStickX; + short sLeftStickY; + short sRightStickX; + short sRightStickY; + + short sLeftPadX; + short sLeftPadY; + unsigned short ucPressureLeft; + + short sRightPadX; + short sRightPadY; + unsigned short ucPressureRight; + TritonMTUIMU_t imu; +} TritonMTUFull_t; + +typedef struct +{ + unsigned char ucBatteryLevel; + unsigned short sBatteryVoltage; + unsigned short sSystemVoltage; + unsigned short sInputVoltage; + unsigned short sCurrent; + unsigned short sInputCurrent; + char cTemperature; +} TritonBatteryStatus_t; + +typedef struct +{ + unsigned char state; +} TritonWirelessStatus_t; + #pragma pack() #endif // _CONTROLLER_STRUCTS diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 62a6c34a00..f34d843fd4 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -149,6 +149,8 @@ #define USB_PRODUCT_THRUSTMASTER_ESWAPX_PRO_PS4 0xd00e #define USB_PRODUCT_THRUSTMASTER_T_FLIGHT_HOTAS_ONE 0xb68c #define USB_PRODUCT_VALVE_STEAM_CONTROLLER_DONGLE 0x1142 +#define USB_PRODUCT_VALVE_STEAM_PROTEUS_DONGLE 0x1304 +#define USB_PRODUCT_VALVE_STEAM_NEREID_DONGLE 0x1305 #define USB_PRODUCT_VICTRIX_FS_PRO 0x0203 #define USB_PRODUCT_VICTRIX_FS_PRO_V2 0x0207 #define USB_PRODUCT_XBOX360_XUSB_CONTROLLER 0x02a1 // XUSB driver software PID