summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/hidapi/ios
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
committer3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
commit5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch)
tree8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/hidapi/ios
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/hidapi/ios')
-rw-r--r--contrib/SDL-3.2.8/src/hidapi/ios/hid.m1038
1 files changed, 1038 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/hidapi/ios/hid.m b/contrib/SDL-3.2.8/src/hidapi/ios/hid.m
new file mode 100644
index 0000000..29e0782
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/hidapi/ios/hid.m
@@ -0,0 +1,1038 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 2021 Valve Corporation
4
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.
8
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:
12
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.
20*/
21#include "SDL_internal.h"
22
23#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS)
24
25#ifndef SDL_HIDAPI_DISABLED
26
27#include "../SDL_hidapi_c.h"
28
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
54
55#include <CoreBluetooth/CoreBluetooth.h>
56#include <QuartzCore/QuartzCore.h>
57#import <UIKit/UIKit.h>
58#import <mach/mach_time.h>
59#include <pthread.h>
60#include <sys/time.h>
61#include <unistd.h>
62#include "../hidapi/hidapi.h"
63
64#define VALVE_USB_VID 0x28DE
65#define D0G_BLE2_PID 0x1106
66
67typedef uint32_t uint32;
68typedef uint64_t uint64;
69
70// enables detailed NSLog logging of feature reports
71#define FEATURE_REPORT_LOGGING 0
72
73#define REPORT_SEGMENT_DATA_FLAG 0x80
74#define REPORT_SEGMENT_LAST_FLAG 0x40
75
76#define VALVE_SERVICE @"100F6C32-1735-4313-B402-38567131E5F3"
77
78// (READ/NOTIFICATIONS)
79#define VALVE_INPUT_CHAR @"100F6C33-1735-4313-B402-38567131E5F3"
80
81//  (READ/WRITE)
82#define VALVE_REPORT_CHAR @"100F6C34-1735-4313-B402-38567131E5F3"
83
84// TODO: create CBUUID's in __attribute__((constructor)) rather than doing [CBUUID UUIDWithString:...] everywhere
85
86#pragma pack(push,1)
87
88typedef struct
89{
90 uint8_t segmentHeader;
91 uint8_t featureReportMessageID;
92 uint8_t length;
93 uint8_t settingIdentifier;
94 union {
95 uint16_t usPayload;
96 uint32_t uPayload;
97 uint64_t ulPayload;
98 uint8_t ucPayload[15];
99 };
100} bluetoothSegment;
101
102typedef struct {
103 uint8_t id;
104 union {
105 bluetoothSegment segment;
106 struct {
107 uint8_t segmentHeader;
108 uint8_t featureReportMessageID;
109 uint8_t length;
110 uint8_t settingIdentifier;
111 union {
112 uint16_t usPayload;
113 uint32_t uPayload;
114 uint64_t ulPayload;
115 uint8_t ucPayload[15];
116 };
117 };
118 };
119} hidFeatureReport;
120
121#pragma pack(pop)
122
123size_t GetBluetoothSegmentSize(bluetoothSegment *segment)
124{
125 return segment->length + 3;
126}
127
128#define RingBuffer_cbElem 19
129#define RingBuffer_nElem 4096
130
131typedef struct {
132 int _first, _last;
133 uint8_t _data[ ( RingBuffer_nElem * RingBuffer_cbElem ) ];
134 pthread_mutex_t accessLock;
135} RingBuffer;
136
137static void RingBuffer_init( RingBuffer *this )
138{
139 this->_first = -1;
140 this->_last = 0;
141 pthread_mutex_init( &this->accessLock, 0 );
142}
143
144static bool RingBuffer_write( RingBuffer *this, const uint8_t *src )
145{
146 pthread_mutex_lock( &this->accessLock );
147 memcpy( &this->_data[ this->_last ], src, RingBuffer_cbElem );
148 if ( this->_first == -1 )
149 {
150 this->_first = this->_last;
151 }
152 this->_last = ( this->_last + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
153 if ( this->_last == this->_first )
154 {
155 this->_first = ( this->_first + RingBuffer_cbElem ) % (RingBuffer_nElem * RingBuffer_cbElem);
156 pthread_mutex_unlock( &this->accessLock );
157 return false;
158 }
159 pthread_mutex_unlock( &this->accessLock );
160 return true;
161}
162
163static bool RingBuffer_read( RingBuffer *this, uint8_t *dst )
164{
165 pthread_mutex_lock( &this->accessLock );
166 if ( this->_first == -1 )
167 {
168 pthread_mutex_unlock( &this->accessLock );
169 return false;
170 }
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 )
174 {
175 this->_first = -1;
176 }
177 pthread_mutex_unlock( &this->accessLock );
178 return true;
179}
180
181
182#pragma mark HIDBLEDevice Definition
183
184typedef enum
185{
186 BLEDeviceWaitState_None,
187 BLEDeviceWaitState_Waiting,
188 BLEDeviceWaitState_Complete,
189 BLEDeviceWaitState_Error
190} BLEDeviceWaitState;
191
192@interface HIDBLEDevice : NSObject <CBPeripheralDelegate>
193{
194 RingBuffer _inputReports;
195 uint8_t _featureReport[20];
196 BLEDeviceWaitState _waitStateForReadFeatureReport;
197 BLEDeviceWaitState _waitStateForWriteFeatureReport;
198}
199
200@property (nonatomic, readwrite) bool connected;
201@property (nonatomic, readwrite) bool ready;
202
203@property (nonatomic, strong) CBPeripheral *bleSteamController;
204@property (nonatomic, strong) CBCharacteristic *bleCharacteristicInput;
205@property (nonatomic, strong) CBCharacteristic *bleCharacteristicReport;
206
207- (id)initWithPeripheral:(CBPeripheral *)peripheral;
208
209@end
210
211
212@interface HIDBLEManager : NSObject <CBCentralManagerDelegate>
213
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;
219
220+ (instancetype)sharedInstance;
221- (void)startScan:(int)duration;
222- (void)stopScan;
223- (int)updateConnectedSteamControllers:(BOOL) bForce;
224- (void)appWillResignActiveNotification:(NSNotification *)note;
225- (void)appDidBecomeActiveNotification:(NSNotification *)note;
226
227@end
228
229
230// singleton class - access using HIDBLEManager.sharedInstance
231@implementation HIDBLEManager
232
233+ (instancetype)sharedInstance
234{
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;
241
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 ) )
245 {
246 [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appWillResignActiveNotification:) name: UIApplicationWillResignActiveNotification object:nil];
247 [[NSNotificationCenter defaultCenter] addObserver:sharedInstance selector:@selector(appDidBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
248
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 ) );
258
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];
263 }
264 sharedInstance.deviceMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory capacity:4];
265 });
266 return sharedInstance;
267}
268
269// called for NSNotification UIApplicationWillResignActiveNotification
270- (void)appWillResignActiveNotification:(NSNotification *)note
271{
272 // we'll get resign-active notification if pairing is happening.
273 if ( self.nPendingPairs > 0 )
274 return;
275
276 for ( CBPeripheral *peripheral in self.deviceMap )
277 {
278 HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
279 if ( steamController )
280 {
281 steamController.connected = NO;
282 steamController.ready = NO;
283 [self.centralManager cancelPeripheralConnection:peripheral];
284 }
285 }
286 [self.deviceMap removeAllObjects];
287}
288
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
293{
294 [self updateConnectedSteamControllers:true];
295 [self startScan:20];
296}
297
298- (int)updateConnectedSteamControllers:(BOOL) bForce
299{
300 static uint64_t s_unLastUpdateTick = 0;
301 static mach_timebase_info_data_t s_timebase_info;
302
303 if ( self.centralManager == nil )
304 {
305 return 0;
306 }
307
308 if (s_timebase_info.denom == 0)
309 {
310 mach_timebase_info( &s_timebase_info );
311 }
312
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;
316
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;
321
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();
324
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;
329
330 NSArray<CBPeripheral *> *peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices: @[ [CBUUID UUIDWithString:@"180A"]]];
331 for ( CBPeripheral *peripheral in peripherals )
332 {
333 // we already know this peripheral
334 if ( [self.deviceMap objectForKey: peripheral] != nil )
335 continue;
336
337 NSLog( @"connected peripheral: %@", peripheral );
338 if ( [peripheral.name hasPrefix:@"Steam"] )
339 {
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];
344 }
345 }
346
347 return (int)self.deviceMap.count;
348}
349
350// manual API for folks to start & stop scanning
351- (void)startScan:(int)duration
352{
353 if ( self.centralManager == nil )
354 {
355 return;
356 }
357
358 NSLog( @"BLE: requesting scan for %d seconds", duration );
359 @synchronized (self)
360 {
361 if ( _nPendingScans++ == 0 )
362 {
363 [self.centralManager scanForPeripheralsWithServices:nil options:nil];
364 }
365 }
366
367 if ( duration != 0 )
368 {
369 dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
370 [self stopScan];
371 });
372 }
373}
374
375- (void)stopScan
376{
377 if ( self.centralManager == nil )
378 {
379 return;
380 }
381
382 NSLog( @"BLE: stopping scan" );
383 @synchronized (self)
384 {
385 if ( --_nPendingScans <= 0 )
386 {
387 _nPendingScans = 0;
388 [self.centralManager stopScan];
389 }
390 }
391}
392
393
394#pragma mark CBCentralManagerDelegate Implementation
395
396// called whenever the BLE hardware state changes.
397- (void)centralManagerDidUpdateState:(CBCentralManager *)central
398{
399 switch ( central.state )
400 {
401 case CBManagerStatePoweredOn:
402 {
403 NSLog( @"CoreBluetooth BLE hardware is powered on and ready" );
404
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 )
411 {
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]];
416 [self startScan:20];
417 }
418 break;
419 }
420
421 case CBManagerStatePoweredOff:
422 NSLog( @"CoreBluetooth BLE hardware is powered off" );
423 break;
424
425 case CBManagerStateUnauthorized:
426 NSLog( @"CoreBluetooth BLE state is unauthorized" );
427 break;
428
429 case CBManagerStateUnknown:
430 NSLog( @"CoreBluetooth BLE state is unknown" );
431 break;
432
433 case CBManagerStateUnsupported:
434 NSLog( @"CoreBluetooth BLE hardware is unsupported on this platform" );
435 break;
436
437 case CBManagerStateResetting:
438 NSLog( @"CoreBluetooth BLE manager is resetting" );
439 break;
440 }
441}
442
443- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
444{
445 HIDBLEDevice *steamController = [_deviceMap objectForKey:peripheral];
446 steamController.connected = YES;
447 self.nPendingPairs -= 1;
448}
449
450- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
451{
452 NSLog( @"Failed to connect: %@", error );
453 [_deviceMap removeObjectForKey:peripheral];
454 self.nPendingPairs -= 1;
455}
456
457- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
458{
459 NSString *localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
460 NSString *log = [NSString stringWithFormat:@"Found '%@'", localName];
461
462 if ( [localName hasPrefix:@"Steam"] )
463 {
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];
469 }
470}
471
472- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
473{
474 HIDBLEDevice *steamController = [self.deviceMap objectForKey:peripheral];
475 if ( steamController )
476 {
477 steamController.connected = NO;
478 steamController.ready = NO;
479 [self.deviceMap removeObjectForKey:peripheral];
480 }
481}
482
483@end
484
485
486// Core Bluetooth devices calling back on event boundaries of their run-loops. so annoying.
487static void process_pending_events(void)
488{
489 CFRunLoopRunResult res;
490 do
491 {
492 res = CFRunLoopRunInMode( kCFRunLoopDefaultMode, 0.001, FALSE );
493 }
494 while( res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut );
495}
496
497@implementation HIDBLEDevice
498
499- (id)init
500{
501 if ( self = [super init] )
502 {
503 RingBuffer_init( &_inputReports );
504 self.bleSteamController = nil;
505 self.bleCharacteristicInput = nil;
506 self.bleCharacteristicReport = nil;
507 _connected = NO;
508 _ready = NO;
509 }
510 return self;
511}
512
513- (id)initWithPeripheral:(CBPeripheral *)peripheral
514{
515 if ( self = [super init] )
516 {
517 RingBuffer_init( &_inputReports );
518 _connected = NO;
519 _ready = NO;
520 self.bleSteamController = peripheral;
521 if ( peripheral )
522 {
523 peripheral.delegate = self;
524 }
525 self.bleCharacteristicInput = nil;
526 self.bleCharacteristicReport = nil;
527 }
528 return self;
529}
530
531- (void)setConnected:(bool)connected
532{
533 _connected = connected;
534 if ( _connected )
535 {
536 [_bleSteamController discoverServices:nil];
537 }
538 else
539 {
540 NSLog( @"Disconnected" );
541 }
542}
543
544- (size_t)read_input_report:(uint8_t *)dst
545{
546 if ( RingBuffer_read( &_inputReports, dst+1 ) )
547 {
548 *dst = 0x03;
549 return 20;
550 }
551 return 0;
552}
553
554- (int)send_report:(const uint8_t *)data length:(size_t)length
555{
556 [_bleSteamController writeValue:[NSData dataWithBytes:data length:length] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
557 return (int)length;
558}
559
560- (int)send_feature_report:(hidFeatureReport *)report
561{
562#if FEATURE_REPORT_LOGGING
563 uint8_t *reportBytes = (uint8_t *)report;
564
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],
569 reportBytes[19] );
570#endif
571
572 int sendSize = (int)GetBluetoothSegmentSize( &report->segment );
573 if ( sendSize > 20 )
574 sendSize = 20;
575
576#if 1
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,
578 // except errors.
579 [_bleSteamController writeValue:[NSData dataWithBytes:&report->segment length:sendSize] forCharacteristic:_bleCharacteristicReport type:CBCharacteristicWriteWithResponse];
580
581 // pretend we received a result anybody cares about
582 return 19;
583
584#else
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];
590
591 while ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Waiting )
592 {
593 process_pending_events();
594 }
595
596 if ( _waitStateForWriteFeatureReport == BLEDeviceWaitState_Error )
597 {
598 _waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
599 return -1;
600 }
601
602 _waitStateForWriteFeatureReport = BLEDeviceWaitState_None;
603 return 19;
604#endif
605}
606
607- (int)get_feature_report:(uint8_t)feature into:(uint8_t *)buffer
608{
609 _waitStateForReadFeatureReport = BLEDeviceWaitState_Waiting;
610 [_bleSteamController readValueForCharacteristic:_bleCharacteristicReport];
611
612 while ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Waiting )
613 process_pending_events();
614
615 if ( _waitStateForReadFeatureReport == BLEDeviceWaitState_Error )
616 {
617 _waitStateForReadFeatureReport = BLEDeviceWaitState_None;
618 return -1;
619 }
620
621 memcpy( buffer, _featureReport, sizeof(_featureReport) );
622
623 _waitStateForReadFeatureReport = BLEDeviceWaitState_None;
624
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],
630 buffer[19] );
631#endif
632
633 return 19;
634}
635
636#pragma mark CBPeripheralDelegate Implementation
637
638- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
639{
640 for (CBService *service in peripheral.services)
641 {
642 NSLog( @"Found Service: %@", service );
643 if ( [service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]] )
644 {
645 [peripheral discoverCharacteristics:nil forService:service];
646 }
647 }
648}
649
650- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
651{
652 // nothing yet needed here, enable for logging
653 if ( /* DISABLES CODE */ (0) )
654 {
655 for ( CBDescriptor *descriptor in characteristic.descriptors )
656 {
657 NSLog( @" - Descriptor '%@'", descriptor );
658 }
659 }
660}
661
662- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
663{
664 if ([service.UUID isEqual:[CBUUID UUIDWithString:VALVE_SERVICE]])
665 {
666 for (CBCharacteristic *aChar in service.characteristics)
667 {
668 NSLog( @"Found Characteristic %@", aChar );
669
670 if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_INPUT_CHAR]] )
671 {
672 self.bleCharacteristicInput = aChar;
673 }
674 else if ( [aChar.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] )
675 {
676 self.bleCharacteristicReport = aChar;
677 [self.bleSteamController discoverDescriptorsForCharacteristic: aChar];
678 }
679 }
680 }
681}
682
683- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
684{
685 static uint64_t s_ticksLastOverflowReport = 0;
686
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 )
691 {
692 self.ready = YES;
693 HIDBLEManager.sharedInstance.nPendingPairs -= 1;
694 }
695
696 if ( [characteristic.UUID isEqual:_bleCharacteristicInput.UUID] )
697 {
698 NSData *data = [characteristic value];
699 if ( data.length != 19 )
700 {
701 NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 19", (unsigned long)data.length );
702 }
703 if ( !RingBuffer_write( &_inputReports, (const uint8_t *)data.bytes ) )
704 {
705 uint64_t ticksNow = mach_approximate_time();
706 if ( ticksNow - s_ticksLastOverflowReport > (5ull * NSEC_PER_SEC / 10) )
707 {
708 NSLog( @"HIDBLE: input report buffer overflow" );
709 s_ticksLastOverflowReport = ticksNow;
710 }
711 }
712 }
713 else if ( [characteristic.UUID isEqual:_bleCharacteristicReport.UUID] )
714 {
715 memset( _featureReport, 0, sizeof(_featureReport) );
716
717 if ( error != nil )
718 {
719 NSLog( @"HIDBLE: get_feature_report error: %@", error );
720 _waitStateForReadFeatureReport = BLEDeviceWaitState_Error;
721 }
722 else
723 {
724 NSData *data = [characteristic value];
725 if ( data.length != 20 )
726 {
727 NSLog( @"HIDBLE: incoming data is %lu bytes should be exactly 20", (unsigned long)data.length );
728 }
729 memcpy( _featureReport, data.bytes, MIN( data.length, sizeof(_featureReport) ) );
730 _waitStateForReadFeatureReport = BLEDeviceWaitState_Complete;
731 }
732 }
733}
734
735- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
736{
737 if ( [characteristic.UUID isEqual:[CBUUID UUIDWithString:VALVE_REPORT_CHAR]] )
738 {
739 if ( error != nil )
740 {
741 NSLog( @"HIDBLE: write_feature_report error: %@", error );
742 _waitStateForWriteFeatureReport = BLEDeviceWaitState_Error;
743 }
744 else
745 {
746 _waitStateForWriteFeatureReport = BLEDeviceWaitState_Complete;
747 }
748 }
749}
750
751- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
752{
753 NSLog( @"didUpdateNotifcationStateForCharacteristic %@ (%@)", characteristic, error );
754}
755
756@end
757
758
759#pragma mark hid_api implementation
760
761struct hid_device_ {
762 void *device_handle;
763 int blocking;
764 struct hid_device_info* device_info;
765 hid_device *next;
766};
767
768int HID_API_EXPORT HID_API_CALL hid_init(void)
769{
770 return ( HIDBLEManager.sharedInstance == nil ) ? -1 : 0;
771}
772
773int HID_API_EXPORT HID_API_CALL hid_exit(void)
774{
775 return 0;
776}
777
778void HID_API_EXPORT HID_API_CALL hid_ble_scan( int bStart )
779{
780 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
781 if ( bStart )
782 {
783 [bleManager startScan:0];
784 }
785 else
786 {
787 [bleManager stopScan];
788 }
789}
790
791HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number)
792{
793 return NULL;
794}
795
796HID_API_EXPORT hid_device * HID_API_CALL hid_open_path( const char *path )
797{
798 hid_device *result = NULL;
799 NSString *nssPath = [NSString stringWithUTF8String:path];
800 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
801 NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator];
802
803 for ( HIDBLEDevice *device in devices )
804 {
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 )
807 continue;
808
809 if ( [device.bleSteamController.identifier.UUIDString isEqualToString:nssPath] )
810 {
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];
817 return result;
818 }
819 }
820 return result;
821}
822
823void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs)
824{
825 /* This function is identical to the Linux version. Platform independent. */
826 struct hid_device_info *d = devs;
827 while (d) {
828 struct hid_device_info *next = d->next;
829 free(d->path);
830 free(d->serial_number);
831 free(d->manufacturer_string);
832 free(d->product_string);
833 free(d);
834 d = next;
835 }
836}
837
838int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
839{
840 /* All Nonblocking operation is handled by the library. */
841 dev->blocking = !nonblock;
842
843 return 0;
844}
845
846static struct hid_device_info *create_device_info_for_hid_device(HIDBLEDevice *device)
847{
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" );
856 device_info->bus_type = HID_API_BUS_BLUETOOTH;
857 return device_info;
858}
859
860struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id)
861{ @autoreleasepool {
862 struct hid_device_info *root = NULL;
863
864 /* See if there are any devices we should skip in enumeration */
865 if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, D0G_BLE2_PID, 0, 0)) {
866 return NULL;
867 }
868
869 if ( ( vendor_id == 0 || vendor_id == VALVE_USB_VID ) &&
870 ( product_id == 0 || product_id == D0G_BLE2_PID ) )
871 {
872 HIDBLEManager *bleManager = HIDBLEManager.sharedInstance;
873 [bleManager updateConnectedSteamControllers:false];
874 NSEnumerator<HIDBLEDevice *> *devices = [bleManager.deviceMap objectEnumerator];
875 for ( HIDBLEDevice *device in devices )
876 {
877 // there are several brief windows in connecting to an already paired device and
878 // one long window waiting for users to confirm pairing where we don't want
879 // to consider a device ready - if we hand it back to SDL or another
880 // Steam Controller consumer, their additional SC setup work will fail
881 // in unusual/silent ways and we can actually corrupt the BLE stack for
882 // the entire system and kill the appletv remote's Menu button (!)
883 if ( device.bleSteamController.state != CBPeripheralStateConnected ||
884 device.connected == NO || device.ready == NO )
885 {
886 if ( device.ready == NO && device.bleCharacteristicInput != nil )
887 {
888 // attempt to register for input reports. this call will silently fail
889 // until the pairing finalizes with user acceptance. oh, apple.
890 [device.bleSteamController setNotifyValue:YES forCharacteristic:device.bleCharacteristicInput];
891 }
892 continue;
893 }
894 struct hid_device_info *device_info = create_device_info_for_hid_device(device);
895 device_info->next = root;
896 root = device_info;
897 }
898 }
899 return root;
900}}
901
902int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen)
903{
904 static wchar_t s_wszManufacturer[] = L"Valve Corporation";
905 wcsncpy( string, s_wszManufacturer, sizeof(s_wszManufacturer)/sizeof(s_wszManufacturer[0]) );
906 return 0;
907}
908
909int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen)
910{
911 static wchar_t s_wszProduct[] = L"Steam Controller";
912 wcsncpy( string, s_wszProduct, sizeof(s_wszProduct)/sizeof(s_wszProduct[0]) );
913 return 0;
914}
915
916int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen)
917{
918 static wchar_t s_wszSerial[] = L"12345";
919 wcsncpy( string, s_wszSerial, sizeof(s_wszSerial)/sizeof(s_wszSerial[0]) );
920 return 0;
921}
922
923int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen)
924{
925 return -1;
926}
927
928struct hid_device_info *hid_get_device_info(hid_device *dev)
929{
930 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
931
932 if (!dev->device_info) {
933 // Lazy initialize device_info
934 dev->device_info = create_device_info_for_hid_device(device_handle);
935 }
936
937 // create_device_info_for_hid_device will set an error if needed
938 return dev->device_info;
939}
940
941int hid_get_report_descriptor(hid_device *device, unsigned char *buf, size_t buf_size)
942{
943 // Not implemented
944 return -1;
945}
946
947int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length)
948{
949 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
950
951 if ( !device_handle.connected )
952 return -1;
953
954 return [device_handle send_report:data length:length];
955}
956
957void HID_API_EXPORT hid_close(hid_device *dev)
958{
959 HIDBLEDevice *device_handle = CFBridgingRelease( dev->device_handle );
960
961 // disable reporting input events on the characteristic
962 if ( device_handle.connected ) {
963 [device_handle.bleSteamController setNotifyValue:NO forCharacteristic:device_handle.bleCharacteristicInput];
964 }
965
966 hid_free_enumeration(dev->device_info);
967
968 free( dev );
969}
970
971int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length)
972{
973 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
974
975 if ( !device_handle.connected )
976 return -1;
977
978 return [device_handle send_feature_report:(hidFeatureReport *)(void *)data];
979}
980
981int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length)
982{
983 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
984
985 if ( !device_handle.connected )
986 return -1;
987
988 size_t written = [device_handle get_feature_report:data[0] into:data];
989
990 return written == length-1 ? (int)length : (int)written;
991}
992
993int HID_API_EXPORT hid_get_input_report(hid_device *dev, unsigned char *data, size_t length)
994{
995 // Not supported
996 return -1;
997}
998
999int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length)
1000{
1001 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
1002
1003 if ( !device_handle.connected )
1004 return -1;
1005
1006 return hid_read_timeout(dev, data, length, 0);
1007}
1008
1009int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds)
1010{
1011 HIDBLEDevice *device_handle = (__bridge HIDBLEDevice *)dev->device_handle;
1012
1013 if ( !device_handle.connected )
1014 return -1;
1015
1016 if ( milliseconds != 0 )
1017 {
1018 NSLog( @"hid_read_timeout with non-zero wait" );
1019 }
1020 int result = (int)[device_handle read_input_report:data];
1021#if FEATURE_REPORT_LOGGING
1022 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,
1023 data[1], data[2], data[3], data[4], data[5], data[6],
1024 data[7], data[8], data[9], data[10], data[11], data[12],
1025 data[13], data[14], data[15], data[16], data[17], data[18],
1026 data[19] );
1027#endif
1028 return result;
1029}
1030
1031HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev)
1032{
1033 return NULL;
1034}
1035
1036#endif /* !SDL_HIDAPI_DISABLED */
1037
1038#endif /* SDL_PLATFORM_IOS || SDL_PLATFORM_TVOS */