Chapter 5. Using the Magnetometer

The magnetometer is a magnetoresistive permalloy sensor found in the iPhone 3GS, iPhone 4 and iPad 2, in addition to the accelerometer. The iPhone 3GS uses the AN-203 integrated circuit produced by Honeywell, while the iPhone 4 and iPad 2 make use of the newer AKM8975 produced by AKM Semiconductor. The sensor is located towards the top right hand corner of the device, and measures fields within a ±2 gauss (200 microtesla) range, and is sensitive to magnetic fields of less than 100 microgauss (0.01 microtesla).

Note

The Earth’s magnetic field is roughly 0.6 gauss (60 microtesla). The field around a rare earth magnet can be 14,000 gauss or more.

The magnetometer measures the strength of the magnetic field surrounding the device. In the absence of any strong local fields, these measurements will be of the ambient magnetic field of the Earth, allowing the device to determine its “heading” with respect to the geomagnetic North Pole and act as a digital compass. The geomagnetic heading and true heading relative to the geographical North Pole can vary widely, by several tens of degrees depending on your location.

About the Magnetometer

Combining the heading (yaw) information (see Figure 5-1) returned by this device with the roll and pitch information returned by the accelerometer will let you determine the true orientation of the device in real time.

Using the magnetometer (a.k.a. the digital compass) in the iPhone 3GS you can determine the heading (yaw) of the device
Figure 5-1. Using the magnetometer (a.k.a. the digital compass) in the iPhone 3GS you can determine the heading (yaw) of the device

As well as reporting the current location, the CLLocationManager class can, in the case where the hardware supports it, report the current heading of the device. If location updates are also enabled, the location manager returns both true heading and magnetic heading values. If location updates are not enabled, the location manager returns only the magnetic heading value.

Note

Magnetic heading updates are available even if the user has switched off location updates in the Settings application. Additionally, users are not prompted to give permission to use heading data, as it is assumed that magnetic heading information cannot compromise user privacy. On an enabled device the magnetic heading data should therefore always be available to your application.

As mentioned previously, the magnetometer readings will be affected by local magnetic fields, so the CLLocationManager may attempt to calibrate its heading readings by displaying a heading calibration panel (see Figure 5-2) before it starts to issue update messages.

The Heading Calibration Panel
Figure 5-2. The Heading Calibration Panel

However, before it does so it will call the locationManagerShouldDisplayHeadingCalibration: delegate method:

- (BOOL)locationManagerShouldDisplayHeadingCalibration:
      (CLLocationManager *)manager {
      return YES;
}

If you return YES from this method, the CLLocationManager will pop up the calibration panel on top of the current window. The calibration panel prompts the user to move the device in a figure-eight pattern so that Core Location can distinguish between the Earth’s magnetic field and any local magnetic fields. The panel will remain visible until calibration is complete or until you dismiss it by calling the dismissHeadingCalibrationDisplay: method in the CLLocationManager class.

Writing a Magnetometer Application

Let’s go ahead and implement a simple view-based application to illustrate how to use the magnetometer. Open Xcode and start a new iPhone project, select a View-based Application template, and name the project “Compass” when prompted for a filename.

Since you’ll be making use of the Core Location framework, the first thing you need to do is add it to our new project. Click on the Compass project file in 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 CoreLocation.framework from the list of available frameworks and click the Add button.

You’re going to build an application that will act as a compass, so you’re going to need an image of an arrow to act as the compass needle. Download or draw in the graphics package of your choice, an image of an arrow pointing upwards on a transparent background. Save or convert it to, a PNG format file. Drag-and-drop this into the Xcode Project, remembering to tick the “Copy items into destination group’s folder (if needed)” check box in the pop up dialog that appears when you drop the files into Xcode.

Click on CompassViewController.xib file to open it in Interface Builder. Drag and drop a UIImageView from the Object Library into the View, positioning it roughly in the center of your window, resizing the bounding box to be a square, as in Figure 5-3. In the Attributes inspector of the Utilities pane set the View mode to be “Aspect Fit”, uncheck the “Opaque” checkbox in the Drawing section, and select the arrow image that you added to your project in the Image drop down.

