| //======== Copyright (c) 2017 Valve Corporation, All rights reserved. ========= |
| // |
| // Purpose: HID device abstraction temporary stub |
| // |
| //============================================================================= |
| #include "../../SDL_internal.h" |
| |
| #ifdef SDL_JOYSTICK_HIDAPI |
| |
| #include <CoreBluetooth/CoreBluetooth.h> |
| #include <QuartzCore/QuartzCore.h> |
| #import <UIKit/UIKit.h> |
| #import <mach/mach_time.h> |
| #include <pthread.h> |
| #include <sys/time.h> |
| #include <unistd.h> |
| #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 <CBPeripheralDelegate> |
| { |
| 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 <CBCentralManagerDelegate> |
| |
| @property (nonatomic) int nPendingScans; |
| @property (nonatomic) int nPendingPairs; |
| @property (nonatomic, strong) CBCentralManager *centralManager; |
| @property (nonatomic, strong) NSMapTable<CBPeripheral *, HIDBLEDevice *> *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<CBPeripheral *> *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"] ) |
| { |
| self.nPendingPairs += 1; |
| 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<HIDBLEDevice *> *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<HIDBLEDevice *> *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; |
| } |
| |
| #endif /* SDL_JOYSTICK_HIDAPI */ |