다음을 통해 공유


WP7 Code: Using the Accelerometer API

 

My previous  blog posts on WP7 Code covered the GeoLocation API, a surface area that provides access to the phone’s location based on signals picked up by one of its radios. However location alone is probably not sufficient for mobile applications. This post shows how to take Windows Phone 7 applications beyond location and enable them to use additional data provided by another onboard sensor, the accelerometer.

The accelerometer is exposed in the Microsoft.Devices.Sensors namespace, and packaged in the DLL with the same name. As in the previous posts a simple application showcases the API, with the emphasis on the phone-specific aspects rather than C#/Silverlight/robustness/etc. This application looks like a bubble level. Just like I said in the past, while this code runs out of the box please don’t build a navigation system on it Winking smile

Start Visual Studio (with the Windows Phone Development Tools installed) and create a new Windows Phone Application called TiltMe. Add references to Microsoft.Devices.Sensors (for the accelerometer API), and to Microsoft.Phone.Reactive and System.Observable (for RxLINQ)—the event-driven code is implemented as Rx queries. In the tradition of a utilitarian UX add the following code (highlighted) to MainPage.xaml:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Canvas Height="607" HorizontalAlignment="Left" Name="canvas1" VerticalAlignment="Top" Width="456">
<Ellipse Height="40" Width="40" Name="ellipse1">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="Black" Offset="1" />
<GradientStop Color="#FFE41717" Offset="0.169" />
<GradientStop Color="#FF851212" Offset="0.808" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
    </Canvas>
</Grid>

The MainPage class holds the accelerometer reference in an instance variable, which gets created in the constructor. Similar to the GeoCoordinateWatcher, the page code starts the accelerometer when the page is navigated to (first override), and stops it when the page is navigated away from (second override). Stopping the sensor when it is not needed reduces the application’s power consumption, an aspect critical to battery-powered code. Don’t forget the using statement for the accelerometer’s namespace.

public partial class MainPage : PhoneApplicationPage
{
    private Accelerometer accelerometer;

    // Constructor
    public MainPage()
    {
        InitializeComponent();

        accelerometer = new Accelerometer();

        this.Loaded += new RoutedEventHandler(MainPage_Loaded);
    }

    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        accelerometer.Start();
    }

    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
    {
        base.OnNavigatedFrom(e);

        accelerometer.Stop();
    }

    // snip
}

Accelerometer readings surface as ReadingChanged events. The following extension method exposes AccelerometerReadingEventArgs as an Rx event stream (don’t forget the using statement for the Microsoft.Phone.Reactive namespace):

public static IObservable<AccelerometerReadingEventArgs> GetAccEventStream(this Accelerometer accelerometer)
{
    return Observable.Create<AccelerometerReadingEventArgs>(observer =>
    {
        EventHandler<AccelerometerReadingEventArgs> handler = (s, e) =>
        {
            observer.OnNext(e);

        };
        accelerometer.ReadingChanged += handler;
        return () => { accelerometer.ReadingChanged -= handler; };
    });
}

To illustrate the accelerometer readings the application shows a sphere (i.e., ellipse with gradient fill) that “falls” towards the edge of the canvas, following the phone’s orientation—2D bubble level. To avoid interfering with the startup path (and thus remove the risk of having the application being perceived as non-responsive) the code connecting the accelerometer to the sphere lies in the event handler for the page loaded even rather than the page’s constructor. It begins with a query representing the accelerometer event stream, and computations for the sphere’s initial position (modified code shown highlighted):

public MainPage()
{
    InitializeComponent();

    accelerometer = new Accelerometer();

    this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{

    var accelerometerReadings = accelerometer.GetAccEventStream();

    var x0 = (double)ellipse1.GetValue(Canvas.TopProperty) + ellipse1.Width / 2;
    var y0 = (double)ellipse1.GetValue(Canvas.LeftProperty) + ellipse1.Height / 2;

}

The sphere’s initial position, accelerometer events (which provide readings in the 3 XYZ dimensions as well as the timestamp) plugged into the laws of motion provide all the ingredients for this simple application. (A physics engine would offer a more realistic interaction; that is left as an exercise to the reader.) Before showing the Rx queries here are a few things that simplify matters. First, an Interval class whose Clip method bounds a value between the interval’s minimum and maximum. Second an extension method that allows the code to position an ellipse by providing its center. Here they are, with the extension method going into the Helpers static class:

public class Interval
{
    public double Min { get; private set; }
    public double Max { get; private set; }

    public Interval(double min, double max)
    {
        Min = min;
        Max = max;
    }

    public double Clip(double value)
    {
        if (value >= Max)
            return Max;
        if (value <= Min)
            return Min;
        return value;
    }
}

public static void SetCenter(this Ellipse ellipse, Point c)
{
    ellipse.SetValue(Canvas.LeftProperty,c.X - ellipse.Width / 2);
    ellipse.SetValue(Canvas.TopProperty, c.Y - ellipse.Height / 2);
}

With the ability to clip (such that the sphere doesn’t go off screen) and position the sphere, Rx queries implement the (simplified) calculation. The first query projects the stream of AccelerometerReadingEventArgs into a stream of objects with X, Y, and Z members having the values provided by the accelerometer, and the dt member representing the delta (in milliseconds) between the last 2 accelerometer readings. This is implemented via the Zip and Skip combinators; ObserveOnDispatcher ensures that further manipulations of the resulting IObservable stream happen on the UI thread.

var accelerationAndDts = accelerometerReadings.Zip(accelerometerReadings.Skip(1), (p, c) =>
    new { X = c.X, Y = - c.Y, Z = c.Z, dt = (c.Timestamp.Subtract(p.Timestamp)).TotalMilliseconds }).ObserveOnDispatcher();

The next query uses the acceleration (X and Y) and dts as well as the sphere’s current coordinates to compute its next position. The Scan combinator injects the initial coordinates into the computation, and then carries it over subsequent events. (In Smalltalk this corresponds to the inject:into: message.)

var xclip = new Interval(0, canvas1.Width);
var yclip = new Interval(0, canvas1.Height);

var positions = accelerationAndDts.Scan(new Point(x0,y0)
               , (pos, acc) =>
                   new Point( xclip.Clip(pos.X + acc.X * 0.5 * acc.dt * acc.dt)
                            , yclip.Clip(pos.Y + acc.Y * 0.5 * acc.dt * acc.dt)
                            )
               );

The resulting stream of positions is suitable for positioning the sphere; the SetCenter extension method introduced above makes the code pretty compact:

positions.Subscribe(c => this.ellipse1.SetCenter(c));

Before showing the code let’s have a look at how the raw data might look like. Here are 256 consecutive readings (X and Y), with the device laying flat on my desk.

image

With the caveat that this data comes from an engineering prototype rather than a production device, here are a few things to note:

  • First, on this particular device the accelerometer is not calibrated. There are offset errors in both the X and Y dimensions, both of which could be eliminated via calibration.
  • Second, the raw sensor data has some jitter, which is probably a combination of vibrations picked up by my desk and other types of noise. Depending on your application a smoothing filter could take out the unwanted jitter.

The full code from MainPage.xaml.cs follows; building and running this application requires the XAML for the UI, and the Interval class shown above.

namespace TiltMe
{
    public partial class MainPage : PhoneApplicationPage
    {
        private Accelerometer accelerometer;

        // Constructor
        public MainPage()
        {
            InitializeComponent();

            accelerometer = new Accelerometer();

            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            accelerometer.Start();
        }

        protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);

            accelerometer.Stop();
        }

       
        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {

            var accelerometerReadings = accelerometer.GetAccEventStream();

            var x0 = (double)ellipse1.GetValue(Canvas.TopProperty) + ellipse1.Width / 2;
            var y0 = (double)ellipse1.GetValue(Canvas.LeftProperty) + ellipse1.Height / 2;
           
            var accelerationAndDts = accelerometerReadings.Zip(accelerometerReadings.Skip(1), (p, c) =>
                new { X = c.X, Y = - c.Y, Z = c.Z, dt = (c.Timestamp.Subtract(p.Timestamp)).TotalMilliseconds }).ObserveOnDispatcher();

            var xclip = new Interval(0, canvas1.Width);
            var yclip = new Interval(0, canvas1.Height);
           
            var positions = accelerationAndDts.Scan(new Point(x0,y0)
                           , (pos, acc) =>
                               new Point( xclip.Clip(pos.X + acc.X * 0.5 * acc.dt * acc.dt)
                                        , yclip.Clip(pos.Y + acc.Y * 0.5 * acc.dt * acc.dt)
                                        )
                           );

            positions.Subscribe(c => this.ellipse1.SetCenter(c));
        }
    }

    public static class Helpers
    {
        public static IObservable<AccelerometerReadingEventArgs> GetAccEventStream(this Accelerometer accelerometer)
        {
            return Observable.Create<AccelerometerReadingEventArgs>(observer =>
            {
                EventHandler<AccelerometerReadingEventArgs> handler = (s, e) =>
                {
                    observer.OnNext(e);

                };
                accelerometer.ReadingChanged += handler;
                return () => { accelerometer.ReadingChanged -= handler; };
            });
        }

        public static void SetCenter(this Ellipse ellipse, Point c)
        {
            ellipse.SetValue(Canvas.LeftProperty,c.X - ellipse.Width / 2);
            ellipse.SetValue(Canvas.TopProperty, c.Y - ellipse.Height / 2);
        }
    }

In summary, this blog post has shown:

  • How to get started with the Windows Phone Accelerometer API,
  • How to bring the accelerometer’s ReadingChanged event into the realm of Rx, and use the corresponding event stream in Rx queries, and
  • How to drive a canvas element based on accelerometer readings.

Perhaps a short video showing the application in action would help? Let me know…

Comments