Next drag-and-drop four UILabel elements from the Object Library into the View, position the four labels as in Figure 5-3, and change the text in the left most two to read “Magnetic Heading:” and “True Heading:”.

Close the Utility pane and switch from the Standard to the Assistant Editor. Control-Click and drag from the two right most UILabel elements to the assistant editor to create a magneticHeadingLabel and trueHeadingLabel outlet, and then again for the UIImageView to create an arrowImage outlet, see Figure 5-3.

Connecting the outlets to the UI elements in Interface Builder
Figure 5-3. Connecting the outlets to the UI elements in Interface Builder

Then click on the CompassViewController.h interface file and go ahead and declare the class as a CLLocationManagerDelegate, remembering to import the CoreLocation.h header file. After doing so the interface should look like this:

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

@interface CompassViewController : UIViewController 
      <CLLocationManagerDelegate> {

    IBOutlet UIImageView *arrowImage;
    IBOutlet UILabel *magneticHeadingLabel;
    IBOutlet UILabel *trueHeadingLabel;
}

@end

Save your changes, and click on the corresponding CompassViewController.m implementation file. Uncomment the viewDidLoad method and the following code to the implementation. This will create an instance of the CLLocationManager class, and will send both location and heading update messages to the designated delegate class:

- (void)viewDidLoad {
    [super viewDidLoad];
    CLLocationManager *locationManager = [[CLLocationManager alloc] init];
    locationManager.delegate = self;
    if( [CLLocationManager locationServicesEnabled] &&
        [CLLocationManager headingAvailable]) {1
          [locationManager startUpdatingLocation];
          [locationManager startUpdatingHeading];
    } else {
          NSLog(@"Can't report heading");
    }
}
1

It is more important to check whether heading information is available than it is to check whether location services are available. The availability of heading information is restricted to the latest generation of devices.

You can (optionally) filter the heading update messages using an angular filter. Changes in heading of less than this amount will not generate an update message to the delegate, for example:

locationManager.headingFilter = 5;  // 5 degrees

The default value of this property is kCLHeadingFilterNone. You should use this value if you want to be notified of all heading updates. In this example, leave the filter set to the default value. However if you want to filter messages from Core Location this way, add the above line to your viewDidLoad method inside the if-block:

    if( [CLLocationManager locationServicesEnabled] &&
        [CLLocationManager headingAvailable]) {
          [locationManager startUpdatingLocation];
          [locationManager startUpdatingHeading];
         locationManager.headingFilter = 5;  // 5 degrees
    } else {
          ... code ...
    }

The CLLocationManagerDelegate protocol calls the locationManager:didUpdateHeading: delegate method when the heading is updated. You’re going to use this method to update the user interface. Add the following code to your view controller:

- (void)locationManager:(CLLocationManager*)manager
      didUpdateHeading:(CLHeading*)newHeading {

    if (newHeading.headingAccuracy > 0) {
          float magneticHeading = newHeading.magneticHeading;
          float trueHeading = newHeading.trueHeading;1

          magneticHeadingLabel.text = 
            [NSString stringWithFormat:@"%f", magneticHeading];
          trueHeadingLabel.text = 
            [NSString stringWithFormat:@"%f", trueHeading];

          float heading = −1.0f * M_PI * newHeading.magneticHeading / 180.0f;
          arrowImage.transform = CGAffineTransformMakeRotation(heading);
    }
}
1

If location updates are also enabled, the location manager returns both true heading and magnetic heading values. If location updates are not enabled, or the location of the device is not yet known, the location manager returns only the magnetic heading value and the value returned by this call will be −1.

Save your changes, then click on the Run button in the Xcode toolbar to deploy your new application to your device. If you hold the device in “Face Up” or “Portrait” mode you should see something very similar to Figure 5-4 below.

The Compass application running on the iPhone 3GS
Figure 5-4. The Compass application running on the iPhone 3GS

As it stands our application has a critical flaw. If the user orientates the device into Landscape Mode, the reported headings will be incorrect, or at least look incorrect to the user.

