//======== Copyright (c) 2017 Valve Corporation, All rights reserved. ========= // // Purpose: HID device abstraction temporary stub // //============================================================================= #include #include #import #import #include #include #include #include "../hidapi/hidapi.h" #define VALVE_USB_VID 0x28DE #define D0G_BLE2_PID 0x1106 typedef uint32_t uint32; typedef uint64_t uint64; // enables detailed NSLog logging of feature reports #define FEATURE_REPORT_LOGGING 0 #define REPORT_SEGMENT_DATA_FLAG 0x80 #define REPORT_SEGMENT_LAST_FLAG 0x40 #define VALVE_SERVICE @"100F6C32-1735-4313-B402-38567131E5F3" // (READ/NOTIFICATIONS) #define VALVE_INPUT_CHAR @"100F6C33-1735-4313-B402-38567131E5F3" //  (READ/WRITE) #define VALVE_REPORT_CHAR @"100F6C34-1735-4313-B402-38567131E5F3" // TODO: create CBUUID's in __attribute__((constructor)) rather than doing [CBUUID UUIDWithString:...] everywhere #pragma pack(push,1) typedef 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; 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]; }; }; }; } hidFeatureReport; #pragma pack(pop) size_t GetBluetoothSegmentSize(bluetoothSegment *segment) { return segment->length + 3; } #define RingBuffer_cbElem 19 #define RingBuffer_nElem 4096 typedef struct { int _first, _last; uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ]; pthread_mutex_t accessLock; } RingBuffer; static void RingBuffer_init( RingBuffer *this ) { this->_first = -1; this->_last = 0; pthread_mutex_init( &this->accessLock, 0 ); } static bool RingBuffer_write( RingBuffer *this, const uint8_t *src ) { pthread_mutex_lock( &this->accessLock ); memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem ); if ( this->_first == -1 ) { this->_first = this->_last; } this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); if ( this->_last == this->_first ) { this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); pthread_mutex_unlock( &this->accessLock ); return false; } pthread_mutex_unlock( &this->accessLock ); return true; } static bool RingBuffer_read( RingBuffer *this, uint8_t *dst ) { 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); if ( this->_first == this->_last ) { this->_first = -1; } pthread_mutex_unlock( &this->accessLock ); return true; } #pragma mark HIDBLEDevice Definition typedef enum { BLEDeviceWaitState_None, BLEDeviceWaitState_Waiting, BLEDeviceWaitState_Complete, BLEDeviceWaitState_Error } BLEDeviceWaitState; @interface HIDBLEDevice : NSObject { RingBuffer _inputReports; uint8_t _featureReport[20]; BLEDeviceWaitState _waitStateForReadFeatureReport; BLEDeviceWaitState _waitStateForWriteFeatureReport; } @property (nonatomic, readwrite) bool connected; @property (nonatomic, readwrite) bool ready; @property (nonatomic, strong) CBPeripheral *bleSteamController; @property (nonatomic, strong) CBCharacteristic *bleCharacteristicInput; @property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport; - (id)initWithPeripheral:(CBPeripheral *)peripheral; @end @interface HIDBLEManager : NSObject @property (nonatomic) int nPendingScans; @property (nonatomic) int nPendingPairs; @property (nonatomic, strong) CBCentralManager *centralManager; @property (nonatomic, strong) NSMapTable *deviceMap; @property (nonatomic, retain) dispatch_queue_t bleSerialQueue; + (instancetype)sharedInstance; - (void)startScan:(int)duration; - (void)stopScan; - (int)updateConnectedSteamControllers:(BOOL) bForce; - (void)appWillResignActiveNotification:(NSNotification *)note; - (void)appDidBecomeActiveNotification:(NSNotification *)note; @end // singleton class - access using HIDBLEManager.sharedInstance @implementation HIDBLEManager + (instancetype)sharedInstance { static HIDBLEManager *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [HIDBLEManager new]; sharedInstance.nPendingScans = 0; sharedInstance.nPendingPairs = 0; [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appWillResignActiveNotification:) name: UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appDidBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil]; // receive reports on a high-priority serial-queue. optionally put writes on the serial queue to avoid logical // race conditions talking to the controller from multiple threads, although BLE fragmentation/assembly means // that we can still screw this up. // most importantly we need to consume reports at a high priority to avoid the OS thinking we aren't really // listening to the BLE device, as iOS on slower devices may stop delivery of packets to the app WITHOUT ACTUALLY // DISCONNECTING FROM THE DEVICE if we don't react quickly enough to their delivery. // see also the error-handling states in the peripheral delegate to re-open the device if it gets closed sharedInstance.bleSerialQueue = dispatch_queue_create( "com.valvesoftware.steamcontroller.ble", DISPATCH_QUEUE_SERIAL ); dispatch_set_target_queue( sharedInstance.bleSerialQueue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) ); // creating a CBCentralManager will always trigger a future centralManagerDidUpdateState: // where any scanning gets started or connecting to existing peripherals happens, it's never already in a // powered-on state for a newly launched application. sharedInstance.centralManager = [[CBCentralManager alloc] initWithDelegate:sharedInstance queue:sharedInstance.bleSerialQueue]; sharedInstance.deviceMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory capacity:4]; }); return sharedInstance; } // called for NSNotification UIApplicationWillResignActiveNotification - (void)appWillResignActiveNotification:(NSNotification *)note { // we'll get resign-active notification if pairing is happening. if ( self.nPendingPairs > 0 ) return; for ( CBPeripheral *peripheral in self.deviceMap ) { HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; if ( steamController ) { steamController.connected = NO; steamController.ready = NO; [self.centralManager cancelPeripheralConnection:peripheral]; } } [self.deviceMap removeAllObjects]; } // called for NSNotification UIApplicationDidBecomeActiveNotification // whenever the application comes back from being inactive, trigger a 20s pairing scan and reconnect // any devices that may have paired while we were inactive. - (void)appDidBecomeActiveNotification:(NSNotification *)note { [self updateConnectedSteamControllers:true]; [self startScan:20]; } - (int)updateConnectedSteamControllers:(BOOL) bForce { static uint64_t s_unLastUpdateTick = 0; static mach_timebase_info_data_t s_timebase_info; if (s_timebase_info.denom == 0) { mach_timebase_info( &s_timebase_info ); } uint64_t ticksNow = mach_approximate_time(); if ( !bForce && ( ( (ticksNow - s_unLastUpdateTick) * s_timebase_info.numer ) / s_timebase_info.denom ) < (5ull * NSEC_PER_SEC) ) return (int)self.deviceMap.count; // we can see previously connected BLE peripherals but can't connect until the CBCentralManager // is fully powered up - only do work when we are in that state if ( self.centralManager.state != CBManagerStatePoweredOn ) return (int)self.deviceMap.count; // only update our last-check-time if we actually did work, otherwise there can be a long delay during initial power-up s_unLastUpdateTick = mach_approximate_time(); // if a pair is in-flight, the central manager may still give it back via retrieveConnected... and // cause the SDL layer to attempt to initialize it while some of its endpoints haven't yet been established if ( self.nPendingPairs > 0 ) return (int)self.deviceMap.count; NSArray *peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices: @[ [CBUUID UUIDWithString:@"180A"]]]; for ( CBPeripheral *peripheral in peripherals ) { // we already know this peripheral if ( [self.deviceMap objectForKey: peripheral] != nil ) continue; NSLog( @"connected peripheral: %@", peripheral ); if ( [peripheral.name isEqualToString:@"SteamController"] ) { HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral]; [self.deviceMap setObject:steamController forKey:peripheral]; [self.centralManager connectPeripheral:peripheral options:nil]; } } return (int)self.deviceMap.count; } // manual API for folks to start & stop scanning - (void)startScan:(int)duration { NSLog( @"BLE: requesting scan for %d seconds", duration ); @synchronized (self) { if ( _nPendingScans++ == 0 ) { [self.centralManager scanForPeripheralsWithServices:nil options:nil]; } } if ( duration != 0 ) { dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self stopScan]; }); } } - (void)stopScan { NSLog( @"BLE: stopping scan" ); @synchronized (self) { if ( --_nPendingScans <= 0 ) { _nPendingScans = 0; [self.centralManager stopScan]; } } } #pragma mark CBCentralManagerDelegate Implementation // called whenever the BLE hardware state changes. - (void)centralManagerDidUpdateState:(CBCentralManager *)central { switch ( central.state ) { case CBCentralManagerStatePoweredOn: { NSLog( @"CoreBluetooth BLE hardware is powered on and ready" ); // at startup, if we have no already attached peripherals, do a 20s scan for new unpaired devices, // otherwise callers should occaisionally do additional scans. we don't want to continuously be // scanning because it drains battery, causes other nearby people to have a hard time pairing their // Steam Controllers, and may also trigger firmware weirdness when a device attempts to start // the pairing sequence multiple times concurrently if ( [self updateConnectedSteamControllers:false] == 0 ) { // TODO: we could limit our scan to only peripherals supporting the SteamController service, but // that service doesn't currently fit in the base advertising packet, we'd need to put it into an // extended scan packet. Useful optimization downstream, but not currently necessary // NSArray *services = @[[CBUUID UUIDWithString:VALVE_SERVICE]]; [self startScan:20]; } break; } case CBCentralManagerStatePoweredOff: NSLog( @"CoreBluetooth BLE hardware is powered off" ); break; case CBCentralManagerStateUnauthorized: NSLog( @"CoreBluetooth BLE state is unauthorized" ); break; case CBCentralManagerStateUnknown: NSLog( @"CoreBluetooth BLE state is unknown" ); break; case CBCentralManagerStateUnsupported: NSLog( @"CoreBluetooth BLE hardware is unsupported on this platform" ); break; case CBCentralManagerStateResetting: NSLog( @"CoreBluetooth BLE manager is resetting" ); break; } } - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { HIDBLEDevice *steamController = [_deviceMap objectForKey:peripheral]; steamController.connected = YES; self.nPendingPairs -= 1; } - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog( @"Failed to connect: %@", error ); [_deviceMap removeObjectForKey:peripheral]; self.nPendingPairs -= 1; } - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; NSString *log = [NSString stringWithFormat:@"Found '%@'", localName]; if ( [localName isEqualToString:@"SteamController"] ) { NSLog( @"%@ : %@ - %@", log, peripheral, advertisementData ); self.nPendingPairs += 1; HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral]; [self.deviceMap setObject:steamController forKey:peripheral]; [self.centralManager connectPeripheral:peripheral options:nil]; } } - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; if ( steamController ) { steamController.connected = NO; steamController.ready = NO; [self.deviceMap removeObjectForKey:peripheral]; } } @end // Core Bluetooth devices calling back on event boundaries of their run-loops. so annoying. static void process_pending_events() { CFRunLoopRunResult res; do { res = CFRunLoopRunInMode( kCFRunLoopDefaultMode, 0.001, FALSE ); } while( res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut ); } @implementation HIDBLEDevice - (id)init { if ( self = [super init] ) { RingBuffer_init( &_inputReports ); self.bleSteamController = nil; self.bleCharacteristicInput = nil; self.bleCharacteristicReport = nil; _connected = NO; _ready = NO; } return self; } - (id)initWithPeripheral:(CBPeripheral *)peripheral { if ( self = [super init] ) { RingBuffer_init( &_inputReports ); _connected = NO; _ready = NO; self.bleSteamController = peripheral; if ( peripheral ) { peripheral.delegate = self; } self.bleCharacteristicInput = nil; self.bleCharacteristicReport = nil; } return self; } - (void)setConnected:(bool)connected { _connected = connected; if ( _connected ) { [_bleSteamController discoverServices:nil]; } else { NSLog( @"Disconnected" ); } } - (size_t)read_input_report:(uint8_t *)dst { if ( RingBuffer_read( &_inputReports, dst+1 ) ) { *dst = 0x03; return 20; } return 0; } - (int)send_report:(const uint8_t *)data length:(size_t)length { [_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; return (int)length; } - (int)send_feature_report:(hidFeatureReport *)report { #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 ), 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]; // pretend we received a result anybody cares about return 19; #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 ] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting ) { process_pending_events(); } if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error ) { _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; return -1; } _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; return 19; #endif } - (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer { _waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting; [_bleSteamController readValueForCharacteristic:_bleCharacteristicReport]; while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting ) process_pending_events(); if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error ) { _waitStateForReadFeatureReport = BLEDeviceWaitState_None; return -1; } memcpy( buffer, _featureReport, sizeof(_featureReport) ); _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] ); #endif return 19; } #pragma mark CBPeripheralDelegate Implementation - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { for (CBService *service in peripheral.services) { NSLog( @"Found Service: %@", service ); if ( [service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]] ) { [peripheral discoverCharacteristics:nil forService:service]; } } } - (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { // nothing yet needed here, enable for logging if ( /* DISABLES CODE */ (0) ) { for ( CBDescriptor *descriptor in characteristic.descriptors ) { NSLog( @" - Descriptor '%@'", descriptor ); } } } - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { if ([service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]]) { for (CBCharacteristic *aChar in service.characteristics) { NSLog( @"Found Characteristic %@", aChar ); if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] ) { self.bleCharacteristicInput = aChar; } else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] ) { self.bleCharacteristicReport = aChar; [self.bleSteamController discoverDescriptorsForCharacteristic: aChar]; } } } } - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { static uint64_t s_ticksLastOverflowReport = 0; // receiving an input report is the final indicator that the user accepted a pairing // request and that we successfully established notification. CoreBluetooth has no // notification of the pairing acknowledgement, which is a bad oversight. if ( self.ready == NO ) { self.ready = YES; HIDBLEManager.sharedInstance.nPendingPairs -= 1; } if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] ) { NSData *data = [characteristic value]; if ( data.length != 19 ) { NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length ); } if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) ) { uint64_t ticksNow = mach_approximate_time(); if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) ) { NSLog( @"HIDBLE: input report buffer overflow" ); s_ticksLastOverflowReport = ticksNow; } } } else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] ) { memset( _featureReport, 0, sizeof(_featureReport) ); if ( error != nil ) { NSLog( @"HIDBLE: get_feature_report error: %@", error ); _waitStateForReadFeatureReport = BLEDeviceWaitState_Error; } 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) ) ); _waitStateForReadFeatureReport = BLEDeviceWaitState_Complete; } } } - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { if ( [characteristic.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] ) { if ( error != nil ) { NSLog( @"HIDBLE: write_feature_report error: %@", error ); _waitStateForWriteFeatureReport = BLEDeviceWaitState_Error; } else { _waitStateForWriteFeatureReport = BLEDeviceWaitState_Complete; } } } - (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { NSLog( @"didUpdateNotifcationStateForCharacteristic %@ (%@)", characteristic, error ); } @end #pragma mark hid_api implementation struct hid_device_ { void *device_handle; int blocking; hid_device *next; }; int HID_API_EXPORT HID_API_CALL hid_init(void) { return ( HIDBLEManager.sharedInstance == nil ) ? -1 : 0; } int HID_API_EXPORT HID_API_CALL hid_exit(void) { return 0; } void HID_API_EXPORT HID_API_CALL hid_ble_scan( bool bStart ) { HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; if ( bStart ) { [bleManager startScan:0]; } else { [bleManager stopScan]; } } hid_device * HID_API_EXPORT hid_open_path( const char *path, int bExclusive /* = false */ ) { hid_device *result = NULL; NSString *nssPath = [NSString stringWithUTF8String:path]; HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; NSEnumerator *devices = [bleManager.deviceMap objectEnumerator]; for ( HIDBLEDevice *device in devices ) { // we have the device but it hasn't found its service or characteristics until it is connected if ( !device.ready || !device.connected || !device.bleCharacteristicInput ) continue; if ( [device.bleSteamController.identifier.UUIDString isEqualToString:nssPath] ) { result = (hid_device *)malloc( sizeof( hid_device ) ); memset( result, 0, sizeof( hid_device ) ); result->device_handle = (void*)CFBridgingRetain( device ); result->blocking = NO; // enable reporting input events on the characteristic [device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput]; return result; } } return result; } void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) { /* This function is identical to the Linux version. Platform independent. */ struct hid_device_info *d = devs; while (d) { struct hid_device_info *next = d->next; free(d->path); free(d->serial_number); free(d->manufacturer_string); free(d->product_string); free(d); d = next; } } int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* All Nonblocking operation is handled by the library. */ dev->blocking = !nonblock; return 0; } struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { @autoreleasepool { struct hid_device_info *root = NULL; if ( ( vendor_id == 0 && product_id == 0 ) || ( vendor_id == VALVE_USB_VID && product_id == D0G_BLE2_PID ) ) { HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; [bleManager updateConnectedSteamControllers:false]; NSEnumerator *devices = [bleManager.deviceMap objectEnumerator]; for ( HIDBLEDevice *device in devices ) { // there are several brief windows in connecting to an already paired device and // one long window waiting for users to confirm pairing where we don't want // to consider a device ready - if we hand it back to SDL or another // Steam Controller consumer, their additional SC setup work will fail // in unusual/silent ways and we can actually corrupt the BLE stack for // the entire system and kill the appletv remote's Menu button (!) if ( device.bleSteamController.state != CBPeripheralStateConnected || device.connected == NO || device.ready == NO ) { if ( device.ready == NO && device.bleCharacteristicInput != nil ) { // attempt to register for input reports. this call will silently fail // until the pairing finalizes with user acceptance. oh, apple. [device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput]; } continue; } struct hid_device_info *device_info = (struct hid_device_info *)malloc( sizeof(struct hid_device_info) ); memset( device_info, 0, sizeof(struct hid_device_info) ); device_info->next = root; root = 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_string = wcsdup( L"Steam Controller" ); device_info->manufacturer_string = wcsdup( L"Valve Corporation" ); } } return root; }} int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) { static wchar_t s_wszManufacturer[] = L"Valve Corporation"; wcsncpy( string, s_wszManufacturer, sizeof(s_wszManufacturer)/sizeof(s_wszManufacturer[0]) ); return 0; } int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) { static wchar_t s_wszProduct[] = L"Steam Controller"; wcsncpy( string, s_wszProduct, sizeof(s_wszProduct)/sizeof(s_wszProduct[0]) ); return 0; } int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) { static wchar_t s_wszSerial[] = L"12345"; wcsncpy( string, s_wszSerial, sizeof(s_wszSerial)/sizeof(s_wszSerial[0]) ); return 0; } int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) { HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; if ( !device_handle.connected ) return -1; return [device_handle send_report:data length:length]; } void HID_API_EXPORT hid_close(hid_device *dev) { HIDBLEDevice *device_handle = CFBridgingRelease( dev->device_handle ); // disable reporting input events on the characteristic if ( device_handle.connected ) { [device_handle.bleSteamController setNotifyValue:NO forCharacteristic:device_handle.bleCharacteristicInput]; } free( dev ); } int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; if ( !device_handle.connected ) return -1; return [device_handle send_feature_report:(hidFeatureReport *)(void *)data]; } int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) { HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; if ( !device_handle.connected ) return -1; size_t written = [device_handle get_feature_report:data[0] into:data]; return written == length-1 ? (int)length : (int)written; } int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) { HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; if ( !device_handle.connected ) return -1; return hid_read_timeout(dev, data, length, 0); } int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) { HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; if ( !device_handle.connected ) return -1; if ( milliseconds != 0 ) { NSLog( @"hid_read_timeout with non-zero wait" ); } int result = (int)[device_handle read_input_report:data]; #if 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], data[13], data[14], data[15], data[16], data[17], data[18], data[19] ); #endif return result; }