Feeling the Earth Move

Want to know if the earth is shaking?  How about if someone is moving your laptop?  Create a utility to detect movement so you'll know right away when something is happening!

The Windows 7 sensors platform exposes a standard way of communicating with hardware for determining location, light, magnetic north (compass), distance, movement, and much more.  Through the accelerometer, movement is measured as force (G's) in up to three dimensions.  The Freescale Badge board that I use for sensor development includes ambient light, touch, and a 3D accelerometer.

Working with the three axes of accelerometer data is incredibly easy.  Each sensor data report indicates the number of G's felt along each axis.  You can watch the values of movement in order to know more about the environment.

My first thought with this application was to create a seismometer to measure ground tremors, but I'm not so sure the one that I have would be nearly sensitive for that.  Then it occurred to me that the accelerometer would work great to detect a laptop being moved.  I see people leaving their laptop on a table and going up for a refill at a coffee shop.  When I do that I always feel pretty nervous.  Knowing that it could sound an alarm if someone moved it would make me feel more secure. 

For this application, you'll need a Windows 7 system, with a built-in or hardware add-on accelerometer.  Some hard drives have them, some Lenovo laptops have them, but you need to be able to access the sensor using standard Windows 7 Devices and Sensors drivers.

With hardware in place, download Visual Studio 2010 Express, the Windows API Code Pack, and the Sensors and Location API, then you're ready to get started!

Good Vibrations

The accelerometer is an interesting sensor.  You wiggle something physical around and get numbers that change.  The numbers represent how many G's are being experienced in up to three dimensions.  This makes it trivial to see when there is movement.  With some additional work you can figure out more fine-grained movement information, but we just need to know how much things are moving around.

Using the Windows 7 managed sensors API, we can instantiate the accelerometer and start reading values with only a small amount of work.  Even better, due to the design of the sensors API we can access it without locking it.  In other words, unlike reading data from a serial port or other hardware connection, applications can share devices.

In this application, I wanted a UI with a lots of the raw and calculated values showing.  This includes the G's for the individual axes, the net magnitude, and even a chart of data.  In a WPF application, you need public properties in order to participate in databinding.  You also need to be able to alert listeners when your values change.  Unfortunately, the X, Y, and Z values from the sensor report are not easily bindable due to their indexed nature.  They also have no way of alerting listeners (like the UI) that they have been updated.

The key to creating the right properties is using the INotifyPropertyChanged interface.  This interface has only a single member – the PropertyChanged event.  Whenever a property value changes, raise this event and any interested listeners will know to grab the new value.

Visual Basic

#Region "Magnitude Property"
    Private _magnitude As Double
    Public Property Magnitude() As Double
        Get
            Return _magnitude
        End Get
        Set(ByVal value As Double)
            _magnitude = value
            OnPropertyChanged("Magnitude")
        End Set
    End Property
#End Region
#Region "INotifyPropertyChanged Members"

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private Sub OnPropertyChanged(ByVal prop As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(prop))
    End Sub
#End Region

Visual C#

#region Magnitude Property
private double _magnitude;
public double Magnitude
{
    get { return _magnitude; }
    set
    {
        _magnitude = value;
        OnPropertyChanged("Magnitude");
    }
}
#endregion
#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(string prop)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(prop));
}

#endregion

This pattern is found in every public property that you want to enable for databinding.  It's great because you can do it on a class for WPF or Silverlight, yet it's not creating a dependency on them.  You can subscribe to this event in some apps, and simply ignore the event in other applications.  Being able to reuse business objects this way is a big deal to me.

Now the UI can display new values as they change without the old Windows forms-style code (tbValue.Text = “value”).  The other part was to actually sound an alarm if the acceleration was more than a preset value.  This is a simple comparison.  This threshold value is called “sensitivity” and accepts a value from 0-3.  This is value is also a property in the view model.  If the measured magnitude value is higher than the threshold, then an alert displays and the system Exclamation sound plays:

Visual Basic

If Me.Magnitude > Me.Sensitivity Then
    Status = String.Format("Big movement! ({0} G's)", Magnitude)
    System.Media.SystemSounds.Exclamation.Play()
End If
Visual C#
if (this.Magnitude > this.Sensitivity)
{ 
    Status = string.Format("Big movement! ({0} G's)", Magnitude);
    System.Media.SystemSounds.Exclamation.Play();
}

 

In XAML, a Slider control is bound to the Sensitivity property in TwoWay mode. The slider is set with minimum and maximum values. Since sliders typically display their current value, you need to add a Label too.  It's not automatic.  The Label's content is actually bound to the Value property of the Slider control.  So, although it sounds complicated to require an extra field for the slider's label, the element databinding makes it simple.

 
XAML
<Label Content="Sensitivity (1-3):" FontWeight="bold"/>
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width=".75*" />
        <ColumnDefinition Width=".25*" />
    </Grid.ColumnDefinitions>
    <Slider x:Name="slider" Margin="10,0,10,0" Value="{Binding Sensitivity, Mode=TwoWay}" Minimum="0" Maximum="3"  />
    <Label Content="{Binding Value, ElementName=slider}" FontWeight="bold" Grid.Column="1"/>
