shithub: cstory

ref: e48b3897e956e2bc0f284d0e2fc23e5ebcc01bbb
dir: /external/SDL2/src/hidapi/ios/hid.m/

View raw version
//======== 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 */