summaryrefslogtreecommitdiff
path: root/source/3rd-party/SDL2/src/hidapi/ios/hid.m
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2019-05-11 22:54:56 +0800
committerchai <chaifix@163.com>2019-05-11 22:54:56 +0800
commit9645be0af1b1d5cb0ad5892d5464e1b23c51b550 (patch)
tree129c716bed8e93312421c3adb2f8e7c4f811602d /source/3rd-party/SDL2/src/hidapi/ios/hid.m
Diffstat (limited to 'source/3rd-party/SDL2/src/hidapi/ios/hid.m')
-rw-r--r--source/3rd-party/SDL2/src/hidapi/ios/hid.m914
1 files changed, 914 insertions, 0 deletions
diff --git a/source/3rd-party/SDL2/src/hidapi/ios/hid.m b/source/3rd-party/SDL2/src/hidapi/ios/hid.m
new file mode 100644
index 0000000..a0ca7cd
--- /dev/null
+++ b/source/3rd-party/SDL2/src/hidapi/ios/hid.m
@@ -0,0 +1,914 @@
+//======== 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"] )
+ {
+ 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 */