</Grid>

Getting the Data

Most sensors support the concept of auto-updating their data on a minimum interval.  I decided to do it on my own using a background thread.  This guaranteed a constant interval making it nicer for charting.  It wasn't really necessary, but what I found was no readings during no movement, and a flood of readings when things changed.

As long as the sensor is enabled, you can subscribe to the DataReportChanged event.  Then you can access the CurrentAcceleration property.  Individual axes are indexed rather than exposed as direct properties.  I suppose this is more flexible for far-future 11-dimensional accelerometers…

Visual Basic

Private Sub UpdateData()
    _optionsControl.Dispatcher.BeginInvoke(Sub() UpdateDataInvoked())
End Sub

Private Sub UpdateDataInvoked()
    If (_accelSensor Is Nothing) OrElse (_accelSensor.TryUpdateData() = False) Then
        Return
    End If

    Dim currentAccel = _accelSensor.CurrentAcceleration
    Dim sampleTime As DateTime = DateTime.Now

    Me.LastDataReportX = currentAccel(AccelerationAxis.X)
    Me.LastDataReportY = currentAccel(AccelerationAxis.Y)
    Me.LastDataReportZ = currentAccel(AccelerationAxis.Z)

    Me.Magnitude = Math.Sqrt(Math.Pow(Me.LastDataReportX, 2) + Math.Pow(Me.LastDataReportY, 2) + Math.Pow(Me.LastDataReportZ, 2))

    If _perfCounter IsNot Nothing Then
        _perfCounter.RawValue = Convert.ToInt64(Me.Magnitude * 10)
    End If

    ReadingsX.Add(New SensorReading() With { _
        .Reading = Me.LastDataReportX, _
        .Timestamp = sampleTime _
    })
    ReadingsY.Add(New SensorReading() With { _
        .Reading = Me.LastDataReportY, _
        .Timestamp = sampleTime _
    })
    ReadingsZ.Add(New SensorReading() With { _
        .Reading = Me.LastDataReportZ, _
        .Timestamp = sampleTime _
    })

    If ReadingsX.Count > 20 Then
        ReadingsX.RemoveAt(0)
    End If
    If ReadingsY.Count > 20 Then
        ReadingsY.RemoveAt(0)
    End If
    If ReadingsZ.Count > 20 Then
        ReadingsZ.RemoveAt(0)
    End If

    If Me.Magnitude > Me.Sensitivity Then
        Status = String.Format("Big movement! ({0} G's)", Magnitude)
        System.Media.SystemSounds.Exclamation.Play()
    End If
End Sub

Visual C#

void UpdateData()
{
    _optionsUI.Dispatcher.Invoke(new Action(
        delegate()
        {
            if ((_accelSensor == null) || (_accelSensor.TryUpdateData() == false)) return;
                    
            var currentAccel = _accelSensor.CurrentAcceleration;
            DateTime sampleTime = DateTime.Now;

            this.LastDataReportX = currentAccel[AccelerationAxis.X];
            this.LastDataReportY = currentAccel[AccelerationAxis.Y];
            this.LastDataReportZ = currentAccel[AccelerationAxis.Z];

            this.Magnitude =
                Math.Sqrt(Math.Pow(this.LastDataReportX, 2) + Math.Pow(this.LastDataReportY, 2) +
                            Math.Pow(this.LastDataReportZ, 2));

            if (_perfCounter != null) _perfCounter.RawValue = Convert.ToInt64(this.Magnitude * 10);

            ReadingsX.Add(new SensorReading { Reading = this.LastDataReportX, Timestamp = sampleTime });
            ReadingsY.Add(new SensorReading { Reading = this.LastDataReportY, Timestamp = sampleTime });
            ReadingsZ.Add(new SensorReading { Reading = this.LastDataReportZ, Timestamp = sampleTime });

            if (ReadingsX.Count > 20) ReadingsX.RemoveAt(0);
            if (ReadingsY.Count > 20) ReadingsY.RemoveAt(0);
            if (ReadingsZ.Count > 20) ReadingsZ.RemoveAt(0);

            if (this.Magnitude > this.Sensitivity)
            { 
                Status = string.Format("Big movement! ({0} G's)", Magnitude);
                System.Media.SystemSounds.Exclamation.Play();
            }
    }));
}

These ReadingsX/Y/Z collections contain the last 20 readings and are used in the next section…

Flooded with Data

With so much data flowing in, it can be hard to really visualize it.  This is why I decided that it made sense to chart it.  There are a few charting options out there that are free and a few that are commercial.  Some free options include Visifire, and Dynamic Data Display (D3).  I chose to use Visifire with a line graph and have a data series for each axis.  I'm not sure how useful this information is, but it looks nice!

