mirror of
https://github.com/OpenEmu/OpenEmu-SDK.git
synced 2025-11-01 10:15:12 +00:00
981 lines
36 KiB
Objective-C
981 lines
36 KiB
Objective-C
// Copyright (c) 2019, OpenEmu Team
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// * Neither the name of the OpenEmu Team nor the
|
|
// names of its contributors may be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
// DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
/*
|
|
* Most of this code has been written based on the reverse-engineered documentation
|
|
* which can be found at https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
|
*
|
|
* As an additional reference, another implementation of a driver for this controller
|
|
* can be found at https://github.com/Davidobot/BetterJoyForCemu
|
|
*/
|
|
|
|
#import "OEDeviceDescription.h"
|
|
#import "OEDeviceManager_Internal.h"
|
|
#import "OEControllerDescription_Internal.h"
|
|
#import "OESwitchProControllerHIDDeviceHandler.h"
|
|
|
|
|
|
#pragma mark - Device Handler Parameters
|
|
|
|
|
|
#define MAX_INPUT_REPORT_SIZE (256)
|
|
#define MAX_RESPONSE_ATTEMPTS (10)
|
|
#define MAX_RESPONSE_WAIT_SECONDS (10.0)
|
|
#define PING_INTERVAL_SECONDS (60.0)
|
|
|
|
//#define LOG_COMMUNICATION
|
|
|
|
|
|
#pragma mark - Input / Output Report Structures
|
|
|
|
|
|
typedef NS_ENUM(uint8_t, OEHACInputReportID) {
|
|
OEHACInputReportIDSubcommandReply = 0x21,
|
|
OEHACInputReportIDNFCIRFirmwareUpdateReply = 0x23,
|
|
OEHACInputReportIDFullReport = 0x30,
|
|
OEHACInputReportIDNFCIRReport = 0x31,
|
|
OEHACInputReportIDSimpleHIDReport = 0x3F,
|
|
OEHACInputReportIDUSBSubcommandReply = 0x81
|
|
};
|
|
|
|
typedef NS_ENUM(uint8_t, OEHACOuputReportID) {
|
|
OEHACOutputReportIDRumbleAndSubcommand = 0x01,
|
|
OEHACOutputReportIDNFCIRFirmwareUpdatePacket = 0x03,
|
|
OEHACOutputReportIDRumble = 0x10,
|
|
OEHACOutputReportIDNFCIR = 0x11,
|
|
OEHACOutputReportIDUSBSubcommand = 0x80
|
|
};
|
|
|
|
|
|
typedef NS_ENUM(uint8_t, OEHACUSBSubcommandID) {
|
|
OEHACUSBSubcommandIDGetStatus = 0x01,
|
|
OEHACUSBSubcommandIDRequestHandshake,
|
|
OEHACUSBSubcommandIDRequestHighDataRate,
|
|
OEHACUSBSubcommandIDDisableUSBHIDTimeout,
|
|
OEHACUSBSubcommandIDEnableUSBHIDTimeout,
|
|
OEHACUSBSubcommandIDReset,
|
|
};
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHACOuputReportID reportID;
|
|
OEHACUSBSubcommandID subcommand;
|
|
} OEHACUSBSubcommandOutputReport;
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHACOuputReportID reportID;
|
|
OEHACUSBSubcommandID subcommand;
|
|
uint8_t data[0];
|
|
} OEHACUSBAcknowledgmentOutputReport;
|
|
|
|
|
|
typedef NS_ENUM(uint8_t, OEHACSubcommandID) {
|
|
OEHACSubcmdNoop = 0,
|
|
OEHACSubcmdManualPairing,
|
|
OEHACSubcmdRequestDeviceInfo,
|
|
OEHACSubcmdSetInputReportMode,
|
|
OEHACSubcmdRequestTriggerButtonHeldTime,
|
|
OEHACSubcmdRequestPageListState,
|
|
OEHACSubcmdSetPowerState,
|
|
OEHACSubcmdResetPairing,
|
|
OEHACSubcmdSetFactoryLowPowerState,
|
|
OEHACSubcmdRequestSPIFlashRead = 0x10,
|
|
OEHACSubcmdRequestSPIFlashWrite,
|
|
OEHACSubcmdRequestSPIFlashErase,
|
|
OEHACSubcmdResetNFCIR = 0x20,
|
|
OEHACSubcmdSetNFCIRConfig,
|
|
OEHACSubcmdSetNFCIRState,
|
|
OEHACSubcmdSetPlayerLights = 0x30,
|
|
OEHACSubcmdRequestPlayerLights,
|
|
OEHACSubcmdSetHomeLight = 0x38
|
|
};
|
|
|
|
|
|
typedef NS_ENUM(uint8_t, OEHACPowerState) {
|
|
OEHACPowerStateSleep = 0,
|
|
OEHACPowerStateResetAndReconnect = 1,
|
|
OEHACPowerStateResetToPairMode = 2,
|
|
OEHACPowerStateResetToHOMEMode = 4
|
|
};
|
|
|
|
|
|
typedef uint8_t OEHAC12BitPackedPair[3];
|
|
|
|
typedef struct {
|
|
uint16_t x;
|
|
uint16_t y;
|
|
} OEHAC16BitUnsignedPair;
|
|
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHACOuputReportID reportID;
|
|
uint8_t seqNumber;
|
|
uint8_t rumbleData[8];
|
|
OEHACSubcommandID subcmdID;
|
|
uint8_t subcmdParam[0x35];
|
|
} OEHACRumbleAndSubcommandOutputReport;
|
|
|
|
|
|
typedef NS_OPTIONS(uint8_t, OEHACButtonState0) {
|
|
OEHACButtonState0Y = 0b00000001,
|
|
OEHACButtonState0X = 0b00000010,
|
|
OEHACButtonState0B = 0b00000100,
|
|
OEHACButtonState0A = 0b00001000,
|
|
OEHACButtonState0SR = 0b00010000,
|
|
OEHACButtonState0SL = 0b00100000,
|
|
OEHACButtonState0R = 0b01000000,
|
|
OEHACButtonState0ZR = 0b10000000
|
|
};
|
|
|
|
typedef NS_OPTIONS(uint8_t, OEHACButtonState1) {
|
|
OEHACButtonState1Minus = 0b00000001,
|
|
OEHACButtonState1Plus = 0b00000010,
|
|
OEHACButtonState1RStick = 0b00000100,
|
|
OEHACButtonState1LStick = 0b00001000,
|
|
OEHACButtonState1Home = 0b00010000,
|
|
OEHACButtonState1Capture = 0b00100000,
|
|
OEHACButtonState1ChargeGrip = 0b10000000
|
|
};
|
|
|
|
typedef NS_OPTIONS(uint8_t, OEHACButtonState2) {
|
|
OEHACButtonState2Down = 0b00000001,
|
|
OEHACButtonState2Up = 0b00000010,
|
|
OEHACButtonState2Right = 0b00000100,
|
|
OEHACButtonState2Left = 0b00001000,
|
|
OEHACButtonState2SR = 0b00010000,
|
|
OEHACButtonState2SL = 0b00100000,
|
|
OEHACButtonState2L = 0b01000000,
|
|
OEHACButtonState2ZL = 0b10000000
|
|
};
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHACInputReportID reportID;
|
|
uint8_t seqNumber;
|
|
uint8_t chargingStatus;
|
|
OEHACButtonState0 buttons0;
|
|
OEHACButtonState1 buttons1;
|
|
OEHACButtonState2 buttons2;
|
|
OEHAC12BitPackedPair leftStick;
|
|
OEHAC12BitPackedPair rightStick;
|
|
uint8_t rumbleStatus;
|
|
} OEHACStandardHIDInputReport;
|
|
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHACStandardHIDInputReport parent;
|
|
uint8_t ackStatus;
|
|
OEHACSubcommandID repliedSubcmdId;
|
|
uint8_t reply[34];
|
|
} OEHACAcknowledgmentHIDInputReport;
|
|
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
uint8_t fwVersion[2];
|
|
uint8_t deviceType;
|
|
uint8_t unk0;
|
|
uint8_t btMacAddress[6];
|
|
} OEHACDeviceStatusSubcommandReply;
|
|
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHAC12BitPackedPair maxDelta;
|
|
OEHAC12BitPackedPair center;
|
|
OEHAC12BitPackedPair minDelta;
|
|
} OEHACStickCalibrationData;
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
OEHAC12BitPackedPair left_maxDelta;
|
|
OEHAC12BitPackedPair left_zero;
|
|
OEHAC12BitPackedPair left_minDelta;
|
|
// the following 3 lines of code are correct
|
|
// anything suspicious is 100% nintendo's fault
|
|
OEHAC12BitPackedPair right_zero;
|
|
OEHAC12BitPackedPair right_minDelta;
|
|
OEHAC12BitPackedPair right_maxDelta;
|
|
} OEHACFactoryStickCalibrationData;
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
uint16_t leftDataAvailable;
|
|
OEHACStickCalibrationData leftStick;
|
|
uint16_t rightDataAvailable;
|
|
OEHACStickCalibrationData rightStick;
|
|
} OEHACUserStickCalibrationData;
|
|
|
|
enum {
|
|
OEHACFactoryStickCalibrationDataAddress = 0x603D,
|
|
OEHACUserStickCalibrationDataAddress = 0x8010,
|
|
OEHACUserStickDataAvailableMagic = 0xA1B2
|
|
};
|
|
|
|
|
|
static void OEHACProControllerHIDReportCallback(
|
|
void * _Nullable context,
|
|
IOReturn result,
|
|
void * _Nullable sender,
|
|
IOHIDReportType type,
|
|
uint32_t reportID,
|
|
uint8_t * report,
|
|
CFIndex reportLength);
|
|
|
|
static OEHAC16BitUnsignedPair OEHACUnpackPair(const OEHAC12BitPackedPair packed)
|
|
{
|
|
OEHAC16BitUnsignedPair res;
|
|
res.x = packed[0] | ((packed[1] & 0x0F) << 8);
|
|
res.y = ((packed[1] & 0xF0) >> 4) | (packed[2] << 4);
|
|
return res;
|
|
}
|
|
|
|
|
|
typedef struct {
|
|
NSInteger min;
|
|
NSInteger zero;
|
|
NSInteger max;
|
|
} OEHACProControllerAxisCalibration;
|
|
|
|
typedef struct {
|
|
OEHACProControllerAxisCalibration x;
|
|
OEHACProControllerAxisCalibration y;
|
|
} OEHACProControllerStickCalibration;
|
|
|
|
static OEHACProControllerStickCalibration OEHACConvertCalibration(
|
|
const OEHAC12BitPackedPair packed_maxdelta,
|
|
const OEHAC12BitPackedPair packed_zero,
|
|
const OEHAC12BitPackedPair packed_mindelta)
|
|
{
|
|
OEHAC16BitUnsignedPair maxdelta, zero, mindelta;
|
|
OEHACProControllerStickCalibration res;
|
|
|
|
maxdelta = OEHACUnpackPair(packed_maxdelta);
|
|
zero = OEHACUnpackPair(packed_zero);
|
|
mindelta = OEHACUnpackPair(packed_mindelta);
|
|
|
|
/* The calibration data is specified in the reference system used by
|
|
* Nintendo's proprietary button status reports. Nintendo's reference
|
|
* system flips the Y axis with respect to the HID specification. */
|
|
res.x.max = (zero.x + maxdelta.x) << 4;
|
|
res.x.zero = zero.x << 4;
|
|
res.x.min = (zero.x - mindelta.x) << 4;
|
|
res.y.max = 0x10000 - (NSInteger)((zero.y - mindelta.y) << 4);
|
|
res.y.zero = 0x10000 - (NSInteger)(zero.y << 4);
|
|
res.y.min = 0x10000 - (NSInteger)((zero.y + maxdelta.y) << 4);
|
|
|
|
return res;
|
|
}
|
|
|
|
static CGFloat OEHACScaleValueWithCalibration(
|
|
const OEHACProControllerAxisCalibration *selectedCalibration,
|
|
NSInteger rawValue)
|
|
{
|
|
CGFloat res = rawValue - selectedCalibration->zero;
|
|
if (res < 0.0) {
|
|
res /= (CGFloat)(selectedCalibration->zero - selectedCalibration->min);
|
|
} else {
|
|
res /= (CGFloat)(selectedCalibration->max - selectedCalibration->zero);
|
|
}
|
|
|
|
/* the factory calibration is a bit generous at the edges; compensate for that */
|
|
res /= 0.90;
|
|
|
|
return MAX(-1, MIN(res, 1));
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
@interface OESwitchProControllerHIDDeviceParser ()
|
|
|
|
+ (OESwitchProControllerHIDDeviceParser *)sharedInstance;
|
|
+ (NSUInteger)_cookieFromUsage:(NSUInteger)usage;
|
|
|
|
- (void)registerDeviceHandler:(OESwitchProControllerHIDDeviceHandler *)dh;
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark - Device Handler
|
|
|
|
|
|
@implementation OESwitchProControllerHIDDeviceHandler
|
|
{
|
|
NSThread *_thread;
|
|
NSTimer *_pingTimer;
|
|
|
|
uint8_t _reportBuffer[MAX_INPUT_REPORT_SIZE];
|
|
uint8_t _packetCounter;
|
|
|
|
NSCondition *_responseAvailable;
|
|
NSData *_currentResponse;
|
|
|
|
OEHACProControllerStickCalibration _leftStickCalibration;
|
|
OEHACProControllerStickCalibration _rightStickCalibration;
|
|
}
|
|
|
|
|
|
@synthesize eventRunLoop = _eventRunLoop;
|
|
|
|
|
|
+ (BOOL)canHandleDevice:(IOHIDDeviceRef)aDevice
|
|
{
|
|
NSString *deviceName = (__bridge id)IOHIDDeviceGetProperty(aDevice, CFSTR(kIOHIDProductKey));
|
|
|
|
if ([deviceName isEqualToString:@"Pro Controller"]) {
|
|
NSNumber *vid = (__bridge id)IOHIDDeviceGetProperty(aDevice, CFSTR(kIOHIDVendorIDKey));
|
|
NSNumber *pid = (__bridge id)IOHIDDeviceGetProperty(aDevice, CFSTR(kIOHIDProductIDKey));
|
|
if ([vid integerValue] == 0x57E && [pid integerValue] == 0x2009) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
+ (OEHIDDeviceParser *)deviceParser
|
|
{
|
|
return [OESwitchProControllerHIDDeviceParser sharedInstance];
|
|
}
|
|
|
|
|
|
- (instancetype)initWithIOHIDDevice:(IOHIDDeviceRef)aDevice deviceDescription:(nullable OEDeviceDescription *)deviceDescription
|
|
{
|
|
self = [super initWithIOHIDDevice:aDevice deviceDescription:deviceDescription];
|
|
|
|
/* plausible default calibration
|
|
* if everything goes according to plan, it will be rewritten by values
|
|
* read from the controller's internal flash memory at connection time */
|
|
_leftStickCalibration = _rightStickCalibration =
|
|
(OEHACProControllerStickCalibration){
|
|
.x = {.min = 8192, .zero = 32768, .max = 57344},
|
|
.y = {.min = 8192, .zero = 32768, .max = 57344},
|
|
};
|
|
|
|
_responseAvailable = [[NSCondition alloc] init];
|
|
|
|
return self;
|
|
}
|
|
|
|
|
|
- (void)setUpCallbacks
|
|
{
|
|
dispatch_semaphore_t done = dispatch_semaphore_create(0);
|
|
_thread = [[NSThread alloc] initWithBlock:^{
|
|
self->_eventRunLoop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent());
|
|
|
|
IOHIDDeviceRegisterInputReportCallback(self.device, self->_reportBuffer, MAX_INPUT_REPORT_SIZE, OEHACProControllerHIDReportCallback, (__bridge void *)self);
|
|
[super setUpCallbacks];
|
|
|
|
dispatch_semaphore_signal(done);
|
|
CFRunLoopRun();
|
|
}];
|
|
[_thread setQualityOfService:NSQualityOfServiceUserInteractive];
|
|
[_thread setName:@"org.openemu.OpenEmuSystem.switchProControllerThread"];
|
|
[_thread start];
|
|
dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER);
|
|
}
|
|
|
|
|
|
- (BOOL)connect
|
|
{
|
|
NSString *transport = IOHIDDeviceGetProperty([self device], CFSTR(kIOHIDTransportKey));
|
|
if ([transport isEqualToString:@kIOHIDTransportUSBValue]) {
|
|
_isUSB = YES;
|
|
[self _enableUSBmode];
|
|
}
|
|
|
|
NSData *info = [self _sendSubcommand:OEHACSubcmdRequestDeviceInfo withData:NULL length:0];
|
|
if (info && info.length >= sizeof(OEHACAcknowledgmentHIDInputReport)) {
|
|
const OEHACAcknowledgmentHIDInputReport *ack = info.bytes;
|
|
const OEHACDeviceStatusSubcommandReply *status = (const OEHACDeviceStatusSubcommandReply *)&(ack->reply);
|
|
NSLog(@"[dev %p] Firmware Version: %02x.%02x", self, status->fwVersion[0], status->fwVersion[1]);
|
|
_internalSerialNumber = [NSData dataWithBytes:status->btMacAddress length:6];
|
|
}
|
|
|
|
[[OESwitchProControllerHIDDeviceParser sharedInstance] registerDeviceHandler:self];
|
|
|
|
[self _setReportMode:OEHACInputReportIDFullReport];
|
|
[self _setPlayerLights:0x0F];
|
|
[self _requestCalibrationData];
|
|
|
|
/* In BetterJoyForCemu there is a comment that says the controller wants to be
|
|
* periodically pinged to keep its connection alive. Even though I've never seen
|
|
* a spontaneous disconnection, it's better to be safe than sorry */
|
|
_pingTimer = [NSTimer timerWithTimeInterval:PING_INTERVAL_SECONDS repeats:YES block:^(NSTimer * _Nonnull timer) {
|
|
[self _sendOneWaySubcommand:OEHACSubcmdNoop withData:NULL length:0];
|
|
}];
|
|
[_pingTimer setTolerance:PING_INTERVAL_SECONDS * 0.1];
|
|
[[NSRunLoop mainRunLoop] addTimer:_pingTimer forMode:NSDefaultRunLoopMode];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void)disconnect
|
|
{
|
|
[super disconnect];
|
|
|
|
[_pingTimer invalidate];
|
|
|
|
/* we don't want to disconnect the controller every time the helper
|
|
* app terminates, just when OpenEmu closes */
|
|
if ([[[NSBundle mainBundle] bundleIdentifier] isEqual:@"org.openemu.OpenEmu"]) {
|
|
[self _setPowerState:OEHACPowerStateSleep];
|
|
}
|
|
|
|
CFRunLoopStop(_eventRunLoop);
|
|
}
|
|
|
|
|
|
- (void)dealloc
|
|
{
|
|
if (_eventRunLoop)
|
|
CFRelease(_eventRunLoop);
|
|
}
|
|
|
|
|
|
#pragma mark - Calibration
|
|
|
|
|
|
- (void)_requestCalibrationData
|
|
{
|
|
NSData *fact = [self _requestSPIFlashReadAtAddress:OEHACFactoryStickCalibrationDataAddress length:sizeof(OEHACFactoryStickCalibrationData)];
|
|
if (!fact) {
|
|
NSLog(@"[dev %p] cannot read stick calibration data from SPI flash!", self);
|
|
return;
|
|
}
|
|
const OEHACFactoryStickCalibrationData *calibData = fact.bytes;
|
|
|
|
self->_leftStickCalibration = OEHACConvertCalibration(calibData->left_maxDelta, calibData->left_zero, calibData->left_minDelta);
|
|
self->_rightStickCalibration = OEHACConvertCalibration(calibData->right_maxDelta, calibData->right_zero, calibData->right_minDelta);
|
|
|
|
NSLog(@"Loaded calibration successfully for Switch Pro Controller %@", self);
|
|
NSLog(@"Left stick (x, y): [+] %d, %d; [0] %d %d; [-] %d %d",
|
|
(int)self->_leftStickCalibration.x.max,
|
|
(int)self->_leftStickCalibration.y.max,
|
|
(int)self->_leftStickCalibration.x.zero,
|
|
(int)self->_leftStickCalibration.y.zero,
|
|
(int)self->_leftStickCalibration.x.min,
|
|
(int)self->_leftStickCalibration.y.min);
|
|
NSLog(@"Right stick (x, y): [+] %d, %d; [0] %d %d; [-] %d %d",
|
|
(int)self->_rightStickCalibration.x.max,
|
|
(int)self->_rightStickCalibration.y.max,
|
|
(int)self->_rightStickCalibration.x.zero,
|
|
(int)self->_rightStickCalibration.y.zero,
|
|
(int)self->_rightStickCalibration.x.min,
|
|
(int)self->_rightStickCalibration.y.min);
|
|
}
|
|
|
|
|
|
#pragma mark - Event Dispatching
|
|
|
|
|
|
- (void)dispatchEventWithHIDValue:(IOHIDValueRef)aValue
|
|
{
|
|
/* Ignore the standard HID reports; over BlueTooth they would be usable,
|
|
* but they're not via USB. Instead we parse Nintendo's proprietary input reports
|
|
* manually instead. */
|
|
return;
|
|
}
|
|
|
|
|
|
- (void)_dispatchEventsWithStandardInputReport:(OEHACStandardHIDInputReport *)report
|
|
{
|
|
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
|
|
NSInteger cookie;
|
|
|
|
/* Buttons */
|
|
const uint8_t button0Map[] = {
|
|
OEHACButtonState0Y, 3,
|
|
OEHACButtonState0X, 4,
|
|
OEHACButtonState0B, 1,
|
|
OEHACButtonState0A, 2,
|
|
OEHACButtonState0R, 6,
|
|
OEHACButtonState0ZR, 8,
|
|
0};
|
|
const uint8_t button1Map[] = {
|
|
OEHACButtonState1Minus, 9,
|
|
OEHACButtonState1Plus, 10,
|
|
OEHACButtonState1RStick, 12,
|
|
OEHACButtonState1LStick, 11,
|
|
OEHACButtonState1Home, 13,
|
|
OEHACButtonState1Capture, 14,
|
|
0};
|
|
const uint8_t button2Map[] = {
|
|
OEHACButtonState2L, 5,
|
|
OEHACButtonState2ZL, 7,
|
|
0};
|
|
[self _dispatchButtonEventsWithButtonMask:report->buttons0 buttonMap:button0Map timestamp:now];
|
|
[self _dispatchButtonEventsWithButtonMask:report->buttons1 buttonMap:button1Map timestamp:now];
|
|
[self _dispatchButtonEventsWithButtonMask:report->buttons2 buttonMap:button2Map timestamp:now];
|
|
|
|
/* D-Pad */
|
|
const OEHIDEventHatDirection buttonToHat[] = {
|
|
/* ..ud ..uD ..Ud ..UD */
|
|
OEHIDEventHatDirectionNull, OEHIDEventHatDirectionSouth, OEHIDEventHatDirectionNorth, OEHIDEventHatDirectionNull, /* lr.. */
|
|
OEHIDEventHatDirectionEast, OEHIDEventHatDirectionSouthEast, OEHIDEventHatDirectionNorthEast, OEHIDEventHatDirectionNull, /* lR.. */
|
|
OEHIDEventHatDirectionWest, OEHIDEventHatDirectionSouthWest, OEHIDEventHatDirectionNorthWest, OEHIDEventHatDirectionNull, /* Lr.. */
|
|
OEHIDEventHatDirectionNull, OEHIDEventHatDirectionNull, OEHIDEventHatDirectionNull, OEHIDEventHatDirectionNull};/* LR.. */
|
|
uint8_t direction = report->buttons2 & (OEHACButtonState2Up | OEHACButtonState2Down | OEHACButtonState2Left | OEHACButtonState2Right);
|
|
NSAssert(direction < 0x10, @"Don't touch my enums! We are making assumptions here");
|
|
OEHIDEventHatDirection hat = buttonToHat[direction];
|
|
cookie = [OESwitchProControllerHIDDeviceParser _cookieFromUsage:0x39];
|
|
OEHIDEvent *hatevt = [OEHIDEvent hatSwitchEventWithDeviceHandler:self timestamp:now type:OEHIDEventHatSwitchType8Ways direction:hat cookie:cookie];
|
|
[self dispatchEvent:hatevt];
|
|
|
|
/* Left stick */
|
|
OEHAC16BitUnsignedPair leftStick = OEHACUnpackPair(report->leftStick);
|
|
[self _dispatchEventsOfXAxis:OEHIDEventAxisX YAxis:OEHIDEventAxisY withData:leftStick calibration:&_leftStickCalibration timestamp:now];
|
|
|
|
/* Right stick */
|
|
OEHAC16BitUnsignedPair rightStick = OEHACUnpackPair(report->rightStick);
|
|
[self _dispatchEventsOfXAxis:OEHIDEventAxisRx YAxis:OEHIDEventAxisRy withData:rightStick calibration:&_rightStickCalibration timestamp:now];
|
|
}
|
|
|
|
|
|
- (void)_dispatchButtonEventsWithButtonMask:(uint8_t)mask buttonMap:(const uint8_t[])map timestamp:(NSTimeInterval)ts
|
|
{
|
|
int i=0;
|
|
while (map[i] != 0) {
|
|
uint8_t thisButtonMask = map[i++];
|
|
uint8_t thisButtonNumber = map[i++];
|
|
NSInteger cookie = [OESwitchProControllerHIDDeviceParser _cookieFromUsage:thisButtonNumber];
|
|
OEHIDEventState state = (mask & thisButtonMask) ? OEHIDEventStateOn : OEHIDEventStateOff;
|
|
OEHIDEvent *evt = [OEHIDEvent buttonEventWithDeviceHandler:self timestamp:ts buttonNumber:thisButtonNumber state:state cookie:cookie];
|
|
[self dispatchEvent:evt];
|
|
}
|
|
}
|
|
|
|
|
|
- (void)_dispatchEventsOfXAxis:(OEHIDEventAxis)xaxis YAxis:(OEHIDEventAxis)yaxis withData:(OEHAC16BitUnsignedPair)stickData calibration:(const OEHACProControllerStickCalibration *)calibration timestamp:(NSTimeInterval)now
|
|
{
|
|
stickData.x <<= 4;
|
|
stickData.y = ~(stickData.y << 4);
|
|
|
|
NSInteger cookie;
|
|
CGFloat value;
|
|
OEHIDEvent *event;
|
|
|
|
cookie = [OESwitchProControllerHIDDeviceParser _cookieFromUsage:xaxis];
|
|
value = OEHACScaleValueWithCalibration(&(calibration->x), stickData.x);
|
|
if (fabs(value) < [self deadZoneForControlCookie:cookie])
|
|
value = 0;
|
|
event = [OEHIDEvent axisEventWithDeviceHandler:self timestamp:now axis:xaxis value:value cookie:cookie];
|
|
[self dispatchEvent:event];
|
|
|
|
cookie = [OESwitchProControllerHIDDeviceParser _cookieFromUsage:yaxis];
|
|
value = OEHACScaleValueWithCalibration(&(calibration->y), stickData.y);
|
|
if (fabs(value) < [self deadZoneForControlCookie:cookie])
|
|
value = 0;
|
|
event = [OEHIDEvent axisEventWithDeviceHandler:self timestamp:now axis:yaxis value:value cookie:cookie];
|
|
[self dispatchEvent:event];
|
|
}
|
|
|
|
|
|
#pragma mark - Controller-Specific functionality
|
|
|
|
|
|
- (void)_setPlayerLights:(uint8_t)mask
|
|
{
|
|
[self _sendSubcommand:OEHACSubcmdSetPlayerLights withData:&mask length:1];
|
|
}
|
|
|
|
|
|
- (void)_setReportMode:(OEHACInputReportID)mode
|
|
{
|
|
[self _sendSubcommand:OEHACSubcmdSetInputReportMode withData:&mode length:1];
|
|
}
|
|
|
|
|
|
- (void)_setPowerState:(OEHACPowerState)powerState
|
|
{
|
|
[self _sendOneWaySubcommand:OEHACSubcmdSetPowerState withData:&powerState length:1];
|
|
}
|
|
|
|
|
|
- (void)_enableUSBmode
|
|
{
|
|
[self _sendUSBSubcommand:OEHACUSBSubcommandIDRequestHandshake];
|
|
[self _sendUSBSubcommand:OEHACUSBSubcommandIDRequestHighDataRate];
|
|
[self _sendUSBSubcommand:OEHACUSBSubcommandIDRequestHandshake];
|
|
[self _sendOneWayUSBSubcommand:OEHACUSBSubcommandIDDisableUSBHIDTimeout];
|
|
}
|
|
|
|
|
|
#pragma mark - Custom HID Reports Send/Receive Primitives
|
|
|
|
|
|
- (NSData *)_requestSPIFlashReadAtAddress:(uint32_t)in_base length:(uint8_t)in_len
|
|
{
|
|
NSAssert(in_len <= 29, @"cannot read more than 29 bytes from SPI Flash at a time");
|
|
|
|
struct __attribute__((packed)) {
|
|
uint32_t base;
|
|
uint8_t len;
|
|
} spiReadSubcmdData = {in_base, in_len};
|
|
|
|
__block NSData *result;
|
|
[self _sendSubcommand:OEHACSubcmdRequestSPIFlashRead withData:&spiReadSubcmdData length:sizeof(spiReadSubcmdData) validationHandler:^BOOL(NSData *data) {
|
|
const OEHACAcknowledgmentHIDInputReport *ack = data.bytes;
|
|
const struct __attribute__((packed)) {
|
|
uint32_t base;
|
|
uint8_t len;
|
|
uint8_t data[29];
|
|
} *reply = (void *)ack->reply;
|
|
|
|
if (reply->base != in_base || reply->len != in_len) {
|
|
NSLog(@"[dev %p] Wrong ACK from controller (SPI Flash read wrong offset/length)", self);
|
|
return NO;
|
|
}
|
|
|
|
result = [NSData dataWithBytes:reply->data length:in_len];
|
|
return YES;
|
|
}];
|
|
|
|
if (!result)
|
|
return nil;
|
|
return result;
|
|
}
|
|
|
|
|
|
- (NSData *)_sendSubcommand:(OEHACSubcommandID)cmdid withData:(const void *)data length:(NSUInteger)length
|
|
{
|
|
return [self _sendSubcommand:cmdid withData:data length:length validationHandler:nil];
|
|
}
|
|
|
|
|
|
- (NSData *)_sendSubcommand:(OEHACSubcommandID)cmdid withData:(const void *)data length:(NSUInteger)length validationHandler:(BOOL (^ __nullable)(NSData *))validator
|
|
{
|
|
OEHACRumbleAndSubcommandOutputReport report = {0};
|
|
NSAssert(length < sizeof(report.subcmdParam), @"too much data for a single report");
|
|
|
|
report.reportID = OEHACOutputReportIDRumbleAndSubcommand;
|
|
report.seqNumber = _packetCounter;
|
|
_packetCounter = (_packetCounter + 1) & 0xF;
|
|
report.subcmdID = cmdid;
|
|
if (data)
|
|
memcpy(report.subcmdParam, data, length);
|
|
|
|
NSData *reportData = [NSData dataWithBytes:&report length:sizeof(OEHACRumbleAndSubcommandOutputReport)];
|
|
return [self _attemptSendingOutputReport:reportData responseHandler:^BOOL(NSData *respData, int attempts) {
|
|
if ([respData length] < sizeof(OEHACAcknowledgmentHIDInputReport)) {
|
|
NSLog(@"[dev %p] Invalid ACK from controller (subcommand %02X)", self, cmdid);
|
|
return NO;
|
|
}
|
|
const OEHACAcknowledgmentHIDInputReport *response = [respData bytes];
|
|
if (!(response->ackStatus & 0x80)) {
|
|
NSLog(@"[dev %p] NACK from controller (subcommand %02X)", self, cmdid);
|
|
return NO;
|
|
}
|
|
if (response->repliedSubcmdId != cmdid) {
|
|
NSLog(@"[dev %p] Wrong ACK from controller (subcommand %02X expected, %02X received) [attempt = %d]", self, cmdid, response->repliedSubcmdId, attempts);
|
|
return NO;
|
|
} else if (validator && !validator(respData)) {
|
|
NSLog(@"[dev %p] Validation failed [attempt = %d]", self, attempts);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}];
|
|
}
|
|
|
|
|
|
- (BOOL)_sendOneWaySubcommand:(OEHACSubcommandID)cmdid withData:(const void *)data length:(NSUInteger)length
|
|
{
|
|
OEHACRumbleAndSubcommandOutputReport report = {0};
|
|
NSAssert(length < sizeof(report.subcmdParam), @"too much data for a single report");
|
|
|
|
report.reportID = OEHACOutputReportIDRumbleAndSubcommand;
|
|
report.seqNumber = _packetCounter;
|
|
_packetCounter = (_packetCounter + 1) & 0xF;
|
|
report.subcmdID = cmdid;
|
|
if (data)
|
|
memcpy(report.subcmdParam, data, length);
|
|
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] sent output report %@", self, [NSData dataWithBytes:(uint8_t *)&report length:sizeof(OEHACRumbleAndSubcommandOutputReport)]);
|
|
#endif
|
|
IOReturn ret = IOHIDDeviceSetReport([self device], kIOHIDReportTypeOutput, report.reportID, (uint8_t *)&report, sizeof(OEHACRumbleAndSubcommandOutputReport));
|
|
if (ret != kIOReturnSuccess) {
|
|
NSLog(@"[dev %p] Could not send command, error: %x", self, ret);
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (NSData *)_sendUSBSubcommand:(OEHACUSBSubcommandID)cmdid
|
|
{
|
|
OEHACUSBSubcommandOutputReport report = {0};
|
|
report.reportID = OEHACOutputReportIDUSBSubcommand;
|
|
report.subcommand = cmdid;
|
|
|
|
NSData *reportData = [NSData dataWithBytes:(void*)&report length:sizeof(OEHACUSBSubcommandOutputReport)];
|
|
|
|
return [self _attemptSendingOutputReport:reportData responseHandler:^BOOL(NSData *respData, int attempts) {
|
|
if ([respData length] < sizeof(OEHACUSBAcknowledgmentOutputReport)) {
|
|
NSLog(@"[dev %p] Invalid ACK from controller (USB subcommand %02X)", self, cmdid);
|
|
return NO;
|
|
}
|
|
const OEHACUSBAcknowledgmentOutputReport *response = respData.bytes;
|
|
if (response->reportID != OEHACInputReportIDUSBSubcommandReply) {
|
|
NSLog(@"[dev %p] Invalid ACK from controller (USB subcommand %02X)", self, cmdid);
|
|
return NO;
|
|
}
|
|
if (response->subcommand != cmdid) {
|
|
NSLog(@"[dev %p] Wrong USB ACK from controller (subcommand %02X expected, %02X received) [attempt = %d]", self, cmdid, response->subcommand, attempts);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}];
|
|
}
|
|
|
|
|
|
- (BOOL)_sendOneWayUSBSubcommand:(OEHACUSBSubcommandID)cmdid
|
|
{
|
|
OEHACUSBSubcommandOutputReport report = {0};
|
|
|
|
report.reportID = OEHACOutputReportIDUSBSubcommand;
|
|
report.subcommand = cmdid;
|
|
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] sent output report %@", self, [NSData dataWithBytes:(uint8_t *)&report length:sizeof(OEHACUSBSubcommandOutputReport)]);
|
|
#endif
|
|
IOReturn ret = IOHIDDeviceSetReport([self device], kIOHIDReportTypeOutput, report.reportID, (uint8_t *)&report, sizeof(OEHACUSBSubcommandOutputReport));
|
|
if (ret != kIOReturnSuccess) {
|
|
NSLog(@"[dev %p] Could not send command, error: %x", self, ret);
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (NSData *)_attemptSendingOutputReport:(NSData *)report responseHandler:(BOOL (^)(NSData *respData, int attempts))respHandler
|
|
{
|
|
NSAssert(report.length > 1, @"HID reports must be at least one byte long!");
|
|
|
|
NSData *ack;
|
|
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:MAX_RESPONSE_WAIT_SECONDS];
|
|
uint8_t reportID = ((uint8_t *)report.bytes)[0];
|
|
|
|
[_responseAvailable lock];
|
|
|
|
_currentResponse = nil;
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] sent output report %@", self, report);
|
|
#endif
|
|
IOReturn ret = IOHIDDeviceSetReport([self device], kIOHIDReportTypeOutput, reportID, report.bytes, report.length);
|
|
if (ret != kIOReturnSuccess) {
|
|
NSLog(@"[dev %p] Could not send command, error: %x", self, ret);
|
|
goto fail;
|
|
}
|
|
|
|
int attempts = 0;
|
|
while (_currentResponse == nil && attempts < MAX_RESPONSE_ATTEMPTS) {
|
|
BOOL notTimeout = YES;
|
|
while (_currentResponse == nil && notTimeout)
|
|
notTimeout = [_responseAvailable waitUntilDate:deadline];
|
|
if (!notTimeout && _currentResponse == nil) {
|
|
NSLog(@"[dev %p] Did not receive ACK from controller after %f s", self, MAX_RESPONSE_WAIT_SECONDS);
|
|
goto fail;
|
|
}
|
|
|
|
BOOL accepted = respHandler(_currentResponse, attempts);
|
|
if (!accepted) {
|
|
_currentResponse = nil;
|
|
attempts++;
|
|
}
|
|
}
|
|
ack = _currentResponse;
|
|
|
|
fail:
|
|
[_responseAvailable unlock];
|
|
|
|
return ack;
|
|
}
|
|
|
|
|
|
- (void)_didReceiveInputReportWithID:(uint8_t)rid data:(uint8_t *)data length:(NSUInteger)length
|
|
{
|
|
if (data[0] == OEHACInputReportIDSubcommandReply || data[0] == OEHACInputReportIDUSBSubcommandReply) {
|
|
[_responseAvailable lock];
|
|
_currentResponse = [NSData dataWithBytes:data length:length];
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] ack report %@", self, _currentResponse);
|
|
#endif
|
|
[_responseAvailable signal];
|
|
[_responseAvailable unlock];
|
|
|
|
} else if (data[0] == OEHACInputReportIDFullReport && length >= sizeof(OEHACStandardHIDInputReport)) {
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] input report %@", self, [NSData dataWithBytes:data length:length]);
|
|
#endif
|
|
/* Latency of an event dispatch as measured on a MacBook Pro (13-inch, 2018,
|
|
* Four Thunderbolt 3 Ports), via timestamp comparison with both the LOG_COMMUNICATION
|
|
* define enalbed and the HID Event Log option enabled:
|
|
* ~500 microseconds (0.030 frames at 60 FPS) */
|
|
__block OEHACStandardHIDInputReport report = *((OEHACStandardHIDInputReport *)(data));
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self _dispatchEventsWithStandardInputReport:&report];
|
|
});
|
|
|
|
} else {
|
|
#ifdef LOG_COMMUNICATION
|
|
NSLog(@"[dev %p] non-ack report %@", self, [NSData dataWithBytes:data length:length]);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
|
|
@end
|
|
|
|
|
|
static void OEHACProControllerHIDReportCallback(
|
|
void * _Nullable context,
|
|
IOReturn result,
|
|
void * _Nullable sender,
|
|
IOHIDReportType type,
|
|
uint32_t reportID,
|
|
uint8_t * report,
|
|
CFIndex reportLength)
|
|
{
|
|
[(__bridge OESwitchProControllerHIDDeviceHandler *)context _didReceiveInputReportWithID:reportID data:report length:reportLength];
|
|
}
|
|
|
|
|
|
#pragma mark - Device Parser (mainly for USB support)
|
|
|
|
|
|
@implementation OESwitchProControllerHIDDeviceParser
|
|
{
|
|
NSMapTable <NSData *, OESwitchProControllerHIDDeviceHandler *> *_serialToHandler;
|
|
}
|
|
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
_serialToHandler = [NSMapTable strongToWeakObjectsMapTable];
|
|
return self;
|
|
}
|
|
|
|
|
|
+ (OESwitchProControllerHIDDeviceParser *)sharedInstance
|
|
{
|
|
static OESwitchProControllerHIDDeviceParser *parser;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
parser = [[OESwitchProControllerHIDDeviceParser alloc] init];
|
|
});
|
|
return parser;
|
|
}
|
|
|
|
|
|
+ (NSUInteger)_cookieFromUsage:(NSUInteger)usage
|
|
{
|
|
NSUInteger cookie;
|
|
/* reproduce the same cookies seen over bluetooth
|
|
* probably unnecessary */
|
|
if (usage == 0x30 || usage == 0x31)
|
|
cookie = usage - 0x30 + 0x4B3;
|
|
else if (usage == 0x33 || usage == 0x34)
|
|
cookie = usage - 0x33 + 0x4B5;
|
|
else if (usage == 0x39)
|
|
cookie = 0x4B2;
|
|
else
|
|
cookie = usage + 1;
|
|
return cookie;
|
|
}
|
|
|
|
|
|
- (OEDeviceHandler *)deviceHandlerForIOHIDDevice:(IOHIDDeviceRef)device
|
|
{
|
|
/* When connected via USB, the exposed HID elements are completely different.
|
|
* To work around that, we build a custom device description which is compatible
|
|
* with the HID elements that are exposed via bluetooth. */
|
|
|
|
NSNumber *vid = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
|
|
NSNumber *pid = (__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
|
|
NSString *pkey = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
|
|
OEControllerDescription *controllerDesc = [OEControllerDescription OE_controllerDescriptionForVendorID:vid.integerValue productID:pid.integerValue product:pkey];
|
|
if ([[controllerDesc controls] count] == 0) {
|
|
NSDictionary *representations = [OEControllerDescription OE_representationForControllerDescription:controllerDesc];
|
|
[representations enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSDictionary *representation, BOOL *stop) {
|
|
OEHIDEventType type = OEHIDEventTypeFromNSString(representation[@"Type"]);
|
|
NSUInteger usage = OEUsageFromUsageStringWithType(representation[@"Usage"], type);
|
|
NSUInteger cookie = [OESwitchProControllerHIDDeviceParser _cookieFromUsage:usage];
|
|
|
|
OEHIDEvent *event;
|
|
switch (type) {
|
|
case OEHIDEventTypeAxis:
|
|
event = [OEHIDEvent axisEventWithDeviceHandler:nil timestamp:0 axis:usage direction:OEHIDEventAxisDirectionNull cookie:cookie];
|
|
break;
|
|
case OEHIDEventTypeButton:
|
|
event = [OEHIDEvent buttonEventWithDeviceHandler:nil timestamp:0 buttonNumber:usage state:OEHIDEventStateOn cookie:cookie];
|
|
break;
|
|
case OEHIDEventTypeHatSwitch:
|
|
event = [OEHIDEvent hatSwitchEventWithDeviceHandler:nil timestamp:0 type:OEHIDEventHatSwitchType8Ways direction:OEHIDEventHatDirectionNull cookie:cookie];
|
|
break;
|
|
default:
|
|
NSLog(@"unexpected item in controller database for Switch Pro Controller");
|
|
return;
|
|
}
|
|
|
|
[controllerDesc addControlWithIdentifier:identifier name:representation[@"Name"] event:event valueRepresentations:representation[@"Values"]];
|
|
}];
|
|
}
|
|
|
|
return [[OESwitchProControllerHIDDeviceHandler alloc] initWithIOHIDDevice:device deviceDescription:[controllerDesc deviceDescriptionForVendorID:vid.integerValue productID:pid.integerValue cookie:0]];
|
|
}
|
|
|
|
|
|
- (void)registerDeviceHandler:(OESwitchProControllerHIDDeviceHandler *)dh
|
|
{
|
|
/* When connecting a Switch Pro Controller via USB, if that same controller
|
|
* was already connected via BlueTooth, it can happen that for a brief instant
|
|
* OpenEmu thinks that the controller is connected both via USB and BT
|
|
* at the same time.
|
|
*
|
|
* Thus, we keep a registry of all the device handlers of each Switch Controller
|
|
* so that we can check if we have to manually disconnect a device because
|
|
* we are changing connection to USB. */
|
|
|
|
NSData *serial = dh.internalSerialNumber;
|
|
OESwitchProControllerHIDDeviceHandler *existing = [_serialToHandler objectForKey:serial];
|
|
OEDeviceManager *devm = [OEDeviceManager sharedDeviceManager];
|
|
|
|
if (![devm.deviceHandlers containsObject:existing]) {
|
|
/* Device is not connected in any other way */
|
|
[_serialToHandler setObject:dh forKey:serial];
|
|
return;
|
|
}
|
|
|
|
NSLog(@"Switch Pro Controller %@ is connected both via bluetooth and USB; removing least recent connection", serial);
|
|
[[OEDeviceManager sharedDeviceManager] OE_removeDeviceHandler:existing];
|
|
[_serialToHandler setObject:dh forKey:serial];
|
|
}
|
|
|
|
|
|
@end
|
|
|