How is it possible to detect the rotation of an iPhone that lies on a table like the compass but is showing a more accurate rotation? I tried to use the compass with the magnetic heading of the iPhone but it appears to be quite unreliable and jumps unexpectedly. The gyroscope can be used but the original reference point drifts with the gyroscope over time. This example combines the compass and the gyroscope using the compass as a reference as long as it is stable and using the fast update of the gyroscope between the times the compass is unstable.
In the app below there are 3 rotating graphics:
- Yellow is magnetic heading.
- Blue is a compass offset (always following the magnetic heading with a decided offset)
- Black is the gyroscope (reset every time the compass is stable)
The application uses the CLLocationManager to access the magnetic heading of the compass and CMMotionManager to access the gyroscope. The values I use are newHeading.magneticHeading and motion.attitude.yaw. The magnetic heading gives a value of 360 degrees. The yaw value gives a value between -180 and 180.
Compass from Location Manager
First we initialize the location manager. This will only work on the device and not in the simulator.
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.delegate=self;
if([CLLocationManager headingAvailable] == YES){
NSLog(@"Heading is available");
} else {
NSLog(@"Heading isn’t available");
}
[locationManager startUpdatingHeading];
As shown in the code above we delegate the listener to RotationViewController. The following code is needed to listen to updates for the compass:
#import <math.h>
#import <CoreMotion/CoreMotion.h> // For the gyroscope
#import <CoreLocation/CoreLocation.h> // For the compass
@interface RotationViewController : UIViewController <CLLocationManagerDelegate> {
Yaw data from gyroscope
Next step is to listen to updates from the gyroscope. We do that by listening to motionManager’s CMAttitude updates. We use the yaw which is retrieved in radians and we convert it to degrees.
motionManager.deviceMotionUpdateInterval = 1.0/60.0;
opQ = [[NSOperationQueuecurrentQueue] retain];
if(motionManager.isDeviceMotionAvailable) {
motionHandler = ^ (CMDeviceMotion *motion, NSError *error) {
CMAttitude *currentAttitude = motion.attitude;
float yawValue = currentAttitude.yaw;
float yawDegrees = CC_RADIANS_TO_DEGREES(yawValue);
};
} else {
[motionManager release];
}
[motionManagerstartDeviceMotionUpdatesToQueue:opQwithHandler:motionHandler];
We now have the compass and the gyroscope. In this example I wanted to offset the magnetic heading so it always points at a certain direction. I decide on that direction when I press the “Calibrate”-button I set my offset from the magnetic heading. updatedHeading is the latest magnetic heading I got from the locationManager. northOffset becomes my reference to where I want the gyroscope to always origin from.
{
northOffest = updatedHeading - 0;
}
Compensating for compass inaccuracies
Now that we have the northOffset we want to use it together with the gyroscope. Since the compass is jumping sometimes we want to only use the compass value when it is stable. A timer is created with the updater method that checks if the value of the magnetic heading has changed. The interval is called every other second. If the magnetic heading hasn’t changed from last time it is considered a stable value. The stable value is added to newCompassTarget which is use for the gyroscope to get a new reference.
{
// Om inte compassen rört sig på ett tag kalibrera gyron efter det
if(updatedHeading == oldHeading) {
NSLog(@"Update gyro");
newCompassTarget = (0 - updatedHeading) + northOffest;
offsetG = currentYaw;
updateCompass = 1;
} else {
updateCompass = 0;
}
oldHeading = updatedHeading;
}
newCompassTarget is used in the code below so that the gyroscope always strive to go to the new reference of the compass but with the offset we use in the variable offsetG which is the difference between where the gyroscope was with the old and compared to the new heading.
CMAttitude *currentAttitude = motion.attitude;
float yawValue = currentAttitude.yaw;
float yawDegrees = CC_RADIANS_TO_DEGREES(yawValue);
currentYaw = yawDegrees;
yawDegrees = newCompassTarget + (yawDegrees - offsetG);
if(yawDegrees < 0) {
yawDegrees = yawDegrees + 360;
}
compassDif.text = [NSString stringWithFormat:@"Gyro: %f",yawDegrees];
float gyroDegrees = (yawDegrees*radianConst);
if(updateCompass) {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[rotateImg setTransform:CGAffineTransformMakeRotation(gyroDegrees)];
[UIView commitAnimations];
updateCompass = 0;
} else {
rotateImg.transform = CGAffineTransformMakeRotation(gyroDegrees);
}
};
Download project source code here.














Lars Mahnkopf 11:37 on September 23, 2011 Permalink
Hi,
one question about the code above: the radianConst just converts degrees into radians or am i wrong? when will you post your example code? ( I can´t wait to see it
)
best regards
Lars
HC 18:49 on September 26, 2011 Permalink
is the source code available for this project yet?
Michael 08:19 on September 28, 2011 Permalink
when will you post your source code?
Michael.
Ellen 06:33 on September 29, 2011 Permalink
The source code will be posted here in a day or 2.
Ellen 06:34 on September 29, 2011 Permalink
@Lars Here is the conversion for radianConst:
radianConst M_PI/180.0
Jack 07:48 on September 29, 2011 Permalink
Dear Ellen:
There are 2 questions
1. Where is the CC_RADIANS_TO_DEGREES function?
2. opQ = [[NSOperationQueuecurrentQueue] retain];
is NSOperationQueuecurrentQueue the normal object in iOS ?
Waiting for your whole project too… ^^
Thanks,
Jack
Ellen 08:02 on September 29, 2011 Permalink
The project is available for download in the post now.
Jack 08:20 on September 29, 2011 Permalink
Thanks~ Ellen~
Michael 09:34 on September 30, 2011 Permalink
Thanks
HC 15:19 on October 3, 2011 Permalink
quick note:
this has been causing a crash for me on a 3G device
} else {
NSLog(@”No Device Motion on device.”);
[motionManager release];
}
when I remove the motionManager release it works fine.
thanks.
Ellen 13:25 on October 4, 2011 Permalink
@HC The motionManager will only work on iPhone 4 since 3G and 3GS doesn’t have the gyroscope.
Joakim 16:02 on January 24, 2012 Permalink
Thanks for great demo! Have used your method in my work. Will just point out that it looks like the same function is integreated in IOS 5. I have not tested it myself.
“CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
Describes the same reference frame as CMAttitudeReferenceFrameXArbitraryZVertical except that the magnetometer, when available and calibrated, is used to improve long-term yaw accuracy. Using this constant instead of CMAttitudeReferenceFrameXArbitraryZVertical results in increased CPU usage.
Available in iOS 5.0 and later.
Declared in CMAttitude.h.”
Dilip 08:31 on October 19, 2012 Permalink
Excellent Tutorial.. Thanks for sharing with us.. I ill look in to it and update if I need any help. Thank you.
sahaninoo 10:15 on October 19, 2012 Permalink
i didn’t yet used your demo, but it will helps me a lot.
thanks !