Behind the scenes, I have three instances of ObservableCollection of type SensorReadingSensorReading simply holds a data point and a point in time.  In XAML, I'm able to perform full data-binding from these collections to datasets in the graph.  Each time I add a value I check to see if I have over twenty items, and if so I remove the oldest one (element zero).  I'm sure I could graph more than that, but it seemed sufficient.

SS-2010-08-20_00.04.16

The other use for the data was to send it to the Windows Performance Monitor.  You've probably seen the charts of the CPU load, memory, disk write, and other low-level data, but you may not realize that you can track your own data this way as well.  Some companies add performance counters for number of simultaneous users, failed logins, cache misses, or whatever else is relevant to their application.  Adding counters for your own data is really easy.  You can choose to either have a number of items, or an increment/decrement counter to keep track of something.

SS-2010-08-20_00.02.56

Start by creating a category, then create the counter.  You can set names, descriptions, and data types.  For this project, I crated a category called “Sensors” and a counter called “Net force.”  This is of type NumberOfItems64 so I set the RawValue property with each new value.  The only caveat with performance counters, is that if you add the counter while Performance Monitor is running you won't see it until you restart it.

Visual Basic

Private Sub CreatePerfCounter()
    If (PerformanceCounterCategory.Exists(PerfCategoryName)) Then
        PerformanceCounterCategory.Delete(PerfCategoryName)
    End If

    ' Create a collection of type CounterCreationDataCollection.
    Dim CounterCreationData As New CounterCreationDataCollection()

    ' Create the counter, set properties, and add to collection
    Dim ccd As New CounterCreationData()
    With (ccd)
        .CounterName = PerfNetForceName
        .CounterHelp = "The net acceleration value (in G's)"
        .CounterType = PerformanceCounterType.NumberOfItems64
    End With

    CounterCreationData.Add(ccd)

    ' Create the category and pass the collection to it.
    _category = PerformanceCounterCategory.Create(PerfCategoryName, _
        "The Sensors performance object tracks the value of various sensors over time", _
        PerformanceCounterCategoryType.SingleInstance, CounterCreationData)

    _perfCounter = New PerformanceCounter(PerfCategoryName, PerfNetForceName, False)
End Sub
Visual C#
private void CreatePerfCounter()
{
    if (PerformanceCounterCategory.Exists(PerfCategoryName))
    {
        PerformanceCounterCategory.Delete(PerfCategoryName);
    }

    // Create a collection of type CounterCreationDataCollection.
    var counterCreationData = new CounterCreationDataCollection();

    // Create the counter, set properties, and add to collection
    counterCreationData.Add(new CounterCreationData
    {
        CounterName = PerfNetForceName,
        CounterHelp = "The net acceleration value (in G's)",
        CounterType = PerformanceCounterType.NumberOfItems64
    });

    // Create the category and pass the collection to it.
    _category =
        PerformanceCounterCategory.Create(
        PerfCategoryName,
        "The Sensors performance object tracks the value of various sensors over time",
        PerformanceCounterCategoryType.SingleInstance,
        counterCreationData);

    _perfCounter = new PerformanceCounter(PerfCategoryName, PerfNetForceName, false);
}

Utility Runner

This is not the first article to use my MEF-based Utility Runner application as the base.  As usual, you'll need to install that package (listed at the top in the prerequisites) in order for this to work.  You'll also need to go to your project's properties, to Debug, and tell it to use a specific executable for debugging.

SS-2010-08-23_16.42.49

If you use Utility Runner and want to add this is a plugin, go into the bin\debug\Addins folder and create a ZIP file from the contents of the MEFUtil-Seismo.util folder and rename it to MEFUtil-Seismo.util.  From the Addins tab in Utility Runner, click the Install Addin From File command from within Utility Runner.

Possible Enhancements

In addition to a sound, it might make sense to support other notifications like growl, Twitter, or email.  It might also make sense to respond to changes in a different sensor, for example ambient light.  If I turn off the light in my office, it could sound an alarm if the light went on and I didn't sign in within a certain amount of time.  That would require a change to monitor a different sensor (not difficult), and to check to see if the desktop is locked or not.

Conclusion

Sensors really change the nature of your code by allowing you to interact with the real world.  If your system isn't equipped with sensors (like mine, unfortunately), make do with an add-on/prototyping board so you can try things out.  These sensors are basically standard on smartphones these days, and will quickly become common on laptops as well.  Windows 7 included a great feature by providing common hardware and device support.  Take advantage of it today!

About Arian

Arian Kulp is a software developer living in Western Oregon.  He creates samples, screencasts, demos, labs, and articles; speaks at programming events about data, UI, Silverlight, and more; and enjoys spending time with his family.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.