How to get data from the compass sensor for Windows Phone 8
[ This article is for Windows Phone 8 developers. If you’re developing for Windows 10, see the latest documentation. ]
This topic walks you through creating a compass app that gives you a numeric display and a graphical representation of the compass data. This walkthrough also shows you how to implement a compass calibration dialog box.
Note
In addition to the APIs described in this topic from the Microsoft.Devices.Sensors namespace, you can also program the phone’s sensors by using the similar classes in the Windows.Devices.Sensors namespace.
This topic contains the following sections.
Compass overview
The Compass, or magnetometer, sensor can be used to determine the angle by which the device is rotated relative to the Earth’s magnetic north pole. An app can also use raw magnetometer readings to detect magnetic forces around the device. The compass sensor is not required for all Windows Phone devices. It is important that you consider this when designing and implementing your app. Your app should always check to see whether the sensor is available and either provide an alternative input mechanism or fail gracefully if it is not.
The compass API uses a single axis to calculate the heading, depending on the orientation of the device. If you want to create an app that uses the devices orientation on all axes, you should use the RotationMatrix property of the Motion class.
The compass sensor in a device can become inaccurate over time, especially if it is exposed to magnetic fields. There is a simple user action that recalibrates the compass. The Calibrate event is raised whenever the system detects that the heading accuracy is greater than +/- 20 degrees. This example will show you how to implement a calibration dialog box to allow the user to calibrate their compass.
Creating the compass app
The following steps show you how to create a compass app.
Important Note: |
---|
You cannot test this app on the emulator. The emulator doesn’t support the compass. You can only test this app on a registered phone. |
To create a compass app
In Visual Studio, create a new **Windows Phone App ** project. This template is in the Windows Phone category.
This app requires a reference to the Microsoft.Devices.Sensors assembly which contains the sensor APIs. It also requires a reference to the XNA Framework because some of the compass data is passed in the form of an XNA Framework Vector3 object. These assemblies are already referenced in a project that targets Windows Phone 8.
In the MainPage.xaml file, place the following XAML code in the Grid element named “ContentPanel”. This XAML code creates the UI that will display the compass data. The TextBlock elements will be used to display the compass data numerically. The Line elements will be used to display the heading and raw compass data graphically.
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Horizontal"> <TextBlock>status: </TextBlock> <TextBlock Name="statusTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>time between updates:</TextBlock> <TextBlock Name="timeBetweenUpdatesTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>magnetic heading: </TextBlock> <TextBlock Name="magneticTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>true heading: </TextBlock> <TextBlock Name="trueTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>heading accuracy: </TextBlock> <TextBlock Name="accuracyTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>compass orientation mode:</TextBlock> <TextBlock Name="orientationTextBlock"></TextBlock> </StackPanel> <Grid Height="200" Name="headingGrid"> <TextBlock Foreground="Yellow" FontSize="16">magnetic heading</TextBlock> <TextBlock Foreground="Orange" FontSize="16" Margin="0,18">true heading</TextBlock> <Line x:Name="magneticLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Yellow" StrokeThickness="4"></Line> <Line x:Name="trueLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Orange" StrokeThickness="4"></Line> </Grid> <TextBlock Text="raw magnetometer data:"></TextBlock> <Grid> <TextBlock Height="30" HorizontalAlignment="Left" Name="xTextBlock" Text="X: 1.0" VerticalAlignment="Top" Foreground="Red" FontWeight="Bold"/> <TextBlock Height="30" HorizontalAlignment="Center" Name="yTextBlock" Text="Y: 1.0" VerticalAlignment="Top" Foreground="Green" FontWeight="Bold"/> <TextBlock Height="30" HorizontalAlignment="Right" Name="zTextBlock" Text="Z: 1.0" VerticalAlignment="Top" Foreground="Blue" FontWeight="Bold"/> </Grid> <Grid Height="140"> <Line x:Name="xLine" X1="240" Y1="40" X2="240" Y2="40" Stroke="Red" StrokeThickness="14"></Line> <Line x:Name="yLine" X1="240" Y1="70" X2="240" Y2="70" Stroke="Green" StrokeThickness="14"></Line> <Line x:Name="zLine" X1="240" Y1="100" X2="240" Y2="100" Stroke="Blue" StrokeThickness="14"></Line> </Grid> </StackPanel>
This is how the UI will appear.
Next, add the XAML that defines the compass calibration UI immediately after the code that you added in the previous step. This UI displays an image to the user and instructions on how to move the device in order to calibrate the compass. Note that the outer StackPanel is set to Visibility..::.Collapsed so that is hidden from the user. The code that displays the UI will be shown later in this walkthrough.
<!--Calibration UI--> <StackPanel Name="calibrationStackPanel" Background="Black" Opacity="1" Visibility="Collapsed"> <Image Source="/Images/calibrate_compass.png" Opacity=".95" HorizontalAlignment="Center"/> <TextBlock TextWrapping="Wrap" TextAlignment="Center">The compass on your device needs to be calibrated. Hold the device in front of you and sweep it through a figure 8 pattern as shown until the calibration is complete.</TextBlock> <StackPanel Orientation="Horizontal" Margin="0,10" HorizontalAlignment="Center"> <TextBlock>heading accuracy:</TextBlock> <TextBlock Name="calibrationTextBlock">0.0°</TextBlock> </StackPanel> <Button Name="calibrationButton" Content="Done" Click="calibrationButton_Click"></Button> </StackPanel> <!--End Calibration UI-->
This is how the calibration UI will appear.
The compass calibration UI defined above uses an image that illustrates the pattern that the user should sweep the device through in order to calibrate the compass. To add an image to your solution, first create a new folder. To add a new folder, right-click your app project in Solution Explorer and select Add -> New Folder. Name the new folder Images. Right-click the new folder in Solution Explorer and select Add -> Existing Item. Select an image and click Add. Rename the image calibrate_compass.png. After adding the image, right-click the image icon in Solution Explorer and select Properties. Make sure that the Build Action property is set to Content.
The image used in this sample is included with the Raw Sensor Data Sample.
The last UI code to add to MainPage.xaml is the definition of an app bar with one button that will start and stop the acquisition of data from the compass. Paste the following code before the last line of XAML code, </phone:PhoneApplicationPage>.
<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True"> <shell:ApplicationBarIconButton IconUri="/Images/onoff.png" Text="on/off" Click="ApplicationBarIconButton_Click"/> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>
Now open the MainPage.xaml.cs code-behind page and add using directives for the sensors and the XNA Framework namespaces to other using directives at the top of the page. This example uses a timer to update the UI, so include the System.Windows.Threading namespace as well.
using Microsoft.Devices.Sensors; using Microsoft.Xna.Framework; using System.Windows.Threading;
Declare some member variables at the top of the MainPage class definition.
public partial class MainPage : PhoneApplicationPage { Compass compass; DispatcherTimer timer; double magneticHeading; double trueHeading; double headingAccuracy; Vector3 rawMagnetometerReading; bool isDataValid; bool calibrating = false;
The first variable is an object of type Compass which will be used to obtain data from the compass sensor. Next, a DispatcherTimer is declared which will be used to periodically update the UI. Then there is a set of variables that will contain the compass data. These will be set using the Compass API and will be displayed in the DispatcherTimer..::.Tick event of the DispatcherTimer. Finally, the Boolean variable, calibrating, will be used to track whether the calibration dialog box is currently being shown.
In the page’s constructor, check to see whether the device on which the app is running supports the compass sensor. Not all devices support all sensors, so you should always check before you use the sensor. If the compass is not supported, a message is displayed to the user and the app bar is hidden. If the compass is supported, the DispatcherTimer is initialized and an event handler is assigned, but the timer is not started at this point. Replace the existing page constructor with the following code.
The timer_Tick event handler will be added later.
// Constructor public MainPage() { InitializeComponent(); if (!Compass.IsSupported) { // The device on which the application is running does not support // the compass sensor. Alert the user and hide the // application bar. statusTextBlock.Text = "device does not support compass"; ApplicationBar.IsVisible = false; } else { // Initialize the timer and add Tick event handler, but don't start it yet. timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(30); timer.Tick += new EventHandler(timer_Tick); } }
Add a handler for the click event for the app bar button. Depending on how you added the XAML code earlier in this topic, Visual Studio may have added this handler for you. If so, remove any code from inside the handler. If the handler was not added automatically, copy and paste the following empty function into the MainPage class definition.
private void ApplicationBarIconButton_Click(object sender, EventArgs e) { }
In the App bar button click handler, first check to see if the Compass object is not null and receiving data. If this is the case, then the user is clicking the button to stop the compass, so call Stop()()() for both the Compass and the DispatcherTimer. Paste the following code inside the empty button click handler.
if (compass != null && compass.IsDataValid) { // Stop data acquisition from the compass. compass.Stop(); timer.Stop(); statusTextBlock.Text = "compass stopped."; }
Next, the code will handle the case where the user is starting the compass. If the Compass object is null, then create a new instance. Set the desired time between updates. Note that sensors on different devices supported different update intervals, in this example, the property is queried after is has been set in order to display the sensor’s actual interval to the user. Next, event handlers are added for the CurrentValueChanged event, which is raised whenever the compass has new data, and the Calibrate event which is raised when the compass needs calibration. Paste this code inside the button click handler, after the previous code section.
The closing curly brace for the else block will be added in the next step.
else { if (compass == null) { // Instantiate the compass. compass = new Compass(); // Specify the desired time between updates. The sensor accepts // intervals in multiples of 20 ms. compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20); // The sensor may not support the requested time between updates. // The TimeBetweenUpdates property reflects the actual rate. timeBetweenUpdatesTextBlock.Text = compass.TimeBetweenUpdates.TotalMilliseconds + " ms"; compass.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<CompassReading>>(compass_CurrentValueChanged); compass.Calibrate += new EventHandler<CalibrationEventArgs>(compass_Calibrate); }
Now, start the compass using the Start()()() method. It is possible for the call to Start to fail, so you should put this call in a try block. In the catch block, you can alert the user that the compass could not be started. This code also starts the DispatcherTimer. Paste this code into the Start button click handler, after the previous code section.
try { statusTextBlock.Text = "starting compass."; compass.Start(); timer.Start(); } catch (InvalidOperationException) { statusTextBlock.Text = "unable to start compass."; } }
Now, implement the CurrentValueChanged event handler. This method will be called by the system with new compass data at the frequency you specified with TimeBetweenUpdates. The handler receives a CompassReading object containing the compass data. This handler is called on a background thread that does not have access to the UI. So, if you want to modify the UI from this method, you must use Dispatcher.BeginInvoke to call the code on the UI thread. This example uses a dispatcher timer to update the UI, so this method simply sets the values of the class member variables to the values of the CompassReading object. The Abs function is used to get the absolute value of the heading accuracy because this app is only concerned with the magnitude of the accuracy, not the sign.
void compass_CurrentValueChanged(object sender, SensorReadingEventArgs<CompassReading> e) { // Note that this event handler is called from a background thread // and therefore does not have access to the UI thread. To update // the UI from this handler, use Dispatcher.BeginInvoke() as shown. // Dispatcher.BeginInvoke(() => { statusTextBlock.Text = "in CurrentValueChanged"; }); isDataValid = compass.IsDataValid; trueHeading = e.SensorReading.TrueHeading; magneticHeading = e.SensorReading.MagneticHeading; headingAccuracy = Math.Abs(e.SensorReading.HeadingAccuracy); rawMagnetometerReading = e.SensorReading.MagnetometerReading; }
Implement the DispatcherTimer..::.Tick event handler to update the UI with the current compass readings. This method will have different behavior depending on whether the compass is currently being calibrated. If calibration is not taking place, the status TextBlock is updated to indicate that data is being received. Next, the TextBlock objects are updated to show the magnetic heading, the heading relative to the magnetic north pole, and the true heading, which is the heading relative to the geographic north pole, and the heading accuracy which shows the error in the compass readings. Then the Line objects are updated to graphically illustrate the compass readings. The MathHelper class in the Microsoft.Xna.Framework library is used to convert the readings from degrees to radians so that they can be used with trigonometric functions. Next, the raw magnetometer readings are displayed both numerically and graphically. Paste the following code into MainPage.xaml.cs. The remainder of this method will be shown in the next step.
The closing curly brace for the timer_Tick method will be added in the next step.
void timer_Tick(object sender, EventArgs e) { if (!calibrating) { if (isDataValid) { statusTextBlock.Text = "receiving data from compass."; } // Update the textblocks with numeric heading values magneticTextBlock.Text = magneticHeading.ToString("0.0"); trueTextBlock.Text = trueHeading.ToString("0.0"); accuracyTextBlock.Text = headingAccuracy.ToString("0.0"); // Update the line objects to graphically display the headings double centerX = headingGrid.ActualWidth / 2.0; double centerY = headingGrid.ActualHeight / 2.0; magneticLine.X2 = centerX - centerY * Math.Sin(MathHelper.ToRadians((float)magneticHeading)); magneticLine.Y2 = centerY - centerY * Math.Cos(MathHelper.ToRadians((float)magneticHeading)); trueLine.X2 = centerX - centerY * Math.Sin(MathHelper.ToRadians((float)trueHeading)); trueLine.Y2 = centerY - centerY * Math.Cos(MathHelper.ToRadians((float)trueHeading)); // Update the textblocks with numeric raw magnetometer readings xTextBlock.Text = rawMagnetometerReading.X.ToString("0.00"); yTextBlock.Text = rawMagnetometerReading.Y.ToString("0.00"); zTextBlock.Text = rawMagnetometerReading.Z.ToString("0.00"); // Update the line objects to graphically display raw data xLine.X2 = xLine.X1 + rawMagnetometerReading.X * 4; yLine.X2 = yLine.X1 + rawMagnetometerReading.Y * 4; zLine.X2 = zLine.X1 + rawMagnetometerReading.Z * 4; }
The second part of the DispatcherTimer..::.Tick event handler will update the UI for the compass calibration. Remember that this UI is not shown to the user by default. The code to show the UI will be shown in the following step. This code simply evaluates the HeadingAccuracy value and if the value is less than or equal to 10 degrees, the compass is considered to be calibrated, so the text color is set to green and the user is informed that the calibration is complete. Otherwise, the text is red and the numeric value of the heading accuracy is displayed. First, add a using statement to support the use of brushes and colors.
using System.Windows.Media;
Then paste the following code after the code from the previous step to complete the DispatcherTimer..::.Tick event handler.
else { if (headingAccuracy <= 10) { calibrationTextBlock.Foreground = new SolidColorBrush(Colors.Green); calibrationTextBlock.Text = "Complete!"; } else { calibrationTextBlock.Foreground = new SolidColorBrush(Colors.Red); calibrationTextBlock.Text = headingAccuracy.ToString("0.0"); } } }
Next, implement the Calibrate event handler. This event is fired if the system detects that the compass heading accuracy is greater than +/20 degrees. In this event handler, simply make the calibration UI visible and set the calibrating member variable to true. Note that the code that updates the UI is called using Dispatcher.Invoke because this event handler is not called on the UI thread.
void compass_Calibrate(object sender, CalibrationEventArgs e) { Dispatcher.BeginInvoke(() => { calibrationStackPanel.Visibility = Visibility.Visible; }); calibrating = true; }
Finally, implement the Click event handler for the button in the calibration UI. The user will tap this when calibration is complete. This method simply hides the calibration UI and sets the calibrating member variable to false.
private void calibrationButton_Click(object sender, RoutedEventArgs e) { calibrationStackPanel.Visibility = Visibility.Collapsed; calibrating = false; }
Determining the compass orientation mode
The compass API will use a different axis to compute the heading, depending on the orientation of the phone. The following code modifies the example app to determine which orientation the compass is using at run time.
To determine compass orientation mode
First, add a member variable of type Accelerometer at the top of the class with the other member variables.
Accelerometer accelerometer;
Next, in the Application Bar Button click handler, call the Stop()()() method of the accelerometer right after the calls to the Stop method for the Compass and the DispatcherTimer.
… compass.Stop(); timer.Stop(); // Add the following line accelerometer.Stop();
Next, also inside the Application Bar Button click handler, initialize the Accelerometer object, attach an event handler for CurrentValueChanged and then call Start()()(). Add this code right after calling Start for the Compass and DispatcherTimer.
… statusTextBlock.Text = "starting compass."; compass.Start(); timer.Start(); // add the following lines accelerometer = new Accelerometer(); accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(accelerometer_CurrentValueChanged); accelerometer.Start();
Finally, implement the event handler for the Accelerometer object’s CurrentValueChanged event. This method gets the Vector3 object representing the acceleration reading and then uses trigonometric functions to determine the orientation of the device. Finally, the UI is updated with the current compass mode. Note that Dispatcher..::.BeginInvoke is used to update the UI because this event is not called on the UI thread.
void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e) { Vector3 v = e.SensorReading.Acceleration; bool isCompassUsingNegativeZAxis = false; if (Math.Abs(v.Z) < Math.Cos(Math.PI / 4) && (v.Y < Math.Sin(7 * Math.PI / 4))) { isCompassUsingNegativeZAxis = true; } Dispatcher.BeginInvoke(() => { orientationTextBlock.Text = (isCompassUsingNegativeZAxis) ? "portrait mode" : "flat mode"; }); }