Determining the Heading in Landscape Mode

The magnetic and true headings are correct when the iPhone device is held like a traditional compass, in portrait mode, if the user rotates the device, the heading readings will still be in the original frame of reference. Even though the user has not changed the direction they are facing the heading values reported by the device will have changed. You’re going to have to correct for orientation before reporting headings back to the user, see Figure 5-5.

The “real” heading of the user when they are holding the device in Landscape mode is the reported heading + 90 degrees
Figure 5-5. The “real” heading of the user when they are holding the device in Landscape mode is the reported heading + 90 degrees

In the Project navigator, click on the CompassViewController.xib file to open it in Interface Builder, then drag-and-drop another UILabel from the Object Library in the Utility pane into the View window. Use the Assistant Editor connect the label to a new outlet in the CompassViewController.h interface file, as in Figure 5-6.

Connecting the orientation label in Interface Builder
Figure 5-6. Connecting the orientation label in Interface Builder

After doing so, the interface file should look as below:

@interface CompassViewController : 
      UIViewController <CLLocationManagerDelegate> {

    IBOutlet UILabel *trueHeadingLabel;
    IBOutlet UILabel *magneticHeadingLabel;
    IBOutlet UILabel *orientationLabel;
    IBOutlet UIImageView *arrowImage;

}

We’re going to use this to report the current device orientation as we did in the Accelerometer application in Chapter 4.

Close the Assistant Editor and reopen the CompassViewController.h interface file in the Standard Editor. Go ahead and add the following convenience methods to the class definition:

- (float)magneticHeading:(float)heading 
      fromOrientation:(UIDeviceOrientation) orientation;
- (float)trueHeading:(float)heading 
      fromOrientation:(UIDeviceOrientation) orientation;
- (NSString *)stringFromOrientation:(UIDeviceOrientation) orientation;

Save your changes, and open the CompassViewController.m implementation file. Since the CLHeading object is read only and you can’t modify it directly, you’l need to add the following method to correct the magnetic heading for the device orientation:

- (float)magneticHeading:(float)heading 
      fromOrientation:(UIDeviceOrientation) orientation {

    float realHeading = heading;
    switch (orientation) {1
          case UIDeviceOrientationPortrait:
               break;
          case UIDeviceOrientationPortraitUpsideDown:
               realHeading = realHeading + 180.0f;
               break;
          case UIDeviceOrientationLandscapeLeft:
               realHeading = realHeading + 90.0f;
               break;
          case UIDeviceOrientationLandscapeRight:
               realHeading = realHeading - 90.0f;
               break;
          default:
               break;
    }
    while ( realHeading > 360.0f ) {
          realHeading = realHeading - 360;
    }
    return realHeading;
}
1

The UIDeviceOrientationFaceUp and UIDeviceOrientationFaceDown orientation cases are undefined and the user is presumed to be holding the device in UIDeviceOrientationPortrait mode.

You will also need to add a corresponding method to correct the true heading:

- (float)trueHeading:(float)heading 
      fromOrientation:(UIDeviceOrientation) orientation {

    float realHeading = heading;
    switch (orientation) {
          case UIDeviceOrientationPortrait:
               break;
          case UIDeviceOrientationPortraitUpsideDown:
               realHeading = realHeading + 180.0f;
               break;
          case UIDeviceOrientationLandscapeLeft:
               realHeading = realHeading + 90.0f;
               break;
          case UIDeviceOrientationLandscapeRight:
               realHeading = realHeading - 90.0f;
               break;
          default:
               break;
    }
    while ( realHeading > 360.0f ) {
          realHeading = realHeading - 360;
    }
    return realHeading;
}

Finally, add the stringFromOrientation: method from the previous section Chapter 5. We’ll use this to update the orientationLabel outlet:

