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