In iOS, Differencebetween the Magnetic Field Values from the Core Location and Core Motion Frameworks

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

enter image description here

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.

Which iOS class/code returns the magnetic North?

The iOS documentation states that the CMMagneticField data is raw, meaning that it includes bias introduced from the device itself and its surroundings. CMDeviceMotion provides the same magnetic field values filtered.

To determine magnetic north you should use the filtered values and the device should lay level with Earth's surface.

Knowing the x and y values of the magnetic field the angle (declination from magnetic north in degrees) can be calculated with the following formula:

if (y>0): heading = 90.0 - [arcTan(x/y)]*180/π
if (y<0): heading = 270.0 - [arcTAN(x/y)]*180/π
if (y=0, x<0): heading = 180.0
if (y=0, x>0): heading = 0.0

In Obj-C, assuming you have a CMMagnetometerData object called magnetometerData, that would look something like:

 double heading = 0.0;
double x = magnetometerData.magneticField.x;
double y = magnetometerData.magneticField.y;
double z = magnetometerData.magneticField.z;

if (y > 0) heading = 90.0 - atan(x/y)*180.0/M_PI;
if (y < 0) heading = 270.0 - atan(x/y)*180.0/M_PI;
if (y == 0 && x < 0) heading = 180.0;
if (y == 0 && x > 0) heading = 0.0;

How do I use Core Motion to output magnetometer data using SwiftUI?

You have a couple of problems with your code.

Your first problem is that you need a binding between your model data and your view - By creating a binding, the view will be updated automatically when the model changes.

The second problem is that you are only accessing the magnetometer data once via motionManager.magnetometerData rather than setting up a closure to monitor updates via startMagnetometerUpdates(to:withHandler:).

You can use ObservableObject from the Combine framework and @ObservedObject in your view to create the appropriate binding.

Start by creating a class to wrap your motion manager:

import Foundation
import Combine
import CoreMotion

class MotionManager: ObservableObject {

private var motionManager: CMMotionManager

@Published
var x: Double = 0.0
@Published
var y: Double = 0.0
@Published
var z: Double = 0.0

init() {
self.motionManager = CMMotionManager()
self.motionManager.magnetometerUpdateInterval = 1/60
self.motionManager.startMagnetometerUpdates(to: .main) { (magnetometerData, error) in
guard error == nil else {
print(error!)
return
}

if let magnetData = magnetometerData {
self.x = magnetData.magneticField.x
self.y = magnetData.magneticField.y
self.z = magnetData.magneticField.z
}

}

}
}

This class conforms to ObservableObject and @Publishes its three properties, x,y and z.

Simply assigning new values to these properties in the magnetometer update closure will cause the publisher to fire and update any observers.

Now, in your view, you can declare an @ObservedObject for your motion manager class and bind the properties.

struct ContentView: View {

@ObservedObject
var motion: MotionManager

var body: some View {
VStack {
Text("Magnetometer Data")
Text("X: \(motion.x)")
Text("Y: \(motion.y)")
Text("Z: \(motion.z)")
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(motion: MotionManager())
}
}

Note that you will need to pass an instance of MotionManager in your SceneDelegate.swift file:

let contentView = ContentView(motion: MotionManager())

Building Pedometer with Core Motion

Yes, in order to run your app in the background, your app has to be in one of a few blessed categories: music, location, VOIP.

EDIT:

You can find the docs here: https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/CoreData/Articles/cdRelationships.html

iOS - Interpreting the CLHeading's x,y,z values

According to my experiences from the moment I asked this question, it seems that yes, the magnetic North vector is in the device's own coordinate system, as well as the gravity vector obtained from the accelerometer.



Related Topics



Leave a reply



Submit