The compass sensor is a required component of Windows Phone devices. In the first release of the Windows Phone OS (7.0), however, no API was provided for the compass, and some devices lacked the native drivers necessary to use the compass even when running the Windows Phone 7.5 OS (mango). If you are maintaining an app for a premango device, it is therefore important to verify that the compass is supported before attempting to use it. For this, the static Compass.IsSupported
property is used, as demonstrated in the CompassViewModel
class (see Listing 16.4). The Start
method of the CompassViewModel
oversees the creation of the Compass
instance.
The default value of the compass TimeBetweenUpdates
property is 25 milliseconds, which on my test device was the minimum allowed value.
As with all sensor types, the CurrentValueChanged
event is used to receive periodic updates from the sensor.
The Calibrate
event allows your app to respond to when the phone is calibrating the sensor. Discussion of calibration continues later in this section.
Note
Calibration is an activity triggered by the OS; there is no PerformCalibration
method that you can call to cause the device to begin calibration.
LISTING 16.4. CompassViewModel.Start
Method (excerpt 1)
public void Start()
{
if (!Compass.IsSupported)
{
MessageService.ShowMessage("Compass is not supported on this device.");
return;
}
compass = new Compass();
compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(updateIntervalMs);
UpdateIntervalMs = (int)compass.TimeBetweenUpdates.TotalMilliseconds;
compass.CurrentValueChanged += HandleCompassCurrentValueChanged;
compass.Calibrate += HandleCompassCalibrate;
compass.Start();
...
}
When the CurrentValueChanged
event is raised, the event handler receives a SensorReadingEventArgs<CompassReading>
object. CompassReading
contains the five properties listed in Table 16.1.
The handler for the Compass
object’s CurrentValueChanged
event in the CompassViewModel
extracts the values from the event arguments and assigns them to various viewmodel properties of the same name. XNA’s Vector3
data type is not amenable to data binding because it exposes its X, Y, and Z dimensions as public fields. Rather than use Vector3
, a custom ThreeDimensionalVector
object is created using the Vector3
values, as shown in the following excerpt:
void HandleCompassCurrentValueChanged(
object sender, SensorReadingEventArgs<CompassReading> e)
{
MagneticHeading = e.SensorReading.MagneticHeading;
TrueHeading = e.SensorReading.TrueHeading;
HeadingAccuracy = e.SensorReading.HeadingAccuracy;
Vector3 reading = e.SensorReading.MagnetometerReading;
MagnetometerReading = new ThreeDimensionalVector(reading.X,
reading.Y,
reading.Z);
SmoothedReading
= readingSmoother.ProcessReading(e.SensorReading.MagneticHeading);
}
The raw sensor readings for MagneticHeading
and TrueHeading
are subject to slight variations in magnetic forces and, like the accelerometer, can make elements look unsteady when bound directly to the UI. A custom class, called ReadingSmoother
, is used to dampen the stream of sensor reading values. I have abstracted the data smoothing code of the EnhancedAccelerometer
, which was presented in the “Accelerometer” section of this chapter, into a new class called ReadingSmoother
. The ReadingSmoother
class uses the strategy pattern to allow you to change its filtering behavior via an IFilterStrategy
interface provided to its constructor, as shown:
public ReadingSmoother(
IFilterStrategy filterStrategy = null, int samplesCount = 25)
{
...
}
Listing 16.5 shows the ReadingSmoother.ProcessReading
method. ReadingSmoother
tracks the previous 25 values provided to it and uses the IFilterStrategy
to smooth the data.
LISTING 16.5. ReadingSmoother.ProcessReading
Method
public double ProcessReading(double rawValue)
{
double result = rawValue;
if (!initialized)
{
lock (initilizedLock)
{
if (!initialized)
{
/* Initialize buffer with first value. */
sampleSum = rawValue * samplesCount;
averageValue = rawValue;
for (int i = 0; i < samplesCount; i++)
{
sampleBuffer[i] = averageValue;
}
initialized = true;
}
}
}
double latestValue;
if (filterStrategy != null)
{
latestValue = result = filterStrategy.ApplyFilter(rawValue, result);
}
else
{
latestValue = rawValue;
}
/* Increment circular buffer insertion index. */
if (++sampleIndex >= samplesCount)
{
/* If at max length then wrap samples back
* to the beginning position in the list. */
sampleIndex = 0;
}
/* Add new and remove old at sampleIndex. */
sampleSum += latestValue;
sampleSum -= sampleBuffer[sampleIndex];
sampleBuffer[sampleIndex] = latestValue;
averageValue = sampleSum / samplesCount;
/* Stability check */
double deltaAcceleration = averageValue - latestValue;
if (Math.Abs(deltaAcceleration) > StabilityDelta)
{
/* Unstable */
deviceStableCount = 0;
}
else
{
if (deviceStableCount < samplesCount)
{
++deviceStableCount;
}
}
if (filterStrategy == null)
{
result = averageValue;
}
return result;
}
The ReadingSmoother
instance is defined as a field of the viewmodel, like so:
readonly ReadingSmoother readingSmoother
= new ReadingSmoother(new LowPassFilterStrategy());
LowPassFilterStrategy
removes noise from the raw sensor values, while allowing fast changes of sufficient amplitude to be detected. See the LowPassFilterStrategy
source in the downloadable sample code for more detail.
The content panel of the CompassView
page displays the various viewmodel properties, including the current magnetic heading and the accuracy of the compass. In addition, the viewmodel’s SmoothedReading
is displayed using an arrow. The arrow is defined as a Path
element. An enclosing Canvas
uses a RotateTransform
, which is bound to the SmoothedReading
property, to rotate to the compass heading value. See the following excerpt:
<Canvas Width="280" Height="100">
<Path Width="275" Height="95"
Canvas.Left="0" Canvas.Top="0" Stretch="Fill"
Fill="{StaticResource PhoneAccentBrush}"
Data="** Arrow points omitted **">
<Path.RenderTransform>
<RotateTransform Angle="-90"
CenterX="140" CenterY="48" />
</Path.RenderTransform>
</Path>
<Canvas.RenderTransform>
<RotateTransform Angle="{Binding SmoothedReading,
Converter={StaticResource NegateConverter}}"
CenterX="140" CenterY="48" />
</Canvas.RenderTransform>
</Canvas>
A custom IValueConverter
, called NegateConverter
, is used to negate the value so that the arrow’s heading corresponds to north, rather than the offset value. The NegateConverter.Convert
method is shown in the following excerpt:
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
double degrees = (double)value;
return -degrees;
}
Figure 16.5 shows the CompassView page with the heading arrow pointing to magnetic north. As the phone is rotated, the arrow maintains its heading.
The compass orientation value, shown in Figure 16.5, is determined by the accelerometer. This value is discussed in the following section.
13.58.114.29