In iOS, what is the difference between the Magnetic Field values from the Core Location and Core Motion frameworks?
To unravel this I've spent a bit too much time digging through the Apple docs.
There are three ways of obtaining magnetometer data
1/ Core Motion framework
CMMotionManagers's CMMagnetometer
class
2/ Core Motion framework
CMDeviceMotion CMCalibratedMagneticField
property
3 / Core Location framework
CLLocationManager's CLHeading
1/ supplies 'raw' data from the magnetometer.
2/ and 3/ return 'derived' data. The numbers in both cases are similar (though not exactly the same).
Difference between Core Motion's CMMagnetometer and CMCalibratedMagneticField
1/ and 2/ - both from the Core Motion framework - differ as follows:
CMDeviceMotion Class Reference
@property(readonly, nonatomic) CMCalibratedMagneticField magneticField
Discussion
The CMCalibratedMagneticField returned by this property gives you the total magnetic field in the device’s vicinity without device bias. Unlike the magneticField property of the CMMagnetometer class, these values reflect the earth’s magnetic field plus surrounding fields, minus device bias.
CMMagnetometer gives us raw data, CMCalibratedMagneticField is adjusted data.
Difference between Core Motion's CMCalibratedMagneticField and Core Location's CLHeading
The docs are not immediately clear on the difference between 2/ and 3/, but they do generate different numbers so let's do some digging….
Core Location framework
CLHeading
From Location Awareness Programming Guide
Getting Heading-Related Events
Heading events are available to apps running on a device that contains a magnetometer. A magnetometer measures nearby magnetic fields emanating from the Earth and uses them to determine the precise orientation of the device. Although a magnetometer can be affected by local magnetic fields, such as those emanating from fixed magnets found in audio speakers, motors, and many other types of electronic devices, Core Location is smart enough to filter out fields that move with the device.
Here are the relevant CLHeading
'raw' properties
@property(readonly, nonatomic) CLHeadingComponentValue x
@property(readonly, nonatomic) CLHeadingComponentValue y
@property(readonly, nonatomic) CLHeadingComponentValue z
The geomagnetic data (measured in microteslas) for the [x|y|z]-axis. (read-only)
This value represents the [x|y|z]-axis deviation from the magnetic field lines being tracked by the device. (older versions of the docs add:) The value reported by this property is normalized to the range -128 to +128.
I am not clear how a microtesla measurement can be 'normalized' (compressed? clipped?) to a range of +/-128 and still represent the unit it claims to measure. Perhaps that's why the sentence was removed from the docs. The units on an iPad mini do seem to conform to this kind of range, but the iPhone4S gives CMMagnetometer readings in higher ranges, eg 200-500.
The API clearly expects you to use the derived properties:
@property(readonly, nonatomic) CLLocationDirection magneticHeading
@property(readonly, nonatomic) CLLocationDirection trueHeading
which give stable N/S E/W compass readings in degrees (0 = North, 180 = South etc). For the true heading, other Core Location services are required (geolocation) to obtain the deviation of magnetic from true north.
Here is a snippet from the CLHeading
header file
/*
* CLHeading
*
* Discussion:
* Represents a vector pointing to magnetic North constructed from
* axis component values x, y, and z. An accuracy of the heading
* calculation is also provided along with timestamp information.
*
* x|y|z
* Discussion:
* Returns a raw value for the geomagnetism measured in the [x|y|z]-axis.
Core Motion framework
CMDeviceMotion CMCalibratedMagneticField
/*
* magneticField
*
* Discussion:
* Returns the magnetic field vector with respect to the device for devices with a magnetometer.
* Note that this is the total magnetic field in the device's vicinity without device
* bias (Earth's magnetic field plus surrounding fields, without device bias),
* unlike CMMagnetometerData magneticField.
*/
@property(readonly, nonatomic) CMCalibratedMagneticField magneticField NS_AVAILABLE(NA,5_0);
CMMagnetometer
* magneticField
*
* Discussion:
* Returns the magnetic field measured by the magnetometer. Note
* that this is the total magnetic field observed by the device which
* is equal to the Earth's geomagnetic field plus bias introduced
* from the device itself and its surroundings.
*/
@property(readonly, nonatomic) CMMagneticField magneticField;
CMMagneticField
This is the struct that holds the vector.
It's the same for CMDeviceMotion
's calibrated magnetic field and CMMagnetometer
's uncalibrated version:
/* CMMagneticField - used in
* CMDeviceMotion.magneticField.field
* CMMagnetometerData.magneticField
*
* Discussion:
* A structure containing 3-axis magnetometer data.
*
* Fields:
* x:
* X-axis magnetic field in microteslas.
* y:
* Y-axis magnetic field in microteslas.
* z:
* Z-axis magnetic field in microteslas.
The difference between 2/ and 3/ are hinted at here:
Core Location CLHeading
Represents a vector pointing to magnetic North constructed from axis component values x, y, and z
Core Location is smart enough to filter out fields that move with the device
Core Motion CMCalibratedMagneticField
[represents] Earth's magnetic field plus surrounding fields, without device bias
So - according to the docs - we have:
1/ CMMagnetometer
Raw readings from the magnetometer
2/ CMDeviceMotion (CMCalibratedMagneticField*) magneticField
Magnetometer readings corrected for device bias (onboard magnetic fields)
3/ CLHeading [x|y|z]
Magnetometer readings corrected for device bias and filtered to eliminate local external magnetic fields (as detected by device movement - if the field moves with the device, ignore it; otherwise measure it)
Testing the theory
I have put a Magnet-O-Meter demo app on gitHub which displays some of these differences. It's quite revealing to wave a magnet around your device when the app is running and watching how the various APIs react:
CMMagnetometer doesn't react much to anything unless you pull a rare earth magnet up close. The onboard magnetic fields seem far more significant than local external fields or the earth's magnetic field. On my iPhone 4S it consistently points to the bottom left of the device; on the iPad mini it points usually to the top right.
CLHeading.[x|y|z] is the most vulnerable (responsive) to local external fields, whether moving or static relative to the device.
(CMDevice)CMCalibratedMagneticField is the most steady in the face of varying external fields, but otherwise tracks it's Core Location counterpart CLHeading.[x|y|z] pretty closely.
CLHeading.magneticHeading - Apple's recommendation for magnetic compass reading - is far more stable than any of these. It is using data from the other sensors to stabilise the magnetometer data. But you don't get a raw breakdown of x,y,z
influenced by
onboard fields local external fields earth's field
yellow X X X
green _ X X
blue _ _ X
red _ _ X
yellow CMMagnetometer
green CLHeading.[x|y|z]
blue CMCalibratedMagneticField
red CLHeading.magneticHeading
This does seem to contradict the docs, which suggest that CLHeading.[x|y|z] should be less influenced by local external fields than CMCalibratedMagneticField.
What approach should you take? Based on my limited testing, I would suggest…
If you want a compass reading
CLHeading's magneticHeading
and trueHeading
will give you the most accurate and most stable compass reading.
If you need to avoid Core Location
CMDeviceMotion's CMCalibratedMagneticField
seems to be the next most desirable, although considerably less stable and accurate than magneticHeading
.
If you are interested in local magnetic fields
CLHeading's 'raw' x y and z properties seem to be more sensitive to local magnetic fields.
If you want all of the data including onboard magnetic fields
Raw magnetometer data from CMMagnetometer. There is really not much point using this unless you are prepared to do tons of filtering, as it is hugely influenced by magnetic fields generated on the device itself.