Skip to content

Instantly share code, notes, and snippets.

@warpling
Created December 10, 2016 04:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save warpling/87fad3481f5dae67438ba4ba8a96fbd6 to your computer and use it in GitHub Desktop.
Save warpling/87fad3481f5dae67438ba4ba8a96fbd6 to your computer and use it in GitHub Desktop.
//
// MotionOrientation.h
//
// Originally based on code by Sangwon Park on 5/3/12.
// Copyright (c) 2012 tastyone@gmail.com. All rights reserved.
// Heavily modified by Ryan McLeod on 10/3/14
// Copyright (c) 2014 ryanmcleod@gmail.com. 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
//
// MotionOrientation.m
//
// Originally based on code by Sangwon Park on 5/3/12.
// Copyright (c) 2012 tastyone@gmail.com. All rights reserved.
// Heavily modified by Ryan McLeod on 10/3/14
// Copyright (c) 2014 ryanmcleod@gmail.com. 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