Created
December 10, 2016 04:21
-
-
Save warpling/87fad3481f5dae67438ba4ba8a96fbd6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MotionOrientation.h | |
// | |
// Originally based on code by Sangwon Park on 5/3/12. | |
// Copyright (c) 2012 [email protected]. All rights reserved. | |
// Heavily modified by Ryan McLeod on 10/3/14 | |
// Copyright (c) 2014 [email protected]. All rights reserved. | |
#import <CoreMotion/CoreMotion.h> | |
#import <CoreGraphics/CoreGraphics.h> | |
#import <UIKit/UIKit.h> | |
// TODO: extern or static? | |
extern NSString* const MotionOrientationChangedNotification; | |
extern NSString* const MotionOrientationInterfaceOrientationChangedNotification; | |
extern NSString* const kMotionOrientationKey; | |
// Knock Detection Struct | |
// Source: https://github.com/Headtalk/Knock/blob/15da0b68b2ae46f7a5b33e89e8528cd76f79e207/HTKnockDetector/HTKnockDetector.h | |
typedef struct{ | |
double alpha; | |
double Yi; | |
double Yim1; | |
double Xi; | |
double Xim1; | |
double delT; | |
double fc; //cutoff frequency | |
double minAccel; | |
NSTimeInterval minKnockSeparation; | |
NSTimeInterval lastKnock; | |
} HighPassFilter; | |
@class MotionOrientation; | |
@protocol MotionOrientationDelegate | |
@optional | |
- (void) deviceDidMoveWithOrientationAngle:(float)angle tiltMagnitude:(float)tiltMagnitude; | |
- (void) deviceDidMoveWithRoll:(CGFloat)roll pitch:(CGFloat)pitch yaw:(CGFloat)yaw; | |
- (void) deviceOrientationDidChangeTo:(UIDeviceOrientation)deviceOrientation; | |
- (void) knockDetectedAtTime:(NSTimeInterval)time; | |
@end | |
@interface MotionOrientation : NSObject { | |
HighPassFilter knockFilter; | |
} | |
@property (readonly) UIInterfaceOrientation interfaceOrientation; | |
@property (readonly) UIDeviceOrientation deviceOrientation; | |
@property (readonly) CGAffineTransform affineTransform; | |
@property (weak) id<MotionOrientationDelegate, NSObject> delegate; | |
@property float lastAngle; | |
+ (void) initialize; | |
+ (MotionOrientation *) sharedInstance; | |
- (void) activate; | |
- (void) deactivate; | |
- (void) motionUpdateWithMotionData:(CMDeviceMotion*)motionData error:(NSError *)error; | |
- (void) checkForOrientationChangeWithOrientationAngle:(float)angle tiltMagnitude:(float) magnitude; | |
+ (bool) angle:(float)angle WithinRangeOf:(float)tolerance TargetAngle:(float)targetAngle; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MotionOrientation.m | |
// | |
// Originally based on code by Sangwon Park on 5/3/12. | |
// Copyright (c) 2012 [email protected]. All rights reserved. | |
// Heavily modified by Ryan McLeod on 10/3/14 | |
// Copyright (c) 2014 [email protected]. All rights reserved. | |
#import "MotionOrientation.h" | |
#define MO_degreesToRadian(x) (M_PI * (x) / 180.0) | |
#define MO_radiansToDegrees(x) (180.0 * (x) / M_PI) | |
NSString* const MotionOrientationChangedNotification = @"MotionOrientationChangedNotification"; | |
NSString* const MotionOrientationInterfaceOrientationChangedNotification = @"MotionOrientationInterfaceOrientationChangedNotification"; | |
NSString* const kMotionOrientationKey = @"MotionOrientationKey"; | |
static float const deviceFaceUpTiltThreshold = 0.99; | |
static float const deviceFaceDownTiltThreshold = 0.95; | |
static float const deviceOnSideTiltThreshold = 0.34; | |
static float const deviceOnSideAngleThreshold = 20; | |
@interface MotionOrientation () | |
@property (strong) CMMotionManager* motionManager; | |
@property (strong) CMMotionManager* accellerationManager; | |
@property (strong) NSOperationQueue* operationQueue; | |
@end | |
@implementation MotionOrientation | |
struct { | |
BOOL deviceDidMoveWithOrientationAngle : 1; | |
BOOL deviceDidMoveWithRollYawPitch : 1; | |
BOOL deviceOrientationDidChangeTo : 1; | |
} delegateRespondsTo; | |
@synthesize delegate = _delegate; | |
@synthesize interfaceOrientation = _interfaceOrientation; | |
@synthesize deviceOrientation = _deviceOrientation; | |
@synthesize motionManager = _motionManager; | |
@synthesize operationQueue = _operationQueue; | |
+ (void)initialize | |
{ | |
[MotionOrientation sharedInstance]; | |
} | |
+ (MotionOrientation *)sharedInstance | |
{ | |
static MotionOrientation *sharedInstance = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sharedInstance = [[MotionOrientation alloc] init]; | |
}); | |
return sharedInstance; | |
} | |
- (void)_initialize | |
{ | |
self.operationQueue = [[NSOperationQueue alloc] init]; | |
self.motionManager = [[CMMotionManager alloc] init]; | |
self.motionManager.accelerometerUpdateInterval = 0.1; | |
[self tuneKnockAlgorithmToCutoffFrequency:15.0 minimumAcceleration:0.75f minimumKnockSeparation:0.1f]; | |
if (![self.motionManager isAccelerometerAvailable]) { | |
DDLogError(@"MotionOrientation - Accelerometer is NOT available"); | |
#if TARGET_IPHONE_SIMULATOR | |
[self affineTransform]; | |
#endif | |
return; | |
} | |
} | |
- (id)init | |
{ | |
self = [super init]; | |
if ( self ) { | |
[self _initialize]; | |
} | |
return self; | |
} | |
- (id<MotionOrientationDelegate, NSObject>) delegate { | |
return _delegate; | |
} | |
- (void) setDelegate:(id<MotionOrientationDelegate, NSObject>)delegate { | |
if (_delegate != delegate) { | |
_delegate = delegate; | |
delegateRespondsTo.deviceDidMoveWithOrientationAngle = [delegate respondsToSelector:@selector(deviceDidMoveWithOrientationAngle:tiltMagnitude:)]; | |
delegateRespondsTo.deviceOrientationDidChangeTo = [delegate respondsToSelector:@selector(deviceOrientationDidChangeTo:)]; | |
delegateRespondsTo.deviceDidMoveWithRollYawPitch = [delegate respondsToSelector:@selector(deviceDidMoveWithRoll:pitch:yaw:)]; | |
} | |
} | |
#pragma Delegate Use | |
- (void) activate { | |
[self.motionManager startDeviceMotionUpdatesToQueue:self.operationQueue withHandler:^(CMDeviceMotion *motion, NSError *error) { | |
[self motionUpdateWithMotionData:motion error:error]; | |
} | |
]; | |
} | |
- (void) deactivate { | |
[self.motionManager stopDeviceMotionUpdates]; | |
_deviceOrientation = UIDeviceOrientationUnknown; | |
_interfaceOrientation = UIInterfaceOrientationUnknown; | |
} | |
- (CGAffineTransform)affineTransform | |
{ | |
int rotationDegree = 0; | |
switch (self.interfaceOrientation) { | |
case UIInterfaceOrientationPortrait: | |
rotationDegree = 0; | |
break; | |
case UIInterfaceOrientationLandscapeLeft: | |
rotationDegree = 90; | |
break; | |
case UIInterfaceOrientationPortraitUpsideDown: | |
rotationDegree = 180; | |
break; | |
case UIInterfaceOrientationLandscapeRight: | |
rotationDegree = 270; | |
break; | |
default: | |
break; | |
} | |
return CGAffineTransformMakeRotation(MO_degreesToRadian(rotationDegree)); | |
} | |
- (void) motionUpdateWithMotionData:(CMDeviceMotion*)motionData error:(NSError *)error | |
{ | |
if ( error ) { | |
DDLogError(@"motionUpdateWithData Error: %@", error); | |
return; | |
} | |
CMAcceleration gravity = motionData.gravity; | |
// Use arctan2 to find the angle of gravity without respect to z and shift the range from [-π, π] to [0, 2π] | |
// then offset by π to match device orientation | |
float gravityAngle = fmodf(atan2f(gravity.x, gravity.y), 2*M_PI) + M_PI; | |
float gravityAngleMagnitude = -1.0 * gravity.z; | |
[self checkForOrientationChangeWithOrientationAngle:gravityAngle tiltMagnitude:gravityAngleMagnitude]; | |
[self processMotionForKnocks:motionData]; | |
CGFloat roll = motionData.attitude.roll; | |
CGFloat pitch = motionData.attitude.pitch; | |
CGFloat yaw = motionData.attitude.yaw; // what we want for spin challenge | |
dispatch_sync_main(^{ | |
[self sendDelegateDeviceAngle:gravityAngle tilt:gravityAngleMagnitude]; | |
[self sendDelegateDeviceRoll:roll pitch:pitch yaw:yaw]; | |
}); | |
} | |
- (void) sendDelegateDeviceAngle:(CGFloat)angle tilt:(CGFloat)tilt { | |
if (delegateRespondsTo.deviceDidMoveWithOrientationAngle) { | |
[self.delegate deviceDidMoveWithOrientationAngle:angle tiltMagnitude:tilt]; | |
} | |
} | |
- (void) sendDelegateDeviceRoll:(CGFloat)roll pitch:(CGFloat)pitch yaw:(CGFloat)yaw { | |
if (delegateRespondsTo.deviceDidMoveWithRollYawPitch) { | |
[self.delegate deviceDidMoveWithRoll:roll pitch:pitch yaw:yaw]; | |
} | |
} | |
- (void) checkForOrientationChangeWithOrientationAngle:(float)angle tiltMagnitude:(float) magnitude { | |
UIInterfaceOrientation newInterfaceOrientation = self.interfaceOrientation; | |
UIDeviceOrientation newDeviceOrientation = self.deviceOrientation; | |
float angleInDegrees = MO_radiansToDegrees(angle); | |
if (magnitude < 0 && fabs(magnitude) > deviceFaceDownTiltThreshold ) { | |
newDeviceOrientation = UIDeviceOrientationFaceDown; | |
} else if (magnitude > 0 && fabs(magnitude) > deviceFaceUpTiltThreshold){ | |
newDeviceOrientation = UIDeviceOrientationFaceUp; | |
} | |
else if(fabsf(magnitude) < deviceOnSideTiltThreshold) | |
{ | |
if([MotionOrientation angle:angleInDegrees WithinRangeOf:deviceOnSideAngleThreshold TargetAngle:0.0]) | |
{ | |
newInterfaceOrientation = UIInterfaceOrientationPortrait; | |
newDeviceOrientation = UIDeviceOrientationPortrait; | |
} | |
else if([MotionOrientation angle:angleInDegrees WithinRangeOf:deviceOnSideAngleThreshold TargetAngle:90.0]) | |
{ | |
newInterfaceOrientation = UIInterfaceOrientationLandscapeLeft; | |
newDeviceOrientation = UIDeviceOrientationLandscapeLeft; | |
} | |
else if([MotionOrientation angle:angleInDegrees WithinRangeOf:deviceOnSideAngleThreshold TargetAngle:180.0]) | |
{ | |
newInterfaceOrientation = UIInterfaceOrientationPortraitUpsideDown; | |
newDeviceOrientation = UIDeviceOrientationPortraitUpsideDown; | |
} | |
else if([MotionOrientation angle:angleInDegrees WithinRangeOf:deviceOnSideAngleThreshold TargetAngle:270.0]) | |
{ | |
newInterfaceOrientation = UIInterfaceOrientationLandscapeRight; | |
newDeviceOrientation = UIDeviceOrientationLandscapeRight; | |
} | |
} | |
else { | |
_deviceOrientation = UIDeviceOrientationUnknown; | |
_interfaceOrientation = UIInterfaceOrientationUnknown; | |
return; | |
} | |
BOOL deviceOrientationChanged = NO; | |
BOOL interfaceOrientationChanged = NO; | |
if (newDeviceOrientation != UIDeviceOrientationUnknown && newDeviceOrientation != self.deviceOrientation) { | |
deviceOrientationChanged = YES; | |
_deviceOrientation = newDeviceOrientation; | |
} | |
if (newInterfaceOrientation != UIInterfaceOrientationUnknown && newInterfaceOrientation != self.interfaceOrientation) { | |
interfaceOrientationChanged = YES; | |
_interfaceOrientation = newInterfaceOrientation; | |
} | |
// Post notifications | |
if ( deviceOrientationChanged ) { | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[[NSNotificationCenter defaultCenter] postNotificationName:MotionOrientationChangedNotification | |
object:nil | |
userInfo:@{@"DeviceOrientation": [NSNumber numberWithInt:self.deviceOrientation]}]; | |
}); | |
[self performSelectorOnMainThread:@selector(sendDelegateDeviceOrientation:) withObject:[NSNumber numberWithInt:self.deviceOrientation] waitUntilDone:NO]; | |
} | |
} | |
- (void) sendDelegateDeviceOrientation:(NSNumber*)deviceOrientation { | |
if (delegateRespondsTo.deviceOrientationDidChangeTo) { | |
[self.delegate deviceOrientationDidChangeTo:(UIDeviceOrientation)[deviceOrientation intValue]]; | |
} | |
} | |
+ (bool) angle:(float)angle WithinRangeOf:(float)tolerance TargetAngle:(float)targetAngle { | |
return (fabsf(targetAngle - angle) < tolerance); | |
} | |
#pragma mark - Knocking | |
// Source: https://github.com/Headtalk/Knock/blob/15da0b68b2ae46f7a5b33e89e8528cd76f79e207/HTKnockDetector/HTKnockDetector.m | |
- (void) tuneKnockAlgorithmToCutoffFrequency:(double)fc minimumAcceleration:(double)minAccel minimumKnockSeparation:(double)separation{ | |
double delT = self.motionManager.deviceMotionUpdateInterval; | |
knockFilter.delT = delT; | |
knockFilter.fc = fc; | |
double RC = 1.0/(2*M_PI*fc); | |
double alpha = RC/ (RC + delT); | |
knockFilter.alpha = alpha; | |
knockFilter.minAccel = minAccel; | |
knockFilter.minKnockSeparation = separation; | |
} | |
- (void) processMotionForKnocks:(CMDeviceMotion*)motion{ | |
double newZ = [motion userAcceleration].z; | |
knockFilter.Xim1 = knockFilter.Xi; | |
knockFilter.Xi = newZ; | |
knockFilter.Yim1 = knockFilter.Yi; | |
knockFilter.Yi = knockFilter.alpha*knockFilter.Yim1 + knockFilter.alpha*(knockFilter.Xi-knockFilter.Xim1); | |
if (fabs(knockFilter.Yi) > knockFilter.minAccel){ | |
if (fabs(knockFilter.lastKnock - motion.timestamp) > knockFilter.minKnockSeparation){ | |
knockFilter.lastKnock = motion.timestamp; | |
dispatch_async_main(^{ | |
if ([self.delegate respondsToSelector:@selector(knockDetectedAtTime:)]) { | |
[self.delegate knockDetectedAtTime:motion.timestamp]; | |
} | |
}); | |
} | |
} | |
} | |
#pragma mark - Simulator | |
// Simulator support | |
#if TARGET_IPHONE_SIMULATOR | |
- (void)simulatorInit | |
{ | |
// Simulator | |
DDLogVerbose(@"MotionOrientation - Simulator in use. Using UIDevice instead"); | |
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; | |
[[NSNotificationCenter defaultCenter] addObserver:self | |
selector:@selector(deviceOrientationChanged:) | |
name:UIDeviceOrientationDidChangeNotification | |
object:nil]; | |
} | |
- (void)deviceOrientationChanged:(NSNotification *)notification | |
{ | |
_deviceOrientation = [UIDevice currentDevice].orientation; | |
[[NSNotificationCenter defaultCenter] postNotificationName:MotionOrientationChangedNotification | |
object:nil | |
userInfo:@{kMotionOrientationKey: self}]; | |
} | |
- (void)dealloc | |
{ | |
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
#endif | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment