Chapter 6. Using Core Motion

The iPhone 4, latest generation iPod touch, and the iPad have a vibrational gyroscope in addition to an accelerometer and a magnetometer. The MicroElectroMechanical (MEMs) gyroscope inside the iPhone 4 and the 4th generation iPod touch is the AGD8 2032, nearly identical to an off-the-shelf STMicroelectronics L3G4200D device The iPad 2 uses an AGD8 2103 sensor, also from STMicroelectronics. These models operate by making use of a plate called a “proof mass” that oscillates when a drive signal is applied to capacitor plates. When the user rotates the phone, the proof mass is displaced in the X, Y and Z directions and an ASIC processor measures the capacitance change of the plates. The capacitance variation is used to detect the angular rate applied to the package.

An accelerometer provides measurement of forces in the X, Y and Z-axes but it cannot measure rotation. On the other hand, since a gyroscope is a rate of change device, you are able to measure the change in rotations around an axis. By using both sensors in combination you can measure the movement of the device in a six degrees-of-freedom inertial system, allowing you to use dead reckoning to find the physical location (and orientation of the device) relative to an initial starting position.

Warning

All inertial systems have an inherent drift, so dead reckoning should not be regarded as being stable over the long term.

Core Motion

The arrival of iOS 4 brought with it the new Core Motion framework; this new framework allows your application to receive motion data from both the accelerometer and (on the latest generation of devices) the gyroscope.

Note

There is no support for Core Motion in the iOS Simulator, therefore all testing of your Core Motion related code must be done on the device. The code in this chapter will only work on devices that have a gyroscope, see Chapter 1 for more information.

With the CMMotionManager class you can start receiving accelerometer, gyroscope, and combined device motion events at a regular interval, or you can poll them periodically:

CMMotionManager *motionManager = [[CMMotionManager alloc] init];
if (!motionManager.isDeviceMotionAvailable) {
    NSLog(@"Device supports motion capture.");
}

Remember to release the manager after you’re done with it:

 [motionManager release];

The CMMotionManager class offers both the raw accelerometer and gyroscope data separately as well a combined CMDeviceMotion object that encapsulates the processed device motion data from both the accelerometer and the gyroscope. With this combined motion measurement Core Motion provides highly accurate measurements of device attitude, the (unbiased) rotation rate of a device, the direction of gravity on a device, and the acceleration that the user is giving to a device.

Note

The rotation rate reported by the CMDeviceMotion object is different than that reported directly by the gyroscope. Even if the device is sitting flat on the table the gyro will not read zero. It will read some non-zero value that differs from device to device and over time due to changes in things like device temperature. Core Motion actively tracks and removes this bias from the gyro data.

Pulling Motion Data

The CMMotionManager class offers two approaches to obtaining motion data. The simplest way is to pull the motion data. Your application will start an instance of the manager class and periodically ask for measurements of the combined device motion:

[motionManager startDeviceMotionUpdates];
CMDeviceMotion *motion = motionManager.deviceMotion;

Although if you are only interested in the raw gyroscope data (or accelerometer data) you can also ask for those directly:

CMGyroData *gyro = motionManager.gyroData;
CMAccelerometerData *accel = motionManager.accelerometerData;

This is the most efficient method of obtaining motion data. However, if there isn’t a natural timer in your application—such as a periodic update of your main view—then you may need an additional timer to trigger your update requests. Remember to stop the updates and release the motion manager after you’re done with them:

[motionManager stopDeviceMotionUpdates];
[motionManager release];

Warning

Your application should create only a single instance of the CMMotionManager class. Multiple instances of this class can affect the rate at which an application receives data from the accelerometer and gyroscope.

Pushing Motion Data

Instead of using this simple pull methodology, you can specify an update interval and implement a block of code for handling the motion data. The manager class can then be asked to deliver updates using the NSOperationsQueue, which allows the handler to push the measurements to the application. For example:

motionManager.deviceMotionUpdateInterval = 1.0/60.0;
[motionManager startDeviceMotionUpdatesToQueue: queue withHandler: handler];

or similarly for the individual accelerometer and gyroscope data:

[motionManager startAccelerometerUpdatesToQueue:queue withHandler: handler];
[motionManager startGyroUpdatesToQueue:queue withHandler:handler];

With this second methodology you’ll get a continuous stream of motion data, but there is a large increased overhead associated with implementing it (see Table 6-1). Your application may not be able to keep up with the associated data rate especially if the device is in rapid motion.

Table 6-1. Example CPU usage for Core Motion push updates at 100 and 20Hz[a]
 

At 100Hz

At 20Hz

Total

Application

Total

Application

DeviceMotion

65%

20%

65%

10%

Accelerometer

50%

15%

46%

5%

Accel + Gyro

51%

10%

50%

5%

[a] Figures for an application running on an iPhone 4 running iOS 4.0 (Reproduced with permission. Credit: Jeffrey Powers, Occipital)

Using Core Motion’s combined CMDeviceMotion object, as opposed to accessing the raw CMAccelerometer or CMGyroData objects, consumes roughly 15% more total CPU regardless of the update rate. The good news is that is not because of the gyroscope itself; reading both the accelerometer and gyroscope directly is not noticeably slower than reading the accelerometer on its own.

Because of this associated CPU overheads push is really only recommended for data collection applications where the point of the application is to obtain the motion data itself. However if your application needs to be rapidly updated as to device motion you can do this easily:

CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1.0/60.0;

if (motionManager.deviceMotionAvailable ) {
   queue = [[NSOperationQueue currentQueue] retain];
  [motionManager startDeviceMotionUpdatesToQueue:queue 
                 withHandler:^ (CMDeviceMotion *motionData, NSError *error) {
                 
          CMAttitude *attitude = motionData.attitude;
          CMAcceleration gravity = motionData.gravity;
          CMAcceleration userAcceleration = motionData.userAcceleration;
          CMRotationRate rotate = motionData.rotationRate;
          // handle data here......
    }];
} else {
    [motionManager release];
}

If we were interested solely in the raw gyroscope data we could do the following:

CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.gyroUpdateInterval = 1.0/60.0;

if (motionManager.gyroAvailable) {
    queue = [[NSOperationQueue currentQueue] retain];
    [motionManager startGyroUpdatesToQueue:queue 
                   withHandler: ^ (CMGyroData *gyroData, NSError *error) {
                   
         CMRotationRate rotate = gyroData.rotationRate;
         NSLog(@"rotate x = %f, y = %f, z = %f", rotate.x, rotate.y, rotate.z);
          // handle rotation-rate data here......
    }];

} else {
    [motionManager release];
}

If we want both the raw and gyroscope and accelerometer readings outside of the CMDeviceMotion object, we could modify the above code as highlighted:

CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.gyroUpdateInterval = 1.0/60.0;
motionManager.accelerometerUpdateInterval = 1.0/60.0;

if (motionManager.gyroAvailable && motionManager.accelerometerAvailable) {
    queue = [[NSOperationQueue currentQueue] retain];
    [motionManager startGyroUpdatesToQueue:queue 
                   withHandler: ^ (CMGyroData *gyroData, NSError *error) {
          CMRotationRate rotate = gyroData.rotationRate;
         NSLog(@"rotate x = %f, y = %f, z = %f", rotate.x, rotate.y, rotate.z);
          // handle rotation-rate data here......
    }];
    [motionManager startAccelerometerUpdatesToQueue:queue 
                               withHandler: ^ (CMAccelerometerData *accelData, 
                                               NSError *error) {
         CMAcceleration accel = accelData.acceleration;
         NSLog( @"accel x = %f, y = %f, z = %f", accel.x, accel.y, accel.z);
          // handle acceleration data here......
    }];
} else {
    [motionManager release];
}

Accessing the Gyroscope

Let’s go ahead and implement a simple view-based application to illustrate how to use the gyroscope on its own before looking again at Core Motion and CMDeviceMotion. Open Xcode and start a new View-based Application iPhone project and name it “Gyroscope” when prompted for a filename.

Since we’ll be making use of the Core Motion framework, the first thing we need to do is add it to our new project. Click on the project file at the top of the Project navigator window on the right in Xcode, select the Target and click on the Build Phases tab, click on the Link with Libraries drop down and click on the + button to open the file pop-up window. Select CoreMotion.framework from the list of available frameworks and click the Add button.

Now go ahead and click on the GyroscopeViewController.xib file to open it in Interface Builder. As you did for the accelerometer back in Chapter 4, you’re going to build a simple interface to report the raw gyroscope readings. Go ahead and drag and drop three UIProgressView from the Object Library into the View window, then add two UILabel elements for each progress bar: one to hold the X, Y, or Z label and the other to hold the rotation measurements. After you do that, the view should look something a lot like Figure 6-1.

The Gyroscope UI
Figure 6-1. The Gyroscope UI

Go ahead and close the Utilities panel and click to open the Assistant Editor. Then Control-Click and drag from the three UIProgressView elements, and the three UILabel elements that will hold the measured values, to the GyroscopeViewController.h header file which should be displayed in the Assistant Editor on the right-hand side of the interface (see Figure 6-2).

Connecting the UI elements to your code in Interface Builder
Figure 6-2. Connecting the UI elements to your code in Interface Builder

This will automatically create and declare three UILabel and three UIProgressView variables as an IBOutlet. Since they aren’t going to be used outside the class, there isn’t much point in declaring them as class properties, which you’d do with a Control-click and drag from the element to outside the curly brace. After doing this, the code should look like this:

#import <UIKit/UIKit.h>

@interface GyroscopeViewController : UIViewController {

    IBOutlet UIProgressView *xBar;
    IBOutlet UIProgressView *yBar;
    IBOutlet UIProgressView *zBar;

    IBOutlet UILabel *xLabel;
    IBOutlet UILabel *yLabel;
    IBOutlet UILabel *zLabel;
}

@end

Close the Assistant Editor, return to the Standard Editor and click on the GyroscopeViewController.h interface file. Go ahead and import the Core Motion header file, and declare a CMMotionManager and NSOperationQueue instance variables. Here’s how the should look when you are done:

#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>

@interface GyroscopeViewController : UIViewController {

    IBOutlet UIProgressView *xBar;
    IBOutlet UIProgressView *yBar;
    IBOutlet UIProgressView *zBar;

    IBOutlet UILabel *xLabel;
    IBOutlet UILabel *yLabel;
    IBOutlet UILabel *zLabel;

    CMMotionManager *motionManager;
    NSOperationQueue *queue;
}

@end

Make sure you’ve saved your changes and click on the corresponding GyroscopeViewController.m implementation file to open it in the Xcode editor. You don’t actually have to do very much here, as Interface Builder handled most of the heavy lifting with respect to the UI, you just need to go ahead an implement the guts of the application to monitor the gyroscope updates:

@implementation GyroscopeViewController

- (void)dealloc {
    [xBar release];
    [yBar release];
    [zBar release];
    [xLabel release];
    [yLabel release];
    [zLabel release];
    [queue release];
    [super dealloc];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle

- (void)viewDidLoad {
    [super viewDidLoad];
    motionManager = [[CMMotionManager alloc] init];
    motionManager.gyroUpdateInterval = 1.0/2.0; // Update every 1/2 second.

    if (motionManager.gyroAvailable) {
        NSLog(@"Gyroscope avaliable");
        queue = [[NSOperationQueue currentQueue] retain];
        [motionManager startGyroUpdatesToQueue:queue
            withHandler: ^ (CMGyroData *gyroData,
                            NSError *error) {
            CMRotationRate rotate = gyroData.rotationRate;
            xLabel.text = [NSString stringWithFormat:@"%f", rotate.x];
            xBar.progress = ABS(rotate.x);

            yLabel.text = [NSString stringWithFormat:@"%f", rotate.y];
            yBar.progress = ABS(rotate.y);

            zLabel.text = [NSString stringWithFormat:@"%f", rotate.z];
            zBar.progress = ABS(rotate.z);

        }];

    } else {
        NSLog(@"Gyroscope not available");
        [motionManager release];
    }
}

- (void)viewDidUnload {
    [motionManager stopGyroUpdates];
    [motionManager release];

    [xBar release];
    xBar = nil;
    [yBar release];
    yBar = nil;
    [zBar release];
    zBar = nil;
    [xLabel release];
    xLabel = nil;
    [yLabel release];
    yLabel = nil;
    [zLabel release];
    zLabel = nil;
    [super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:
      (UIInterfaceOrientation)interfaceOrientation {
      
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

@end

The CMRotationRate data structure provides the rate of rotations around X-, Y-, and Z-axes in units of radians per second. When inspecting the structure remember the right-hand rule to determine the direction of positive rotation. With your thumb in the positive direction on an axis, your fingers curl will give the positive rotation direction around that axis (see Figure 6-3). A negative rotation goes away from the tips of those fingers.

The iPhone gyroscope axes
Figure 6-3. The iPhone gyroscope axes

Let’s test that: click on the Run button in the Xcode toolbar to build and deploy the application to your device (remember you can’t test this code in the iOS Simulator). If all goes well you should see something much like Figure 6-4 as you roll the device around the Y-axis.

Measuring rotation on the iPhone 4 whilst rolling it around the Y-axis
Figure 6-4. Measuring rotation on the iPhone 4 whilst rolling it around the Y-axis

Warning

As mentioned before the measurement of rotation rate encapsulated by a CMGyroData object is biased by various factors. You can obtain a much more accurate (unbiased) measurement by accessing the rotationRate property of CMDeviceMotion if that is needed by your application.

Measuring Device Motion

Let’s go ahead and build a similar application to the one above, but this time reporting the data exposed by the CMDeviceMotion object:

CMAttitude *attitude = motionData.attitude;
CMAcceleration gravity = motionData.gravity;
CMAcceleration userAcceleration = motionData.userAcceleration;
CMRotationRate rotate = motionData.rotationRate;

Open Xcode and start a new iPhone project, select a View-based Application template, and name the project “Motion” when prompted for a filename. As before, import the Core Motion framework into the project and then click on the MotionViewController.xib file to open it in Interface Builder, and then proceed to drag-and-drop UIProgressBar and UILabel elements into your View in a similar manner as you did for the Gyroscope application earlier in the chapter. You’ll need labels for the yaw, pitch and roll values, along with progress bars and labels for the user acceleration, gravity and rotation values.

Once you’ve done this, go ahead and connect the various bars and labels as IBOutlet using the Assistant Editor into the MotionViewController.h interface file as in Figure 6-5.

The Motion application UI with the IBOutlet connected in Interface Builder to the MotionViewController.h interface file
Figure 6-5. The Motion application UI with the IBOutlet connected in Interface Builder to the MotionViewController.h interface file

Once you’ve done this, save your changes and open up the MotionViewController.h interface file in the Standard Editor. Go ahead and import the Core Motion framework:

#import <CoreMotion/CoreMotion.h>

For this example you’re going to pull the device motion updates rather than push them using the NSOperationQueue and a handler block. Add the following instance variables to the view controller class:

CMMotionManager *motionManager;
NSTimer *timer;

Then in the corresponding MotionViewController.m implementation file, modify the viewDidLoad method as follows:

- (void)viewDidLoad {
    [super viewDidLoad];

    motionManager = [[CMMotionManager alloc] init];
    motionManager.deviceMotionUpdateInterval =  1.0 / 10.0;
    [motionManager startDeviceMotionUpdates];
    if (motionManager.deviceMotionAvailable ) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.2f
                                                 target:self
                                               selector:@selector(updateView:)
                                               userInfo:nil
                                                repeats:YES];
    } else {
        [motionManager stopDeviceMotionUpdates];
        [motionManager release];
    }
}

This will start the motion manager and begin polling for device motion updates. Add the following lines to the viewDidUnload method to corresponding stop the timer and updates:

    [timer invalidate];
    [motionManager stopDeviceMotionUpdates];
    [motionManager release];

Once you have done this you should go ahead and implement the updateView: method that will be called by the NSTimer object:

-(void) updateView:(NSTimer *)timer  {

    CMDeviceMotion *motionData = motionManager.deviceMotion;

    CMAttitude *attitude = motionData.attitude;
    CMAcceleration gravity = motionData.gravity;
    CMAcceleration userAcceleration = motionData.userAcceleration;
    CMRotationRate rotate = motionData.rotationRate;


    yawLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.yaw];
    pitchLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.pitch];
    rollLabel.text = [NSString stringWithFormat:@"%2.2f", attitude.roll];

    accelIndicatorX.progress = ABS(userAcceleration.x);
    accelIndicatorY.progress = ABS(userAcceleration.y);
    accelIndicatorZ.progress = ABS(userAcceleration.z);
    accelLabelX.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.x];
    accelLabelY.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.y];
    accelLabelZ.text = [NSString stringWithFormat:@"%2.2f",userAcceleration.z];

    gravityIndicatorX.progress = ABS(gravity.x);
    gravityIndicatorY.progress = ABS(gravity.y);
    gravityIndicatorZ.progress = ABS(gravity.z);
    gravityLabelX.text = [NSString stringWithFormat:@"%2.2f",gravity.x];
    gravityLabelY.text = [NSString stringWithFormat:@"%2.2f",gravity.y];
    gravityLabelZ.text = [NSString stringWithFormat:@"%2.2f",gravity.z];

    rotIndicatorX.progress = ABS(rotate.x);
    rotIndicatorY.progress = ABS(rotate.y);
    rotIndicatorZ.progress = ABS(rotate.z);
    rotLabelX.text = [NSString stringWithFormat:@"%2.2f",rotate.x];
    rotLabelY.text = [NSString stringWithFormat:@"%2.2f",rotate.y];
    rotLabelZ.text = [NSString stringWithFormat:@"%2.2f",rotate.z];

}

