Add native rumble support for GameSir G8+ MFi
This commit is contained in:
5 files changed
+371
-1
No files matched your search
@@ -20,6 +20,8 @@
|
|||||||
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
||||||
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
||||||
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
||||||
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumble.m in Sources */ = {isa = PBXBuildFile; fileRef = A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */; };
|
||||||
|
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76B00042FFCC00100000004 /* ExternalAccessory.framework */; };
|
||||||
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
||||||
@@ -200,6 +202,9 @@
|
|||||||
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
||||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
||||||
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||||
|
A76B00012FFCC00100000001 /* GameSirG8MFiRumble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GameSirG8MFiRumble.h; sourceTree = "<group>"; };
|
||||||
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GameSirG8MFiRumble.m; sourceTree = "<group>"; };
|
||||||
|
A76B00042FFCC00100000004 /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; };
|
||||||
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
||||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
||||||
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
||||||
@@ -471,6 +476,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A76B00052FFCC00100000005 /* ExternalAccessory.framework in Frameworks */,
|
||||||
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
|
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
|
||||||
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
|
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
|
||||||
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
||||||
@@ -648,6 +654,7 @@
|
|||||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A76B00042FFCC00100000004 /* ExternalAccessory.framework */,
|
||||||
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
||||||
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
||||||
98882A052AF60F5300C5A11C /* libavformat.a */,
|
98882A052AF60F5300C5A11C /* libavformat.a */,
|
||||||
@@ -750,6 +757,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
84351B052C29ACAF00A77989 /* CustomOSC */,
|
84351B052C29ACAF00A77989 /* CustomOSC */,
|
||||||
|
A76B00012FFCC00100000001 /* GameSirG8MFiRumble.h */,
|
||||||
|
A76B00022FFCC00100000002 /* GameSirG8MFiRumble.m */,
|
||||||
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
||||||
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
||||||
FB89460C19F646E200339C8A /* StreamView.h */,
|
FB89460C19F646E200339C8A /* StreamView.h */,
|
||||||
@@ -1243,6 +1252,7 @@
|
|||||||
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */,
|
||||||
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */,
|
||||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */,
|
||||||
|
A76B00032FFCC00100000003 /* GameSirG8MFiRumble.m in Sources */,
|
||||||
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
||||||
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
FB9AFD3D1A7E111600872C98 /* AppAssetResponse.m in Sources */,
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
|
#import "GameSirG8MFiRumble.h"
|
||||||
|
#import "OnScreenControls.h"
|
||||||
#import "VoidController.h"
|
#import "VoidController.h"
|
||||||
#import "VoidLink-Swift.h"
|
#import "VoidLink-Swift.h"
|
||||||
#import "OnScreenControls.h"
|
|
||||||
|
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
@@ -118,6 +119,9 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
TemporarySettings* tempSettings;
|
TemporarySettings* tempSettings;
|
||||||
OSCProfile* oscProfile;
|
OSCProfile* oscProfile;
|
||||||
OSCProfilesManager* oscProfileMan;
|
OSCProfilesManager* oscProfileMan;
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
GameSirG8MFiRumble *_gameSirG8MFiRumble;
|
||||||
|
#endif
|
||||||
|
|
||||||
#define EMULATING_SELECT 0x1
|
#define EMULATING_SELECT 0x1
|
||||||
#define EMULATING_SPECIAL 0x2
|
#define EMULATING_SPECIAL 0x2
|
||||||
@@ -174,22 +178,48 @@ static void ApplyAdaptiveTriggerEffect(GCDualSenseAdaptiveTrigger* trigger,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
BOOL useGameSirG8MFiRumble = [_gameSirG8MFiRumble canHandleController:voidController.gamepad];
|
||||||
|
#endif
|
||||||
|
|
||||||
// physical controller connected:
|
// physical controller connected:
|
||||||
switch (preference) {
|
switch (preference) {
|
||||||
case HapticEngineAuto:
|
case HapticEngineAuto:
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (useGameSirG8MFiRumble) {
|
||||||
|
[_gameSirG8MFiRumble setLowFrequencyMotor:lowFreqMotor highFrequencyMotor:highFreqMotor];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
// if controller has no haptic profile, it already falled bakc to device engine
|
// if controller has no haptic profile, it already falled bakc to device engine
|
||||||
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||||
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||||
break;
|
break;
|
||||||
case RumbleDevice:
|
case RumbleDevice:
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (useGameSirG8MFiRumble) {
|
||||||
|
[_gameSirG8MFiRumble setLowFrequencyMotor:0 highFrequencyMotor:0];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||||
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||||
break;
|
break;
|
||||||
case LeftRightSwapped:
|
case LeftRightSwapped:
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (useGameSirG8MFiRumble) {
|
||||||
|
[_gameSirG8MFiRumble setLowFrequencyMotor:highFreqMotor highFrequencyMotor:lowFreqMotor];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
[voidController.lowFreqMotor setMotorAmplitude:highFreqMotor];
|
[voidController.lowFreqMotor setMotorAmplitude:highFreqMotor];
|
||||||
[voidController.highFreqMotor setMotorAmplitude:lowFreqMotor];
|
[voidController.highFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||||
break;
|
break;
|
||||||
case RumbleOff:
|
case RumbleOff:
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if (useGameSirG8MFiRumble) {
|
||||||
|
[_gameSirG8MFiRumble setLowFrequencyMotor:0 highFrequencyMotor:0];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -2005,6 +2035,9 @@ double rc_expo(double x, double expo) {
|
|||||||
_voidControllers = [[NSMutableDictionary alloc] init];
|
_voidControllers = [[NSMutableDictionary alloc] init];
|
||||||
[ControllerUtil.activeStreamingGCControllers removeAllObjects];
|
[ControllerUtil.activeStreamingGCControllers removeAllObjects];
|
||||||
_controllerNumbers = 0;
|
_controllerNumbers = 0;
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
_gameSirG8MFiRumble = [[GameSirG8MFiRumble alloc] init];
|
||||||
|
#endif
|
||||||
|
|
||||||
_captureMouse = (streamConfig.localMousePointerMode == 0);
|
_captureMouse = (streamConfig.localMousePointerMode == 0);
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||||
@@ -2064,6 +2097,11 @@ double rc_expo(double x, double expo) {
|
|||||||
|
|
||||||
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||||
if (voidController) {
|
if (voidController) {
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
if ([self->_gameSirG8MFiRumble isTargetController:controller]) {
|
||||||
|
[self->_gameSirG8MFiRumble stopAndClose];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
[self stopTimerForController:voidController];
|
[self stopTimerForController:voidController];
|
||||||
|
|
||||||
// Stop haptics on this controller
|
// Stop haptics on this controller
|
||||||
@@ -2267,6 +2305,9 @@ double rc_expo(double x, double expo) {
|
|||||||
-(void) cleanup
|
-(void) cleanup
|
||||||
{
|
{
|
||||||
[ControllerUtil stopAllDualSenseHaptics];
|
[ControllerUtil stopAllDualSenseHaptics];
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
[_gameSirG8MFiRumble invalidate];
|
||||||
|
#endif
|
||||||
|
|
||||||
if (VLSharedControllerSupport == self) {
|
if (VLSharedControllerSupport == self) {
|
||||||
VLSharedControllerSupport = nil;
|
VLSharedControllerSupport = nil;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// GameSirG8MFiRumble.h
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
@class GCController;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface GameSirG8MFiRumble : NSObject
|
||||||
|
|
||||||
|
- (BOOL)canHandleController:(GCController *)controller;
|
||||||
|
- (BOOL)isTargetController:(GCController *)controller;
|
||||||
|
- (void)setLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor;
|
||||||
|
- (void)stopAndClose;
|
||||||
|
- (void)invalidate;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
//
|
||||||
|
// GameSirG8MFiRumble.m
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "GameSirG8MFiRumble.h"
|
||||||
|
|
||||||
|
@import ExternalAccessory;
|
||||||
|
@import GameController;
|
||||||
|
@import UIKit;
|
||||||
|
|
||||||
|
static NSString *const GameSirG8MFiProtocol = @"com.xiaoji.M2boot";
|
||||||
|
static NSString *const GameSirManufacturer = @"GameSir";
|
||||||
|
static NSString *const GameSirG8MFiModel = @"G8+ MFi";
|
||||||
|
|
||||||
|
@interface GameSirG8MFiRumble () <NSStreamDelegate>
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation GameSirG8MFiRumble {
|
||||||
|
EASession *_session;
|
||||||
|
NSData *_inFlightPacket;
|
||||||
|
NSUInteger _inFlightOffset;
|
||||||
|
NSData *_queuedPacket;
|
||||||
|
NSData *_lastSentPacket;
|
||||||
|
BOOL _appActive;
|
||||||
|
BOOL _invalidated;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_appActive = UIApplication.sharedApplication.applicationState == UIApplicationStateActive;
|
||||||
|
[NSNotificationCenter.defaultCenter addObserver:self
|
||||||
|
selector:@selector(applicationWillResignActive:)
|
||||||
|
name:UIApplicationWillResignActiveNotification
|
||||||
|
object:nil];
|
||||||
|
[NSNotificationCenter.defaultCenter addObserver:self
|
||||||
|
selector:@selector(applicationDidBecomeActive:)
|
||||||
|
name:UIApplicationDidBecomeActiveNotification
|
||||||
|
object:nil];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||||
|
if (NSThread.isMainThread) {
|
||||||
|
[self writeStopPacketBestEffort];
|
||||||
|
}
|
||||||
|
[self closeSession];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||||
|
_appActive = NO;
|
||||||
|
[self stopAndCloseOnMainThread];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||||
|
_appActive = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isTargetController:(GCController *)controller {
|
||||||
|
NSString *vendorName = controller.vendorName;
|
||||||
|
if (vendorName.length == 0) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSStringCompareOptions options = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;
|
||||||
|
return [vendorName rangeOfString:GameSirManufacturer options:options].location != NSNotFound &&
|
||||||
|
[vendorName rangeOfString:@"G8+" options:options].location != NSNotFound && [vendorName rangeOfString:@"MFi" options:options].location != NSNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (EAAccessory *)connectedAccessory {
|
||||||
|
for (EAAccessory *accessory in EAAccessoryManager.sharedAccessoryManager.connectedAccessories) {
|
||||||
|
if (!accessory.connected || ![accessory.protocolStrings containsObject:GameSirG8MFiProtocol]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (accessory.manufacturer.length == 0 || accessory.modelNumber.length == 0 ||
|
||||||
|
[accessory.manufacturer caseInsensitiveCompare:GameSirManufacturer] != NSOrderedSame ||
|
||||||
|
[accessory.modelNumber caseInsensitiveCompare:GameSirG8MFiModel] != NSOrderedSame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return accessory;
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)canHandleController:(GCController *)controller {
|
||||||
|
return [self isTargetController:controller] && [self connectedAccessory] != nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (uint8_t)convertMotorAmplitude:(uint16_t)amplitude {
|
||||||
|
return (uint8_t)(((uint32_t)amplitude * 255u + 32767u) / 65535u);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)packetForLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor {
|
||||||
|
const uint8_t packet[] = {
|
||||||
|
0x04,
|
||||||
|
[GameSirG8MFiRumble convertMotorAmplitude:lowFrequencyMotor],
|
||||||
|
0x01,
|
||||||
|
[GameSirG8MFiRumble convertMotorAmplitude:highFrequencyMotor],
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
};
|
||||||
|
return [NSData dataWithBytes:packet length:sizeof(packet)];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)openSessionIfNeeded {
|
||||||
|
if (_session != nil) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
EAAccessory *accessory = [self connectedAccessory];
|
||||||
|
if (accessory == nil) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
EASession *session = [[EASession alloc] initWithAccessory:accessory forProtocol:GameSirG8MFiProtocol];
|
||||||
|
if (session == nil || session.inputStream == nil || session.outputStream == nil) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = session;
|
||||||
|
session.inputStream.delegate = self;
|
||||||
|
session.outputStream.delegate = self;
|
||||||
|
[session.inputStream scheduleInRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
||||||
|
[session.outputStream scheduleInRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
||||||
|
[session.inputStream open];
|
||||||
|
[session.outputStream open];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setLowFrequencyMotor:(uint16_t)lowFrequencyMotor highFrequencyMotor:(uint16_t)highFrequencyMotor {
|
||||||
|
@synchronized(self) {
|
||||||
|
if (_invalidated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *packet = [self packetForLowFrequencyMotor:lowFrequencyMotor highFrequencyMotor:highFrequencyMotor];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
@synchronized(self) {
|
||||||
|
if (self->_invalidated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!self->_appActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL duplicateQueuedPacket = [self->_queuedPacket isEqualToData:packet];
|
||||||
|
BOOL duplicateInFlightPacket = self->_queuedPacket == nil && [self->_inFlightPacket isEqualToData:packet];
|
||||||
|
BOOL duplicateSentPacket = self->_queuedPacket == nil && self->_inFlightPacket == nil && [self->_lastSentPacket isEqualToData:packet];
|
||||||
|
if (!duplicateQueuedPacket && !duplicateInFlightPacket && !duplicateSentPacket) {
|
||||||
|
self->_queuedPacket = packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([self openSessionIfNeeded]) {
|
||||||
|
[self flushOutput];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)flushOutput {
|
||||||
|
NSOutputStream *outputStream = _session.outputStream;
|
||||||
|
if (outputStream.streamStatus != NSStreamStatusOpen || !outputStream.hasSpaceAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (outputStream.hasSpaceAvailable) {
|
||||||
|
if (_inFlightPacket == nil) {
|
||||||
|
if (_queuedPacket == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_inFlightPacket = _queuedPacket;
|
||||||
|
_queuedPacket = nil;
|
||||||
|
_inFlightOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *bytes = _inFlightPacket.bytes;
|
||||||
|
NSUInteger remainingLength = _inFlightPacket.length - _inFlightOffset;
|
||||||
|
NSInteger written = [outputStream write:&bytes[_inFlightOffset] maxLength:remainingLength];
|
||||||
|
if (written <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_inFlightOffset += (NSUInteger)written;
|
||||||
|
if (_inFlightOffset == _inFlightPacket.length) {
|
||||||
|
_lastSentPacket = _inFlightPacket;
|
||||||
|
_inFlightPacket = nil;
|
||||||
|
_inFlightOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)writeStopPacketBestEffort {
|
||||||
|
NSOutputStream *outputStream = _session.outputStream;
|
||||||
|
if (outputStream.streamStatus != NSStreamStatusOpen || !outputStream.hasSpaceAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedPacket = nil;
|
||||||
|
[self flushOutput];
|
||||||
|
if (_inFlightPacket != nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSData *stopPacket = [self packetForLowFrequencyMotor:0 highFrequencyMotor:0];
|
||||||
|
NSUInteger offset = 0;
|
||||||
|
while (offset < stopPacket.length && outputStream.hasSpaceAvailable) {
|
||||||
|
const uint8_t *bytes = stopPacket.bytes;
|
||||||
|
NSInteger written = [outputStream write:&bytes[offset] maxLength:stopPacket.length - offset];
|
||||||
|
if (written <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += (NSUInteger)written;
|
||||||
|
}
|
||||||
|
if (offset == stopPacket.length) {
|
||||||
|
_lastSentPacket = stopPacket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)closeSession {
|
||||||
|
if (_session != nil) {
|
||||||
|
[_session.inputStream close];
|
||||||
|
[_session.outputStream close];
|
||||||
|
[_session.inputStream removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
||||||
|
[_session.outputStream removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSDefaultRunLoopMode];
|
||||||
|
_session.inputStream.delegate = nil;
|
||||||
|
_session.outputStream.delegate = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = nil;
|
||||||
|
_inFlightPacket = nil;
|
||||||
|
_inFlightOffset = 0;
|
||||||
|
_queuedPacket = nil;
|
||||||
|
_lastSentPacket = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopAndCloseOnMainThread {
|
||||||
|
[self writeStopPacketBestEffort];
|
||||||
|
[self closeSession];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopAndClose {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self stopAndCloseOnMainThread];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)invalidate {
|
||||||
|
@synchronized(self) {
|
||||||
|
_invalidated = YES;
|
||||||
|
}
|
||||||
|
[self stopAndClose];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
|
||||||
|
if (stream != _session.inputStream && stream != _session.outputStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (eventCode) {
|
||||||
|
case NSStreamEventHasBytesAvailable: {
|
||||||
|
uint8_t buffer[64];
|
||||||
|
while (_session.inputStream.hasBytesAvailable) {
|
||||||
|
if ([_session.inputStream read:buffer maxLength:sizeof(buffer)] <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NSStreamEventOpenCompleted:
|
||||||
|
case NSStreamEventHasSpaceAvailable:
|
||||||
|
if (stream == _session.outputStream) {
|
||||||
|
[self flushOutput];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NSStreamEventErrorOccurred:
|
||||||
|
case NSStreamEventEndEncountered:
|
||||||
|
[self writeStopPacketBestEffort];
|
||||||
|
[self closeSession];
|
||||||
|
break;
|
||||||
|
case NSStreamEventNone:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -116,6 +116,10 @@ VoidLink uses microphone to stream your voice to PC host. Microphone redirection
|
|||||||
<false/>
|
<false/>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UISupportedExternalAccessoryProtocols</key>
|
||||||
|
<array>
|
||||||
|
<string>com.xiaoji.M2boot</string>
|
||||||
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
|||||||
Reference in new issue
Block a user