- (NSString *)stringFromOrientation:(UIDeviceOrientation) orientation {

    NSString *orientationString;
    switch (orientation) {
          case UIDeviceOrientationPortrait:
               orientationString =  @"Portrait";
               break;
          case UIDeviceOrientationPortraitUpsideDown:
               orientationString =  @"Portrait Upside Down";
               break;
          case UIDeviceOrientationLandscapeLeft:
               orientationString =  @"Landscape Left";
               break;
          case UIDeviceOrientationLandscapeRight:
               orientationString =  @"Landscape Right";
               break;
          case UIDeviceOrientationFaceUp:
               orientationString =  @"Face Up";
               break;
          case UIDeviceOrientationFaceDown:
               orientationString =  @"Face Down";
               break;
          case UIDeviceOrientationUnknown:
               orientationString = @"Unknown";
               break;
          default:
               orientationString = @"Not Known";
               break;
    }
    return orientationString;
}

Return to the locationManager:didUpdateHeading: delegate method and modify the lines highlighted below to use the new methods and update the headings depending on the device orientation:

- (void)locationManager:(CLLocationManager*)manager 
      didUpdateHeading:(CLHeading*)newHeading {

    UIDevice *device = [UIDevice currentDevice];
    orientationLabel.text = [self stringFromOrientation:device.orientation];

    if (newHeading.headingAccuracy > 0) {
      float magneticHeading = [self magneticHeading:newHeading.magneticHeading
                                    fromOrientation:device.orientation];
      float trueHeading = [self trueHeading:newHeading.trueHeading
                            fromOrientation:device.orientation];

      magneticHeadingLabel.text = 
        [NSString stringWithFormat:@"%f", magneticHeading];
      trueHeadingLabel.text = [NSString stringWithFormat:@"%f", trueHeading];

      float heading = −1.0f * M_PI * newHeading.magneticHeading / 180.0f;1
      arrowImage.transform = CGAffineTransformMakeRotation(heading);
    }
}
1

You should still use the directly reported newHeading.magneticHeading for the compass needle rather than the adjusted heading. Otherwise the compass will not point correctly.

Make sure you’ve saved all the changes to the implementation file and click on the Run button in the Xcode toolbar to deploy the application onto the device. If all goes well you should see the same compass display as before. However this time if you rotate the display, sees Figure 5-7, the heading values should be the same irrespective of the device orientation.

Heading values are now the same irrespective of orientation
Figure 5-7. Heading values are now the same irrespective of orientation

Although I have not discussed or implemented it here, if the CLLocationManager object encounters an error, it will call the locationManager:didFailWithError: delegate method:

- (void)locationManager:(CLLocationManager *)manager 
      didFailWithError:(NSError *)error {
      
    if ([error code] == kCLErrorDenied) {
        // User has denied the application's request to use location services.
        [manager stopUpdatingHeading];

    } else if ([error code] == kCLErrorHeadingFailure) {
        // Heading could not be determined
    }
}

Measuring a Magnetic Field

To use the device to measure a magnetic field—for instance that of a small bar magnet, or the field generated by an electric current in a wire—you should first make a zero-point measurement of the ambient magnetic field of the Earth. Further readings should subtract this zero-point measurement.

When measuring, move the magnet to the device rather than moving the device to the magnet. Moving the device will cause the magnetic field of the Earth across the measuring sensor to change, which will spoil the zero point calibration you took earlier. If the device must be moved, only small movements should be attempted.

You can retrieve the raw magnetic field measurements along the X, Y and Z-axes by querying the CLHeading object passed to the locationManager:didUpdateHeading: method:

- (void)locationManager:(CLLocationManager *)manager 
      didUpdateHeading:(CLHeading *)heading {

    double x = heading.x;
    double y = heading.y;
    double z = heading.z;

    double magnitude = sqrt(x*x + y*y + z*z);

    ... code ...
}

The values returned are normalized into a ±128 range, measured in microtesla (µT), representing the offset from the magnetic field lines measured by the magnetometer.

Note

Apple provides sample code that displays the raw x, y, and z magnetometer values, a plotted history of those values, and a computed magnitude (size or strength) of the magnetic field. The code can be downloaded from the iPhone developer website at http://developer.apple.com/library/ios/#samplecode/Teslameter.

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

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