Save your changes and hit the Run button in the Xcode toolbar to build and deploy the application to your device. If all goes well you should see something much like Figure 6-6.

The Motion application running on an iPhone 4 sitting flat on the desk, with gravity in the negative Z-direction without any rotation or user acceleration
Figure 6-6. The Motion application running on an iPhone 4 sitting flat on the desk, with gravity in the negative Z-direction without any rotation or user acceleration

Comparing Device Motion with the Accelerometer

At this stage we can illustrate the difference between gravity and user-contributed acceleration values reported by Core Motion to the raw acceleration values reported by the UIAccelerometer, discussed back in Chapter 4.

Re-open the MotionViewController.xib file in Interface Builder and add another section to the UI, which will report the raw readings from the UIAccelerometer object. Go ahead and connect these three bars to IBOutlet instance variables in the MotionViewController.h interface file using the Assistant Editor as before, see Figure 6-7.

The additional UIProgressView and UILabel elements to report the raw UIAccelerometer readings to the user
Figure 6-7. The additional UIProgressView and UILabel elements to report the raw UIAccelerometer readings to the user

As you can see from Figure 6-7, I’ve changed the UIProgressView style from “Default” to “Bar” using the Attributes inspector in the Utility pane. This will help differentiate this section—data reported from the UIAccelerometer—from the other sections whose values are reported by the CMMotionManager.

Once that is done, close the Assistant Editor and open the MotionViewController.h interface file using the Standard Editor. Go ahead and declare the view controller as a UIAccelerometerDelegate and add a UIAccelerometer instance variable as shown here:

@interface MotionViewController : UIViewController <UIAccelerometerDelegate> {

    ...

    UIAccelerometer *accelerometer;
}

@end

before opening the corresponding implementation file. In the viewDidLoad method, add the following code to initialize the UIAccelerometer object. You should see Chapter 4 for more details on the UIAccelerometer class and associated methods:

- (void)viewDidLoad {
    [super viewDidLoad];

    motionManager = [[CMMotionManager alloc] init];
    motionManager.deviceMotionUpdateInterval =  1.0 / 10.0;
    [motionManager startDeviceMotionUpdates];
    if (motionManager.deviceMotionAvailable ) {
        timer = [NSTimer scheduledTimerWithTimeInterval:0.2f 
                                                 target:self 
                                               selector:@selector(updateView:)
                                               userInfo:nil 
                                                repeats:YES];
    } else {
        [motionManager stopDeviceMotionUpdates];
        [motionManager release];
    }

    accelerometer = [UIAccelerometer sharedAccelerometer];
    accelerometer.updateInterval = 0.2f;
    accelerometer.delegate = self;
}

After doing this all you need to do is add the accelerometer:didAccelerate: delegate method:

- (void)accelerometer:(UIAccelerometer *)meter  
      didAccelerate:(UIAcceleration *)acceleration {
      
    rawAccelLabelX.text = 
      [NSString stringWithFormat:@"%2.2f", acceleration.x];
    rawAccelIndicatorX.progress = ABS(acceleration.x);

    rawAccelLabelY.text = 
      [NSString stringWithFormat:@"%2.2f", acceleration.y];
    rawAccelIndicatorY.progress = ABS(acceleration.y);

    rawAccelLabelZ.text = 
      [NSString stringWithFormat:@"%2.2f", acceleration.z];
    rawAccelIndicatorZ.progress = ABS(acceleration.z);
}

Click on the Run button in the Xcode toolbar to build and deploy the application to the device as before. If all goes well you should see something much like Figure 6-8.

The Motion application running on an iPhone 4 sitting flat on the desk
Figure 6-8. The Motion application running on an iPhone 4 sitting flat on the desk

Move the device around and you can see how the raw accelerometer values and the derived gravity and user acceleration values correspond to each other.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset