1//======== Copyright (c) 2017 Valve Corporation, All rights reserved. ========= 2// 3// Purpose: HID device abstraction temporary stub 4// 5//============================================================================= 6#include "../../SDL_internal.h" 7 8#ifdef SDL_JOYSTICK_HIDAPI 9 10#include <CoreBluetooth/CoreBluetooth.h> 11#include <QuartzCore/QuartzCore.h> 12#import <UIKit/UIKit.h> 13#import <mach/mach_time.h> 14#include <pthread.h> 15#include <sys/time.h> 16#include <unistd.h> 17#include "../hidapi/hidapi.h" 18 19#define VALVE_USB_VID 0x28DE 20#define D0G_BLE2_PID 0x1106 21 22typedef uint32_t uint32; 23typedef uint64_t uint64; 24 25// enables detailed NSLog logging of feature reports 26#define FEATURE_REPORT_LOGGING 0 27 28#define REPORT_SEGMENT_DATA_FLAG 0x80 29#define REPORT_SEGMENT_LAST_FLAG 0x40 30 31#define VALVE_SERVICE @"100F6C32-1735-4313-B402-38567131E5F3" 32 33// (READ/NOTIFICATIONS) 34#define VALVE_INPUT_CHAR @"100F6C33-1735-4313-B402-38567131E5F3" 35 36// (READ/WRITE) 37#define VALVE_REPORT_CHAR @"100F6C34-1735-4313-B402-38567131E5F3" 38 39// TODO: create CBUUID's in __attribute__((constructor)) rather than doing [CBUUID UUIDWithString:...] everywhere 40 41#pragma pack(push,1) 42 43typedef struct 44{ 45 uint8_t segmentHeader; 46 uint8_t featureReportMessageID; 47 uint8_t length; 48 uint8_t settingIdentifier; 49 union { 50 uint16_t usPayload; 51 uint32_t uPayload; 52 uint64_t ulPayload; 53 uint8_t ucPayload[15]; 54 }; 55} bluetoothSegment; 56 57typedef struct { 58 uint8_t id; 59 union { 60 bluetoothSegment segment; 61 struct { 62 uint8_t segmentHeader; 63 uint8_t featureReportMessageID; 64 uint8_t length; 65 uint8_t settingIdentifier; 66 union { 67 uint16_t usPayload; 68 uint32_t uPayload; 69 uint64_t ulPayload; 70 uint8_t ucPayload[15]; 71 }; 72 }; 73 }; 74} hidFeatureReport; 75 76#pragma pack(pop) 77 78size_t GetBluetoothSegmentSize(bluetoothSegment *segment) 79{ 80 return segment->length + 3; 81} 82 83#define RingBuffer_cbElem 19 84#define RingBuffer_nElem 4096 85 86typedef struct { 87 int _first, _last; 88 uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ]; 89 pthread_mutex_t accessLock; 90} RingBuffer; 91 92static void RingBuffer_init( RingBuffer *this ) 93{ 94 this->_first = -1; 95 this->_last = 0; 96 pthread_mutex_init( &this->accessLock, 0 ); 97} 98 99static bool RingBuffer_write( RingBuffer *this, const uint8_t *src ) 100{ 101 pthread_mutex_lock( &this->accessLock ); 102 memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem ); 103 if ( this->_first == -1 ) 104 { 105 this->_first = this->_last; 106 } 107 this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); 108 if ( this->_last == this->_first ) 109 { 110 this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); 111 pthread_mutex_unlock( &this->accessLock ); 112 return false; 113 } 114 pthread_mutex_unlock( &this->accessLock ); 115 return true; 116} 117 118static bool RingBuffer_read( RingBuffer *this, uint8_t *dst ) 119{ 120 pthread_mutex_lock( &this->accessLock ); 121 if ( this->_first == -1 ) 122 { 123 pthread_mutex_unlock( &this->accessLock ); 124 return false; 125 } 126 memcpy( dst, &this->_data[ this->_first ], RingBuffer_cbElem ); 127 this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem); 128 if ( this->_first == this->_last ) 129 { 130 this->_first = -1; 131 } 132 pthread_mutex_unlock( &this->accessLock ); 133 return true; 134} 135 136 137#pragma mark HIDBLEDevice Definition 138 139typedef enum 140{ 141 BLEDeviceWaitState_None, 142 BLEDeviceWaitState_Waiting, 143 BLEDeviceWaitState_Complete, 144 BLEDeviceWaitState_Error 145} BLEDeviceWaitState; 146 147@interface HIDBLEDevice : NSObject <CBPeripheralDelegate> 148{ 149 RingBuffer _inputReports; 150 uint8_t _featureReport[20]; 151 BLEDeviceWaitState _waitStateForReadFeatureReport; 152 BLEDeviceWaitState _waitStateForWriteFeatureReport; 153} 154 155@property (nonatomic, readwrite) bool connected; 156@property (nonatomic, readwrite) bool ready; 157 158@property (nonatomic, strong) CBPeripheral *bleSteamController; 159@property (nonatomic, strong) CBCharacteristic *bleCharacteristicInput; 160@property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport; 161 162- (id)initWithPeripheral:(CBPeripheral *)peripheral; 163 164@end 165 166 167@interface HIDBLEManager : NSObject <CBCentralManagerDelegate> 168 169@property (nonatomic) int nPendingScans; 170@property (nonatomic) int nPendingPairs; 171@property (nonatomic, strong) CBCentralManager *centralManager; 172@property (nonatomic, strong) NSMapTable<CBPeripheral *, HIDBLEDevice *> *deviceMap; 173@property (nonatomic, retain) dispatch_queue_t bleSerialQueue; 174 175+ (instancetype)sharedInstance; 176- (void)startScan:(int)duration; 177- (void)stopScan; 178- (int)updateConnectedSteamControllers:(BOOL) bForce; 179- (void)appWillResignActiveNotification:(NSNotification *)note; 180- (void)appDidBecomeActiveNotification:(NSNotification *)note; 181 182@end 183 184 185// singleton class - access using HIDBLEManager.sharedInstance 186@implementation HIDBLEManager 187 188+ (instancetype)sharedInstance 189{ 190 static HIDBLEManager *sharedInstance = nil; 191 static dispatch_once_t onceToken; 192 dispatch_once(&onceToken, ^{ 193 sharedInstance = [HIDBLEManager new]; 194 sharedInstance.nPendingScans = 0; 195 sharedInstance.nPendingPairs = 0; 196 197 [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appWillResignActiveNotification:) name: UIApplicationWillResignActiveNotification object:nil]; 198 [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appDidBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil]; 199 200 // receive reports on a high-priority serial-queue. optionally put writes on the serial queue to avoid logical 201 // race conditions talking to the controller from multiple threads, although BLE fragmentation/assembly means 202 // that we can still screw this up. 203 // most importantly we need to consume reports at a high priority to avoid the OS thinking we aren't really 204 // listening to the BLE device, as iOS on slower devices may stop delivery of packets to the app WITHOUT ACTUALLY 205 // DISCONNECTING FROM THE DEVICE if we don't react quickly enough to their delivery. 206 // see also the error-handling states in the peripheral delegate to re-open the device if it gets closed 207 sharedInstance.bleSerialQueue = dispatch_queue_create( "com.valvesoftware.steamcontroller.ble", DISPATCH_QUEUE_SERIAL ); 208 dispatch_set_target_queue( sharedInstance.bleSerialQueue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) ); 209 210 // creating a CBCentralManager will always trigger a future centralManagerDidUpdateState: 211 // where any scanning gets started or connecting to existing peripherals happens, it's never already in a 212 // powered-on state for a newly launched application. 213 sharedInstance.centralManager = [[CBCentralManager alloc] initWithDelegate:sharedInstance queue:sharedInstance.bleSerialQueue]; 214 sharedInstance.deviceMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory capacity:4]; 215 }); 216 return sharedInstance; 217} 218 219// called for NSNotification UIApplicationWillResignActiveNotification 220- (void)appWillResignActiveNotification:(NSNotification *)note 221{ 222 // we'll get resign-active notification if pairing is happening. 223 if ( self.nPendingPairs > 0 ) 224 return; 225 226 for ( CBPeripheral *peripheral in self.deviceMap ) 227 { 228 HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; 229 if ( steamController ) 230 { 231 steamController.connected = NO; 232 steamController.ready = NO; 233 [self.centralManager cancelPeripheralConnection:peripheral]; 234 } 235 } 236 [self.deviceMap removeAllObjects]; 237} 238 239// called for NSNotification UIApplicationDidBecomeActiveNotification 240// whenever the application comes back from being inactive, trigger a 20s pairing scan and reconnect 241// any devices that may have paired while we were inactive. 242- (void)appDidBecomeActiveNotification:(NSNotification *)note 243{ 244 [self updateConnectedSteamControllers:true]; 245 [self startScan:20]; 246} 247 248- (int)updateConnectedSteamControllers:(BOOL) bForce 249{ 250 static uint64_t s_unLastUpdateTick = 0; 251 static mach_timebase_info_data_t s_timebase_info; 252 253 if (s_timebase_info.denom == 0) 254 { 255 mach_timebase_info( &s_timebase_info ); 256 } 257 258 uint64_t ticksNow = mach_approximate_time(); 259 if ( !bForce && ( ( (ticksNow - s_unLastUpdateTick) * s_timebase_info.numer ) / s_timebase_info.denom ) < (5ull * NSEC_PER_SEC) ) 260 return (int)self.deviceMap.count; 261 262 // we can see previously connected BLE peripherals but can't connect until the CBCentralManager 263 // is fully powered up - only do work when we are in that state 264 if ( self.centralManager.state != CBManagerStatePoweredOn ) 265 return (int)self.deviceMap.count; 266 267 // only update our last-check-time if we actually did work, otherwise there can be a long delay during initial power-up 268 s_unLastUpdateTick = mach_approximate_time(); 269 270 // if a pair is in-flight, the central manager may still give it back via retrieveConnected... and 271 // cause the SDL layer to attempt to initialize it while some of its endpoints haven't yet been established 272 if ( self.nPendingPairs > 0 ) 273 return (int)self.deviceMap.count; 274 275 NSArray<CBPeripheral *> *peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices: @[ [CBUUID UUIDWithString:@"180A"]]]; 276 for ( CBPeripheral *peripheral in peripherals ) 277 { 278 // we already know this peripheral 279 if ( [self.deviceMap objectForKey: peripheral] != nil ) 280 continue; 281 282 NSLog( @"connected peripheral: %@", peripheral ); 283 if ( [peripheral.name isEqualToString:@"SteamController"] ) 284 { 285 self.nPendingPairs += 1; 286 HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral]; 287 [self.deviceMap setObject:steamController forKey:peripheral]; 288 [self.centralManager connectPeripheral:peripheral options:nil]; 289 } 290 } 291 292 return (int)self.deviceMap.count; 293} 294 295// manual API for folks to start & stop scanning 296- (void)startScan:(int)duration 297{ 298 NSLog( @"BLE: requesting scan for %d seconds", duration ); 299 @synchronized (self) 300 { 301 if ( _nPendingScans++ == 0 ) 302 { 303 [self.centralManager scanForPeripheralsWithServices:nil options:nil]; 304 } 305 } 306 307 if ( duration != 0 ) 308 { 309 dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 310 [self stopScan]; 311 }); 312 } 313} 314 315- (void)stopScan 316{ 317 NSLog( @"BLE: stopping scan" ); 318 @synchronized (self) 319 { 320 if ( --_nPendingScans <= 0 ) 321 { 322 _nPendingScans = 0; 323 [self.centralManager stopScan]; 324 } 325 } 326} 327 328 329#pragma mark CBCentralManagerDelegate Implementation 330 331// called whenever the BLE hardware state changes. 332- (void)centralManagerDidUpdateState:(CBCentralManager *)central 333{ 334 switch ( central.state ) 335 { 336 case CBCentralManagerStatePoweredOn: 337 { 338 NSLog( @"CoreBluetooth BLE hardware is powered on and ready" ); 339 340 // at startup, if we have no already attached peripherals, do a 20s scan for new unpaired devices, 341 // otherwise callers should occaisionally do additional scans. we don't want to continuously be 342 // scanning because it drains battery, causes other nearby people to have a hard time pairing their 343 // Steam Controllers, and may also trigger firmware weirdness when a device attempts to start 344 // the pairing sequence multiple times concurrently 345 if ( [self updateConnectedSteamControllers:false] == 0 ) 346 { 347 // TODO: we could limit our scan to only peripherals supporting the SteamController service, but 348 // that service doesn't currently fit in the base advertising packet, we'd need to put it into an 349 // extended scan packet. Useful optimization downstream, but not currently necessary 350 // NSArray *services = @[[CBUUID UUIDWithString:VALVE_SERVICE]]; 351 [self startScan:20]; 352 } 353 break; 354 } 355 356 case CBCentralManagerStatePoweredOff: 357 NSLog( @"CoreBluetooth BLE hardware is powered off" ); 358 break; 359 360 case CBCentralManagerStateUnauthorized: 361 NSLog( @"CoreBluetooth BLE state is unauthorized" ); 362 break; 363 364 case CBCentralManagerStateUnknown: 365 NSLog( @"CoreBluetooth BLE state is unknown" ); 366 break; 367 368 case CBCentralManagerStateUnsupported: 369 NSLog( @"CoreBluetooth BLE hardware is unsupported on this platform" ); 370 break; 371 372 case CBCentralManagerStateResetting: 373 NSLog( @"CoreBluetooth BLE manager is resetting" ); 374 break; 375 } 376} 377 378- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral 379{ 380 HIDBLEDevice *steamController = [_deviceMap objectForKey:peripheral]; 381 steamController.connected = YES; 382 self.nPendingPairs -= 1; 383} 384 385- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error 386{ 387 NSLog( @"Failed to connect: %@", error ); 388 [_deviceMap removeObjectForKey:peripheral]; 389 self.nPendingPairs -= 1; 390} 391 392- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI 393{ 394 NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; 395 NSString *log = [NSString stringWithFormat:@"Found '%@'", localName]; 396 397 if ( [localName isEqualToString:@"SteamController"] ) 398 { 399 NSLog( @"%@ : %@ - %@", log, peripheral, advertisementData ); 400 self.nPendingPairs += 1; 401 HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral]; 402 [self.deviceMap setObject:steamController forKey:peripheral]; 403 [self.centralManager connectPeripheral:peripheral options:nil]; 404 } 405} 406 407- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error 408{ 409 HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral]; 410 if ( steamController ) 411 { 412 steamController.connected = NO; 413 steamController.ready = NO; 414 [self.deviceMap removeObjectForKey:peripheral]; 415 } 416} 417 418@end 419 420 421// Core Bluetooth devices calling back on event boundaries of their run-loops. so annoying. 422static void process_pending_events() 423{ 424 CFRunLoopRunResult res; 425 do 426 { 427 res = CFRunLoopRunInMode( kCFRunLoopDefaultMode, 0.001, FALSE ); 428 } 429 while( res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut ); 430} 431 432@implementation HIDBLEDevice 433 434- (id)init 435{ 436 if ( self = [super init] ) 437 { 438 RingBuffer_init( &_inputReports ); 439 self.bleSteamController = nil; 440 self.bleCharacteristicInput = nil; 441 self.bleCharacteristicReport = nil; 442 _connected = NO; 443 _ready = NO; 444 } 445 return self; 446} 447 448- (id)initWithPeripheral:(CBPeripheral *)peripheral 449{ 450 if ( self = [super init] ) 451 { 452 RingBuffer_init( &_inputReports ); 453 _connected = NO; 454 _ready = NO; 455 self.bleSteamController = peripheral; 456 if ( peripheral ) 457 { 458 peripheral.delegate = self; 459 } 460 self.bleCharacteristicInput = nil; 461 self.bleCharacteristicReport = nil; 462 } 463 return self; 464} 465 466- (void)setConnected:(bool)connected 467{ 468 _connected = connected; 469 if ( _connected ) 470 { 471 [_bleSteamController discoverServices:nil]; 472 } 473 else 474 { 475 NSLog( @"Disconnected" ); 476 } 477} 478 479- (size_t)read_input_report:(uint8_t *)dst 480{ 481 if ( RingBuffer_read( &_inputReports, dst+1 ) ) 482 { 483 *dst = 0x03; 484 return 20; 485 } 486 return 0; 487} 488 489- (int)send_report:(const uint8_t *)data length:(size_t)length 490{ 491 [_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; 492 return (int)length; 493} 494 495- (int)send_feature_report:(hidFeatureReport *)report 496{ 497#if FEATURE_REPORT_LOGGING 498 uint8_t *reportBytes = (uint8_t *)report; 499 500 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 ), 501 reportBytes[1], reportBytes[2], reportBytes[3], reportBytes[4], reportBytes[5], reportBytes[6], 502 reportBytes[7], reportBytes[8], reportBytes[9], reportBytes[10], reportBytes[11], reportBytes[12], 503 reportBytes[13], reportBytes[14], reportBytes[15], reportBytes[16], reportBytes[17], reportBytes[18], 504 reportBytes[19] ); 505#endif 506 507 int sendSize = (int)GetBluetoothSegmentSize( &report->segment ); 508 if ( sendSize > 20 ) 509 sendSize = 20; 510 511#if 1 512 // fire-and-forget - we are going to not wait for the response here because all Steam Controller BLE send_feature_report's are ignored, 513 // except errors. 514 [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; 515 516 // pretend we received a result anybody cares about 517 return 19; 518 519#else 520 // this is technically the correct send_feature_report logic if you want to make sure it gets through and is 521 // acknowledged or errors out 522 _waitStateForWriteFeatureReport = BLEDeviceWaitState_Waiting; 523 [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize 524 ] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse]; 525 526 while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting ) 527 { 528 process_pending_events(); 529 } 530 531 if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error ) 532 { 533 _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; 534 return -1; 535 } 536 537 _waitStateForWriteFeatureReport = BLEDeviceWaitState_None; 538 return 19; 539#endif 540} 541 542- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer 543{ 544 _waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting; 545 [_bleSteamController readValueForCharacteristic:_bleCharacteristicReport]; 546 547 while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting ) 548 process_pending_events(); 549 550 if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error ) 551 { 552 _waitStateForReadFeatureReport = BLEDeviceWaitState_None; 553 return -1; 554 } 555 556 memcpy( buffer, _featureReport, sizeof(_featureReport) ); 557 558 _waitStateForReadFeatureReport = BLEDeviceWaitState_None; 559 560#if FEATURE_REPORT_LOGGING 561 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]", 562 buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], 563 buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12], 564 buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18], 565 buffer[19] ); 566#endif 567 568 return 19; 569} 570 571#pragma mark CBPeripheralDelegate Implementation 572 573- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error 574{ 575 for (CBService *service in peripheral.services) 576 { 577 NSLog( @"Found Service: %@", service ); 578 if ( [service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]] ) 579 { 580 [peripheral discoverCharacteristics:nil forService:service]; 581 } 582 } 583} 584 585- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 586{ 587 // nothing yet needed here, enable for logging 588 if ( /* DISABLES CODE */ (0) ) 589 { 590 for ( CBDescriptor *descriptor in characteristic.descriptors ) 591 { 592 NSLog( @" - Descriptor '%@'", descriptor ); 593 } 594 } 595} 596 597- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error 598{ 599 if ([service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]]) 600 { 601 for (CBCharacteristic *aChar in service.characteristics) 602 { 603 NSLog( @"Found Characteristic %@", aChar ); 604 605 if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] ) 606 { 607 self.bleCharacteristicInput = aChar; 608 } 609 else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] ) 610 { 611 self.bleCharacteristicReport = aChar; 612 [self.bleSteamController discoverDescriptorsForCharacteristic: aChar]; 613 } 614 } 615 } 616} 617 618- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 619{ 620 static uint64_t s_ticksLastOverflowReport = 0; 621 622 // receiving an input report is the final indicator that the user accepted a pairing 623 // request and that we successfully established notification. CoreBluetooth has no 624 // notification of the pairing acknowledgement, which is a bad oversight. 625 if ( self.ready == NO ) 626 { 627 self.ready = YES; 628 HIDBLEManager.sharedInstance.nPendingPairs -= 1; 629 } 630 631 if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] ) 632 { 633 NSData *data = [characteristic value]; 634 if ( data.length != 19 ) 635 { 636 NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length ); 637 } 638 if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) ) 639 { 640 uint64_t ticksNow = mach_approximate_time(); 641 if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) ) 642 { 643 NSLog( @"HIDBLE: input report buffer overflow" ); 644 s_ticksLastOverflowReport = ticksNow; 645 } 646 } 647 } 648 else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] ) 649 { 650 memset( _featureReport, 0, sizeof(_featureReport) ); 651 652 if ( error != nil ) 653 { 654 NSLog( @"HIDBLE: get_feature_report error: %@", error ); 655 _waitStateForReadFeatureReport = BLEDeviceWaitState_Error; 656 } 657 else 658 { 659 NSData *data = [characteristic value]; 660 if ( data.length != 20 ) 661 { 662 NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 20", (unsigned long)data.length ); 663 } 664 memcpy( _featureReport, data.bytes, MIN( data.length, sizeof(_featureReport) ) ); 665 _waitStateForReadFeatureReport = BLEDeviceWaitState_Complete; 666 } 667 } 668} 669 670- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 671{ 672 if ( [characteristic.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] ) 673 { 674 if ( error != nil ) 675 { 676 NSLog( @"HIDBLE: write_feature_report error: %@", error ); 677 _waitStateForWriteFeatureReport = BLEDeviceWaitState_Error; 678 } 679 else 680 { 681 _waitStateForWriteFeatureReport = BLEDeviceWaitState_Complete; 682 } 683 } 684} 685 686- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error 687{ 688 NSLog( @"didUpdateNotifcationStateForCharacteristic %@ (%@)", characteristic, error ); 689} 690 691@end 692 693 694#pragma mark hid_api implementation 695 696struct hid_device_ { 697 void *device_handle; 698 int blocking; 699 hid_device *next; 700}; 701 702int HID_API_EXPORT HID_API_CALL hid_init(void) 703{ 704 return ( HIDBLEManager.sharedInstance == nil ) ? -1 : 0; 705} 706 707int HID_API_EXPORT HID_API_CALL hid_exit(void) 708{ 709 return 0; 710} 711 712void HID_API_EXPORT HID_API_CALL hid_ble_scan( bool bStart ) 713{ 714 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; 715 if ( bStart ) 716 { 717 [bleManager startScan:0]; 718 } 719 else 720 { 721 [bleManager stopScan]; 722 } 723} 724 725hid_device * HID_API_EXPORT hid_open_path( const char *path, int bExclusive /* = false */ ) 726{ 727 hid_device *result = NULL; 728 NSString *nssPath = [NSString stringWithUTF8String:path]; 729 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; 730 NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator]; 731 732 for ( HIDBLEDevice *device in devices ) 733 { 734 // we have the device but it hasn't found its service or characteristics until it is connected 735 if ( !device.ready || !device.connected || !device.bleCharacteristicInput ) 736 continue; 737 738 if ( [device.bleSteamController.identifier.UUIDString isEqualToString:nssPath] ) 739 { 740 result = (hid_device *)malloc( sizeof( hid_device ) ); 741 memset( result, 0, sizeof( hid_device ) ); 742 result->device_handle = (void*)CFBridgingRetain( device ); 743 result->blocking = NO; 744 // enable reporting input events on the characteristic 745 [device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput]; 746 return result; 747 } 748 } 749 return result; 750} 751 752void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) 753{ 754 /* This function is identical to the Linux version. Platform independent. */ 755 struct hid_device_info *d = devs; 756 while (d) { 757 struct hid_device_info *next = d->next; 758 free(d->path); 759 free(d->serial_number); 760 free(d->manufacturer_string); 761 free(d->product_string); 762 free(d); 763 d = next; 764 } 765} 766 767int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) 768{ 769 /* All Nonblocking operation is handled by the library. */ 770 dev->blocking = !nonblock; 771 772 return 0; 773} 774 775struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) 776{ @autoreleasepool { 777 struct hid_device_info *root = NULL; 778 779 if ( ( vendor_id == 0 && product_id == 0 ) || 780 ( vendor_id == VALVE_USB_VID && product_id == D0G_BLE2_PID ) ) 781 { 782 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance; 783 [bleManager updateConnectedSteamControllers:false]; 784 NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator]; 785 for ( HIDBLEDevice *device in devices ) 786 { 787 // there are several brief windows in connecting to an already paired device and 788 // one long window waiting for users to confirm pairing where we don't want 789 // to consider a device ready - if we hand it back to SDL or another 790 // Steam Controller consumer, their additional SC setup work will fail 791 // in unusual/silent ways and we can actually corrupt the BLE stack for 792 // the entire system and kill the appletv remote's Menu button (!) 793 if ( device.bleSteamController.state != CBPeripheralStateConnected || 794 device.connected == NO || device.ready == NO ) 795 { 796 if ( device.ready == NO && device.bleCharacteristicInput != nil ) 797 { 798 // attempt to register for input reports. this call will silently fail 799 // until the pairing finalizes with user acceptance. oh, apple. 800 [device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput]; 801 } 802 continue; 803 } 804 struct hid_device_info *device_info = (struct hid_device_info *)malloc( sizeof(struct hid_device_info) ); 805 memset( device_info, 0, sizeof(struct hid_device_info) ); 806 device_info->next = root; 807 root = device_info; 808 device_info->path = strdup( device.bleSteamController.identifier.UUIDString.UTF8String ); 809 device_info->vendor_id = VALVE_USB_VID; 810 device_info->product_id = D0G_BLE2_PID; 811 device_info->product_string = wcsdup( L"Steam Controller" ); 812 device_info->manufacturer_string = wcsdup( L"Valve Corporation" ); 813 } 814 } 815 return root; 816}} 817 818int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) 819{ 820 static wchar_t s_wszManufacturer[] = L"Valve Corporation"; 821 wcsncpy( string, s_wszManufacturer, sizeof(s_wszManufacturer)/sizeof(s_wszManufacturer[0]) ); 822 return 0; 823} 824 825int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) 826{ 827 static wchar_t s_wszProduct[] = L"Steam Controller"; 828 wcsncpy( string, s_wszProduct, sizeof(s_wszProduct)/sizeof(s_wszProduct[0]) ); 829 return 0; 830} 831 832int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) 833{ 834 static wchar_t s_wszSerial[] = L"12345"; 835 wcsncpy( string, s_wszSerial, sizeof(s_wszSerial)/sizeof(s_wszSerial[0]) ); 836 return 0; 837} 838 839int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) 840{ 841 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; 842 843 if ( !device_handle.connected ) 844 return -1; 845 846 return [device_handle send_report:data length:length]; 847} 848 849void HID_API_EXPORT hid_close(hid_device *dev) 850{ 851 HIDBLEDevice *device_handle = CFBridgingRelease( dev->device_handle ); 852 853 // disable reporting input events on the characteristic 854 if ( device_handle.connected ) { 855 [device_handle.bleSteamController setNotifyValue:NO forCharacteristic:device_handle.bleCharacteristicInput]; 856 } 857 858 free( dev ); 859} 860 861int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) 862{ 863 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; 864 865 if ( !device_handle.connected ) 866 return -1; 867 868 return [device_handle send_feature_report:(hidFeatureReport *)(void *)data]; 869} 870 871int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) 872{ 873 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; 874 875 if ( !device_handle.connected ) 876 return -1; 877 878 size_t written = [device_handle get_feature_report:data[0] into:data]; 879 880 return written == length-1 ? (int)length : (int)written; 881} 882 883int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) 884{ 885 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; 886 887 if ( !device_handle.connected ) 888 return -1; 889 890 return hid_read_timeout(dev, data, length, 0); 891} 892 893int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) 894{ 895 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle; 896 897 if ( !device_handle.connected ) 898 return -1; 899 900 if ( milliseconds != 0 ) 901 { 902 NSLog( @"hid_read_timeout with non-zero wait" ); 903 } 904 int result = (int)[device_handle read_input_report:data]; 905#if FEATURE_REPORT_LOGGING 906 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, 907 data[1], data[2], data[3], data[4], data[5], data[6], 908 data[7], data[8], data[9], data[10], data[11], data[12], 909 data[13], data[14], data[15], data[16], data[17], data[18], 910 data[19] ); 911#endif 912 return result; 913} 914 915#endif /* SDL_JOYSTICK_HIDAPI */ 916