2
Simple DirectMedia Layer
3
Copyright (C) 2021 Valve Corporation
5
This software is provided 'as-is', without any express or implied
6
warranty. In no event will the authors be held liable for any damages
7
arising from the use of this software.
9
Permission is granted to anyone to use this software for any purpose,
10
including commercial applications, and to alter it and redistribute it
11
freely, subject to the following restrictions:
13
1. The origin of this software must not be misrepresented; you must not
14
claim that you wrote the original software. If you use this software
15
in a product, an acknowledgment in the product documentation would be
16
appreciated but is not required.
17
2. Altered source versions must be plainly marked as such, and must not be
18
misrepresented as being the original software.
19
3. This notice may not be removed or altered from any source distribution.
21
#include "SDL_internal.h"
23
#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS)
25
#ifndef SDL_HIDAPI_DISABLED
27
#include "../SDL_hidapi_c.h"
29
#define hid_close PLATFORM_hid_close
30
#define hid_device PLATFORM_hid_device
31
#define hid_device_ PLATFORM_hid_device_
32
#define hid_enumerate PLATFORM_hid_enumerate
33
#define hid_error PLATFORM_hid_error
34
#define hid_exit PLATFORM_hid_exit
35
#define hid_free_enumeration PLATFORM_hid_free_enumeration
36
#define hid_get_device_info PLATFORM_hid_get_device_info
37
#define hid_get_feature_report PLATFORM_hid_get_feature_report
38
#define hid_get_indexed_string PLATFORM_hid_get_indexed_string
39
#define hid_get_input_report PLATFORM_hid_get_input_report
40
#define hid_get_manufacturer_string PLATFORM_hid_get_manufacturer_string
41
#define hid_get_product_string PLATFORM_hid_get_product_string
42
#define hid_get_report_descriptor PLATFORM_hid_get_report_descriptor
43
#define hid_get_serial_number_string PLATFORM_hid_get_serial_number_string
44
#define hid_init PLATFORM_hid_init
45
#define hid_open_path PLATFORM_hid_open_path
46
#define hid_open PLATFORM_hid_open
47
#define hid_read PLATFORM_hid_read
48
#define hid_read_timeout PLATFORM_hid_read_timeout
49
#define hid_send_feature_report PLATFORM_hid_send_feature_report
50
#define hid_set_nonblocking PLATFORM_hid_set_nonblocking
51
#define hid_version PLATFORM_hid_version
52
#define hid_version_str PLATFORM_hid_version_str
53
#define hid_write PLATFORM_hid_write
55
#include <CoreBluetooth/CoreBluetooth.h>
56
#include <QuartzCore/QuartzCore.h>
57
#import <UIKit/UIKit.h>
58
#import <mach/mach_time.h>
62
#include "../hidapi/hidapi.h"
64
#define VALVE_USB_VID 0x28DE
65
#define D0G_BLE2_PID 0x1106
67
typedef uint32_t uint32;
68
typedef uint64_t uint64;
70
// enables detailed NSLog logging of feature reports
71
#define FEATURE_REPORT_LOGGING 0
73
#define REPORT_SEGMENT_DATA_FLAG 0x80
74
#define REPORT_SEGMENT_LAST_FLAG 0x40
76
#define VALVE_SERVICE @"100F6C32-1735-4313-B402-38567131E5F3"
78
// (READ/NOTIFICATIONS)
79
#define VALVE_INPUT_CHAR @"100F6C33-1735-4313-B402-38567131E5F3"
82
#define VALVE_REPORT_CHAR @"100F6C34-1735-4313-B402-38567131E5F3"
84
// TODO: create CBUUID's in __attribute__((constructor)) rather than doing [CBUUID UUIDWithString:...] everywhere
90
uint8_t segmentHeader;
91
uint8_t featureReportMessageID;
93
uint8_t settingIdentifier;
98
uint8_t ucPayload[15];
105
bluetoothSegment segment;
107
uint8_t segmentHeader;
108
uint8_t featureReportMessageID;
110
uint8_t settingIdentifier;
115
uint8_t ucPayload[15];
123
size_t GetBluetoothSegmentSize(bluetoothSegment *segment)
125
return segment->length + 3;
128
#define RingBuffer_cbElem 19
129
#define RingBuffer_nElem 4096
133
uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ];
134
pthread_mutex_t accessLock;
137
static void RingBuffer_init( RingBuffer *this )
141
pthread_mutex_init( &this->accessLock, 0 );
144
static bool RingBuffer_write( RingBuffer *this, const uint8_t *src )
146
pthread_mutex_lock( &this->accessLock );
147
memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem );
148
if ( this->_first == -1 )
150
this->_first = this->_last;
152
this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
153
if ( this->_last == this->_first )
155
this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
156
pthread_mutex_unlock( &this->accessLock );
159
pthread_mutex_unlock( &this->accessLock );
163
static bool RingBuffer_read( RingBuffer *this, uint8_t *dst )
165
pthread_mutex_lock( &this->accessLock );
166
if ( this->_first == -1 )
168
pthread_mutex_unlock( &this->accessLock );
171
memcpy( dst, &this->_data[ this->_first ], RingBuffer_cbElem );
172
this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
173
if ( this->_first == this->_last )
177
pthread_mutex_unlock( &this->accessLock );
182
#pragma mark HIDBLEDevice Definition
186
BLEDeviceWaitState_None,
187
BLEDeviceWaitState_Waiting,
188
BLEDeviceWaitState_Complete,
189
BLEDeviceWaitState_Error
192
@interface HIDBLEDevice : NSObject <CBPeripheralDelegate>
194
RingBuffer _inputReports;
195
uint8_t _featureReport[20];
196
BLEDeviceWaitState _waitStateForReadFeatureReport;
197
BLEDeviceWaitState _waitStateForWriteFeatureReport;
200
@property (nonatomic, readwrite) bool connected;
201
@property (nonatomic, readwrite) bool ready;
203
@property (nonatomic, strong) CBPeripheral *bleSteamController;
204
@property (nonatomic, strong) CBCharacteristic *bleCharacteristicInput;
205
@property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport;
207
- (id)initWithPeripheral:(CBPeripheral *)peripheral;
212
@interface HIDBLEManager : NSObject <CBCentralManagerDelegate>
214
@property (nonatomic) int nPendingScans;
215
@property (nonatomic) int nPendingPairs;
216
@property (nonatomic, strong) CBCentralManager *centralManager;
217
@property (nonatomic, strong) NSMapTable<CBPeripheral *, HIDBLEDevice *> *deviceMap;
218
@property (nonatomic, retain) dispatch_queue_t bleSerialQueue;
220
+ (instancetype)sharedInstance;
221
- (void)startScan:(int)duration;
223
- (int)updateConnectedSteamControllers:(BOOL) bForce;
224
- (void)appWillResignActiveNotification:(NSNotification *)note;
225
- (void)appDidBecomeActiveNotification:(NSNotification *)note;
230
// singleton class - access using HIDBLEManager.sharedInstance
231
@implementation HIDBLEManager
233
+ (instancetype)sharedInstance
235
static HIDBLEManager *sharedInstance = nil;
236
static dispatch_once_t onceToken;
237
dispatch_once(&onceToken, ^{
238
sharedInstance = [HIDBLEManager new];
239
sharedInstance.nPendingScans = 0;
240
sharedInstance.nPendingPairs = 0;
242
// Bluetooth is currently only used for Steam Controllers, so check that hint
243
// before initializing Bluetooth, which will prompt the user for permission.
244
if ( SDL_GetHintBoolean( SDL_HINT_JOYSTICK_HIDAPI_STEAM, false ) )
246
[[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appWillResignActiveNotification:) name: UIApplicationWillResignActiveNotification object:nil];
247
[[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appDidBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
249
// receive reports on a high-priority serial-queue. optionally put writes on the serial queue to avoid logical
250
// race conditions talking to the controller from multiple threads, although BLE fragmentation/assembly means
251
// that we can still screw this up.
252
// most importantly we need to consume reports at a high priority to avoid the OS thinking we aren't really
253
// listening to the BLE device, as iOS on slower devices may stop delivery of packets to the app WITHOUT ACTUALLY
254
// DISCONNECTING FROM THE DEVICE if we don't react quickly enough to their delivery.
255
// see also the error-handling states in the peripheral delegate to re-open the device if it gets closed
256
sharedInstance.bleSerialQueue = dispatch_queue_create( "com.valvesoftware.steamcontroller.ble", DISPATCH_QUEUE_SERIAL );
257
dispatch_set_target_queue( sharedInstance.bleSerialQueue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
259
// creating a CBCentralManager will always trigger a future centralManagerDidUpdateState:
260
// where any scanning gets started or connecting to existing peripherals happens, it's never already in a
261
// powered-on state for a newly launched application.
262
sharedInstance.centralManager = [[CBCentralManager alloc] initWithDelegate:sharedInstance queue:sharedInstance.bleSerialQueue];
264
sharedInstance.deviceMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory capacity:4];
266
return sharedInstance;
269
// called for NSNotification UIApplicationWillResignActiveNotification
270
- (void)appWillResignActiveNotification:(NSNotification *)note
272
// we'll get resign-active notification if pairing is happening.
273
if ( self.nPendingPairs > 0 )
276
for ( CBPeripheral *peripheral in self.deviceMap )
278
HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
279
if ( steamController )
281
steamController.connected = NO;
282
steamController.ready = NO;
283
[self.centralManager cancelPeripheralConnection:peripheral];
286
[self.deviceMap removeAllObjects];
289
// called for NSNotification UIApplicationDidBecomeActiveNotification
290
// whenever the application comes back from being inactive, trigger a 20s pairing scan and reconnect
291
// any devices that may have paired while we were inactive.
292
- (void)appDidBecomeActiveNotification:(NSNotification *)note
294
[self updateConnectedSteamControllers:true];
298
- (int)updateConnectedSteamControllers:(BOOL) bForce
300
static uint64_t s_unLastUpdateTick = 0;
301
static mach_timebase_info_data_t s_timebase_info;
303
if ( self.centralManager == nil )
308
if (s_timebase_info.denom == 0)
310
mach_timebase_info( &s_timebase_info );
313
uint64_t ticksNow = mach_approximate_time();
314
if ( !bForce && ( ( (ticksNow - s_unLastUpdateTick) * s_timebase_info.numer ) / s_timebase_info.denom ) < (5ull * NSEC_PER_SEC) )
315
return (int)self.deviceMap.count;
317
// we can see previously connected BLE peripherals but can't connect until the CBCentralManager
318
// is fully powered up - only do work when we are in that state
319
if ( self.centralManager.state != CBManagerStatePoweredOn )
320
return (int)self.deviceMap.count;
322
// only update our last-check-time if we actually did work, otherwise there can be a long delay during initial power-up
323
s_unLastUpdateTick = mach_approximate_time();
325
// if a pair is in-flight, the central manager may still give it back via retrieveConnected... and
326
// cause the SDL layer to attempt to initialize it while some of its endpoints haven't yet been established
327
if ( self.nPendingPairs > 0 )
328
return (int)self.deviceMap.count;
330
NSArray<CBPeripheral *> *peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices: @[ [CBUUID UUIDWithString:@"180A"]]];
331
for ( CBPeripheral *peripheral in peripherals )
333
// we already know this peripheral
334
if ( [self.deviceMap objectForKey: peripheral] != nil )
337
NSLog( @"connected peripheral: %@", peripheral );
338
if ( [peripheral.name isEqualToString:@"SteamController"] )
340
self.nPendingPairs += 1;
341
HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral];
342
[self.deviceMap setObject:steamController forKey:peripheral];
343
[self.centralManager connectPeripheral:peripheral options:nil];
347
return (int)self.deviceMap.count;
350
// manual API for folks to start & stop scanning
351
- (void)startScan:(int)duration
353
if ( self.centralManager == nil )
358
NSLog( @"BLE: requesting scan for %d seconds", duration );
361
if ( _nPendingScans++ == 0 )
363
[self.centralManager scanForPeripheralsWithServices:nil options:nil];
369
dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
377
if ( self.centralManager == nil )
382
NSLog( @"BLE: stopping scan" );
385
if ( --_nPendingScans <= 0 )
388
[self.centralManager stopScan];
394
#pragma mark CBCentralManagerDelegate Implementation
396
// called whenever the BLE hardware state changes.
397
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
399
switch ( central.state )
401
case CBCentralManagerStatePoweredOn:
403
NSLog( @"CoreBluetooth BLE hardware is powered on and ready" );
405
// at startup, if we have no already attached peripherals, do a 20s scan for new unpaired devices,
406
// otherwise callers should occaisionally do additional scans. we don't want to continuously be
407
// scanning because it drains battery, causes other nearby people to have a hard time pairing their
408
// Steam Controllers, and may also trigger firmware weirdness when a device attempts to start
409
// the pairing sequence multiple times concurrently
410
if ( [self updateConnectedSteamControllers:false] == 0 )
412
// TODO: we could limit our scan to only peripherals supporting the SteamController service, but
413
// that service doesn't currently fit in the base advertising packet, we'd need to put it into an
414
// extended scan packet. Useful optimization downstream, but not currently necessary
415
// NSArray *services = @[[CBUUID UUIDWithString:VALVE_SERVICE]];
421
case CBCentralManagerStatePoweredOff:
422
NSLog( @"CoreBluetooth BLE hardware is powered off" );
425
case CBCentralManagerStateUnauthorized:
426
NSLog( @"CoreBluetooth BLE state is unauthorized" );
429
case CBCentralManagerStateUnknown:
430
NSLog( @"CoreBluetooth BLE state is unknown" );
433
case CBCentralManagerStateUnsupported:
434
NSLog( @"CoreBluetooth BLE hardware is unsupported on this platform" );
437
case CBCentralManagerStateResetting:
438
NSLog( @"CoreBluetooth BLE manager is resetting" );
443
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
445
HIDBLEDevice *steamController = [_deviceMap objectForKey:peripheral];
446
steamController.connected = YES;
447
self.nPendingPairs -= 1;
450
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
452
NSLog( @"Failed to connect: %@", error );
453
[_deviceMap removeObjectForKey:peripheral];
454
self.nPendingPairs -= 1;
457
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
459
NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
460
NSString *log = [NSString stringWithFormat:@"Found '%@'", localName];
462
if ( [localName isEqualToString:@"SteamController"] )
464
NSLog( @"%@ : %@ - %@", log, peripheral, advertisementData );
465
self.nPendingPairs += 1;
466
HIDBLEDevice *steamController = [[HIDBLEDevice alloc] initWithPeripheral:peripheral];
467
[self.deviceMap setObject:steamController forKey:peripheral];
468
[self.centralManager connectPeripheral:peripheral options:nil];
472
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
474
HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
475
if ( steamController )
477
steamController.connected = NO;
478
steamController.ready = NO;
479
[self.deviceMap removeObjectForKey:peripheral];
486
// Core Bluetooth devices calling back on event boundaries of their run-loops. so annoying.
487
static void process_pending_events(void)
489
CFRunLoopRunResult res;
492
res = CFRunLoopRunInMode( kCFRunLoopDefaultMode, 0.001, FALSE );
494
while( res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut );
497
@implementation HIDBLEDevice
501
if ( self = [super init] )
503
RingBuffer_init( &_inputReports );
504
self.bleSteamController = nil;
505
self.bleCharacteristicInput = nil;
506
self.bleCharacteristicReport = nil;
513
- (id)initWithPeripheral:(CBPeripheral *)peripheral
515
if ( self = [super init] )
517
RingBuffer_init( &_inputReports );
520
self.bleSteamController = peripheral;
523
peripheral.delegate = self;
525
self.bleCharacteristicInput = nil;
526
self.bleCharacteristicReport = nil;
531
- (void)setConnected:(bool)connected
533
_connected = connected;
536
[_bleSteamController discoverServices:nil];
540
NSLog( @"Disconnected" );
544
- (size_t)read_input_report:(uint8_t *)dst
546
if ( RingBuffer_read( &_inputReports, dst+1 ) )
554
- (int)send_report:(const uint8_t *)data length:(size_t)length
556
[_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
560
- (int)send_feature_report:(hidFeatureReport *)report
562
#if FEATURE_REPORT_LOGGING
563
uint8_t *reportBytes = (uint8_t *)report;
565
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 ),
566
reportBytes[1], reportBytes[2], reportBytes[3], reportBytes[4], reportBytes[5], reportBytes[6],
567
reportBytes[7], reportBytes[8], reportBytes[9], reportBytes[10], reportBytes[11], reportBytes[12],
568
reportBytes[13], reportBytes[14], reportBytes[15], reportBytes[16], reportBytes[17], reportBytes[18],
572
int sendSize = (int)GetBluetoothSegmentSize( &report->segment );
577
// fire-and-forget - we are going to not wait for the response here because all Steam Controller BLE send_feature_report's are ignored,
579
[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
581
// pretend we received a result anybody cares about
585
// this is technically the correct send_feature_report logic if you want to make sure it gets through and is
586
// acknowledged or errors out
587
_waitStateForWriteFeatureReport = BLEDeviceWaitState_Waiting;
588
[_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize
589
] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
591
while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting )
593
process_pending_events();
596
if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error )
598
_waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
602
_waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
607
- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer
609
_waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting;
610
[_bleSteamController readValueForCharacteristic:_bleCharacteristicReport];
612
while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting )
613
process_pending_events();
615
if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error )
617
_waitStateForReadFeatureReport = BLEDeviceWaitState_None;
621
memcpy( buffer, _featureReport, sizeof(_featureReport) );
623
_waitStateForReadFeatureReport = BLEDeviceWaitState_None;
625
#if FEATURE_REPORT_LOGGING
626
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]",
627
buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6],
628
buffer[7], buffer[8], buffer[9], buffer[10], buffer[11], buffer[12],
629
buffer[13], buffer[14], buffer[15], buffer[16], buffer[17], buffer[18],
636
#pragma mark CBPeripheralDelegate Implementation
638
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
640
for (CBService *service in peripheral.services)
642
NSLog( @"Found Service: %@", service );
643
if ( [service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]] )
645
[peripheral discoverCharacteristics:nil forService:service];
650
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
652
// nothing yet needed here, enable for logging
653
if ( /* DISABLES CODE */ (0) )
655
for ( CBDescriptor *descriptor in characteristic.descriptors )
657
NSLog( @" - Descriptor '%@'", descriptor );
662
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
664
if ([service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]])
666
for (CBCharacteristic *aChar in service.characteristics)
668
NSLog( @"Found Characteristic %@", aChar );
670
if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] )
672
self.bleCharacteristicInput = aChar;
674
else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] )
676
self.bleCharacteristicReport = aChar;
677
[self.bleSteamController discoverDescriptorsForCharacteristic: aChar];
683
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
685
static uint64_t s_ticksLastOverflowReport = 0;
687
// receiving an input report is the final indicator that the user accepted a pairing
688
// request and that we successfully established notification. CoreBluetooth has no
689
// notification of the pairing acknowledgement, which is a bad oversight.
690
if ( self.ready == NO )
693
HIDBLEManager.sharedInstance.nPendingPairs -= 1;
696
if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] )
698
NSData *data = [characteristic value];
699
if ( data.length != 19 )
701
NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length );
703
if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) )
705
uint64_t ticksNow = mach_approximate_time();
706
if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) )
708
NSLog( @"HIDBLE: input report buffer overflow" );
709
s_ticksLastOverflowReport = ticksNow;
713
else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] )
715
memset( _featureReport, 0, sizeof(_featureReport) );
719
NSLog( @"HIDBLE: get_feature_report error: %@", error );
720
_waitStateForReadFeatureReport = BLEDeviceWaitState_Error;
724
NSData *data = [characteristic value];
725
if ( data.length != 20 )
727
NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 20", (unsigned long)data.length );
729
memcpy( _featureReport, data.bytes, MIN( data.length, sizeof(_featureReport) ) );
730
_waitStateForReadFeatureReport = BLEDeviceWaitState_Complete;
735
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
737
if ( [characteristic.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] )
741
NSLog( @"HIDBLE: write_feature_report error: %@", error );
742
_waitStateForWriteFeatureReport = BLEDeviceWaitState_Error;
746
_waitStateForWriteFeatureReport = BLEDeviceWaitState_Complete;
751
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
753
NSLog( @"didUpdateNotifcationStateForCharacteristic %@ (%@)", characteristic, error );
759
#pragma mark hid_api implementation
764
struct hid_device_info* device_info;
768
int HID_API_EXPORT HID_API_CALL hid_init(void)
770
return ( HIDBLEManager.sharedInstance == nil ) ? -1 : 0;
773
int HID_API_EXPORT HID_API_CALL hid_exit(void)
778
void HID_API_EXPORT HID_API_CALL hid_ble_scan( int bStart )
780
HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
783
[bleManager startScan:0];
787
[bleManager stopScan];
791
HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number)
796
HID_API_EXPORT hid_device * HID_API_CALL hid_open_path( const char *path )
798
hid_device *result = NULL;
799
NSString *nssPath = [NSString stringWithUTF8String:path];
800
HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
801
NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator];
803
for ( HIDBLEDevice *device in devices )
805
// we have the device but it hasn't found its service or characteristics until it is connected
806
if ( !device.ready || !device.connected || !device.bleCharacteristicInput )
809
if ( [device.bleSteamController.identifier.UUIDString isEqualToString:nssPath] )
811
result = (hid_device *)malloc( sizeof( hid_device ) );
812
memset( result, 0, sizeof( hid_device ) );
813
result->device_handle = (void*)CFBridgingRetain( device );
814
result->blocking = NO;
815
// enable reporting input events on the characteristic
816
[device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput];
823
void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs)
825
/* This function is identical to the Linux version. Platform independent. */
826
struct hid_device_info *d = devs;
828
struct hid_device_info *next = d->next;
830
free(d->serial_number);
831
free(d->manufacturer_string);
832
free(d->product_string);
838
int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
840
/* All Nonblocking operation is handled by the library. */
841
dev->blocking = !nonblock;
846
static struct hid_device_info *create_device_info_for_hid_device(HIDBLEDevice *device)
848
// We currently only support the Steam Controller
849
struct hid_device_info *device_info = (struct hid_device_info *)malloc( sizeof(struct hid_device_info) );
850
memset( device_info, 0, sizeof(struct hid_device_info) );
851
device_info->path = strdup( device.bleSteamController.identifier.UUIDString.UTF8String );
852
device_info->vendor_id = VALVE_USB_VID;
853
device_info->product_id = D0G_BLE2_PID;
854
device_info->product_string = wcsdup( L"Steam Controller" );
855
device_info->manufacturer_string = wcsdup( L"Valve Corporation" );
859
struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id)
861
struct hid_device_info *root = NULL;
863
/* See if there are any devices we should skip in enumeration */
864
if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, D0G_BLE2_PID, 0, 0)) {
868
if ( ( vendor_id == 0 || vendor_id == VALVE_USB_VID ) &&
869
( product_id == 0 || product_id == D0G_BLE2_PID ) )
871
HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
872
[bleManager updateConnectedSteamControllers:false];
873
NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator];
874
for ( HIDBLEDevice *device in devices )
876
// there are several brief windows in connecting to an already paired device and
877
// one long window waiting for users to confirm pairing where we don't want
878
// to consider a device ready - if we hand it back to SDL or another
879
// Steam Controller consumer, their additional SC setup work will fail
880
// in unusual/silent ways and we can actually corrupt the BLE stack for
881
// the entire system and kill the appletv remote's Menu button (!)
882
if ( device.bleSteamController.state != CBPeripheralStateConnected ||
883
device.connected == NO || device.ready == NO )
885
if ( device.ready == NO && device.bleCharacteristicInput != nil )
887
// attempt to register for input reports. this call will silently fail
888
// until the pairing finalizes with user acceptance. oh, apple.
889
[device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput];
893
struct hid_device_info *device_info = create_device_info_for_hid_device(device);
894
device_info->next = root;
901
int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen)
903
static wchar_t s_wszManufacturer[] = L"Valve Corporation";
904
wcsncpy( string, s_wszManufacturer, sizeof(s_wszManufacturer)/sizeof(s_wszManufacturer[0]) );
908
int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen)
910
static wchar_t s_wszProduct[] = L"Steam Controller";
911
wcsncpy( string, s_wszProduct, sizeof(s_wszProduct)/sizeof(s_wszProduct[0]) );
915
int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen)
917
static wchar_t s_wszSerial[] = L"12345";
918
wcsncpy( string, s_wszSerial, sizeof(s_wszSerial)/sizeof(s_wszSerial[0]) );
922
int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen)
927
struct hid_device_info *hid_get_device_info(hid_device *dev)
929
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
931
if (!dev->device_info) {
932
// Lazy initialize device_info
933
dev->device_info = create_device_info_for_hid_device(device_handle);
936
// create_device_info_for_hid_device will set an error if needed
937
return dev->device_info;
940
int hid_get_report_descriptor(hid_device *device, unsigned char *buf, size_t buf_size)
946
int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length)
948
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
950
if ( !device_handle.connected )
953
return [device_handle send_report:data length:length];
956
void HID_API_EXPORT hid_close(hid_device *dev)
958
HIDBLEDevice *device_handle = CFBridgingRelease( dev->device_handle );
960
// disable reporting input events on the characteristic
961
if ( device_handle.connected ) {
962
[device_handle.bleSteamController setNotifyValue:NO forCharacteristic:device_handle.bleCharacteristicInput];
965
hid_free_enumeration(dev->device_info);
970
int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length)
972
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
974
if ( !device_handle.connected )
977
return [device_handle send_feature_report:(hidFeatureReport *)(void *)data];
980
int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length)
982
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
984
if ( !device_handle.connected )
987
size_t written = [device_handle get_feature_report:data[0] into:data];
989
return written == length-1 ? (int)length : (int)written;
992
int HID_API_EXPORT hid_get_input_report(hid_device *dev, unsigned char *data, size_t length)
998
int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length)
1000
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
1002
if ( !device_handle.connected )
1005
return hid_read_timeout(dev, data, length, 0);
1008
int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds)
1010
HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
1012
if ( !device_handle.connected )
1015
if ( milliseconds != 0 )
1017
NSLog( @"hid_read_timeout with non-zero wait" );
1019
int result = (int)[device_handle read_input_report:data];
1020
#if FEATURE_REPORT_LOGGING
1021
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,
1022
data[1], data[2], data[3], data[4], data[5], data[6],
1023
data[7], data[8], data[9], data[10], data[11], data[12],
1024
data[13], data[14], data[15], data[16], data[17], data[18],
1030
HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev)
1035
#endif /* !SDL_HIDAPI_DISABLED */
1037
#endif /* SDL_PLATFORM_IOS || SDL_PLATFORM_TVOS */